diff --git a/.github/workflows/autoblack.yml b/.github/workflows/autoblack.yml new file mode 100644 index 000000000..561cad656 --- /dev/null +++ b/.github/workflows/autoblack.yml @@ -0,0 +1,30 @@ +name: Check / auto apply Black + +on: + push: + branches: [ "dev" ] + +jobs: + black: + name: Check / auto apply black + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check files using the black formatter + uses: psf/black@stable + id: black + with: + options: "." + continue-on-error: true + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "Format Python code with Black" + commit-message: ":art: Format Python code with Black" + body: | + This pull request uses the [psf/black](https://github.com/psf/black) formatter. + base: ${{ github.head_ref }} # Creates pull request onto pull request or commit branch + branch: actions/black diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 01b917f6e..da8ea2f4c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/n_updater.yml b/.github/workflows/n_updater.yml index ce3e9c925..340a5893f 100644 --- a/.github/workflows/n_updater.yml +++ b/.github/workflows/n_updater.yml @@ -11,37 +11,29 @@ jobs: runs-on: ubuntu-latest steps: - name: Fetch the source code - uses: actions/checkout@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} + uses: actions/checkout@v4 + - name: Run the updater script id: run_updater run: | - # Setting up Git user - git config --global user.name 'yunohost-bot' - git config --global user.email 'yunohost-bot@users.noreply.github.com' - # Run the updater script + # Download n wget https://raw.githubusercontent.com/tj/n/master/bin/n --output-document=helpers/vendor/n/n - [[ -z "$(git diff helpers/vendor/n/n)" ]] || echo "PROCEED=true" >> $GITHUB_ENV - - name: Commit changes - id: commit - if: ${{ env.PROCEED == 'true' }} - run: | - git commit -am "Upgrade n to v$VERSION" + + echo "VERSION=$(sed -n 's/^VERSION=\"\(.*\)\"/\1/p' < helpers/vendor/n/n)" >> $GITHUB_ENV + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 id: cpr - if: ${{ env.PROCEED == 'true' }} - uses: peter-evans/create-pull-request@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - commit-message: Update n to version ${{ env.VERSION }} + commit-message: Update n to ${{ env.VERSION }} committer: 'yunohost-bot ' author: 'yunohost-bot ' signoff: false base: dev - branch: ci-auto-update-n-v${{ env.VERSION }} + branch: ci-auto-update-n-${{ env.VERSION }} delete-branch: true - title: 'Upgrade n to version ${{ env.VERSION }}' + title: 'Upgrade n to ${{ env.VERSION }}' body: | - Upgrade `n` to v${{ env.VERSION }} + Upgrade `n` to ${{ env.VERSION }} draft: false diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3e030940b..a49573455 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,11 +1,10 @@ --- stages: + - lint - build - install - test - - lint - - doc - - translation + - bot default: tags: diff --git a/.gitlab/ci/bot.gitlab-ci.yml b/.gitlab/ci/bot.gitlab-ci.yml new file mode 100644 index 000000000..cd44527a4 --- /dev/null +++ b/.gitlab/ci/bot.gitlab-ci.yml @@ -0,0 +1,53 @@ +generate-helpers-doc: + stage: bot + image: "before-install" + needs: [] + before_script: + - git config --global user.email "yunohost@yunohost.org" + - git config --global user.name "$GITHUB_USER" + script: + - cd doc + - python3 generate_helper_doc.py 2 + - python3 generate_helper_doc.py 2.1 + - python3 generate_resource_doc.py > resources.md + - python3 generate_configpanel_and_formoptions_doc.py > forms.md + - hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo + - cp helpers.v2.md doc_repo/pages/06.contribute/10.packaging_apps/20.scripts/10.helpers/packaging_app_scripts_helpers.md + - cp helpers.v2.1.md doc_repo/pages/06.contribute/10.packaging_apps/20.scripts/12.helpers21/packaging_app_scripts_helpers_v21.md + - cp resources.md doc_repo/pages/06.contribute/10.packaging_apps/10.manifest/10.appresources/packaging_app_manifest_resources.md + - cp forms doc_repo/pages/06.contribute/15.dev/03.forms/forms.md + - cd doc_repo + # replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ? + - hub checkout -b "${CI_COMMIT_REF_NAME}" + - 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 + +autofix-translated-strings: + stage: bot + image: "before-install" + needs: [] + before_script: + - git config --global user.email "yunohost@yunohost.org" + - git config --global user.name "$GITHUB_USER" + - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo + - cd github_repo + script: + # create a local branch that will overwrite distant one + - 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 --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 + only: + variables: + - $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + changes: + - locales/* diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index 610580dac..c4d77da6a 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -1,5 +1,8 @@ .build-stage: stage: build + needs: + - job: actionsmap + - job: invalidcode311 image: "before-install" variables: YNH_SOURCE: "https://github.com/yunohost" @@ -13,6 +16,8 @@ .build_script: &build_script - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" install devscripts --no-install-recommends - cd $YNH_BUILD_DIR/$PACKAGE + - git status || true + - git log -n 1 || true - VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null) - VERSION_NIGHTLY="${VERSION}+$(date +%Y%m%d%H%M)" - dch --package "${PACKAGE}" --force-bad-version -v "${VERSION_NIGHTLY}" -D "unstable" --force-distribution "Daily build." diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml deleted file mode 100644 index 4f6ea6ba1..000000000 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ /dev/null @@ -1,30 +0,0 @@ -######################################## -# DOC -######################################## - -generate-helpers-doc: - stage: doc - image: "before-install" - needs: [] - before_script: - - apt-get update -y && apt-get install git hub -y - - git config --global user.email "yunohost@yunohost.org" - - git config --global user.name "$GITHUB_USER" - script: - - 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/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] 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 65409c6eb..0f5571a57 100644 --- a/.gitlab/ci/install.gitlab-ci.yml +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -17,7 +17,9 @@ upgrade: image: "after-install" script: - apt-get update -o Acquire::Retries=3 + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname install-postinstall: @@ -25,5 +27,7 @@ install-postinstall: image: "before-install" script: - apt-get update -o Acquire::Retries=3 + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - systemctl restart nginx || journalctl -u nginx -n 50 --no-pager --no-hostname - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 65b74ddca..5a8351413 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -3,6 +3,14 @@ ######################################## # later we must fix lint and format-check jobs and remove "allow_failure" +actionsmap: + stage: lint + image: "before-install" + needs: [] + script: + - python -c 'import yaml; yaml.safe_load(open("share/actionsmap.yml"))' + - python -c 'import yaml; yaml.safe_load(open("share/actionsmap-portal.yml"))' + lint311: stage: lint image: "before-install" @@ -25,23 +33,8 @@ mypy: script: - tox -e py311-mypy -black: +i18n-keys: stage: lint - image: "before-install" needs: [] - before_script: - - apt-get update -y && apt-get install git hub -y - - git config --global user.email "yunohost@yunohost.org" - - git config --global user.name "$GITHUB_USER" - - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo - - cd github_repo script: - # create a local branch that will overwrite distant one - - git checkout -b "ci-format-${CI_COMMIT_REF_NAME}" --no-track - - tox -e py311-black-run - - '[ $(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:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd - only: - - tags + - python3 maintenance/missing_i18n_keys.py --check diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index a49fc13b7..6161eb0c9 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -1,6 +1,6 @@ .install_debs: &install_debs - apt-get update -o Acquire::Retries=3 - - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb php8.2-cli mariadb-client mariadb-server + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb .test-stage: stage: test @@ -26,174 +26,172 @@ # TESTS ######################################## -full-tests: - stage: test - image: "before-install" - variables: - PYTEST_ADDOPTS: "--color=yes" - before_script: - - *install_debs - - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace - script: - - python3 -m pytest --cov=yunohost tests/ src/tests/ --junitxml=report.xml - - cd tests - - bash test_helpers.sh - needs: - - job: build-yunohost - artifacts: true - - job: build-ssowat - artifacts: true - - job: build-moulinette - artifacts: true - coverage: '/TOTAL.*\s+(\d+%)/' - artifacts: - reports: - junit: report.xml +#full-tests: +# stage: test +# image: "before-install" +# variables: +# PYTEST_ADDOPTS: "--color=yes" +# before_script: +# - *install_debs +# - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace +# script: +# - python3 -m pytest --cov=yunohost tests/ src/tests/ --junitxml=report.xml +# needs: +# - job: build-yunohost +# artifacts: true +# - job: build-ssowat +# artifacts: true +# - job: build-moulinette +# artifacts: true +# coverage: '/TOTAL.*\s+(\d+%)/' +# artifacts: +# reports: +# junit: report.xml -test-actionmap: - extends: .test-stage - script: - - python3 -m pytest tests/test_actionmap.py - only: - changes: - - share/actionsmap.yml - -test-helpers: +test-helpers2: extends: .test-stage script: - cd tests - bash test_helpers.sh - only: - changes: - - helpers/* + +test-helpers2.1: + extends: .test-stage + script: + - cd tests + - bash test_helpers.sh 2.1 test-domains: extends: .test-stage script: - python3 -m pytest src/tests/test_domains.py - only: - changes: - - src/domain.py +# only: +# changes: +# - src/domain.py test-dns: extends: .test-stage script: - python3 -m pytest src/tests/test_dns.py - only: - changes: - - src/dns.py - - src/utils/dns.py +# only: +# changes: +# - src/dns.py +# - src/utils/dns.py test-apps: extends: .test-stage script: - python3 -m pytest src/tests/test_apps.py - only: - changes: - - src/app.py +# only: +# changes: +# - src/app.py test-appscatalog: extends: .test-stage script: - python3 -m pytest src/tests/test_app_catalog.py - only: - changes: - - src/app_calalog.py +# only: +# changes: +# - src/app_calalog.py test-appurl: extends: .test-stage script: - python3 -m pytest src/tests/test_appurl.py - only: - changes: - - src/app.py +# only: +# changes: +# - src/app.py test-questions: extends: .test-stage script: - python3 -m pytest src/tests/test_questions.py - only: - changes: - - src/utils/config.py +# only: +# changes: +# - src/utils/config.py test-app-config: extends: .test-stage script: - python3 -m pytest src/tests/test_app_config.py - only: - changes: - - src/app.py - - src/utils/config.py +# only: +# changes: +# - 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 +# only: +# changes: +# - src/app.py +# - src/utils/resources.py test-changeurl: extends: .test-stage script: - python3 -m pytest src/tests/test_changeurl.py - only: - changes: - - src/app.py +# only: +# changes: +# - src/app.py test-backuprestore: extends: .test-stage script: - python3 -m pytest src/tests/test_backuprestore.py - only: - changes: - - src/backup.py +# only: +# changes: +# - src/backup.py test-permission: extends: .test-stage script: - python3 -m pytest src/tests/test_permission.py - only: - changes: - - src/permission.py +# only: +# changes: +# - src/permission.py test-settings: extends: .test-stage script: - python3 -m pytest src/tests/test_settings.py - only: - changes: - - src/settings.py +# only: +# changes: +# - src/settings.py test-user-group: extends: .test-stage script: - python3 -m pytest src/tests/test_user-group.py - only: - changes: - - src/user.py +# only: +# changes: +# - src/user.py test-regenconf: extends: .test-stage script: - python3 -m pytest src/tests/test_regenconf.py - only: - changes: - - src/regenconf.py +# only: +# changes: +# - src/regenconf.py test-service: extends: .test-stage script: - python3 -m pytest src/tests/test_service.py - only: - changes: - - src/service.py +# only: +# changes: +# - src/service.py test-ldapauth: extends: .test-stage script: - python3 -m pytest src/tests/test_ldapauth.py - only: - changes: - - src/authenticators/*.py +# only: +# changes: +# - src/authenticators/*.py + +test-sso-and-portalapi: + extends: .test-stage + script: + - python3 -m pytest src/tests/test_sso_and_portalapi.py diff --git a/.gitlab/ci/translation.gitlab-ci.yml b/.gitlab/ci/translation.gitlab-ci.yml deleted file mode 100644 index 83db2b5a4..000000000 --- a/.gitlab/ci/translation.gitlab-ci.yml +++ /dev/null @@ -1,37 +0,0 @@ -######################################## -# TRANSLATION -######################################## -test-i18n-keys: - stage: translation - script: - - python3 maintenance/missing_i18n_keys.py --check - only: - changes: - - locales/en.json - - src/*.py - - src/diagnosers/*.py - -autofix-translated-strings: - stage: translation - image: "before-install" - needs: [] - before_script: - - apt-get update -y && apt-get install git hub -y - - git config --global user.email "yunohost@yunohost.org" - - git config --global user.name "$GITHUB_USER" - - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo - - cd github_repo - script: - # create a local branch that will overwrite distant one - - 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 --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 - only: - variables: - - $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH - changes: - - locales/* diff --git a/README.md b/README.md index 07ee04de0..59971ce59 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

YunoHost

- + ![Version](https://img.shields.io/github/v/tag/yunohost/yunohost?label=version&sort=semver) [![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) @@ -19,31 +19,31 @@ YunoHost is an operating system aiming to simplify as much as possible the admin This repository corresponds to the core code of YunoHost, mainly written in Python and Bash. -- [Project features](https://yunohost.org/#/whatsyunohost) +- [Project features](https://yunohost.org/whatsyunohost) - [Project website](https://yunohost.org) - [Install documentation](https://yunohost.org/install) - [Issue tracker](https://github.com/YunoHost/issues) -# Screenshots +## Screenshots Webadmin ([Yunohost-Admin](https://github.com/YunoHost/yunohost-admin)) | Single sign-on user portal ([SSOwat](https://github.com/YunoHost/ssowat)) ---- | --- -![](https://raw.githubusercontent.com/YunoHost/doc/master/images/webadmin.png) | ![](https://raw.githubusercontent.com/YunoHost/doc/master/images/user_panel.png) +--- | --- +![Web admin insterface screenshot](https://raw.githubusercontent.com/YunoHost/doc/master/images/webadmin.png) | ![User portal screenshot](https://raw.githubusercontent.com/YunoHost/doc/master/images/user_panel.png) ## Contributing - You can learn how to get started with developing on YunoHost by reading [this piece of documentation](https://yunohost.org/dev). -- Come chat with us on the [dev chatroom](https://yunohost.org/#/chat_rooms) ! -- You can help translate YunoHost on our [translation platform](https://translate.yunohost.org/engage/yunohost/?utm_source=widget) +- Come chat with us on the [dev chatroom](https://yunohost.org/chat_rooms)! +- You can help translate YunoHost on our [translation platform](https://translate.yunohost.org/engage/yunohost/?utm_source=widget).

-Translation status +View of the translation rate for the different languages available in YunoHost

## License -As [other components of YunoHost](https://yunohost.org/#/faq_en), this repository is licensed under GNU AGPL v3. +As [other components of YunoHost](https://yunohost.org/faq), this repository is licensed under GNU AGPL v3. ## They support us <3 @@ -51,16 +51,16 @@ We are thankful for our sponsors providing us with infrastructure and grants!

- - - +NLnet Foundation +Next Generation Internet +Code Lutin

- - - - - +Globenet +Gitoyen +tetaneutral.net +LDN (Lorraine Data Network) +NBS System

diff --git a/bin/yunohost-portal-api b/bin/yunohost-portal-api new file mode 100755 index 000000000..66751e66f --- /dev/null +++ b/bin/yunohost-portal-api @@ -0,0 +1,53 @@ +#! /usr/bin/python3 +# -*- coding: utf-8 -*- + +import argparse +import yunohost + +# Default server configuration +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 6788 + + +def _parse_api_args(): + """Parse main arguments for the api""" + parser = argparse.ArgumentParser( + add_help=False, + description="Run the YunoHost API to manage your server.", + ) + srv_group = parser.add_argument_group("server configuration") + srv_group.add_argument( + "-h", + "--host", + action="store", + default=DEFAULT_HOST, + help="Host to listen on (default: %s)" % DEFAULT_HOST, + ) + srv_group.add_argument( + "-p", + "--port", + action="store", + default=DEFAULT_PORT, + type=int, + help="Port to listen on (default: %d)" % DEFAULT_PORT, + ) + glob_group = parser.add_argument_group("global arguments") + glob_group.add_argument( + "--debug", + action="store_true", + default=False, + help="Set log level to DEBUG", + ) + glob_group.add_argument( + "--help", + action="help", + help="Show this help message and exit", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + opts = _parse_api_args() + # Run the server + yunohost.portalapi(debug=opts.debug, host=opts.host, port=opts.port) diff --git a/bin/yunopaste b/bin/yunopaste index edf8d55c8..f6bdecae2 100755 --- a/bin/yunopaste +++ b/bin/yunopaste @@ -1,77 +1,34 @@ -#!/bin/bash +#!/usr/bin/env python3 -set -e -set -u +import sys +import requests +import json -PASTE_URL="https://paste.yunohost.org" +SERVER_URL = "https://paste.yunohost.org" +TIMEOUT = 3 -_die() { - printf "Error: %s\n" "$*" - exit 1 -} +def create_snippet(data): + try: + url = SERVER_URL + "/documents" + response = requests.post(url, data=data.encode('utf-8'), timeout=TIMEOUT) + response.raise_for_status() + dockey = json.loads(response.text)['key'] + return SERVER_URL + "/raw/" + dockey + except requests.exceptions.RequestException as e: + print("\033[31mError: {}\033[0m".format(e)) + sys.exit(1) -check_dependencies() { - curl -V > /dev/null 2>&1 || _die "This script requires curl." -} -paste_data() { - json=$(curl -X POST -s -d "$1" "${PASTE_URL}/documents") - [[ -z "$json" ]] && _die "Unable to post the data to the server." +def main(): + output = sys.stdin.read() - key=$(echo "$json" \ - | python3 -c 'import json,sys;o=json.load(sys.stdin);print(o["key"])' \ - 2>/dev/null) - [[ -z "$key" ]] && _die "Unable to parse the server response." + if not output: + print("\033[31mError: No input received from stdin.\033[0m") + sys.exit(1) - echo "${PASTE_URL}/${key}" -} + url = create_snippet(output) -usage() { - printf "Usage: ${0} [OPTION]... + print("\033[32mURL: {}\033[0m".format(url)) -Read from input stream and paste the data to the YunoHost -Haste server. - -For example, to paste the output of the YunoHost diagnosis, you -can simply execute the following: - yunohost diagnosis show | ${0} - -It will return the URL where you can access the pasted data. - -Options: - -h, --help show this help message and exit -" -} - -main() { - # parse options - while (( ${#} )); do - case "${1}" in - --help|-h) - usage - exit 0 - ;; - *) - echo "Unknown parameter detected: ${1}" >&2 - echo >&2 - usage >&2 - exit 1 - ;; - esac - - shift 1 - done - - # check input stream - read -t 0 || { - echo -e "Invalid usage: No input is provided.\n" >&2 - usage - exit 1 - } - - paste_data "$(cat)" -} - -check_dependencies - -main "${@}" +if __name__ == "__main__": + main() diff --git a/conf/dnsmasq/domain.tpl b/conf/dnsmasq/domain.tpl index 50b946176..e72511f48 100644 --- a/conf/dnsmasq/domain.tpl +++ b/conf/dnsmasq/domain.tpl @@ -1,13 +1,9 @@ {% set interfaces_list = interfaces.split(' ') %} {% for interface in interfaces_list %} interface-name={{ domain }},{{ interface }} -interface-name=xmpp-upload.{{ domain }},{{ interface }} {% endfor %} {% if ipv6 %} host-record={{ domain }},{{ ipv6 }} -host-record=xmpp-upload.{{ domain }},{{ ipv6 }} {% endif %} txt-record={{ domain }},"v=spf1 mx a -all" mx-host={{ domain }},{{ domain }},5 -srv-host=_xmpp-client._tcp.{{ domain }},{{ domain }},5222,0,5 -srv-host=_xmpp-server._tcp.{{ domain }},{{ domain }},5269,0,5 diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index e614c3796..810fcd4d2 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -13,9 +13,8 @@ protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %} mail_plugins = $mail_plugins quota notify push_notification ############################################################################### - -# generated 2020-08-18, Mozilla Guideline v5.6, Dovecot 2.3.4, OpenSSL 1.1.1d, intermediate configuration -# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.4&config=intermediate&openssl=1.1.1d&guideline=5.6 +# generated 2023-06-13, Mozilla Guideline v5.7, Dovecot 2.3.19, OpenSSL 3.0.9, intermediate configuration +# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.19&config=intermediate&openssl=3.0.9&guideline=5.7 ssl = required @@ -32,20 +31,32 @@ ssl_dh = , # and return true if the IP is to be ignored. False otherwise. @@ -56,15 +98,18 @@ ignoreip = 127.0.0.1/8 ignorecommand = # "bantime" is the number of seconds that a host is banned. -bantime = 600 +bantime = 10m # A host is banned if it has generated "maxretry" during the last "findtime" # seconds. -findtime = 600 +findtime = 10m # "maxretry" is the number of failures before a host get banned. maxretry = 10 +# "maxmatches" is the number of matches stored in ticket (resolvable via tag in actions). +maxmatches = %(maxretry)s + # "backend" specifies the backend used to get files modification. # Available options are "pyinotify", "gamin", "polling", "systemd" and "auto". # This option can be overridden in each jail as well. @@ -113,10 +158,13 @@ logencoding = auto enabled = false +# "mode" defines the mode of the filter (see corresponding filter implementation for more info). +mode = normal + # "filter" defines the filter to use by the jail. # By default jails have names matching their filter name # -filter = %(__name__)s +filter = %(__name__)s[mode=%(mode)s] # @@ -140,7 +188,7 @@ mta = sendmail # Default protocol protocol = tcp -# Specify chain where jumps would need to be added in iptables-* actions +# Specify chain where jumps would need to be added in ban-actions expecting parameter chain chain = INPUT # Ports to be banned @@ -161,51 +209,53 @@ banaction = iptables-multiport banaction_allports = iptables-allports # The simplest action to take: ban only -action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] +action_ = %(banaction)s[port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report to the destemail. -action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] +action_mw = %(action_)s + %(mta)s-whois[sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report and relevant log lines # to the destemail. -action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] +action_mwl = %(action_)s + %(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"] # See the IMPORTANT note in action.d/xarf-login-attack for when to use this action # # ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines # to the destemail. -action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"] +action_xarf = %(action_)s + xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"] + +# ban & send a notification to one or more of the 50+ services supported by Apprise. +# See https://github.com/caronc/apprise/wiki for details on what is supported. +# +# You may optionally over-ride the default configuration line (containing the Apprise URLs) +# by using 'apprise[config="/alternate/path/to/apprise.cfg"]' otherwise +# /etc/fail2ban/apprise.conf is sourced for your supported notification configuration. +# action = %(action_)s +# apprise # ban IP on CloudFlare & send an e-mail with whois report and relevant log lines # to the destemail. action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"] - %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] + %(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"] # Report block via blocklist.de fail2ban reporting service API # -# See the IMPORTANT note in action.d/blocklist_de.conf for when to -# use this action. Create a file jail.d/blocklist_de.local containing -# [Init] -# blocklist_de_apikey = {api key from registration] +# See the IMPORTANT note in action.d/blocklist_de.conf for when to use this action. +# Specify expected parameters in file action.d/blocklist_de.local or if the interpolation +# `action_blocklist_de` used for the action, set value of `blocklist_de_apikey` +# in your `jail.local` globally (section [DEFAULT]) or per specific jail section (resp. in +# corresponding jail.d/my-jail.local file). # -action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] +action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] -# Report ban via badips.com, and use as blacklist +# Report ban via abuseipdb.com. # -# See BadIPsAction docstring in config/action.d/badips.py for -# documentation for this action. +# See action.d/abuseipdb.conf for usage example and details. # -# NOTE: This action relies on banaction being present on start and therefore -# should be last action defined for a jail. -# -action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"] -# -# Report ban via badips.com (uses action.d/badips.conf for reporting only) -# -action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"] +action_abuseipdb = abuseipdb # Choose default action. To change, just override value of 'action' with the # interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local @@ -223,15 +273,10 @@ action = %(action_)s [sshd] -port = ssh -logpath = %(sshd_log)s -backend = %(sshd_backend)s - - -[sshd-ddos] -# This jail corresponds to the standard configuration in Fail2ban. -# The mail-whois action send a notification e-mail with a whois request -# in the body. +# To use more aggressive sshd modes set filter parameter "mode" in jail.local: +# normal (default), ddos, extra or aggressive (combines all). +# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details. +#mode = normal port = ssh logpath = %(sshd_log)s backend = %(sshd_backend)s @@ -265,7 +310,7 @@ logpath = %(apache_error_log)s # for email addresses. The mail outputs are buffered. port = http,https logpath = %(apache_access_log)s -bantime = 172800 +bantime = 48h maxretry = 1 @@ -301,7 +346,7 @@ maxretry = 2 port = http,https logpath = %(apache_access_log)s maxretry = 1 -ignorecommand = %(ignorecommands_dir)s/apache-fakegooglebot +ignorecommand = %(fail2ban_confpath)s/filter.d/ignorecommands/apache-fakegooglebot [apache-modsecurity] @@ -321,12 +366,15 @@ maxretry = 1 [openhab-auth] filter = openhab -action = iptables-allports[name=NoAuthFailures] +banaction = %(banaction_allports)s logpath = /opt/openhab/logs/request.log +# To use more aggressive http-auth modes set filter parameter "mode" in jail.local: +# normal (default), aggressive (combines all), auth or fallback +# See "tests/files/logs/nginx-http-auth" or "filter.d/nginx-http-auth.conf" for usage example and details. [nginx-http-auth] - +# mode = normal port = http,https logpath = %(nginx_error_log)s @@ -342,8 +390,10 @@ logpath = %(nginx_error_log)s port = http,https logpath = %(nginx_error_log)s -maxretry = 2 +[nginx-bad-request] +port = http,https +logpath = %(nginx_access_log)s # Ban attackers that try to use PHP's URL-fopen() functionality # through GET/POST variables. - Experimental, with more than a year @@ -377,6 +427,8 @@ logpath = %(lighttpd_error_log)s port = http,https logpath = %(roundcube_errors_log)s +# Use following line in your jail.local if roundcube logs to journal. +#backend = %(syslog_backend)s [openwebmail] @@ -426,11 +478,13 @@ backend = %(syslog_backend)s port = http,https logpath = /var/log/tomcat*/catalina.out +#logpath = /var/log/guacamole.log [monit] #Ban clients brute-forcing the monit gui login port = 2812 logpath = /var/log/monit + /var/log/monit.log [webmin-auth] @@ -513,27 +567,29 @@ logpath = %(vsftpd_log)s # ASSP SMTP Proxy Jail [assp] -port = smtp,submission +port = smtp,465,submission logpath = /root/path/to/assp/logs/maillog.txt [courier-smtp] -port = smtp,submission +port = smtp,465,submission logpath = %(syslog_mail)s backend = %(syslog_backend)s [postfix] - -port = smtp,submission -logpath = %(postfix_log)s -backend = %(postfix_backend)s +# To use another modes set filter parameter "mode" in jail.local: +mode = more +port = smtp,465,submission +logpath = %(postfix_log)s +backend = %(postfix_backend)s [postfix-rbl] -port = smtp,submission +filter = postfix[mode=rbl] +port = smtp,465,submission logpath = %(postfix_log)s backend = %(postfix_backend)s maxretry = 1 @@ -541,14 +597,17 @@ maxretry = 1 [sendmail-auth] -port = submission,smtp +port = submission,465,smtp logpath = %(syslog_mail)s backend = %(syslog_backend)s [sendmail-reject] - -port = smtp,submission +# To use more aggressive modes set filter parameter "mode" in jail.local: +# normal (default), extra or aggressive +# See "tests/files/logs/sendmail-reject" or "filter.d/sendmail-reject.conf" for usage example and details. +#mode = normal +port = smtp,465,submission logpath = %(syslog_mail)s backend = %(syslog_backend)s @@ -556,7 +615,7 @@ backend = %(syslog_backend)s [qmail-rbl] filter = qmail -port = smtp,submission +port = smtp,465,submission logpath = /service/qmail/log/main/current @@ -564,14 +623,14 @@ logpath = /service/qmail/log/main/current # but can be set by syslog_facility in the dovecot configuration. [dovecot] -port = pop3,pop3s,imap,imaps,submission,sieve +port = pop3,pop3s,imap,imaps,submission,465,sieve logpath = %(dovecot_log)s backend = %(dovecot_backend)s [sieve] -port = smtp,submission +port = smtp,465,submission logpath = %(dovecot_log)s backend = %(dovecot_backend)s @@ -583,20 +642,21 @@ logpath = %(solidpop3d_log)s [exim] - -port = smtp,submission +# see filter.d/exim.conf for further modes supported from filter: +#mode = normal +port = smtp,465,submission logpath = %(exim_main_log)s [exim-spam] -port = smtp,submission +port = smtp,465,submission logpath = %(exim_main_log)s [kerio] -port = imap,smtp,imaps +port = imap,smtp,imaps,465 logpath = /opt/kerio/mailserver/store/logs/security.log @@ -607,14 +667,15 @@ logpath = /opt/kerio/mailserver/store/logs/security.log [courier-auth] -port = smtp,submission,imaps,pop3,pop3s +port = smtp,465,submission,imap,imaps,pop3,pop3s logpath = %(syslog_mail)s backend = %(syslog_backend)s [postfix-sasl] -port = smtp,submission,imap,imaps,pop3,pop3s +filter = postfix[mode=auth] +port = smtp,465,submission,imap,imaps,pop3,pop3s # You might consider monitoring /var/log/mail.warn instead if you are # running postfix since it would provide the same log lines at the # "warn" level but overall at the smaller filesize. @@ -631,7 +692,7 @@ backend = %(syslog_backend)s [squirrelmail] -port = smtp,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks +port = smtp,465,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks logpath = /var/lib/squirrelmail/prefs/squirrelmail_access_log @@ -684,8 +745,8 @@ logpath = /var/log/named/security.log [nsd] port = 53 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/nsd.log @@ -696,9 +757,8 @@ logpath = /var/log/nsd.log [asterisk] port = 5060,5061 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] - %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/asterisk/messages maxretry = 10 @@ -706,16 +766,22 @@ maxretry = 10 [freeswitch] port = 5060,5061 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] - %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/freeswitch.log maxretry = 10 +# enable adminlog; it will log to a file inside znc's directory by default. +[znc-adminlog] + +port = 6667 +logpath = /var/lib/znc/moddata/adminlog/znc.log + + # To log wrong MySQL access attempts add to /etc/my.cnf in [mysqld] or # equivalent section: -# log-warning = 2 +# log-warnings = 2 # # for syslog (daemon facility) # [mysqld_safe] @@ -731,6 +797,14 @@ logpath = %(mysql_log)s backend = %(mysql_backend)s +[mssql-auth] +# Default configuration for Microsoft SQL Server for Linux +# See the 'mssql-conf' manpage how to change logpath or port +logpath = /var/opt/mssql/log/errorlog +port = 1433 +filter = mssql-auth + + # Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf') [mongodb-auth] # change port when running with "--shardsvr" or "--configsvr" runtime operation @@ -749,8 +823,8 @@ logpath = /var/log/mongodb/mongodb.log logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s -bantime = 604800 ; 1 week -findtime = 86400 ; 1 day +bantime = 1w +findtime = 1d # Generic filter for PAM. Has to be used with action which bans all @@ -786,11 +860,31 @@ logpath = /var/log/ejabberd/ejabberd.log [counter-strike] logpath = /opt/cstrike/logs/L[0-9]*.log -# Firewall: http://www.cstrike-planet.com/faq/6 tcpport = 27030,27031,27032,27033,27034,27035,27036,27037,27038,27039 udpport = 1200,27000,27001,27002,27003,27004,27005,27006,27007,27008,27009,27010,27011,27012,27013,27014,27015 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] +action_ = %(default/action_)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp"] + +[softethervpn] +port = 500,4500 +protocol = udp +logpath = /usr/local/vpnserver/security_log/*/sec.log + +[gitlab] +port = http,https +logpath = /var/log/gitlab/gitlab-rails/application.log + +[grafana] +port = http,https +logpath = /var/log/grafana/grafana.log + +[bitwarden] +port = http,https +logpath = /home/*/bwdata/logs/identity/Identity/log.txt + +[centreon] +port = http,https +logpath = /var/log/centreon/login.log # consider low maxretry and a long bantime # nobody except your own Nagios server should ever probe nrpe @@ -824,7 +918,9 @@ filter = apache-pass[knocking_url="%(knocking_url)s"] logpath = %(apache_access_log)s blocktype = RETURN returntype = DROP -bantime = 3600 +action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s, + actionstart_on_demand=false, actionrepair_on_unban=true] +bantime = 1h maxretry = 1 findtime = 1 @@ -832,8 +928,8 @@ findtime = 1 [murmur] # AKA mumble-server port = 64738 -action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s", actname=%(banaction)s-tcp] - %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol=udp, chain="%(chain)s", actname=%(banaction)s-udp] +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] logpath = /var/log/mumble-server/mumble-server.log @@ -851,5 +947,34 @@ logpath = /var/log/haproxy.log [slapd] port = ldap,ldaps -filter = slapd logpath = /var/log/slapd.log + +[domino-smtp] +port = smtp,ssmtp +logpath = /home/domino01/data/IBM_TECHNICAL_SUPPORT/console.log + +[phpmyadmin-syslog] +port = http,https +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +[zoneminder] +# Zoneminder HTTP/HTTPS web interface auth +# Logs auth failures to apache2 error log +port = http,https +logpath = %(apache_error_log)s + +[traefik-auth] +# to use 'traefik-auth' filter you have to configure your Traefik instance, +# see `filter.d/traefik-auth.conf` for details and service example. +port = http,https +logpath = /var/log/traefik/access.log + +[scanlogd] +logpath = %(syslog_local0)s +banaction = %(banaction_allports)s + +[monitorix] +port = 8080 +logpath = /var/log/monitorix-httpd diff --git a/conf/fail2ban/yunohost-jails.conf b/conf/fail2ban/yunohost-jails.conf index 911f9cd85..d04ea41fd 100644 --- a/conf/fail2ban/yunohost-jails.conf +++ b/conf/fail2ban/yunohost-jails.conf @@ -31,3 +31,12 @@ protocol = tcp filter = yunohost logpath = /var/log/nginx/*error.log /var/log/nginx/*access.log + +[yunohost-portal] +enabled = true +port = http,https +protocol = tcp +filter = yunohost-portal +logpath = /var/log/nginx/*error.log + /var/log/nginx/*access.log +maxretry = 20 diff --git a/conf/fail2ban/yunohost-portal.conf b/conf/fail2ban/yunohost-portal.conf new file mode 100644 index 000000000..c4a16570f --- /dev/null +++ b/conf/fail2ban/yunohost-portal.conf @@ -0,0 +1,3 @@ +[Definition] +failregex = ^ -.*\"POST /yunohost/portalapi/login HTTP/\d.\d\" 401 +ignoreregex = diff --git a/conf/fail2ban/yunohost.conf b/conf/fail2ban/yunohost.conf index 26d732740..be20e231b 100644 --- a/conf/fail2ban/yunohost.conf +++ b/conf/fail2ban/yunohost.conf @@ -1,24 +1,3 @@ -# Fail2Ban configuration file -# -# Author: Adrien Beudin -# -# $Revision: 2 $ -# - [Definition] - -# Option: failregex -# Notes.: regex to match the password failure messages in the logfile. The -# host must be matched by a group named "host". The tag "" can -# be used for standard IP/hostname matching and is only an alias for -# (?:::f{4,6}:)?(?P[\w\-.^_]+) -# Values: TEXT -# -failregex = helpers.lua:[0-9]+: authenticate\(\): Connection failed for: .*, client: - ^ -.*\"POST /yunohost/api/login HTTP/\d.\d\" 401 - -# Option: ignoreregex -# Notes.: regex to ignore. If this regex matches, the line is ignored. -# Values: TEXT -# +failregex = ^ -.*\"POST /yunohost/api/login HTTP/\d.\d\" 401 ignoreregex = diff --git a/conf/metronome/domain.tpl.cfg.lua b/conf/metronome/domain.tpl.cfg.lua deleted file mode 100644 index e5e169791..000000000 --- a/conf/metronome/domain.tpl.cfg.lua +++ /dev/null @@ -1,75 +0,0 @@ -VirtualHost "{{ domain }}" - enable = true - ssl = { - key = "/etc/yunohost/certs/{{ domain }}/key.pem"; - certificate = "/etc/yunohost/certs/{{ domain }}/crt.pem"; - } - authentication = "ldap2" - ldap = { - hostname = "localhost", - user = { - basedn = "ou=users,dc=yunohost,dc=org", - filter = "(&(objectClass=posixAccount)(mail=*@{{ domain }})(permission=cn=xmpp.main,ou=permission,dc=yunohost,dc=org))", - usernamefield = "mail", - namefield = "cn", - }, - } - - -- Discovery items - disco_items = { - { "muc.{{ domain }}" }, - { "pubsub.{{ domain }}" }, - { "jabber.{{ domain }}" }, - { "vjud.{{ domain }}" }, - { "xmpp-upload.{{ domain }}" }, - }; - --- contact_info = { --- abuse = { "mailto:abuse@{{ domain }}", "xmpp:admin@{{ domain }}" }; --- admin = { "mailto:root@{{ domain }}", "xmpp:admin@{{ domain }}" }; --- }; - ------- Components ------ --- You can specify components to add hosts that provide special services, --- like multi-user conferences, and transports. - ----Set up a MUC (multi-user chat) room server -Component "muc.{{ domain }}" "muc" - name = "{{ domain }} Chatrooms" - - modules_enabled = { - "muc_limits"; - "muc_log"; - "muc_log_mam"; - "muc_log_http"; - "muc_vcard"; - } - - muc_event_rate = 0.5 - muc_burst_factor = 10 - room_default_config = { - logging = true, - persistent = true - }; - ----Set up a PubSub server -Component "pubsub.{{ domain }}" "pubsub" - name = "{{ domain }} Publish/Subscribe" - - unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server) - ----Set up a HTTP Upload service -Component "xmpp-upload.{{ domain }}" "http_upload" - name = "{{ domain }} Sharing Service" - - http_file_path = "/var/xmpp-upload/{{ domain }}/upload" - http_external_url = "https://xmpp-upload.{{ domain }}:443" - http_file_base_path = "/upload" - http_file_size_limit = 6*1024*1024 - http_file_quota = 60*1024*1024 - http_upload_file_size_limit = 100 * 1024 * 1024 -- bytes - http_upload_quota = 10 * 1024 * 1024 * 1024 -- bytes - ----Set up a VJUD service -Component "vjud.{{ domain }}" "vjud" - vjud_disco_name = "{{ domain }} User Directory" diff --git a/conf/metronome/metronome.cfg.lua b/conf/metronome/metronome.cfg.lua deleted file mode 100644 index 9e21016d9..000000000 --- a/conf/metronome/metronome.cfg.lua +++ /dev/null @@ -1,123 +0,0 @@ --- ** Metronome's config file example ** --- --- The format is exactly equal to Prosody's: --- --- Lists are written { "like", "this", "one" } --- Lists can also be of { 1, 2, 3 } numbers, etc. --- Either commas, or semi-colons; may be used as seperators. --- --- A table is a list of values, except each value has a name. An --- example would be: --- --- ssl = { key = "keyfile.key", certificate = "certificate.cert" } --- --- Tip: You can check that the syntax of this file is correct when you have finished --- by running: luac -p metronome.cfg.lua --- If there are any errors, it will let you know what and where they are, otherwise it --- will keep quiet. - --- Global settings go in this section - --- This is the list of modules Metronome will load on startup. --- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. - -modules_enabled = { - -- Generally required - "roster"; -- Allow users to have a roster. Recommended. - "saslauth"; -- Authentication for clients. Recommended if you want to log in. - "tls"; -- Add support for secure TLS on c2s/s2s connections - "disco"; -- Service discovery - - -- Not essential, but recommended - "private"; -- Private XML storage (for room bookmarks, etc.) - "vcard"; -- Allow users to set vCards - "pep"; -- Allows setting of mood, tune, etc. - "pubsub"; -- Publish-subscribe XEP-0060 - "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. - "bidi"; -- Enables Bidirectional Server-to-Server Streams. - - -- Nice to have - "version"; -- Replies to server version requests - "uptime"; -- Report how long server has been running - "time"; -- Let others know the time here on this server - "ping"; -- Replies to XMPP pings with pongs - "register"; -- Allow users to register on this server using a client and change passwords - "stream_management"; -- Allows clients and servers to use Stream Management - "stanza_optimizations"; -- Allows clients to use Client State Indication and SIFT - "message_carbons"; -- Allows clients to enable carbon copies of messages - "mam"; -- Enable server-side message archives using Message Archive Management - "push"; -- Enable Push Notifications via PubSub using XEP-0357 - "lastactivity"; -- Enables clients to know the last presence status of an user - "adhoc_cm"; -- Allow to set client certificates to login through SASL External via adhoc - "admin_adhoc"; -- administration adhoc commands - "bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage - "sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs. - "privacy"; -- Add privacy lists and simple blocking command support - - -- Other specific functionality - --"admin_telnet"; -- administration console, telnet to port 5582 - --"admin_web"; -- administration web interface - "bosh"; -- Enable support for BOSH clients, aka "XMPP over Bidirectional Streams over Synchronous HTTP" - --"compression"; -- Allow clients to enable Stream Compression - --"spim_block"; -- Require authorization via OOB form for messages from non-contacts and block unsollicited messages - --"gate_guard"; -- Enable config-based blacklisting and hit-based auto-banning features - --"incidents_handling"; -- Enable Incidents Handling support (can be administered via adhoc commands) - --"server_presence"; -- Enables Server Buddies extension support - --"service_directory"; -- Enables Service Directories extension support - --"public_service"; -- Enables Server vCard support for public services in directories and advertises in features - --"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification - "websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" -}; - --- Server PID -pidfile = "/var/run/metronome/metronome.pid" - --- HTTP server -http_ports = { 5290 } -http_interfaces = { "127.0.0.1", "::1" } - ---https_ports = { 5291 } ---https_interfaces = { "127.0.0.1", "::1" } - --- Enable IPv6 -use_ipv6 = true - --- BOSH configuration (mod_bosh) -consider_bosh_secure = true -cross_domain_bosh = true - --- WebSocket configuration (mod_websocket) -consider_websocket_secure = true -cross_domain_websocket = true - --- Disable account creation by default, for security -allow_registration = false - --- Use LDAP storage backend for all stores -storage = "ldap" - --- stanza optimization -csi_config_queue_all_muc_messages_but_mentions = false; - - --- Logging configuration -log = { - info = "/var/log/metronome/metronome.log"; -- Change 'info' to 'debug' for verbose logging - error = "/var/log/metronome/metronome.err"; - -- "*syslog"; -- Uncomment this for logging to syslog - -- "*console"; -- Log to the console, useful for debugging with daemonize=false -} - ------- Components ------ --- You can specify components to add hosts that provide special services, --- like multi-user conferences, and transports. - ----Set up a local BOSH service -Component "localhost" "http" - modules_enabled = { "bosh" } - ------------ Virtual hosts ----------- --- You need to add a VirtualHost entry for each domain you wish Metronome to serve. --- Settings under each VirtualHost entry apply *only* to that host. - -Include "conf.d/*.cfg.lua" diff --git a/conf/metronome/modules/ldap.lib.lua b/conf/metronome/modules/ldap.lib.lua deleted file mode 100644 index 6774e735f..000000000 --- a/conf/metronome/modules/ldap.lib.lua +++ /dev/null @@ -1,270 +0,0 @@ --- vim:sts=4 sw=4 - --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- Copyright (C) 2012 Rob Hoelz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local ldap; -local connection; -local params = module:get_option("ldap"); -local format = string.format; -local tconcat = table.concat; - -local _M = {}; - -local config_params = { - hostname = 'string', - user = { - basedn = 'string', - namefield = 'string', - filter = 'string', - usernamefield = 'string', - }, - groups = { - basedn = 'string', - namefield = 'string', - memberfield = 'string', - - _member = { - name = 'string', - admin = 'boolean?', - }, - }, - admin = { - _optional = true, - basedn = 'string', - namefield = 'string', - filter = 'string', - } -} - -local function run_validation(params, config, prefix) - prefix = prefix or ''; - - -- verify that every required member of config is present in params - for k, v in pairs(config) do - if type(k) == 'string' and k:sub(1, 1) ~= '_' then - local is_optional; - if type(v) == 'table' then - is_optional = v._optional; - else - is_optional = v:sub(-1) == '?'; - end - - if not is_optional and params[k] == nil then - return nil, prefix .. k .. ' is required'; - end - end - end - - for k, v in pairs(params) do - local expected_type = config[k]; - - local ok, err = true; - - if type(k) == 'string' then - -- verify that this key is present in config - if k:sub(1, 1) == '_' or expected_type == nil then - return nil, 'invalid parameter ' .. prefix .. k; - end - - -- type validation - if type(expected_type) == 'string' then - if expected_type:sub(-1) == '?' then - expected_type = expected_type:sub(1, -2); - end - - if type(v) ~= expected_type then - return nil, 'invalid type for parameter ' .. prefix .. k; - end - else -- it's a table (or had better be) - if type(v) ~= 'table' then - return nil, 'invalid type for parameter ' .. prefix .. k; - end - - -- recurse into child - ok, err = run_validation(v, expected_type, prefix .. k .. '.'); - end - else -- it's an integer (or had better be) - if not config._member then - return nil, 'invalid parameter ' .. prefix .. tostring(k); - end - ok, err = run_validation(v, config._member, prefix .. tostring(k) .. '.'); - end - - if not ok then - return ok, err; - end - end - - return true; -end - -local function validate_config() - if true then - return true; -- XXX for now - end - - -- this is almost too clever (I mean that in a bad - -- maintainability sort of way) - -- - -- basically this allows a free pass for a key in group members - -- equal to params.groups.namefield - setmetatable(config_params.groups._member, { - __index = function(_, k) - if k == params.groups.namefield then - return 'string'; - end - end - }); - - local ok, err = run_validation(params, config_params); - - setmetatable(config_params.groups._member, nil); - - if ok then - -- a little extra validation that doesn't fit into - -- my recursive checker - local group_namefield = params.groups.namefield; - for i, group in ipairs(params.groups) do - if not group[group_namefield] then - return nil, format('groups.%d.%s is required', i, group_namefield); - end - end - - -- fill in params.admin if you can - if not params.admin and params.groups then - local admingroup; - - for _, groupconfig in ipairs(params.groups) do - if groupconfig.admin then - admingroup = groupconfig; - break; - end - end - - if admingroup then - params.admin = { - basedn = params.groups.basedn, - namefield = params.groups.memberfield, - filter = group_namefield .. '=' .. admingroup[group_namefield], - }; - end - end - end - - return ok, err; -end - --- what to do if connection isn't available? -local function connect() - return ldap.open_simple(params.hostname, params.bind_dn, params.bind_password, params.use_tls); -end - --- this is abstracted so we can maintain persistent connections at a later time -function _M.getconnection() - return connect(); -end - -function _M.getparams() - return params; -end - --- XXX consider renaming this...it doesn't bind the current connection -function _M.bind(username, password) - local conn = _M.getconnection(); - local filter = format('%s=%s', params.user.usernamefield, username); - if params.user.usernamefield == 'mail' then - filter = format('mail=%s@*', username); - end - - if filter then - filter = _M.filter.combine_and(filter, params.user.filter); - end - - local who = _M.singlematch { - attrs = params.user.usernamefield, - base = params.user.basedn, - filter = filter, - }; - - if who then - who = who.dn; - module:log('debug', '_M.bind - who: %s', who); - else - module:log('debug', '_M.bind - no DN found for username = %s', username); - return nil, format('no DN found for username = %s', username); - end - - local conn, err = ldap.open_simple(params.hostname, who, password, params.use_tls); - - if conn then - conn:close(); - return true; - end - - return conn, err; -end - -function _M.singlematch(query) - local ld = _M.getconnection(); - - query.sizelimit = 1; - query.scope = 'subtree'; - - for dn, attribs in ld:search(query) do - attribs.dn = dn; - return attribs; - end -end - -_M.filter = {}; - -function _M.filter.combine_and(...) - local parts = { '(&' }; - - local arg = { ... }; - - for _, filter in ipairs(arg) do - if filter:sub(1, 1) ~= '(' and filter:sub(-1) ~= ')' then - filter = '(' .. filter .. ')' - end - parts[#parts + 1] = filter; - end - - parts[#parts + 1] = ')'; - - return tconcat(parts, ''); -end - -do - local ok, err; - - metronome.unlock_globals(); - ok, ldap = pcall(require, 'lualdap'); - metronome.lock_globals(); - if not ok then - module:log("error", "Failed to load the LuaLDAP library for accessing LDAP: %s", ldap); - module:log("error", "More information on install LuaLDAP can be found at http://www.keplerproject.org/lualdap"); - return; - end - - if not params then - module:log("error", "LDAP configuration required to use the LDAP storage module"); - return; - end - - ok, err = validate_config(); - - if not ok then - module:log("error", "LDAP configuration is invalid: %s", tostring(err)); - return; - end -end - -return _M; diff --git a/conf/metronome/modules/mod_auth_ldap2.lua b/conf/metronome/modules/mod_auth_ldap2.lua deleted file mode 100644 index f961885da..000000000 --- a/conf/metronome/modules/mod_auth_ldap2.lua +++ /dev/null @@ -1,90 +0,0 @@ --- vim:sts=4 sw=4 - --- Metronome IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- Copyright (C) 2012 Rob Hoelz --- Copyright (C) 2015 YUNOHOST.ORG --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- --- https://github.com/YunoHost/yunohost-config-metronome/blob/unstable/lib/modules/mod_auth_ldap2.lua --- adapted to use common LDAP store on Metronome - -local ldap = module:require 'ldap'; -local new_sasl = require 'util.sasl'.new; -local jsplit = require 'util.jid'.split; - -local log = module._log - -if not ldap then - return; -end - -function new_default_provider(host) - local provider = { name = "ldap2" }; - log("debug", "initializing ldap2 authentication provider for host '%s'", host); - - function provider.test_password(username, password) - return ldap.bind(username, password); - end - - function provider.user_exists(username) - local params = ldap.getparams() - - local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username); - if params.user.usernamefield == 'mail' then - filter = ldap.filter.combine_and(params.user.filter, 'mail=' .. username .. '@*'); - end - - return ldap.singlematch { - base = params.user.basedn, - filter = filter, - }; - end - - function provider.get_password(username) - return nil, "Passwords unavailable for LDAP."; - end - - function provider.set_password(username, password) - return nil, "Passwords unavailable for LDAP."; - end - - function provider.create_user(username, password) - return nil, "Account creation/modification not available with LDAP."; - end - - function provider.get_sasl_handler(session) - local testpass_authentication_profile = { - session = session, - plain_test = function(sasl, username, password, realm) - return provider.test_password(username, password), true; - end, - order = { "plain_test" }, - }; - return new_sasl(module.host, testpass_authentication_profile); - end - - function provider.is_admin(jid) - local admin_config = ldap.getparams().admin; - - if not admin_config then - return; - end - - local ld = ldap:getconnection(); - local username = jsplit(jid); - local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username); - - return ldap.singlematch { - base = admin_config.basedn, - filter = filter, - }; - end - - return provider; -end - -module:add_item("auth-provider", new_default_provider(module.host)); diff --git a/conf/metronome/modules/mod_legacyauth.lua b/conf/metronome/modules/mod_legacyauth.lua deleted file mode 100644 index 3ee8b978b..000000000 --- a/conf/metronome/modules/mod_legacyauth.lua +++ /dev/null @@ -1,87 +0,0 @@ --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - - - -local st = require "util.stanza"; -local t_concat = table.concat; - -local secure_auth_only = module:get_option("c2s_require_encryption") - or module:get_option("require_encryption") - or not(module:get_option("allow_unencrypted_plain_auth")); - -local sessionmanager = require "core.sessionmanager"; -local usermanager = require "core.usermanager"; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local resourceprep = require "util.encodings".stringprep.resourceprep; - -module:add_feature("jabber:iq:auth"); -module:hook("stream-features", function(event) - local origin, features = event.origin, event.features; - if secure_auth_only and not origin.secure then - -- Sorry, not offering to insecure streams! - return; - elseif not origin.username then - features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up(); - end -end); - -module:hook("stanza/iq/jabber:iq:auth:query", function(event) - local session, stanza = event.origin, event.stanza; - - if session.type ~= "c2s_unauthed" then - (session.sends2s or session.send)(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections.")); - return true; - end - - if secure_auth_only and not session.secure then - session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server")); - return true; - end - - local username = stanza.tags[1]:child_with_name("username"); - local password = stanza.tags[1]:child_with_name("password"); - local resource = stanza.tags[1]:child_with_name("resource"); - if not (username and password and resource) then - local reply = st.reply(stanza); - session.send(reply:query("jabber:iq:auth") - :tag("username"):up() - :tag("password"):up() - :tag("resource"):up()); - else - username, password, resource = t_concat(username), t_concat(password), t_concat(resource); - username = nodeprep(username); - resource = resourceprep(resource) - if not (username and resource) then - session.send(st.error_reply(stanza, "modify", "bad-request")); - return true; - end - if usermanager.test_password(username, session.host, password) then - -- Authentication successful! - local success, err = sessionmanager.make_authenticated(session, username); - if success then - local err_type, err_msg; - success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource); - if not success then - session.send(st.error_reply(stanza, err_type, err, err_msg)); - session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager? - return true; - elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth - session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session.")); - session:close(); -- FIXME undo resource bind and auth instead of closing the session? - return true; - end - end - session.send(st.reply(stanza)); - else - session.send(st.error_reply(stanza, "auth", "not-authorized")); - end - end - return true; -end); - diff --git a/conf/metronome/modules/mod_storage_ldap.lua b/conf/metronome/modules/mod_storage_ldap.lua deleted file mode 100644 index 87092382c..000000000 --- a/conf/metronome/modules/mod_storage_ldap.lua +++ /dev/null @@ -1,243 +0,0 @@ --- vim:sts=4 sw=4 - --- Metronome IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- Copyright (C) 2012 Rob Hoelz --- Copyright (C) 2015 YUNOHOST.ORG --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. - ----------------------------------------- --- Constants and such -- ----------------------------------------- - -local setmetatable = setmetatable; - -local get_config = require "core.configmanager".get; -local ldap = module:require 'ldap'; -local vcardlib = module:require 'vcard'; -local st = require 'util.stanza'; -local gettime = require 'socket'.gettime; - -local log = module._log - -if not ldap then - return; -end - -local CACHE_EXPIRY = 300; - ----------------------------------------- --- Utility Functions -- ----------------------------------------- - -local function ldap_record_to_vcard(record, format) - return vcardlib.create { - record = record, - format = format, - } -end - -local get_alias_for_user; - -do - local user_cache; - local last_fetch_time; - - local function populate_user_cache() - local user_c = get_config(module.host, 'ldap').user; - if not user_c then return; end - - local ld = ldap.getconnection(); - - local usernamefield = user_c.usernamefield; - local namefield = user_c.namefield; - - user_cache = {}; - - for _, attrs in ld:search { base = user_c.basedn, scope = 'onelevel', filter = user_c.filter } do - user_cache[attrs[usernamefield]] = attrs[namefield]; - end - last_fetch_time = gettime(); - end - - function get_alias_for_user(user) - if last_fetch_time and last_fetch_time + CACHE_EXPIRY < gettime() then - user_cache = nil; - end - if not user_cache then - populate_user_cache(); - end - return user_cache[user]; - end -end - ----------------------------------------- --- Base LDAP store class -- ----------------------------------------- - -local function ldap_store(config) - local self = {}; - local config = config; - - function self:get(username) - return nil, "Data getting is not available for this storage backend"; - end - - function self:set(username, data) - return nil, "Data setting is not available for this storage backend"; - end - - return self; -end - -local adapters = {}; - ----------------------------------------- --- Roster Storage Implementation -- ----------------------------------------- - -adapters.roster = function (config) - -- Validate configuration requirements - if not config.groups then return nil; end - - local self = ldap_store(config) - - function self:get(username) - local ld = ldap.getconnection(); - local contacts = {}; - - local memberfield = config.groups.memberfield; - local namefield = config.groups.namefield; - local filter = memberfield .. '=' .. tostring(username); - - local groups = {}; - for _, config in ipairs(config.groups) do - groups[ config[namefield] ] = config.name; - end - - log("debug", "Found %d group(s) for user %s", select('#', groups), username) - - -- XXX this kind of relies on the way we do groups at INOC - for _, attrs in ld:search { base = config.groups.basedn, scope = 'onelevel', filter = filter } do - if groups[ attrs[namefield] ] then - local members = attrs[memberfield]; - - for _, user in ipairs(members) do - if user ~= username then - local jid = user .. '@' .. module.host; - local record = contacts[jid]; - - if not record then - record = { - subscription = 'both', - groups = {}, - name = get_alias_for_user(user), - }; - contacts[jid] = record; - end - - record.groups[ groups[ attrs[namefield] ] ] = true; - end - end - end - end - - return contacts; - end - - function self:set(username, data) - log("warn", "Setting data in Roster LDAP storage is not supported yet") - return nil, "not supported"; - end - - return self; -end - ----------------------------------------- --- vCard Storage Implementation -- ----------------------------------------- - -adapters.vcard = function (config) - -- Validate configuration requirements - if not config.vcard_format or not config.user then return nil; end - - local self = ldap_store(config) - - function self:get(username) - local ld = ldap.getconnection(); - local filter = config.user.usernamefield .. '=' .. tostring(username); - - log("debug", "Retrieving vCard for user '%s'", username); - - local match = ldap.singlematch { - base = config.user.basedn, - filter = filter, - }; - if match then - match.jid = username .. '@' .. module.host - return st.preserialize(ldap_record_to_vcard(match, config.vcard_format)); - else - return nil, "username not found"; - end - end - - function self:set(username, data) - log("warn", "Setting data in vCard LDAP storage is not supported yet") - return nil, "not supported"; - end - - return self; -end - ----------------------------------------- --- Driver Definition -- ----------------------------------------- - -cache = {}; - -local driver = { name = "ldap" }; - -function driver:open(store) - log("debug", "Opening ldap storage backend for host '%s' and store '%s'", module.host, store); - - if not cache[module.host] then - log("debug", "Caching adapters for the host '%s'", module.host); - - local ad_config = get_config(module.host, "ldap"); - local ad_cache = {}; - for k, v in pairs(adapters) do - ad_cache[k] = v(ad_config); - end - - cache[module.host] = ad_cache; - end - - local adapter = cache[module.host][store]; - - if not adapter then - log("info", "Unavailable adapter for store '%s'", store); - return nil, "unsupported-store"; - end - return adapter; -end - -function driver:stores(username, type, pattern) - return nil, "not implemented"; -end - -function driver:store_exists(username, type) - return nil, "not implemented"; -end - -function driver:purge(username) - return nil, "not implemented"; -end - -function driver:nodes(type) - return nil, "not implemented"; -end - -module:add_item("data-driver", driver); diff --git a/conf/metronome/modules/vcard.lib.lua b/conf/metronome/modules/vcard.lib.lua deleted file mode 100644 index dcbd0106a..000000000 --- a/conf/metronome/modules/vcard.lib.lua +++ /dev/null @@ -1,162 +0,0 @@ --- vim:sts=4 sw=4 - --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- Copyright (C) 2012 Rob Hoelz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local st = require 'util.stanza'; - -local VCARD_NS = 'vcard-temp'; - -local builder_methods = {}; - -local base64_encode = require('util.encodings').base64.encode; - -function builder_methods:addvalue(key, value) - self.vcard:tag(key):text(value):up(); -end - -function builder_methods:addphotofield(tagname, format_section) - local record = self.record; - local format = self.format; - local vcard = self.vcard; - local config = format[format_section]; - - if not config then - return; - end - - if config.extval then - if record[config.extval] then - local tag = vcard:tag(tagname); - tag:tag('EXTVAL'):text(record[config.extval]):up(); - end - elseif config.type and config.binval then - if record[config.binval] then - local tag = vcard:tag(tagname); - tag:tag('TYPE'):text(config.type):up(); - tag:tag('BINVAL'):text(base64_encode(record[config.binval])):up(); - end - else - module:log('error', 'You have an invalid %s config section', tagname); - return; - end - - vcard:up(); -end - -function builder_methods:addregularfield(tagname, format_section) - local record = self.record; - local format = self.format; - local vcard = self.vcard; - - if not format[format_section] then - return; - end - - local tag = vcard:tag(tagname); - - for k, v in pairs(format[format_section]) do - tag:tag(string.upper(k)):text(record[v]):up(); - end - - vcard:up(); -end - -function builder_methods:addmultisectionedfield(tagname, format_section) - local record = self.record; - local format = self.format; - local vcard = self.vcard; - - if not format[format_section] then - return; - end - - for k, v in pairs(format[format_section]) do - local tag = vcard:tag(tagname); - - if type(k) == 'string' then - tag:tag(string.upper(k)):up(); - end - - for k2, v2 in pairs(v) do - if type(v2) == 'boolean' then - tag:tag(string.upper(k2)):up(); - else - tag:tag(string.upper(k2)):text(record[v2]):up(); - end - end - - vcard:up(); - end -end - -function builder_methods:build() - local record = self.record; - local format = self.format; - - self:addvalue( 'VERSION', '2.0'); - self:addvalue( 'FN', record[format.displayname]); - self:addregularfield( 'N', 'name'); - self:addvalue( 'NICKNAME', record[format.nickname]); - self:addphotofield( 'PHOTO', 'photo'); - self:addvalue( 'BDAY', record[format.birthday]); - self:addmultisectionedfield('ADR', 'address'); - self:addvalue( 'LABEL', nil); -- we don't support LABEL...yet. - self:addmultisectionedfield('TEL', 'telephone'); - self:addmultisectionedfield('EMAIL', 'email'); - self:addvalue( 'JABBERID', record.jid); - self:addvalue( 'MAILER', record[format.mailer]); - self:addvalue( 'TZ', record[format.timezone]); - self:addregularfield( 'GEO', 'geo'); - self:addvalue( 'TITLE', record[format.title]); - self:addvalue( 'ROLE', record[format.role]); - self:addphotofield( 'LOGO', 'logo'); - self:addvalue( 'AGENT', nil); -- we don't support AGENT...yet. - self:addregularfield( 'ORG', 'org'); - self:addvalue( 'CATEGORIES', nil); -- we don't support CATEGORIES...yet. - self:addvalue( 'NOTE', record[format.note]); - self:addvalue( 'PRODID', nil); -- we don't support PRODID...yet. - self:addvalue( 'REV', record[format.rev]); - self:addvalue( 'SORT-STRING', record[format.sortstring]); - self:addregularfield( 'SOUND', 'sound'); - self:addvalue( 'UID', record[format.uid]); - self:addvalue( 'URL', record[format.url]); - self:addvalue( 'CLASS', nil); -- we don't support CLASS...yet. - self:addregularfield( 'KEY', 'key'); - self:addvalue( 'DESC', record[format.description]); - - return self.vcard; -end - -local function new_builder(params) - local vcard_tag = st.stanza('vCard', { xmlns = VCARD_NS }); - - local object = { - vcard = vcard_tag, - __index = builder_methods, - }; - - for k, v in pairs(params) do - object[k] = v; - end - - setmetatable(object, object); - - return object; -end - -local _M = {}; - -function _M.create(params) - local builder = new_builder(params); - - return builder:build(); -end - -return _M; diff --git a/conf/nginx/plain/acme-challenge.conf.inc b/conf/nginx/acme-challenge.conf.inc similarity index 62% rename from conf/nginx/plain/acme-challenge.conf.inc rename to conf/nginx/acme-challenge.conf.inc index 35c4b80c2..859aa6817 100644 --- a/conf/nginx/plain/acme-challenge.conf.inc +++ b/conf/nginx/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/global.conf b/conf/nginx/global.conf similarity index 100% rename from conf/nginx/plain/global.conf rename to conf/nginx/global.conf diff --git a/conf/nginx/plain/yunohost_panel.conf.inc b/conf/nginx/plain/yunohost_panel.conf.inc deleted file mode 100644 index 16a6e6b29..000000000 --- a/conf/nginx/plain/yunohost_panel.conf.inc +++ /dev/null @@ -1,8 +0,0 @@ -# Insert YunoHost button + portal overlay -sub_filter ''; -sub_filter_once on; -# Apply to other mime types than text/html -sub_filter_types application/xhtml+xml; -# Prevent YunoHost panel files from being blocked by specific app rules -location ~ (ynh_portal.js|ynh_overlay.css|ynh_userinfo.json|ynhtheme/custom_portal.js|ynhtheme/custom_overlay.css) { -} diff --git a/conf/nginx/plain/yunohost_sso.conf.inc b/conf/nginx/plain/yunohost_sso.conf.inc deleted file mode 100644 index 308e5a9a4..000000000 --- a/conf/nginx/plain/yunohost_sso.conf.inc +++ /dev/null @@ -1,7 +0,0 @@ -# Avoid the nginx path/alias traversal weakness ( #1037 ) -rewrite ^/yunohost/sso$ /yunohost/sso/ permanent; - -location /yunohost/sso/ { - # This is an empty location, only meant to avoid other locations - # from matching /yunohost/sso, such that it's correctly handled by ssowat -} 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 fe853155b..c4b4d723f 100644 --- a/conf/nginx/security.conf.inc +++ b/conf/nginx/security.conf.inc @@ -3,16 +3,16 @@ ssl_session_cache shared:SSL:50m; # about 200000 sessions ssl_session_tickets off; {% if compatibility == "modern" %} -# generated 2020-08-14, Mozilla Guideline v5.6, nginx 1.14.2, OpenSSL 1.1.1d, modern configuration -# https://ssl-config.mozilla.org/#server=nginx&version=1.14.2&config=modern&openssl=1.1.1d&guideline=5.6 +# generated 2023-06-13, Mozilla Guideline v5.7, nginx 1.22.1, OpenSSL 3.0.9, modern configuration +# https://ssl-config.mozilla.org/#server=nginx&version=1.22.1&config=modern&openssl=3.0.9&guideline=5.7 ssl_protocols TLSv1.3; ssl_prefer_server_ciphers off; {% else %} # Ciphers with intermediate compatibility -# generated 2020-08-14, Mozilla Guideline v5.6, nginx 1.14.2, OpenSSL 1.1.1d, intermediate configuration -# https://ssl-config.mozilla.org/#server=nginx&version=1.14.2&config=intermediate&openssl=1.1.1d&guideline=5.6 +# generated 2023-06-13, Mozilla Guideline v5.7, nginx 1.22.1, OpenSSL 3.0.9, intermediate configuration +# https://ssl-config.mozilla.org/#server=nginx&version=1.22.1&config=intermediate&openssl=3.0.9&guideline=5.7 ssl_protocols TLSv1.2 TLSv1.3; -ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; # Pre-defined FFDHE group (RFC 7919) @@ -26,7 +26,7 @@ ssl_dhparam /usr/share/yunohost/ffdhe2048.pem; # https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://observatory.mozilla.org/ {% if experimental == "True" %} -more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'"; +more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob:;"; {% else %} more_set_headers "Content-Security-Policy : upgrade-insecure-requests"; {% endif %} diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index d3ff77714..1cfb4e95b 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -6,14 +6,14 @@ map $http_upgrade $connection_upgrade { server { listen 80; listen [::]:80; - server_name {{ domain }}{% if xmpp_enabled == "True" %} xmpp-upload.{{ domain }} muc.{{ domain }}{% endif %}; + server_name {{ domain }}; 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" %} @@ -25,7 +25,7 @@ server { {# Note that this != "False" is meant to be failure-safe, in the case the redrect_to_https would happen to contain empty string or whatever value. We absolutely don't want to disable the HTTPS redirect *except* when it's explicitly being asked to be disabled. #} {% if redirect_to_https != "False" %} location / { - return 301 https://$http_host$request_uri; + return 301 https://$host$request_uri; } {# The app config snippets are not included in the HTTP conf unless HTTPS redirect is disabled, because app's location may blocks will conflict or bypass/ignore the HTTPS redirection. #} {% else %} @@ -56,7 +56,7 @@ server { ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 127.0.0.1 127.0.1.1 valid=300s; + resolver 1.1.1.1 9.9.9.9 valid=300s; resolver_timeout 5s; {% endif %} @@ -78,48 +78,3 @@ server { 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; - listen [::]:443 ssl http2; - server_name xmpp-upload.{{ domain }}; - root /dev/null; - - location /upload/ { - alias /var/xmpp-upload/{{ domain }}/upload/; - # Pass all requests to metronome, except for GET and HEAD requests. - limit_except GET HEAD { - proxy_pass http://localhost:5290; - } - - include proxy_params; - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Methods' 'HEAD, GET, PUT, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'Authorization'; - add_header 'Access-Control-Allow-Credentials' 'true'; - client_max_body_size 105M; # Choose a value a bit higher than the max upload configured in XMPP server - } - - include /etc/nginx/conf.d/security.conf.inc; - - ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; - - {% if domain_cert_ca != "selfsigned" %} - more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; - {% endif %} - {% if domain_cert_ca == "letsencrypt" %} - # OCSP settings - ssl_stapling on; - ssl_stapling_verify on; - ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; - resolver 127.0.0.1 127.0.1.1 valid=300s; - resolver_timeout 5s; - {% endif %} - - 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/plain/ssowat.conf b/conf/nginx/ssowat.conf similarity index 100% rename from conf/nginx/plain/ssowat.conf rename to conf/nginx/ssowat.conf diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc index c9ae34f82..9cb4ff00d 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(',') %} @@ -23,3 +23,24 @@ location = /yunohost/api/error/502 { add_header Content-Type text/plain; internal; } + +location /yunohost/portalapi/ { + + proxy_read_timeout 5s; + proxy_pass http://127.0.0.1:6788/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + + # Custom 502 error page + error_page 502 /yunohost/portalapi/error/502; +} + + +# Yunohost admin output complete 502 error page, so use only plain text. +location = /yunohost/portalapi/error/502 { + return 502 '502 - Bad Gateway'; + add_header Content-Type text/plain; + internal; +} diff --git a/conf/nginx/plain/yunohost_http_errors.conf.inc b/conf/nginx/yunohost_http_errors.conf.inc similarity index 100% rename from conf/nginx/plain/yunohost_http_errors.conf.inc rename to conf/nginx/yunohost_http_errors.conf.inc diff --git a/conf/nginx/yunohost_sso.conf.inc b/conf/nginx/yunohost_sso.conf.inc new file mode 100644 index 000000000..7e9207305 --- /dev/null +++ b/conf/nginx/yunohost_sso.conf.inc @@ -0,0 +1,21 @@ +# Avoid the nginx path/alias traversal weakness ( #1037 ) +rewrite ^/yunohost/sso$ /yunohost/sso/ permanent; + +location /yunohost/sso/ { + alias /usr/share/yunohost/portal/; + default_type text/html; + index index.html; + try_files $uri $uri/ /index.html; + + location = /yunohost/sso/index.html { + etag off; + expires off; + more_set_headers "Cache-Control: no-store, no-cache, must-revalidate"; + } + + location /yunohost/sso/applogos/ { + alias /usr/share/yunohost/applogos/; + } + + more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none'; img-src 'self' data:;"; +} diff --git a/conf/opendkim/opendkim.conf b/conf/opendkim/opendkim.conf new file mode 100644 index 000000000..303e504b7 --- /dev/null +++ b/conf/opendkim/opendkim.conf @@ -0,0 +1,31 @@ +# General daemon config +Socket inet:8891@localhost +PidFile /run/opendkim/opendkim.pid +UserID opendkim +UMask 007 + +AutoRestart yes +AutoRestartCount 10 +AutoRestartRate 10/1h + +# Logging +Syslog yes +SyslogSuccess yes +LogWhy yes + +# Common signing and verification parameters. In Debian, the "From" header is +# oversigned, because it is often the identity key used by reputation systems +# and thus somewhat security sensitive. +Canonicalization relaxed/simple +Mode sv +OversignHeaders From +#On-BadSignature reject + +# Key / signing table +KeyTable file:/etc/dkim/keytable +SigningTable refile:/etc/dkim/signingtable + +# The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided +# by the package dns-root-data. +TrustAnchorFile /usr/share/dns/root.key +#Nameservers 127.0.0.1 diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 19b40aefb..193875619 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -30,8 +30,8 @@ smtpd_tls_chain_files = tls_server_sni_maps = hash:/etc/postfix/sni {% if compatibility == "intermediate" %} -# generated 2020-08-18, Mozilla Guideline v5.6, Postfix 3.4.14, OpenSSL 1.1.1d, intermediate configuration -# https://ssl-config.mozilla.org/#server=postfix&version=3.4.14&config=intermediate&openssl=1.1.1d&guideline=5.6 +# generated 2023-06-13, Mozilla Guideline v5.7, Postfix 3.7.5, OpenSSL 3.0.9, intermediate configuration +# https://ssl-config.mozilla.org/#server=postfix&version=3.7.5&config=intermediate&openssl=3.0.9&guideline=5.7 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 @@ -41,10 +41,10 @@ smtpd_tls_mandatory_ciphers = medium # not actually 1024 bits, this applies to all DHE >= 1024 bits smtpd_tls_dh1024_param_file = /usr/share/yunohost/ffdhe2048.pem -tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 +tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 {% else %} -# generated 2020-08-18, Mozilla Guideline v5.6, Postfix 3.4.14, OpenSSL 1.1.1d, modern configuration -# https://ssl-config.mozilla.org/#server=postfix&version=3.4.14&config=modern&openssl=1.1.1d&guideline=5.6 +# generated 2023-06-13, Mozilla Guideline v5.7, Postfix 3.7.5, OpenSSL 3.0.9, modern configuration +# https://ssl-config.mozilla.org/#server=postfix&version=3.7.5&config=modern&openssl=3.0.9&guideline=5.7 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2 smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2 @@ -93,21 +93,25 @@ recipient_delimiter = + inet_interfaces = all #### Fit to the maximum message size to 25mb, more than allowed by GMail or Yahoo #### -# /!\ This size is the size of the attachment in base64. +# /!\ This size is the size of the attachment in base64. # BASE64_SIZE_IN_BYTE = ORIGINAL_SIZE_IN_MEGABYTE * 1,37 *1024*1024 + 980 # See https://serverfault.com/questions/346895/postfix-mail-size-counting message_size_limit = 35914708 # Virtual Domains Control virtual_mailbox_domains = ldap:/etc/postfix/ldap-domains.cf -virtual_mailbox_maps = ldap:/etc/postfix/ldap-accounts.cf +virtual_mailbox_maps = ldap:/etc/postfix/ldap-accounts.cf,hash:/etc/postfix/app_senders_login_maps virtual_mailbox_base = 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 virtual_gid_maps = static:mail -smtpd_sender_login_maps= ldap:/etc/postfix/ldap-accounts.cf +smtpd_sender_login_maps = unionmap:{ + # Regular Yunohost accounts + ldap:/etc/postfix/ldap-accounts.cf, + # Extra maps for app system users who need to send emails + hash:/etc/postfix/app_senders_login_maps } # Dovecot LDA virtual_transport = dovecot @@ -178,9 +182,10 @@ smtp_header_checks = regexp:/etc/postfix/header_checks smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter # Rmilter -milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} +milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} {auth_type} milter_protocol = 6 -smtpd_milters = inet:localhost:11332 +smtpd_milters = inet:localhost:8891 +non_smtpd_milters = inet:localhost:8891 # Skip email without checking if milter has died milter_default_action = accept diff --git a/conf/postfix/sni b/conf/postfix/sni index 29ed2e043..b57b7e05f 100644 --- a/conf/postfix/sni +++ b/conf/postfix/sni @@ -1,2 +1,4 @@ +# This maps domain to certificates to properly handle multi-domain context +# (also we need a comment in this file such that it's never empty to prevent regenconf issues) {% for domain in domain_list.split() %}{{ domain }} /etc/yunohost/certs/{{ domain }}/key.pem /etc/yunohost/certs/{{ domain }}/crt.pem -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/conf/rspamd/dkim_signing.conf b/conf/rspamd/dkim_signing.conf deleted file mode 100644 index 26718e021..000000000 --- a/conf/rspamd/dkim_signing.conf +++ /dev/null @@ -1,16 +0,0 @@ -allow_envfrom_empty = true; -allow_hdrfrom_mismatch = false; -allow_hdrfrom_multiple = false; -allow_username_mismatch = true; - -auth_only = true; -path = "/etc/dkim/$domain.$selector.key"; -selector = "mail"; -sign_local = true; -symbol = "DKIM_SIGNED"; -try_fallback = true; -use_domain = "header"; -use_esld = false; -use_redis = false; -key_prefix = "DKIM_KEYS"; - diff --git a/conf/rspamd/metrics.local.conf b/conf/rspamd/metrics.local.conf deleted file mode 100644 index 583280e70..000000000 --- a/conf/rspamd/metrics.local.conf +++ /dev/null @@ -1,8 +0,0 @@ -# Metrics settings -# This define overridden options. - -actions { - reject = 21; - add_header = 8; - greylist = 4; -} diff --git a/conf/rspamd/milter_headers.conf b/conf/rspamd/milter_headers.conf deleted file mode 100644 index d57aa6958..000000000 --- a/conf/rspamd/milter_headers.conf +++ /dev/null @@ -1,9 +0,0 @@ -use = ["spam-header"]; - -routines { - spam-header { - header = "X-Spam"; - value = "Yes"; - remove = 1; - } -} diff --git a/conf/rspamd/rspamd.sieve b/conf/rspamd/rspamd.sieve deleted file mode 100644 index 56a30c3c1..000000000 --- a/conf/rspamd/rspamd.sieve +++ /dev/null @@ -1,4 +0,0 @@ -require ["fileinto"]; -if header :is "X-Spam" "Yes" { - fileinto "Junk"; -} diff --git a/conf/slapd/config.ldif b/conf/slapd/config.ldif index 1037e8bed..7daab7829 100644 --- a/conf/slapd/config.ldif +++ b/conf/slapd/config.ldif @@ -159,7 +159,7 @@ olcAccess: {2}to dn.base="" # can read everything. olcAccess: {3}to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write - by group/groupOfNames/member.exact="cn=admins,ou=groups,dc=yunohost,dc=org" write + by group/groupOfNamesYnh/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 8703afb85..55fad59c6 100644 --- a/conf/slapd/db_init.ldif +++ b/conf/slapd/db_init.ldif @@ -56,7 +56,6 @@ objectClass: groupOfNamesYnh gidNumber: 4002 cn: all_users permission: cn=mail.main,ou=permission,dc=yunohost,dc=org -permission: cn=xmpp.main,ou=permission,dc=yunohost,dc=org dn: cn=visitors,ou=groups,dc=yunohost,dc=org objectClass: posixGroup @@ -75,17 +74,6 @@ gidNumber: 5001 showTile: FALSE authHeader: FALSE -dn: cn=xmpp.main,ou=permission,dc=yunohost,dc=org -groupPermission: cn=all_users,ou=groups,dc=yunohost,dc=org -cn: xmpp.main -objectClass: posixGroup -objectClass: permissionYnh -isProtected: TRUE -label: XMPP -gidNumber: 5002 -showTile: FALSE -authHeader: FALSE - dn: cn=ssh.main,ou=permission,dc=yunohost,dc=org cn: ssh.main objectClass: posixGroup diff --git a/conf/ssh/sshd_config b/conf/ssh/sshd_config index eaa0c7380..4a239d2ad 100644 --- a/conf/ssh/sshd_config +++ b/conf/ssh/sshd_config @@ -64,7 +64,7 @@ PasswordAuthentication no {% endif %} # Post-login stuff -Banner /etc/issue.net +# Banner none PrintMotd no PrintLastLog yes ClientAliveInterval 60 diff --git a/conf/ssl/openssl.cnf b/conf/ssl/openssl.cnf index a19a9c3df..84da1b9a0 100644 --- a/conf/ssl/openssl.cnf +++ b/conf/ssl/openssl.cnf @@ -192,7 +192,7 @@ authorityKeyIdentifier=keyid,issuer basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment -subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org,DNS:xmpp-upload.yunohost.org +subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org [ v3_ca ] diff --git a/conf/yunohost/services.yml b/conf/yunohost/services.yml index 45621876e..f18111a96 100644 --- a/conf/yunohost/services.yml +++ b/conf/yunohost/services.yml @@ -8,11 +8,6 @@ fail2ban: log: /var/log/fail2ban.log category: security test_conf: fail2ban-server --test -metronome: - log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err] - needs_exposed_ports: [5222, 5269] - category: xmpp - ignore_if_package_is_not_installed: metronome mysql: log: [/var/log/mysql.log,/var/log/mysql.err,/var/log/mysql/error.log] actual_systemd_service: mariadb @@ -28,21 +23,21 @@ nginx: # log: /var/log/php7.4-fpm.log # test_conf: php-fpm7.4 --test # category: web +opendkim: + category: email + test_conf: opendkim -n postfix: log: [/var/log/mail.log,/var/log/mail.err] actual_systemd_service: postfix@- needs_exposed_ports: [25, 587] category: email postgresql: - actual_systemd_service: 'postgresql@13-main' + actual_systemd_service: 'postgresql@15-main' category: database - ignore_if_package_is_not_installed: postgresql-13 + ignore_if_package_is_not_installed: postgresql-15 redis-server: log: /var/log/redis/redis-server.log category: database -rspamd: - log: /var/log/rspamd/rspamd.log - category: email slapd: category: database test_conf: slapd -Tt @@ -51,6 +46,9 @@ ssh: test_conf: sshd -t needs_exposed_ports: [22] category: admin +yunohost-portal-api: + log: /var/log/yunohost-portal-api.log + category: userportal yunohost-api: log: /var/log/yunohost/yunohost-api.log category: admin @@ -60,21 +58,6 @@ yunohost-firewall: category: security yunomdns: category: mdns -glances: null -nsswitch: null -ssl: null -yunohost: null -bind9: null -tahoe-lafs: null -memcached: null -udisks2: null -udisk-glue: null -amavis: null -postgrey: null -spamassassin: null -rmilter: null php5-fpm: null php7.0-fpm: null php7.3-fpm: null -nslcd: null -avahi-daemon: null diff --git a/conf/yunohost/yunohost-portal-api.service b/conf/yunohost/yunohost-portal-api.service new file mode 100644 index 000000000..006af0080 --- /dev/null +++ b/conf/yunohost/yunohost-portal-api.service @@ -0,0 +1,48 @@ +[Unit] +Description=YunoHost Portal API +After=network.target + +[Service] +User=ynh-portal +Group=ynh-portal +Type=simple +ExecStart=/usr/bin/yunohost-portal-api +Restart=always +RestartSec=5 +TimeoutStopSec=30 + +# Sandboxing options to harden security +# Details for these options: https://www.freedesktop.org/software/systemd/man/systemd.exec.html +NoNewPrivileges=yes +PrivateTmp=yes +PrivateDevices=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +RestrictNamespaces=yes +RestrictRealtime=yes +DevicePolicy=closed +ProtectClock=yes +ProtectHostname=yes +ProtectProc=invisible +ProtectSystem=full +ProtectControlGroups=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +LockPersonality=yes +SystemCallArchitectures=native +SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap @cpu-emulation @privileged + +# Denying access to capabilities that should not be relevant +# Doc: https://man7.org/linux/man-pages/man7/capabilities.7.html +CapabilityBoundingSet=~CAP_RAWIO CAP_MKNOD +CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE +CapabilityBoundingSet=~CAP_SYS_BOOT CAP_SYS_TIME CAP_SYS_MODULE CAP_SYS_PACCT +CapabilityBoundingSet=~CAP_LEASE CAP_LINUX_IMMUTABLE CAP_IPC_LOCK +CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_WAKE_ALARM +CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG +CapabilityBoundingSet=~CAP_MAC_ADMIN CAP_MAC_OVERRIDE +CapabilityBoundingSet=~CAP_NET_ADMIN CAP_NET_BROADCAST CAP_NET_RAW +CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYSLOG + + +[Install] +WantedBy=multi-user.target diff --git a/debian/changelog b/debian/changelog index ce58db1df..2565d546b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,496 @@ yunohost (12.0.0) unstable; urgency=low -- Alexandre Aubin Thu, 04 May 2023 20:30:19 +0200 +yunohost (11.2.21.2) stable; urgency=low + + - bullseye->bookworm migration: tweak message to reflect the fact that metronome and rspamd will be applications starting with bookworm (64c8d9e8) + - helpers/apt: unbound variable (8a65053a) + + -- Alexandre Aubin Mon, 15 Jul 2024 23:07:08 +0200 + +yunohost (11.2.21.1) stable; urgency=low + + - helpers2.1: forgot to patch ynh_remove_fpm_config -> ynh_config_remove_phpfpm (bb20020c) + + -- Alexandre Aubin Mon, 15 Jul 2024 22:13:39 +0200 + +yunohost (11.2.21) stable; urgency=low + + - log: optimize log list perf by creating a 'cache' symlink pointing to the log's parent ([#1907](http://github.com/YunoHost/yunohost/pull/1907)) + - log: small hack when dumping log right after script failure, prevent a weird edge case where it'll dump the log of the resource provisioning instead of the script (1bb81e8f) + - debian: Bullseye->Bookworm migration ('hidden' but easier to test) ([#1759](http://github.com/YunoHost/yunohost/pull/1759), ab8e0e66, e54e99bf) + - helpers/apt: rely on simpler dpkg-deb --build rather than equivs to create .deb for app virtual dependencies (f6fbd69c, 8be726b9) + - helpers/apt: Support apt repositories with [trusted=yes] ([#1903](http://github.com/YunoHost/yunohost/pull/1903)) + - backups: one should be able to restore a backup archive by providing a path to the archive without moving it to /home/yunohost.backup/archives/ (c8a18129, b266e398) + - backups: yunohost should not ask confirmation that 'YunoHost is already installed' when restoring only apps (9c22d36c) + - i18n: translate _diagnosis_ignore function ([#1894](http://github.com/YunoHost/yunohost/pull/1894)) + - i18n: Translations updated for Basque, Catalan, French, Galician, German, Indonesian, Japanese, Russian, Spanish, Ukrainian + + Thanks to all contributors <3 ! (alexAubin, Anonymous, cjdw, Félix Piédallu, Ivan Davydov, José M, Kayou, OniriCorpe, ppr, Zwiebel) + + -- Alexandre Aubin Mon, 15 Jul 2024 16:22:26 +0200 + +yunohost (11.2.20.2) stable; urgency=low + + - Fix service enable/disable auto-ignoring diagnosis entries ([#1886](http://github.com/YunoHost/yunohost/pull/1886)) + + Thanks to all contributors <3 ! (OniriCorpe) + + -- Alexandre Aubin Wed, 03 Jul 2024 21:51:50 +0200 + +yunohost (11.2.20.1) stable; urgency=low + + - helpers2.1: typo (1ed56952e) + - helpers2.1: add unit tests (92807afb1) + + -- Alexandre Aubin Mon, 01 Jul 2024 23:38:29 +0200 + +yunohost (11.2.20) stable; urgency=low + + - helpers2.1: fix automigration of phpversion to php_version (3f973669) + - helpers2.1: change source patches location + raise an error instead of a warning when a patch fails to apply on CI (a48bfa67) + - helpers2.1: when using ynh_die, also return the error via YNH_STDRETURN such that it can be obtained from the python and displayed in the main error message, to increase the chance that people may read it and have something more useful than "An error happened in the script" (f2b5f0f2) + - helpers2.1: remove the ynh_clean_setup mechanism underused/useless.. (1c62960e) + - helpers2.1: switch to posisional args for ynh_multimedia_addaccess because that's what 99% of apps already do (ef622ffe) + - helpers2.1: add support for downloading .tar files ([#1889](http://github.com/YunoHost/yunohost/pull/1889)) + - services/diagnosis: automatically ignore the service in diagnosis if it has been deactivated with the ynh cli ([#1886](http://github.com/YunoHost/yunohost/pull/1886)) + + Thanks to all contributors <3 ! (alexAubin, OniriCorpe, Sebastian Gumprich) + + -- Alexandre Aubin Mon, 01 Jul 2024 18:46:52 +0200 + +yunohost (11.2.19) stable; urgency=low + + - apps: tweaks to be more robust and prevent the stupid flood of 'sh: 0: getcwd() failed: No such file or directory' when running an app upgrade/remove from /var/www/$app, sometimes making it look like the upgrade failed when it didnt (a349fc03) + - apps: be more robust when an app upgrade succeeds but for some reason is marked with 'broke the system' ... ending up in inconsistent state between the app settings vs the app scritpts (for example in v1->v2 transitions but not only) (e5b57590) + - helpers2.1: Fix getopts error handling ... (3e1c9eba) + - helpers2.1: also run _ynh_apply_default_permissions in ynh_restore to be consistent (also because the user uid on the new system may be different than in the archive etc) (eee84c5f) + + -- Alexandre Aubin Sat, 29 Jun 2024 23:55:52 +0200 + +yunohost (11.2.18) stable; urgency=low + + - helpers2.1: Rework _ynh_apply_default_permissions to hopefully remove the necessity to chown/chmod in the app scripts ([#1883](http://github.com/YunoHost/yunohost/pull/1883)) + - helpers2.1: in logrotate, make sure to also chown $app the log dir (1dfc47d1d) + - helpers2.1: forgot to rename the apt call in mongodb helpers (7b2959a3e) + - helpers2.1: in ynh_safe_rm, check if target is not a broken symlink before erorring out ([#1716](http://github.com/YunoHost/yunohost/pull/1716)) + + Thanks to all contributors <3 ! (Félix Piédallu) + + -- Alexandre Aubin Sat, 29 Jun 2024 18:05:04 +0200 + +yunohost (11.2.17.1) stable; urgency=low + + - helpers2.1: fix __PATH__/ handling (997388dc) + - ci: Fix helpers 2.1 doc location (7347b08e) + - helpers/doc: De-hide some helpers v1 in documentation now that the structure is less bloated sort of ? (2a7fefae) + - helpers/doc: fix detail block, cant use the HTML
because grav doesnt interpret markdown in it (feb9a095) + + -- Alexandre Aubin Tue, 25 Jun 2024 14:19:58 +0200 + +yunohost (11.2.17) stable; urgency=low + + - helpers: Misc cleaning / reorganizing to prepare new doc (2895d4d9) + - helpers: rework helper doc now that we have multiple versions of helpers in parallel + improve structure (group helper file in categories) (094cd9dd) + - helpers/mongo: less noisy output when checking the avx flag is here in /proc/cpuinfo (2af4c157) + - apps/helpers2.1: fix app env in resource upgrade context ending up in incorrect helper version being used (ed426f05) + - helpers2.1: forgot to propagate the 'goenv latest' fix from helpers v1 (d8c3ff4c) + - helpers2.1: drop ynh_apps helper because only a single app is using it ... (1fb80e5d) + - helpers2.1: other typo fixes + + -- Alexandre Aubin Mon, 24 Jun 2024 22:36:32 +0200 + +yunohost (11.2.16) stable; urgency=low + + - apps/logs: fix some information not being redacted because of the packaging v2 flows (a25033bb) + - logs: misc ad-hoc tweaks to limit the noise in log sharing (06c8fbc8) + - helpers: (1/2/2.1) add a new ynh_app_setting_set_default to replace the unecessarily complex 'if [ -z ${foo:-} ]' trick ([#1873](http://github.com/YunoHost/yunohost/pull/1873)) + - helpers2.1: drop unused 'local source' mechanism from ynh_setup_source (dd8db188) + - helpers2.1: fix positional arg parsing in ynh_psql_create_user (e5585136) + - helpers2.1: rework the fpm usage/footprint madness ([#1874](http://github.com/YunoHost/yunohost/pull/1874)) + - helpers2.1: fix ynh_config_add_logrotate when no arg is passed (3942ea12) + - helpers2.1: sudo -u$app -> sudo -u $app (d4857834) + - helpers2.1: change default timeout of ynh_systemctl to 60s instead of 300s (262453f1) + - helpers2.1: display 100 lines instead of 20 in CI context when service fails to start (9298738d) + - helpers2.1: when using ynh_systemctl to reload/start/restart a service with a wait_until and it timesout, handle it as a failure rather than keep going (b3409729) + - helpers2.1: for some reason sudo -E doesn't preserve PATH even though it's exported, so we gotta explicitly use --preserve-env=PATH (5f6df6a8) + - helpers2.1: var rename / cosmetic etc for nodejs/ruby/go version and install directories (2b1f7426) + - i18n: Translations updated for Basque, Slovak + + Thanks to all contributors <3 ! (alexAubin, Jose Riha, xabirequejo) + + -- Alexandre Aubin Sun, 23 Jun 2024 15:30:22 +0200 + +yunohost (11.2.15) stable; urgency=low + + - apps: new experimentals "2.1" helpers ([#1855](http://github.com/YunoHost/yunohost/pull/1855)) + - apps: when removing an app with --purge, also remove /var/log/{app} + - apps: drop clumsy auto-update of nodejs via cron job which fills up disk space with nodejs copies and doesnt actually restart the app services... + - apps: fix apt resources when multiple extras are set ([#1869](http://github.com/YunoHost/yunohost/pull/1869)) + - mail: allow aliases for sender addresses of apps ([#1843](http://github.com/YunoHost/yunohost/pull/1843)) + + Thanks to all contributors <3 ! (alexAubin, Chris Vogel, Félix Piédallu) + + -- Alexandre Aubin Thu, 20 Jun 2024 21:20:47 +0200 + +yunohost (11.2.14.1) stable; urgency=low + + - helpers: Fix typo in ynh_read_manifest documentation ([#1866](http://github.com/YunoHost/yunohost/pull/1866)) + - helpers/go: fix goenv call ([#1868](http://github.com/YunoHost/yunohost/pull/1868)) + + Thanks to all contributors <3 ! (Chris Vogel, clecle226, Félix Piédallu, OniriCorpe) + + -- Alexandre Aubin Mon, 10 Jun 2024 12:34:25 +0200 + +yunohost (11.2.14) testing; urgency=low + + - helpers/go: fix missing git fetch (5676a7275) + + -- Félix Piédallu Wed, 05 Jun 2024 15:52:06 +0200 + +yunohost (11.2.13) stable; urgency=low + + - helpers: add a --jinja option to ynh_add_config ([#1851](http://github.com/YunoHost/yunohost/pull/1851)) + - helpers: add mongodb helpers ([#1844](http://github.com/YunoHost/yunohost/pull/1844)) + - helpers: update getopts to accept arguments that are valid arguments to echo ([#1847](http://github.com/YunoHost/yunohost/pull/1847)) + - helpers: create versionned directories of the helpers ([#1717](http://github.com/YunoHost/yunohost/pull/1717)) + - helpers: fix goenv broken when checking out latest master commit ([#1863](http://github.com/YunoHost/yunohost/pull/1863)) + + Thanks to all contributors <3 ! (alexAubin, Chris Vogel, Félix Piédallu, Josué Tille, Salamandar, tituspijean) + + -- Alexandre Aubin Tue, 04 Jun 2024 16:43:42 +0200 + +yunohost (11.2.12) stable; urgency=low + + - doc: Remove internal/packagingv1 helpers from helpers doc ([#1832](http://github.com/YunoHost/yunohost/pull/1832)) + - helpers: Document ynh_add_source --full_replace=1 ([#1834](http://github.com/YunoHost/yunohost/pull/1834)) + - helpers/apt: Actually remove the newly added repo. ([#1835](http://github.com/YunoHost/yunohost/pull/1835)) + - ldap: fix ldap write access for admin users ([#1836](http://github.com/YunoHost/yunohost/pull/1836)) + - helpers: Add Go Helper to the core ([#1837](http://github.com/YunoHost/yunohost/pull/1837)) + - helpers: Prevent yet another Node and Corepack madness ([#1842](http://github.com/YunoHost/yunohost/pull/1842)) + - certs: fix renew cert for sub subdomain ([#1819](http://github.com/YunoHost/yunohost/pull/1819)) + - cli: [enh] Implement 'yunohost log show last' to display the last log file. ([#1805](http://github.com/YunoHost/yunohost/pull/1805)) + - helpers: Add redis and ruby helpers ([#1838](http://github.com/YunoHost/yunohost/pull/1838)) + - [i18n] Translations updated for Basque, Catalan, Chinese (Simplified), Esperanto, French, Galician, German, Indonesian, Italian, Japanese, Persian, Slovak, Spanish, Ukrainian + + Thanks to all contributors <3 ! (alexAubin, BELLAHBIB Ayoub, eric_G, José M, Kayou, manor-tile, Mateusz, rosbeef andino, selfhoster1312, tituspijean, xabirequejo, Yann Autissier) + + -- OniriCorpe Mon, 20 May 2024 00:02:47 +0200 + +yunohost (11.2.11.3) stable; urgency=low + + - fix: edge case when parsing app upstream version from resource manager (5e4e59a1, a5560c30) + - helpers: fix 'ls: cannot access No such file or directory' errors on CI (537699ca) + - maintenance: Upgrade n to 9.2.3 ([#1818](http://github.com/YunoHost/yunohost/pull/1818)) + + Thanks to all contributors <3 ! (Alexandre Aubin, OniriCorpe) + + -- tituspijean Sun, 21 Apr 2024 19:10:02 +0200 + + yunohost (11.2.11.2) stable; urgency=low + + - More oopsies (22b30c79) + + -- Alexandre Aubin Thu, 11 Apr 2024 16:03:20 +0200 + +yunohost (11.2.11.1) stable; urgency=low + + - Missing import oopsi (29c597ed) + + -- Alexandre Aubin Thu, 11 Apr 2024 14:32:52 +0200 + +yunohost (11.2.11) stable; urgency=low + + - maintenance: make_changelog.sh enhancements ([#1790](http://github.com/YunoHost/yunohost/pull/1790)) + - maintenance: switch from gitlab CI to github actions for autoblacking code ([#1800](http://github.com/YunoHost/yunohost/pull/1800)) + - readme: add images alt text, fix some links and some markdown formating ([#1802](http://github.com/YunoHost/yunohost/pull/1802)) + - doc: fix markdown for autogenerated doc for app helpers and resources ([#1793](http://github.com/YunoHost/yunohost/pull/1793)) + - helpers/apt: Do not wait for dpkg lock when calling ynh_package_is_installed ([#1811](http://github.com/YunoHost/yunohost/pull/1811)) + - helpers/misc: Protect more path on ynh secure remove ([#1810](http://github.com/YunoHost/yunohost/pull/1810)) + - perf: add cache for system utils that fetch debian_version, debian_version_id, system_arch, system_virt (85f83af8) + - app resources: be able to use __APP__, __YNH_ARCH__ and __YNH_DEBIAN_VERSION__, __YNH_DEBIAN_VERSION_ID__ in properties ([#1751](http://github.com/YunoHost/yunohost/pull/1751), a3ab7c91) + - app configpanels: add settings in bash context when running config scripts (c9d570e6, 006318ef) + - app configpanels: fix quoting issue when returning values from config scripts ([#1789](http://github.com/YunoHost/yunohost/pull/1789)) + - i18n: Translations updated for Arabic, Basque, Catalan, Chinese (Simplified), Czech, Dutch, English, Esperanto, French, Galician, German, Hindi, Indonesian, Italian, Japanese, Norwegian Bokmål, Occitan, Persian, Polish, Portuguese, Russian, Slovak, Spanish, Telugu, Turkish, Ukrainian + + Thanks to all contributors <3 ! (Bram, Christian Wehrli, Félix Piédallu, Francescc, José M, Josué Tille, Kayou, OniriCorpe, ppr, Tagada, tituspijean, xabirequejo, xaloc33, yolateng0) + + -- Alexandre Aubin Thu, 11 Apr 2024 12:24:07 +0200 + +yunohost (11.2.10.3) stable; urgency=low + + - fix: latest release was tagged 'testing' by error + + Thanks to all contributors <3 ! (Alexandre Aubin, Tagada, OniriCorpe) + + -- OniriCorpe Thu, 29 Feb 2024 23:49:11 +0100 + +yunohost (11.2.10.2) stable; urgency=low + + - docs: add autoupdate.version_regex to the doc ([#1781](http://github.com/YunoHost/yunohost/pull/1781)) + - chores: update actions/checkout & peter-evans/create-pull-request to nodejs20 ([#1784](http://github.com/YunoHost/yunohost/pull/1784)) + - apps: fix readonly questions at install (display_text, etc) ([#1786](http://github.com/YunoHost/yunohost/pull/1786)) + - chores: upgrade n to v9.2.1 ([#1783](http://github.com/YunoHost/yunohost/pull/1783)) + - helpers/logrotate: fix logs folder permissions ([#1787](http://github.com/YunoHost/yunohost/pull/1787)) + - fix: list root ssh keys ([#1788](http://github.com/YunoHost/yunohost/pull/1788)) + - [i18n] Translations updated for German + + Thanks to all contributors <3 ! (Alexandre Aubin, Félix Piédallu, Kay0u, ljf (zamentur), Tagada, tituspijean, YunoHost Bot) + + -- OniriCorpe Thu, 29 Feb 2024 23:49:11 +0100 + +yunohost (11.2.10.1) stable; urgency=low + + - apps/autoupdate: update docs ([#1776](http://github.com/YunoHost/yunohost/pull/1776)) + - fix: sury apt key/purge all expired apt keys ([#1777](http://github.com/YunoHost/yunohost/pull/1777)) + - helpers/logrotate: fix logs folders perms ([#1774](http://github.com/YunoHost/yunohost/pull/1774)) + - [i18n] Translations updated for Catalan, Italian + + Thanks to all contributors <3 ! (Alexandre Aubin, Bram, Francescc, Kayou, OniriCorpe, Tagada, Tommi, yunohost-bot) + + -- Kay0u Tue, 20 Feb 2024 23:33:20 +0100 + +yunohost (11.2.10) stable; urgency=low + + - helpers: document --after= in for ynh_read_var_in_file and ynh_write_var_in_file ([#1758](https://github.com/yunohost/yunohost/pull/1758)) + - resources: document changelog link for latest_github_release ([#1760](https://github.com/yunohost/yunohost/pull/1760)) + - apps/helpers: Reword YNH_APP_UPGRADE_TYPE ([#1762](https://github.com/yunohost/yunohost/pull/1762)) + - app shells: auto-source venv for python apps ([#1756](https://github.com/yunohost/yunohost/pull/1756)) + - tools: Add a 'yunohost tools basic-space-cleanup' command ([#1761](https://github.com/yunohost/yunohost/pull/1761)) + - certs/xmpp: Fix DNS suffix edge case during XMPP certificate setup ([#1763](https://github.com/yunohost/yunohost/pull/1763)) + - helpers/php: quote vars to avoid stupid issues with name in path which may happen in backup restore context... (05f7c3a3b) + - multimedia: fix again edgecase where setfacl crashes because of broken symlinks.. (1ce606d46) + - helpers: disable super verbose logging during ynh_replace_vars poluting logs, it's kinda stable now... (981956051, c2af17667) + - apps: people insist on trying to install Nextcloud after creating a user called nextcloud ... So let's check this stupid case (fc12cb198) + - apps: fix port reuse during provisionning ([#1769](https://github.com/yunohost/yunohost/pull/1769)) + - configpanels: some helpers behavior depend on YNH_APP_PACKAGING_FORMAT which is not set when calling the config script... (077b745d6) + - global settings: Add warning regarding ssh ports below 1024 ([#1765](https://github.com/yunohost/yunohost/pull/1765)) + - global settings: mention cidr notation support in webadmin allowlist help ([#1770](https://github.com/yunohost/yunohost/pull/1770)) + - chores: update copyright headers to 2024 using maintenance/update_copyright_headers.sh (a44ea1414) + - i18n: remove stale i18n strings, fix format inconsistencies (890fcee05, [#1764](https://github.com/yunohost/yunohost/pull/1764)) + - i18n: Translations updated for Arabic, Basque, Catalan, French, Galician, German, Slovak, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Bram, Carlos Solís, Christian Wehrli, cube, Éric Gaspar, Félix Piédallu, Francescc, José M, Jose Riha, Lasse Gismo, ljf (zamentur), OniriCorpe, ppr, Saeba Ryo, tituspijean, xabirequejo) + + -- Alexandre Aubin Fri, 09 Feb 2024 20:05:36 +0100 + +yunohost (11.2.9.1) stable; urgency=low + + - helpers/utils: replace the damn ynh_die with a warning when patch fails to apply ... (0ed6769fc) + + -- Alexandre Aubin Thu, 28 Dec 2023 02:45:33 +0100 + +yunohost (11.2.9) stable; urgency=low + + - users: Allow dots in usernames ([#1750](https://github.com/yunohost/yunohost/pull/1750)) + - ynh_setup_source: properly handle --keep for directories when the dir already exists in the new setup (8e3e78884) + - ynh_setup_source: fix first source patches failure not triggering an error (770fdb686) + - ynh_use_logrotate: Refactor this madness (308ed0e17) + - systemutils: when checking debian version and system arch, redirect stderr to /dev/null to prevent stupid issues (830d7b47e) + - mail/apps: add mailbox/IMAP support for apps that declared a system user with mail enabled (#1745) + - mail: fix edge case bug with the postfix sni file when no domain has mail enabled (155418409) + - i18n: Translations updated for Basque, Polish + + Thanks to all contributors <3 ! (Josue-T, Kuba Bazan, ljf, selfhoster1312, xabirequejo, YapWC) + + -- Alexandre Aubin Wed, 27 Dec 2023 18:45:30 +0100 + +yunohost (11.2.8.2) stable; urgency=low + + - Aleks forgot to remove pdb.set_trace ... (54a6a1b3) + + -- Alexandre Aubin Sat, 09 Dec 2023 18:26:10 +0100 + +yunohost (11.2.8.1) stable; urgency=low + + - apps: fix change_url again, otherwise the lack of path_url default to the old path and fucks up the nginx regen (169c9214) + - i18n: Translations updated for German + + Thanks to all contributors <3 ! (Christian Wehrli) + + -- Alexandre Aubin Sat, 09 Dec 2023 15:56:20 +0100 + +yunohost (11.2.8) stable; urgency=low + + - domains: also regen dovecot configuration when adding a domain (59875cae) + - helpers/fail2ban: grep logpath is likely to match comments in the file that contain the word logpath... (26796807) + - helpers: Further simplify the change url helper ([#1746](https://github.com/yunohost/yunohost/pull/1746)) + + Thanks to all contributors <3 ! (Josué Tille) + + -- Alexandre Aubin Tue, 05 Dec 2023 19:21:38 +0100 + +yunohost (11.2.7) stable; urgency=low + + - helpers: fix fail2ban helper when using using --use_template arg ([#1743](https://github.com/yunohost/yunohost/pull/1743)) + - i18n: Translations updated for Basque, French, Galician + + Thanks to all contributors <3 ! (José M, OniriCorpe, ppr, xabirequejo) + + -- Alexandre Aubin Mon, 27 Nov 2023 14:13:54 +0100 + +yunohost (11.2.6) stable; urgency=low + + - mail: Improve dovecots rspamd integration wrt junk/spam folder naming ([#1731](https://github.com/yunohost/yunohost/pull/1731)) + - mail: add redis database configuration in rspamd ([#1730](https://github.com/yunohost/yunohost/pull/1730)) + - mail: let dovecot create folders on first login ([#1735](https://github.com/yunohost/yunohost/pull/1735)) + - apps: Support packages_from_raw_bash in extra packages ([#1729](https://github.com/yunohost/yunohost/pull/1729)) + - apps/configpanel: support bind 'heritage', avoid repeating the same bind statement for multiple options ([#1706](https://github.com/yunohost/yunohost/pull/1706)) + - helpers: Upgrade n to version 9.2.0 ([#1727](https://github.com/yunohost/yunohost/pull/1727)) + - helpers: Update docker-image-extract to support more recent docker images ([#1733](https://github.com/yunohost/yunohost/pull/1733)) + - helpers: Add ynh_exec_and_print_stderr_only_if_error that only prints stderr when command fails ([#1723](https://github.com/yunohost/yunohost/pull/1723)) + - helpers: fix logrotate config file permission ([#1736](https://github.com/yunohost/yunohost/pull/1736)) + - helpers: make sure logfile exist when calling fail2ban helper ([#1737](https://github.com/yunohost/yunohost/pull/1737)) + - backup: Add post_app_restore hook ([#1708](https://github.com/yunohost/yunohost/pull/1708)) + - perf: speedup firewall reload ([#1734](https://github.com/yunohost/yunohost/pull/1734)) + - perf: prevent unecessary queries when building UserOption form ([#1738](https://github.com/yunohost/yunohost/pull/1738)) + - i18n: Translations updated for Basque, Catalan, French, Galician, Italian, Slovak, Spanish + + Thanks to all contributors <3 ! (chri2, Chris Vogel, cristian amoyao, Éric Gaspar, Félix Piédallu, Jorge-vitrubio.net, José M, Jose Riha, ljf, mh4ckt3mh4ckt1c4s, OniriCorpe, Sebastian Gumprich, selfhoster1312, Tharyrok, Thomas, tituspijean, xabirequejo) + + -- Alexandre Aubin Fri, 24 Nov 2023 22:01:50 +0100 + +yunohost (11.2.5) stable; urgency=low + + - debian: fix conflict with openssl that is too harsh, openssl version on bullseye is now 1.1.1w, bookworm has 3.x (e8700bfe7) + - dyndns: tweak dyndns subscribe/unsubscribe for dyndns recovery password integration in webadmin ([#1715](https://github.com/yunohost/yunohost/pull/1715)) + - helpers: ynh_setup_source: check and re-download a prefetched file that doesn't match the checksum (3dfab89c1) + - helpers: ynh_setup_source: fix misleading example ([#1714](https://github.com/yunohost/yunohost/pull/1714)) + - helpers: php/apt: allow `phpX.Y` as sole dependency for `$phpversion=X.Y` ([#1722](https://github.com/yunohost/yunohost/pull/1722)) + - apps: fix typo in log statement ([#1709](https://github.com/yunohost/yunohost/pull/1709)) + - apps: allow system users to send mails from IPv6 localhost. ([#1710](https://github.com/yunohost/yunohost/pull/1710)) + - apps: add "support_purge" to app info for webadmin integration ([#1719](https://github.com/yunohost/yunohost/pull/1719)) + - diagnosis: be more flexible regarding accepted values for DMARC DNS records ([#1713](https://github.com/yunohost/yunohost/pull/1713)) + - dns: add home.arpa as special TLD (#1718) (bb097fedc) + - i18n: Translations updated for Basque, French + + Thanks to all contributors <3 ! (axolotle, Florian, Kayou, orhtej2, Pierre de La Morinerie, ppr, stanislas, tituspijean, xabirequejo) + + -- Alexandre Aubin Mon, 09 Oct 2023 23:16:13 +0200 + +yunohost (11.2.4) stable; urgency=low + + - doc: Improve --help for 'yunohost app install' ([#1702](https://github.com/yunohost/yunohost/pull/1702)) + - helpers: add new --group option for ynh_add_fpm_config to customize the Group parameter (65d25710) + - apps: allow to use jinja {% if foobar %} blocks in their notifications/doc pages (57699289) + - apps: BACKUP_CORE_ONLY was not set for pre-upgrade safety backups, resulting in unecessarily large pre-upgrade backups (07daa687) + - apps: Use the existing db_name setting for database provising to ease v1->v2 transition with specific db_name ([#1704](https://github.com/yunohost/yunohost/pull/1704)) + - configpanels/forms: more edge cases with some questions not implementing some methods/attributes (b0fe49ae) + - diagnosis: reverse DNS check should be case-insensitive #2235 ([#1705](https://github.com/yunohost/yunohost/pull/1705)) + - i18n: Translations updated for Galician, Indonesian, Polish, Spanish, Turkish + + Thanks to all contributors <3 ! (Grzegorz Cichocki, José M, Kuba Bazan, ljf (zamentur), massyas, Neko Nekowazarashi, selfhoster1312, Suleyman Harmandar, taco, Tagada) + + -- Alexandre Aubin Thu, 31 Aug 2023 17:30:21 +0200 + +yunohost (11.2.3) stable; urgency=low + + - apps: fix another case of no attribute 'value' due to config panels/questions refactoring (4fda8ed49) + + -- Alexandre Aubin Sat, 22 Jul 2023 16:48:22 +0200 + +yunohost (11.2.2) stable; urgency=low + + - domains: Gandi's `api_protocol` field should be a `select` type ([#1693](https://github.com/yunohost/yunohost/pull/1693)) + - configpanel: fix .value call for readonly-type options (e1ceb084) + - i18n: Translations updated for French, Galician + + Thanks to all contributors <3 ! (axolotle, José M, ppr, tituspijean) + + -- Alexandre Aubin Wed, 19 Jul 2023 02:35:28 +0200 + +yunohost (11.2.1) stable; urgency=low + + - doc: fix resource doc generation .. not sure why this line that removed legit indent was there (ced222ea) + - apps: hotfix for funky issue, apps getting named 'undefined' (781f924e) + + -- Alexandre Aubin Mon, 17 Jul 2023 21:13:54 +0200 + +yunohost (11.2) stable; urgency=low + + - dyndns: add support for recovery passwords ([#1475](https://github.com/YunoHost/yunohost/pull/1475)) + - mail/apps: allow system users to auth on the mail stack and send emails ([#815](https://github.com/YunoHost/yunohost/pull/815)) + - nginx: fix OCSP stapling errors ([#1543](https://github.com/YunoHost/yunohost/pull/1534)) + - ssh: disable banner by default ([#1605](https://github.com/YunoHost/yunohost/pull/1605)) + - configpanels: another partial refactoring of config panels / questions, paving the way for Pydantic ([#1676](https://github.com/YunoHost/yunohost/pull/1676)) + - misc: rewrite the `yunopaste` tool ([#1667](https://github.com/YunoHost/yunohost/pull/1667)) + - apps: simplify the use of `ynh_add_fpm_config` ([#1684](https://github.com/YunoHost/yunohost/pull/1684)) + - apps: in ynh_systemd_action, check the actual timestamp when checking for timeout, because for some reason journalctl may take a ridiculous amount of time to run (f3eef43d) + - i18n: Translations updated for German, Japanese + + Thanks to all contributors <3 ! (André Théo LAURET, axolotle, Christian Wehrli, Éric Gaspar, ljf, motcha, theo-is-taken) + + -- Alexandre Aubin Mon, 17 Jul 2023 16:14:58 +0200 + +yunohost (11.1.22) stable; urgency=low + + - security: replace $http_host by $host in nginx conf, cf https://github.com/yandex/gixy/blob/master/docs/en/plugins/hostspoofing.md / Credit to A.Wolski (3957b10e) + - 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)) @@ -4812,4 +5302,3 @@ moulinette-yunohost (1.0~megusta1) megusta; urgency=low * Init -- Adrien Beudin Thu, 15 May 2014 13:16:03 +0200 - diff --git a/debian/control b/debian/control index 3674a62a4..fe5cb5a20 100644 --- a/debian/control +++ b/debian/control @@ -2,34 +2,34 @@ Source: yunohost Section: utils Priority: extra Maintainer: YunoHost Contributors -Build-Depends: debhelper (>=9), debhelper-compat (= 13), dh-python, python3-all (>= 3.11), python3-yaml, python3-jinja2 +Build-Depends: debhelper (>=9), debhelper-compat (= 13), dh-python, python3-all (>= 3.11), python3-yaml, python3-jinja2 (>= 3.0) Standards-Version: 3.9.6 Homepage: https://yunohost.org/ Package: yunohost Essential: yes Architecture: all -Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 11.1), ssowat (>= 11.1) +Depends: python3-all (>= 3.11), + , moulinette (>= 12.0), ssowat (>= 12.0), , python3-psutil, python3-requests, python3-dnspython, python3-openssl - , python3-miniupnpc, python3-dbus, python3-jinja2 + , python3-miniupnpc, python3-dbus, python3-jinja2 (>= 3.0) , python3-toml, python3-packaging, python3-publicsuffix2 - , python3-ldap, python3-zeroconf (>=0.47), python3-lexicon, - , python-is-python3 + , python3-ldap, python3-zeroconf (>= 0.47), python3-lexicon, + , python3-cryptography, python3-jwt, python3-passlib, python3-magic + , python-is-python3, python3-pydantic, python3-email-validator , nginx, nginx-extras (>=1.22) - , apt, apt-transport-https, apt-utils, dirmngr + , apt, apt-transport-https, apt-utils, aptitude, dirmngr , openssh-server, iptables, fail2ban, bind9-dnsutils , openssl, ca-certificates, netcat-openbsd, iproute2 , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd , dnsmasq, resolvconf, libnss-myhostname , postfix, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre , dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam - , rspamd, opendkim-tools, postsrsd, procmail, mailutils - , redis-server + , opendkim-tools, opendkim, postsrsd, procmail, mailutils , acl - , git, curl, wget, cron, unzip, jq, bc, at, procps - , lsb-release, haveged, fake-hwclock, equivs, lsof, whois -Recommends: yunohost-admin + , git, curl, wget, cron, unzip, jq, bc, at, procps, j2cli + , lsb-release, haveged, fake-hwclock, lsof, whois +Recommends: yunohost-admin, yunohost-portal (>= 12.0) , ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog , unattended-upgrades @@ -37,11 +37,12 @@ Recommends: yunohost-admin Conflicts: iptables-persistent , apache2 , bind9 + , openresolv + , systemd-resolved , nginx-extras (>= 1.23) , openssl (>= 3.1) , slapd (>= 2.6) , dovecot-core (>= 1:2.4) - , redis-server (>= 5:7.1) , fail2ban (>= 1.1) , iptables (>= 1.8.10) Description: manageable and configured self-hosting server diff --git a/debian/install b/debian/install index 5169d0b62..1a0cf583c 100644 --- a/debian/install +++ b/debian/install @@ -1,10 +1,9 @@ bin/* /usr/bin/ share/* /usr/share/yunohost/ hooks/* /usr/share/yunohost/hooks/ -helpers/* /usr/share/yunohost/helpers.d/ +helpers/* /usr/share/yunohost/ conf/* /usr/share/yunohost/conf/ locales/* /usr/share/yunohost/locales/ doc/yunohost.8.gz /usr/share/man/man8/ doc/bash_completion.d/* /etc/bash_completion.d/ -conf/metronome/modules/* /usr/lib/metronome/modules/ src/* /usr/lib/python3/dist-packages/yunohost/ diff --git a/debian/postinst b/debian/postinst index 238817cd7..66747d5f3 100644 --- a/debian/postinst +++ b/debian/postinst @@ -4,6 +4,10 @@ set -e do_configure() { + mkdir -p /etc/yunohost + mkdir -p /etc/yunohost/apps + mkdir -p /etc/yunohost/portal + if [ ! -f /etc/yunohost/installed ]; then # If apps/ is not empty, we're probably already installed in the past and # something funky happened ... @@ -33,6 +37,8 @@ do_configure() { yunohost tools update apps --output-as none || true fi + systemctl restart yunohost-portal-api + # Trick to let yunohost handle the restart of the API, # to prevent the webadmin from cutting the branch it's sitting on if systemctl is-enabled yunohost-api --quiet diff --git a/doc/generate_bash_completion.py b/doc/generate_bash_completion.py index 88aa273fd..460447ab1 100644 --- a/doc/generate_bash_completion.py +++ b/doc/generate_bash_completion.py @@ -8,6 +8,7 @@ adds `--help` at the end if one presses [tab] again. author: Christophe Vuillot """ + import os import yaml diff --git a/doc/generate_configpanel_and_formoptions_doc.py b/doc/generate_configpanel_and_formoptions_doc.py new file mode 100644 index 000000000..156a769fc --- /dev/null +++ b/doc/generate_configpanel_and_formoptions_doc.py @@ -0,0 +1,181 @@ +import ast +import datetime +import subprocess + +version = open("../debian/changelog").readlines()[0].split()[1].strip("()") +today = datetime.datetime.now().strftime("%d/%m/%Y") + + +def get_current_commit(): + p = subprocess.Popen( + "git rev-parse --verify HEAD", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout, stderr = p.communicate() + + current_commit = stdout.strip().decode("utf-8") + return current_commit + + +current_commit = get_current_commit() + + +def print_config_panel_docs(): + fname = "../src/utils/configpanel.py" + content = open(fname).read() + + # NB: This magic is because we want to be able to run this script outside of a YunoHost context, + # in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... + tree = ast.parse(content) + + ConfigPanelClasses = reversed( + [ + c + for c in tree.body + if isinstance(c, ast.ClassDef) + and c.name in {"SectionModel", "PanelModel", "ConfigPanelModel"} + ] + ) + + print("## Configuration panel structure") + + for c in ConfigPanelClasses: + doc = ast.get_docstring(c) + print("") + print(f"### {c.name.replace('Model', '')}") + print("") + print(doc) + print("") + print("---") + + +def print_form_doc(): + fname = "../src/utils/form.py" + content = open(fname).read() + + # NB: This magic is because we want to be able to run this script outside of a YunoHost context, + # in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... + tree = ast.parse(content) + + OptionClasses = [ + c + for c in tree.body + if isinstance(c, ast.ClassDef) and c.name.endswith("Option") + ] + + OptionDocString = {} + + print("## List of all option types") + + for c in OptionClasses: + if not isinstance(c.body[0], ast.Expr): + continue + option_type = None + + if c.name in {"BaseOption", "BaseInputOption"}: + option_type = c.name + elif c.body[1].target.id == "type": + option_type = c.body[1].value.attr + + generaltype = ( + c.bases[0].id.replace("Option", "").replace("Base", "").lower() + if c.bases + else None + ) + + docstring = ast.get_docstring(c) + if docstring: + if "#### Properties" not in docstring: + docstring += """ +#### Properties + +- [common properties](#common-properties)""" + OptionDocString[option_type] = { + "doc": docstring, + "generaltype": generaltype, + } + + # Dirty hack to have "BaseOption" as first and "BaseInputOption" as 2nd in list + + base = OptionDocString.pop("BaseOption") + baseinput = OptionDocString.pop("BaseInputOption") + OptionDocString2 = { + "BaseOption": base, + "BaseInputOption": baseinput, + } + OptionDocString2.update(OptionDocString) + + for option_type, infos in OptionDocString2.items(): + if option_type == "display_text": + # display_text is kind of legacy x_x + continue + print("") + if option_type == "BaseOption": + print("### Common properties") + elif option_type == "BaseInputOption": + print("### Common inputs properties") + else: + print( + f"### `{option_type}`" + + (f" ({infos['generaltype']})" if infos["generaltype"] else "") + ) + print("") + print(infos["doc"]) + print("") + print("---") + + +print( + rf"""--- +title: Technical details for config panel structure and form option types +template: docs +taxonomy: + category: docs +routes: + default: '/dev/forms' +--- + +Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version}) + +## Glossary + +You may encounter some named types which are used for simplicity. + +- `Translation`: a translated property + - used for properties: `ask`, `help` and `Pattern.error` + - a `dict` with locales as keys and translations as values: + ```toml + ask.en = "The text in english" + ask.fr = "Le texte en français" + ``` + It is not currently possible for translators to translate those string in weblate. + - a single `str` for a single english default string + ```toml + help = "The text in english" + ``` +- `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`: + - used for properties: `visible` and `enabled` + - operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()` +- `Binding`: bind a value to a file/property/variable/getter/setter/validator + - save the value in `settings.yaml` when not defined + - nothing at all with `"null"` + - a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config` + - a variable/property in a file with `:__FINALPATH__/my_file.php` + - a whole file with `__FINALPATH__/my_file.php` +- `Pattern`: a `dict` with a regex to match the value against and an error message + ```toml + pattern.regexp = '^[A-F]\d\d$' + pattern.error = "Provide a room number such as F12: one uppercase and 2 numbers" + # or with translated error + pattern.error.en = "Provide a room number such as F12: one uppercase and 2 numbers" + pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres." + ``` + - IMPORTANT: your `pattern.regexp` should be between simple quote, not double. + +""" +) + +print_config_panel_docs() +print_form_doc() diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 110d1d4cd..d572b03da 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -1,10 +1,55 @@ #!/usr/env/python3 +import sys import os import glob import datetime import subprocess +tree = { + "sources": { + "title": "Sources", + "notes": "This is coupled to the 'sources' resource in the manifest.toml", + "subsections": ["sources"], + }, + "tech": { + "title": "App technologies", + "notes": "These allow to install specific version of the technology required to run some apps", + "subsections": ["nodejs", "ruby", "go", "composer"], + }, + "db": { + "title": "Databases", + "notes": "This is coupled to the 'database' resource in the manifest.toml - at least for mysql/postgresql. Mongodb/redis may have better integration in the future.", + "subsections": ["mysql", "postgresql", "mongodb", "redis"], + }, + "conf": { + "title": "Configurations / templating", + "subsections": [ + "templating", + "nginx", + "php", + "systemd", + "fail2ban", + "logrotate", + ], + }, + "misc": { + "title": "Misc tools", + "subsections": [ + "utils", + "setting", + "string", + "backup", + "logging", + "multimedia", + ], + }, + "meh": { + "title": "Deprecated or handled by the core / app resources since v2", + "subsections": ["permission", "apt", "systemuser"], + }, +} + def get_current_commit(): p = subprocess.Popen( @@ -19,14 +64,7 @@ def get_current_commit(): return current_commit -def render(helpers): - current_commit = get_current_commit() - - data = { - "helpers": helpers, - "date": datetime.datetime.now().strftime("%d/%m/%Y"), - "version": open("../debian/changelog").readlines()[0].split()[1].strip("()"), - } +def render(tree, helpers_version): from jinja2 import Template from ansi2html import Ansi2HTMLConverter @@ -42,12 +80,15 @@ def render(helpers): t = Template(template) t.globals["now"] = datetime.datetime.utcnow result = t.render( - current_commit=current_commit, - data=data, + tree=tree, + date=datetime.datetime.now().strftime("%d/%m/%Y"), + version=open("../debian/changelog").readlines()[0].split()[1].strip("()"), + helpers_version=helpers_version, + current_commit=get_current_commit(), convert=shell_to_html, shell_css=shell_css, ) - open("helpers.md", "w").write(result) + open(f"helpers.v{helpers_version}.md", "w").write(result) ############################################################################## @@ -87,7 +128,7 @@ class Parser: # We're still in a comment bloc assert line.startswith("# ") or line == "#", malformed_error(i) current_block["comments"].append(line[2:]) - elif line.strip() == "": + elif line.strip() == "" or line.startswith("_ynh"): # Well eh that was not an actual helper definition ... start over ? current_reading = "void" current_block = { @@ -119,7 +160,14 @@ class Parser: # Then we keep this bloc and start a new one # (we ignore helpers containing [internal] ...) - if "[internal]" not in current_block["comments"]: + if ( + "[packagingv1]" not in current_block["comments"] + and not any( + line.startswith("[internal]") + for line in current_block["comments"] + ) + and not current_block["name"].startswith("_") + ): self.blocks.append(current_block) current_block = { "name": None, @@ -209,23 +257,27 @@ 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 + if len(sys.argv) == 1: + print("This script needs the helper version (1, 2, 2.1) as an argument") + sys.exit(1) - category_name = os.path.basename(helper_file) - print("Parsing %s ..." % category_name) - p = Parser(helper_file) - p.parse_blocks() - for b in p.blocks: - p.parse_block(b) + version = sys.argv[1] - helpers.append((category_name, p.blocks)) + for section in tree.values(): + section["helpers"] = {} + for subsection in section["subsections"]: + print(f"Parsing {subsection} ...") + helper_file = f"../helpers/helpers.v{version}.d/{subsection}" + assert os.path.isfile(helper_file), f"Uhoh, {file} doesn't exists?" + p = Parser(helper_file) + p.parse_blocks() + for b in p.blocks: + p.parse_block(b) - render(helpers) + section["helpers"][subsection] = p.blocks + + render(tree, version) main() diff --git a/doc/generate_json_schema.py b/doc/generate_json_schema.py new file mode 100644 index 000000000..1abf88915 --- /dev/null +++ b/doc/generate_json_schema.py @@ -0,0 +1,4 @@ +from yunohost.utils.configpanel import ConfigPanelModel + + +print(ConfigPanelModel.schema_json(indent=2)) diff --git a/doc/generate_resource_doc.py b/doc/generate_resource_doc.py index 201d25265..a673c066a 100644 --- a/doc/generate_resource_doc.py +++ b/doc/generate_resource_doc.py @@ -62,9 +62,7 @@ for c in ResourceClasses: for resource_id, doc in sorted(ResourceDocString.items()): - doc = doc.replace("\n ", "\n") - - print("----------------") + print("---") print("") print(f"## {resource_id.replace('_', ' ').title()}") print("") diff --git a/doc/helper_doc_template.md b/doc/helper_doc_template.md index ac5d455fb..53bec79fc 100644 --- a/doc/helper_doc_template.md +++ b/doc/helper_doc_template.md @@ -1,18 +1,26 @@ --- -title: App helpers +title: App helpers (v{{ helpers_version }}) template: docs taxonomy: category: docs routes: - default: '/packaging_apps_helpers' + default: '/packaging_apps_helpers{% if helpers_version not in ["1", "2"] %}_v{{ helpers_version }}{% endif %}' --- -Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/doc/generate_helper_doc.py) on {{data.date}} (YunoHost version {{data.version}}) +Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/doc/generate_helper_doc.py) on {{date}} (YunoHost version {{version}}) -{% for category, helpers in data.helpers %} -## {{ category.upper() }} -{% for h in helpers %} + +{% for section_id, section in tree.items() %} +## {{ section["title"].title() }} + +{% if section['notes'] %}

{{ section['notes'] }}

{% endif %} + + {% for subsection, helpers in section["helpers"].items() %} + +### {{ subsection.upper() }} + {% for h in helpers %} #### {{ h.name }} + [details summary="{{ h.brief }}" class="helper-card-subtitle text-muted"] **Usage**: `{{ h.usage }}` @@ -48,12 +56,12 @@ Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ {%- endif %} {%- if h.details %} -**Details**:
+**Details**: {{ h.details }} {%- endif %} - -[Dude, show me the code!](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/helpers/{{ category }}#L{{ h.line + 1 }}) +[Dude, show me the code!](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/helpers/helpers.v{{ helpers_version if helpers_version != "2" else "1" }}.d/{{ subsection }}#L{{ h.line + 1 }}) [/details] ----------------- -{% endfor %} + {% endfor %} +--- + {% endfor %} {% endfor %} diff --git a/helpers/apps b/helpers/apps deleted file mode 100644 index 85b74de15..000000000 --- a/helpers/apps +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash - -# Install others YunoHost apps -# -# usage: ynh_install_apps --apps="appfoo?domain=domain.foo&path=/foo appbar?domain=domain.bar&path=/bar&admin=USER&language=fr&is_public=1&pass?word=pass&port=666" -# | arg: -a, --apps= - apps to install -# -# Requires YunoHost version *.*.* or higher. -ynh_install_apps() { - # Declare an array to define the options of this helper. - local legacy_args=a - local -A args_array=([a]=apps=) - local apps - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - # Split the list of apps in an array - local apps_list=($(echo $apps | tr " " "\n")) - local apps_dependencies="" - - # For each app - for one_app_and_its_args in "${apps_list[@]}" - do - # Retrieve the name of the app (part before ?) - local one_app=$(cut -d "?" -f1 <<< "$one_app_and_its_args") - [ -z "$one_app" ] && ynh_die --message="You didn't provided a YunoHost app to install" - - yunohost tools update apps - - # Installing or upgrading the app depending if it's installed or not - if ! yunohost app list --output-as json --quiet | jq -e --arg id $one_app '.apps[] | select(.id == $id)' >/dev/null - then - # Retrieve the arguments of the app (part after ?) - local one_argument="" - if [[ "$one_app_and_its_args" == *"?"* ]]; then - one_argument=$(cut -d "?" -f2- <<< "$one_app_and_its_args") - one_argument="--args $one_argument" - fi - - # Install the app with its arguments - yunohost app install $one_app $one_argument - else - # Upgrade the app - yunohost app upgrade $one_app - fi - - if [ ! -z "$apps_dependencies" ] - then - apps_dependencies="$apps_dependencies, $one_app" - else - apps_dependencies="$one_app" - fi - done - - ynh_app_setting_set --app=$app --key=apps_dependencies --value="$apps_dependencies" -} - -# Remove other YunoHost apps -# -# Other YunoHost apps will be removed only if no other apps need them. -# -# usage: ynh_remove_apps -# -# Requires YunoHost version *.*.* or higher. -ynh_remove_apps() { - # Retrieve the apps dependencies of the app - local apps_dependencies=$(ynh_app_setting_get --app=$app --key=apps_dependencies) - ynh_app_setting_delete --app=$app --key=apps_dependencies - - if [ ! -z "$apps_dependencies" ] - then - # Split the list of apps dependencies in an array - local apps_dependencies_list=($(echo $apps_dependencies | tr ", " "\n")) - - # For each apps dependencies - for one_app in "${apps_dependencies_list[@]}" - do - # Retrieve the list of installed apps - local installed_apps_list=$(yunohost app list --output-as json --quiet | jq -r .apps[].id) - local required_by="" - local installed_app_required_by="" - - # For each other installed app - for one_installed_app in $installed_apps_list - do - # Retrieve the other apps dependencies - one_installed_apps_dependencies=$(ynh_app_setting_get --app=$one_installed_app --key=apps_dependencies) - if [ ! -z "$one_installed_apps_dependencies" ] - then - one_installed_apps_dependencies_list=($(echo $one_installed_apps_dependencies | tr ", " "\n")) - - # For each dependency of the other apps - for one_installed_app_dependency in "${one_installed_apps_dependencies_list[@]}" - do - if [[ $one_installed_app_dependency == $one_app ]]; then - required_by="$required_by $one_installed_app" - fi - done - fi - done - - # If $one_app is no more required - if [[ -z "$required_by" ]] - then - # Remove $one_app - ynh_print_info --message="Removing of $one_app" - yunohost app remove $one_app --purge - else - ynh_print_info --message="$one_app was not removed because it's still required by${required_by}" - fi - done - fi -} diff --git a/helpers/helpers b/helpers/helpers new file mode 100644 index 000000000..64f9322ae --- /dev/null +++ b/helpers/helpers @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Entrypoint for the helpers scripts +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# Helpers version can be specified via an environment variable or default to 1. +YNH_HELPERS_VERSION=${YNH_HELPERS_VERSION:-1} + +# This is a trick to later only restore set -x if it was set when calling this script +readonly XTRACE_ENABLE=$(set +o | grep xtrace) +set +x + +YNH_HELPERS_DIR="$SCRIPT_DIR/helpers.v${YNH_HELPERS_VERSION}.d" +case "$YNH_HELPERS_VERSION" in + "1" | "2" | "2.1") + readarray -t HELPERS < <(find -L "$YNH_HELPERS_DIR" -mindepth 1 -maxdepth 1 -type f) + for helper in "${HELPERS[@]}"; do + [ -r "$helper" ] && source "$helper" + done + ;; + *) + echo "Helpers are not available in version '$YNH_HELPERS_VERSION'." >&2 + exit 1 +esac + +eval "$XTRACE_ENABLE" diff --git a/helpers/helpers.v1.d/apps b/helpers/helpers.v1.d/apps new file mode 100644 index 000000000..81a5717eb --- /dev/null +++ b/helpers/helpers.v1.d/apps @@ -0,0 +1,215 @@ +#!/bin/bash + +# Install others YunoHost apps +# +# usage: ynh_install_apps --apps="appfoo?domain=domain.foo&path=/foo appbar?domain=domain.bar&path=/bar&admin=USER&language=fr&is_public=1&pass?word=pass&port=666" +# | arg: -a, --apps= - apps to install +# +# Requires YunoHost version *.*.* or higher. +ynh_install_apps() { + # Declare an array to define the options of this helper. + local legacy_args=a + local -A args_array=([a]=apps=) + local apps + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Split the list of apps in an array + local apps_list=($(echo $apps | tr " " "\n")) + local apps_dependencies="" + + # For each app + for one_app_and_its_args in "${apps_list[@]}" + do + # Retrieve the name of the app (part before ?) + local one_app=$(cut -d "?" -f1 <<< "$one_app_and_its_args") + [ -z "$one_app" ] && ynh_die --message="You didn't provided a YunoHost app to install" + + yunohost tools update apps + + # Installing or upgrading the app depending if it's installed or not + if ! yunohost app list --output-as json --quiet | jq -e --arg id $one_app '.apps[] | select(.id == $id)' >/dev/null + then + # Retrieve the arguments of the app (part after ?) + local one_argument="" + if [[ "$one_app_and_its_args" == *"?"* ]]; then + one_argument=$(cut -d "?" -f2- <<< "$one_app_and_its_args") + one_argument="--args $one_argument" + fi + + # Install the app with its arguments + yunohost app install $one_app $one_argument + else + # Upgrade the app + yunohost app upgrade $one_app + fi + + if [ ! -z "$apps_dependencies" ] + then + apps_dependencies="$apps_dependencies, $one_app" + else + apps_dependencies="$one_app" + fi + done + + ynh_app_setting_set --app=$app --key=apps_dependencies --value="$apps_dependencies" +} + +# Remove other YunoHost apps +# +# Other YunoHost apps will be removed only if no other apps need them. +# +# usage: ynh_remove_apps +# +# Requires YunoHost version *.*.* or higher. +ynh_remove_apps() { + # Retrieve the apps dependencies of the app + local apps_dependencies=$(ynh_app_setting_get --app=$app --key=apps_dependencies) + ynh_app_setting_delete --app=$app --key=apps_dependencies + + if [ ! -z "$apps_dependencies" ] + then + # Split the list of apps dependencies in an array + local apps_dependencies_list=($(echo $apps_dependencies | tr ", " "\n")) + + # For each apps dependencies + for one_app in "${apps_dependencies_list[@]}" + do + # Retrieve the list of installed apps + local installed_apps_list=$(yunohost app list --output-as json --quiet | jq -r .apps[].id) + local required_by="" + local installed_app_required_by="" + + # For each other installed app + for one_installed_app in $installed_apps_list + do + # Retrieve the other apps dependencies + one_installed_apps_dependencies=$(ynh_app_setting_get --app=$one_installed_app --key=apps_dependencies) + if [ ! -z "$one_installed_apps_dependencies" ] + then + one_installed_apps_dependencies_list=($(echo $one_installed_apps_dependencies | tr ", " "\n")) + + # For each dependency of the other apps + for one_installed_app_dependency in "${one_installed_apps_dependencies_list[@]}" + do + if [[ $one_installed_app_dependency == $one_app ]]; then + required_by="$required_by $one_installed_app" + fi + done + fi + done + + # If $one_app is no more required + if [[ -z "$required_by" ]] + then + # Remove $one_app + ynh_print_info --message="Removing of $one_app" + yunohost app remove $one_app --purge + else + ynh_print_info --message="$one_app was not removed because it's still required by${required_by}" + fi + 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 + + # Activate the Python environment, if it exists + if [ -f $install_dir/venv/bin/activate ] + then + # set -/+a enables and disables new variables being automatically exported. Needed when using `source`. + set -a + source $install_dir/venv/bin/activate + 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/helpers.v1.d/apt similarity index 93% rename from helpers/apt rename to helpers/helpers.v1.d/apt index a2f2d3de8..a9aca8d93 100644 --- a/helpers/apt +++ b/helpers/helpers.v1.d/apt @@ -58,7 +58,6 @@ ynh_package_is_installed() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - ynh_wait_dpkg_free dpkg-query --show --showformat='${Status}' "$package" 2>/dev/null \ | grep --count "ok installed" &>/dev/null } @@ -67,6 +66,8 @@ ynh_package_is_installed() { # # example: version=$(ynh_package_version --package=yunohost) # +# [internal] +# # usage: ynh_package_version --package=name # | arg: -p, --package= - the package name to get version # | ret: the version or an empty string @@ -101,6 +102,8 @@ ynh_apt() { # Update package index files # +# [internal] +# # usage: ynh_package_update # # Requires YunoHost version 2.2.4 or higher. @@ -110,6 +113,8 @@ ynh_package_update() { # Install package(s) # +# [internal] +# # usage: ynh_package_install name [name [...]] # | arg: name - the package name to install # @@ -121,6 +126,8 @@ ynh_package_install() { # Remove package(s) # +# [internal] +# # usage: ynh_package_remove name [name [...]] # | arg: name - the package name to remove # @@ -131,6 +138,8 @@ ynh_package_remove() { # Remove package(s) and their uneeded dependencies # +# [internal] +# # usage: ynh_package_autoremove name [name [...]] # | arg: name - the package name to remove # @@ -141,6 +150,8 @@ ynh_package_autoremove() { # Purge package(s) and their uneeded dependencies # +# [internal] +# # usage: ynh_package_autopurge name [name [...]] # | arg: name - the package name to autoremove and purge # @@ -175,21 +186,21 @@ ynh_package_install_from_equivs() { # Build and install the package local TMPDIR=$(mktemp --directory) - - # Make sure to delete the legacy compat file - # It's now handle somewhat magically through the control file - rm -f /usr/share/equivs/template/debian/compat + mkdir -p ${TMPDIR}/${pkgname}/DEBIAN/ + # For some reason, dpkg-deb insists for folder perm to be 755 and sometimes it's 777 o_O? + chmod -R 755 ${TMPDIR}/${pkgname} # Note that the cd executes into a sub shell # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg # Install missing dependencies with ynh_package_install ynh_wait_dpkg_free - cp "$controlfile" "${TMPDIR}/control" + cp "$controlfile" "${TMPDIR}/${pkgname}/DEBIAN/control" ( cd "$TMPDIR" - LC_ALL=C equivs-build ./control 2>&1 - LC_ALL=C dpkg --force-depends --install "./${pkgname}_${pkgversion}_all.deb" 2>&1 | tee ./dpkg_log + # Install the fake package without its dependencies with dpkg --force-depends + LC_ALL=C dpkg-deb --build ${pkgname} ${pkgname}.deb > ./dpkg_log 2>&1 || { cat ./dpkg_log; false; } + LC_ALL=C dpkg --force-depends --install "./${pkgname}.deb" 2>&1 | tee ./dpkg_log ) ynh_package_install --fix-broken \ @@ -250,7 +261,7 @@ ynh_install_app_dependencies() { # Check for specific php dependencies which requires sury # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" # The (?<=php) syntax corresponds to lookbehind ;) - local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>)' | sort -u) + local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>|)' | sort -u) if [[ -n "$specific_php_version" ]] then @@ -312,6 +323,7 @@ Package: ${dep_app}-ynh-deps Version: ${version} Depends: ${dependencies} Architecture: all +Maintainer: root@localhost Description: Fake package for ${app} (YunoHost app) dependencies This meta-package is only responsible of installing its dependencies. EOF @@ -331,6 +343,8 @@ EOF # Add dependencies to install with ynh_install_app_dependencies # +# [packagingv1] +# # usage: ynh_add_app_dependencies --package=phpversion [--replace] # | arg: -p, --package= - Packages to add as dependencies for the app. # @@ -418,7 +432,7 @@ ynh_install_extra_app_dependencies() { [ -z "$apps_auto_installed" ] || apt-mark auto $apps_auto_installed # Remove this extra repository after packages are installed - ynh_remove_extra_repo --name=$app + ynh_remove_extra_repo --name=$name } # Add an extra repository correctly, pin it and get the key. @@ -457,21 +471,29 @@ ynh_install_extra_repo() { wget_append="tee" fi - # Split the repository into uri, suite and components. + if [[ "$key" == "trusted=yes" ]]; then + trusted="--trusted" + else + trusted="" + fi + + IFS=', ' read -r -a repo_parts <<< "$repo" + index=0 + # Remove "deb " at the beginning of the repo. - repo="${repo#deb }" - - # Get the uri - local uri="$(echo "$repo" | awk '{ print $1 }')" - - # Get the suite - local suite="$(echo "$repo" | awk '{ print $2 }')" + if [[ "${repo_parts[0]}" == "deb" ]]; then + index=1 + fi + uri="${repo_parts[$index]}" ; index=$((index+1)) + suite="${repo_parts[$index]}" ; index=$((index+1)) # Get the components - local component="${repo##$uri $suite }" + if (( "${#repo_parts[@]}" > 0 )); then + component="${repo_parts[*]:$index}" + fi # Add the repository into sources.list.d - ynh_add_repo --uri="$uri" --suite="$suite" --component="$component" --name="$name" $append + ynh_add_repo --uri="$uri" --suite="$suite" --component="$component" --name="$name" $append $trusted # Pin the new repo with the default priority, so it won't be used for upgrades. # Build $pin from the uri without http and any sub path @@ -484,7 +506,7 @@ ynh_install_extra_repo() { ynh_pin_repo --package="*" --pin="origin \"$pin\"" $priority --name="$name" $append # Get the public key for the repo - if [ -n "$key" ]; then + if [ -n "$key" ] && [[ "$key" != "trusted=yes" ]]; then mkdir --parents "/etc/apt/trusted.gpg.d" # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) wget --timeout 900 --quiet "$key" --output-document=- | gpg --dearmor | $wget_append /etc/apt/trusted.gpg.d/$name.gpg >/dev/null @@ -537,6 +559,7 @@ ynh_remove_extra_repo() { # | arg: -c, --component= - Component of the repository. # | arg: -n, --name= - Name for the files for this repo, $app as default value. # | arg: -a, --append - Do not overwrite existing files. +# | arg: -t, --trusted - Add trusted=yes to the repository (not recommended) # # Example for a repo like deb http://forge.yunohost.org/debian/ stretch stable # uri suite component @@ -545,27 +568,34 @@ ynh_remove_extra_repo() { # Requires YunoHost version 3.8.1 or higher. ynh_add_repo() { # Declare an array to define the options of this helper. - local legacy_args=uscna - local -A args_array=([u]=uri= [s]=suite= [c]=component= [n]=name= [a]=append) + local legacy_args=uscnat + local -A args_array=([u]=uri= [s]=suite= [c]=component= [n]=name= [a]=append [t]=trusted) local uri local suite local component local name local append + local trusted # Manage arguments with getopts ynh_handle_getopts_args "$@" name="${name:-$app}" append=${append:-0} + trusted=${trusted:-0} if [ $append -eq 1 ]; then append="tee --append" else append="tee" fi + if [[ "$trusted" -eq 1 ]]; then + trust="[trusted=yes]" + else + trust="" + fi mkdir --parents "/etc/apt/sources.list.d" # Add the new repo in sources.list.d - echo "deb $uri $suite $component" \ + echo "deb $trust $uri $suite $component" \ | $append "/etc/apt/sources.list.d/$name.list" } diff --git a/helpers/backup b/helpers/helpers.v1.d/backup similarity index 99% rename from helpers/backup rename to helpers/helpers.v1.d/backup index ade3ce5e5..a596ac9e0 100644 --- a/helpers/backup +++ b/helpers/helpers.v1.d/backup @@ -417,6 +417,8 @@ ynh_backup_archive_exists() { # Make a backup in case of failed upgrade # +# [packagingv1] +# # usage: ynh_backup_before_upgrade # # Usage in a package script: @@ -465,6 +467,8 @@ ynh_backup_before_upgrade() { # Restore a previous backup if the upgrade process failed # +# [packagingv1] +# # usage: ynh_restore_upgradebackup # # Usage in a package script: diff --git a/helpers/helpers.v1.d/composer b/helpers/helpers.v1.d/composer new file mode 100644 index 000000000..506ab8713 --- /dev/null +++ b/helpers/helpers.v1.d/composer @@ -0,0 +1,82 @@ +#!/bin/bash + +readonly YNH_DEFAULT_COMPOSER_VERSION=1.10.17 +# Declare the actual composer version to use. +# A packager willing to use another version of composer can override the variable into its _common.sh. +YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} + +# Execute a command with Composer +# +# 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 $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=) + local phpversion + local workdir + local commands + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + 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 \ + -d "$workdir" --no-interaction --no-ansi 2>&1 +} + +# Install and initialize Composer in the given directory +# +# 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 $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=) + local phpversion + local workdir + local install_args + local composerversion + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + 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}" + + curl -sS https://getcomposer.org/installer \ + | COMPOSER_HOME="$workdir/.composer" \ + php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \ + || ynh_die --message="Unable to install Composer." + + # install dependencies + ynh_composer_exec --phpversion="${phpversion}" --workdir="$workdir" --commands="install --no-dev $install_args" \ + || ynh_die --message="Unable to install core dependencies with Composer." +} diff --git a/helpers/config b/helpers/helpers.v1.d/config similarity index 86% rename from helpers/config rename to helpers/helpers.v1.d/config index 77f118c5f..de35c7744 100644 --- a/helpers/config +++ b/helpers/helpers.v1.d/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@__INSTALL_DIR__@$install_dir@ | 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@__INSTALL_DIR__@$install_dir@ | 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@__INSTALL_DIR__@$install_dir@ | 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,7 +73,7 @@ _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@__INSTALL_DIR__@$install_dir@ | 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" @@ -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@__INSTALL_DIR__@$install_dir@ | 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 @@ -108,12 +108,12 @@ _ynh_app_config_apply_one() { else local bind_after="" local bind_key_="$(echo "$bind" | cut -d: -f1)" - bind_key_=${bind_key_:-$short_setting} if [[ "$bind_key_" == *">"* ]]; then bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" fi - local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@$install_dir@ | sed s@__FINALPATH__@$final_path@ | sed s/__APP__/$app/)" + bind_key_=${bind_key_:-$short_setting} + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s@__FINALPATH__@${final_path:-}@ | sed s/__APP__/$app/)" ynh_backup_if_checksum_is_different --file="$bind_file" ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}" @@ -139,21 +139,49 @@ loaded_toml = toml.loads(file_content, _dict=OrderedDict) for panel_name, panel in loaded_toml.items(): if not isinstance(panel, dict): continue + bind_panel = panel.get('bind') for section_name, section in panel.items(): if not isinstance(section, dict): continue + bind_section = section.get('bind') + if not bind_section: + bind_section = bind_panel + elif bind_section[-1] == ":" and bind_panel and ":" in bind_panel: + regex, bind_panel_file = bind_panel.split(":") + if ">" in bind_section: + bind_section = bind_section + bind_panel_file + else: + bind_section = regex + bind_section + bind_panel_file + for name, param in section.items(): if not isinstance(param, dict): continue - print(';'.join([ + + bind = param.get('bind') + + if not bind: + if bind_section: + bind = bind_section + else: + bind = 'settings' + elif bind[-1] == ":" and bind_section and ":" in bind_section: + regex, bind_file = bind_section.split(":") + if ">" in bind: + bind = bind + bind_file + else: + bind = regex + bind + bind_file + if bind == "settings" and param.get('type', 'string') == 'file': + bind = 'null' + + print('|'.join([ name, param.get('type', 'string'), - param.get('bind', 'settings' if param.get('type', 'string') != 'file' else 'null') + bind ])) EOL ) for line in $lines; do # Split line into short_setting, type and bind - IFS=';' read short_setting type bind <<<"$line" + IFS='|' read short_setting type bind <<<"$line" binds[${short_setting}]="$bind" types[${short_setting}]="$type" file_hash[${short_setting}]="" @@ -176,8 +204,7 @@ _ynh_app_config_show() { ynh_return "${short_setting}:" ynh_return "$(echo "${old[$short_setting]}" | sed 's/^/ /g')" else - ynh_return "${short_setting}: "'"'"$(echo "${old[$short_setting]}" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\n\n/g')"'"' - + ynh_return "${short_setting}: '$(echo "${old[$short_setting]}" | sed "s/'/''/g" | sed ':a;N;$!ba;s/\n/\n\n/g')'" fi fi done diff --git a/helpers/fail2ban b/helpers/helpers.v1.d/fail2ban similarity index 81% rename from helpers/fail2ban rename to helpers/helpers.v1.d/fail2ban index 31f55b312..3b05c907a 100644 --- a/helpers/fail2ban +++ b/helpers/helpers.v1.d/fail2ban @@ -8,8 +8,6 @@ # | arg: -m, --max_retry= - Maximum number of retries allowed before banning IP address - default: 3 # | arg: -p, --ports= - Ports blocked for a banned IP address - default: http,https # -# ----------------------------------------------------------------------------- -# # usage 2: ynh_add_fail2ban_config --use_template # | arg: -t, --use_template - Use this helper in template mode # @@ -42,9 +40,7 @@ # ignoreregex = # ``` # -# ----------------------------------------------------------------------------- -# -# Note about the "failregex" option: +# ##### Note about the "failregex" option: # # regex to match the password failure messages in the logfile. The host must be # matched by a group named "`host`". The tag "``" can be used for standard @@ -53,8 +49,6 @@ # You can find some more explainations about how to make a regex here : # https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters # -# Note that the logfile need to exist before to call this helper !! -# # To validate your regex you can test with this command: # ``` # fail2ban-regex /var/log/YOUR_LOG_FILE_PATH /etc/fail2ban/filter.d/YOUR_APP.conf @@ -76,7 +70,7 @@ ynh_add_fail2ban_config() { ports=${ports:-http,https} use_template="${use_template:-0}" - if [ $use_template -ne 1 ]; then + if [ "$use_template" -ne 1 ]; then # Usage 1, no template. Build a config file from scratch. test -n "$logpath" || ynh_die --message="ynh_add_fail2ban_config expects a logfile path as first argument and received nothing." test -n "$failregex" || ynh_die --message="ynh_add_fail2ban_config expects a failure regex as second argument and received nothing." @@ -88,7 +82,7 @@ port = __PORTS__ filter = __APP__ logpath = __LOGPATH__ maxretry = __MAX_RETRY__ -" >$YNH_APP_BASEDIR/conf/f2b_jail.conf +" >"$YNH_APP_BASEDIR/conf/f2b_jail.conf" echo " [INCLUDES] @@ -96,13 +90,30 @@ before = common.conf [Definition] failregex = __FAILREGEX__ ignoreregex = -" >$YNH_APP_BASEDIR/conf/f2b_filter.conf +" >"$YNH_APP_BASEDIR/conf/f2b_filter.conf" fi ynh_add_config --template="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 + # if "$logpath" doesn't exist (as if using --use_template argument), assign + # "$logpath" using the one in the previously generated fail2ban conf file + if [ -z "${logpath:-}" ]; then + # the first sed deletes possibles spaces and the second one extract the path + logpath=$(grep "^logpath" "/etc/fail2ban/jail.d/$app.conf" | sed "s/ //g" | sed "s/logpath=//g") + fi + + # Create the folder and logfile if they doesn't exist, + # as fail2ban require an existing logfile before configuration + mkdir -p "/var/log/$app" + if [ ! -f "$logpath" ]; then + touch "$logpath" + fi + # Make sure log folder's permissions are correct + chown -R "$app:$app" "/var/log/$app" + chmod -R u=rwX,g=rX,o= "/var/log/$app" + + ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) fail2ban.service" --log_path=systemd local fail2ban_error="$(journalctl --no-hostname --unit=fail2ban | tail --lines=50 | grep "WARNING.*$app.*")" if [[ -n "$fail2ban_error" ]]; then diff --git a/helpers/getopts b/helpers/helpers.v1.d/getopts similarity index 97% rename from helpers/getopts rename to helpers/helpers.v1.d/getopts index e912220e4..f9ef5dc0b 100644 --- a/helpers/getopts +++ b/helpers/helpers.v1.d/getopts @@ -77,9 +77,9 @@ ynh_handle_getopts_args() { # And replace long option (value of the option_flag) by the short option, the option_flag itself # (e.g. for [u]=user, --user will be -u) # Replace long option with = (match the beginning of the argument) - arguments[arg]="$(echo "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]}/-${option_flag} /")" + arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]}/-${option_flag} /")" # And long option without = (match the whole line) - arguments[arg]="$(echo "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]%=}$/-${option_flag} /")" + arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]%=}$/-${option_flag} /")" done done diff --git a/helpers/helpers.v1.d/go b/helpers/helpers.v1.d/go new file mode 100644 index 000000000..86b2b83a8 --- /dev/null +++ b/helpers/helpers.v1.d/go @@ -0,0 +1,241 @@ +#!/bin/bash + +ynh_go_try_bash_extension() { + if [ -x src/configure ]; then + src/configure && make -C src || { + ynh_print_info --message="Optional bash extension failed to build, but things will still work normally." + } + fi +} + +goenv_install_dir="/opt/goenv" +go_version_path="$goenv_install_dir/versions" +# goenv_ROOT is the directory of goenv, it needs to be loaded as a environment variable. +export GOENV_ROOT="$goenv_install_dir" + +# Load the version of Go for an app, and set variables. +# +# ynh_use_go has to be used in any app scripts before using Go for the first time. +# This helper will provide alias and variables to use in your scripts. +# +# To use gem or Go, use the alias `ynh_gem` and `ynh_go` +# Those alias will use the correct version installed for the app +# For example: use `ynh_gem install` instead of `gem install` +# +# With `sudo` or `ynh_exec_as`, use instead the fallback variables `$ynh_gem` and `$ynh_go` +# And propagate $PATH to sudo with $ynh_go_load_path +# Exemple: `ynh_exec_as $app $ynh_go_load_path $ynh_gem install` +# +# $PATH contains the path of the requested version of Go. +# However, $PATH is duplicated into $go_path to outlast any manipulation of $PATH +# You can use the variable `$ynh_go_load_path` to quickly load your Go version +# in $PATH for an usage into a separate script. +# Exemple: `$ynh_go_load_path $install_dir/script_that_use_gem.sh` +# +# +# Finally, to start a Go service with the correct version, 2 solutions +# Either the app is dependent of Go or gem, but does not called it directly. +# In such situation, you need to load PATH +# `Environment="__YNH_GO_LOAD_PATH__"` +# `ExecStart=__INSTALL_DIR__/my_app` +# You will replace __YNH_GO_LOAD_PATH__ with $ynh_go_load_path +# +# Or Go start the app directly, then you don't need to load the PATH variable +# `ExecStart=__YNH_GO__ my_app run` +# You will replace __YNH_GO__ with $ynh_go +# +# +# one other variable is also available +# - $go_path: The absolute path to Go binaries for the chosen version. +# +# usage: ynh_use_go +# +# Requires YunoHost version 3.2.2 or higher. +ynh_use_go () { + go_version=$(ynh_app_setting_get --app=$app --key=go_version) + + # Get the absolute path of this version of Go + go_path="$go_version_path/$go_version/bin" + + # Allow alias to be used into bash script + shopt -s expand_aliases + + # Create an alias for the specific version of Go and a variable as fallback + ynh_go="$go_path/go" + alias ynh_go="$ynh_go" + + # Load the path of this version of Go in $PATH + if [[ :$PATH: != *":$go_path"* ]]; then + PATH="$go_path:$PATH" + fi + # Create an alias to easily load the PATH + ynh_go_load_path="PATH=$PATH" + + # Sets the local application-specific Go version + pushd $install_dir + $goenv_install_dir/bin/goenv local $go_version + popd +} + +# Install a specific version of Go +# +# ynh_install_go will install the version of Go provided as argument by using goenv. +# +# This helper creates a /etc/profile.d/goenv.sh that configures PATH environment for goenv +# for every LOGIN user, hence your user must have a defined shell (as opposed to /usr/sbin/nologin) +# +# Don't forget to execute go-dependent command in a login environment +# (e.g. sudo --login option) +# When not possible (e.g. in systemd service definition), please use direct path +# to goenv shims (e.g. $goenv_ROOT/shims/bundle) +# +# usage: ynh_install_go --go_version=go_version +# | arg: -v, --go_version= - Version of go to install. +# +# Requires YunoHost version 3.2.2 or higher. +ynh_install_go () { + # Declare an array to define the options of this helper. + local legacy_args=v + local -A args_array=( [v]=go_version= ) + local go_version + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Load goenv path in PATH + local CLEAR_PATH="$goenv_install_dir/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Go prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Move an existing Go binary, to avoid to block goenv + test -x /usr/bin/go && mv /usr/bin/go /usr/bin/go_goenv + + # Install or update goenv + mkdir -p $goenv_install_dir + pushd "$goenv_install_dir" + if ! [ -x "$goenv_install_dir/bin/goenv" ]; then + ynh_print_info --message="Downloading goenv..." + git init -q + git remote add origin https://github.com/syndbg/goenv.git + else + ynh_print_info --message="Updating goenv..." + fi + git fetch -q --tags --prune origin + local git_latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)") + git checkout -q "$git_latest_tag" + ynh_go_try_bash_extension + goenv=$goenv_install_dir/bin/goenv + popd + + # Install or update xxenv-latest + goenv_latest_dir="$goenv_install_dir/plugins/xxenv-latest" + mkdir -p "$goenv_latest_dir" + pushd "$goenv_latest_dir" + if ! [ -x "$goenv_latest_dir/bin/goenv-latest" ]; then + ynh_print_info --message="Downloading xxenv-latest..." + git init -q + git remote add origin https://github.com/momo-lab/xxenv-latest.git + else + ynh_print_info --message="Updating xxenv-latest..." + fi + git fetch -q --tags --prune origin + local git_latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)") + git checkout -q "$git_latest_tag" + popd + + # Enable caching + mkdir -p "${goenv_install_dir}/cache" + + # Create shims directory if needed + mkdir -p "${goenv_install_dir}/shims" + + # Restore /usr/local/bin in PATH + PATH=$CLEAR_PATH + + # And replace the old Go binary + test -x /usr/bin/go_goenv && mv /usr/bin/go_goenv /usr/bin/go + + # Install the requested version of Go + local final_go_version=$("$goenv_latest_dir/bin/goenv-latest" --print "$go_version") + ynh_print_info --message="Installation of Go-$final_go_version" + goenv install --skip-existing "$final_go_version" + + # Store go_version into the config of this app + ynh_app_setting_set --app="$app" --key="go_version" --value="$final_go_version" + + # Cleanup Go versions + ynh_cleanup_go + + # Set environment for Go users + echo "#goenv +export GOENV_ROOT=$goenv_install_dir +export PATH=\"$goenv_install_dir/bin:$PATH\" +eval \"\$(goenv init -)\" +#goenv" > /etc/profile.d/goenv.sh + + # Load the environment + eval "$(goenv init -)" +} + +# Remove the version of Go used by the app. +# +# This helper will also cleanup Go versions +# +# usage: ynh_remove_go +ynh_remove_go () { + local go_version=$(ynh_app_setting_get --app="$app" --key="go_version") + + # Load goenv path in PATH + local CLEAR_PATH="$goenv_install_dir/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Go prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Remove the line for this app + ynh_app_setting_delete --app="$app" --key="go_version" + + # Cleanup Go versions + ynh_cleanup_go +} + +# Remove no more needed versions of Go used by the app. +# +# This helper will check what Go version are no more required, +# and uninstall them +# If no app uses Go, goenv will be also removed. +# +# usage: ynh_cleanup_go +ynh_cleanup_go () { + + # List required Go versions + local installed_apps=$(yunohost app list --output-as json --quiet | jq -r .apps[].id) + local required_go_versions="" + for installed_app in $installed_apps + do + local installed_app_go_version=$(ynh_app_setting_get --app=$installed_app --key="go_version") + if [[ $installed_app_go_version ]] + then + required_go_versions="${installed_app_go_version}\n${required_go_versions}" + fi + done + + # Remove no more needed Go versions + local installed_go_versions=$(goenv versions --bare --skip-aliases | grep -Ev '/') + for installed_go_version in $installed_go_versions + do + if ! `echo ${required_go_versions} | grep "${installed_go_version}" 1>/dev/null 2>&1` + then + ynh_print_info --message="Removing of Go-$installed_go_version" + $goenv_install_dir/bin/goenv uninstall --force "$installed_go_version" + fi + done + + # If none Go version is required + if [[ ! $required_go_versions ]] + then + # Remove goenv environment configuration + ynh_print_info --message="Removing of goenv" + ynh_secure_remove --file="$goenv_install_dir" + ynh_secure_remove --file="/etc/profile.d/goenv.sh" + fi +} diff --git a/helpers/hardware b/helpers/helpers.v1.d/hardware similarity index 99% rename from helpers/hardware rename to helpers/helpers.v1.d/hardware index 3ccf7ffe8..091f023f6 100644 --- a/helpers/hardware +++ b/helpers/helpers.v1.d/hardware @@ -2,6 +2,8 @@ # Get the total or free amount of RAM+swap on the system # +# [packagingv1] +# # usage: ynh_get_ram [--free|--total] [--ignore_swap|--only_swap] # | arg: -f, --free - Count free RAM+swap # | arg: -t, --total - Count total RAM+swap @@ -63,6 +65,8 @@ ynh_get_ram() { # Return 0 or 1 depending if the system has a given amount of RAM+swap free or total # +# [packagingv1] +# # usage: ynh_require_ram --required=RAM [--free|--total] [--ignore_swap|--only_swap] # | arg: -r, --required= - The amount to require, in MB # | arg: -f, --free - Count free RAM+swap diff --git a/helpers/logging b/helpers/helpers.v1.d/logging similarity index 94% rename from helpers/logging rename to helpers/helpers.v1.d/logging index ab5d564aa..accb8f9b0 100644 --- a/helpers/logging +++ b/helpers/helpers.v1.d/logging @@ -186,6 +186,26 @@ ynh_exec_fully_quiet() { fi } +# Execute a command and redirect stderr in /dev/null. Print stderr on error. +# +# usage: ynh_exec_and_print_stderr_only_if_error your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_and_print_stderr_only_if_error +# +# Requires YunoHost version 11.2 or higher. +ynh_exec_and_print_stderr_only_if_error() { + logfile="$(mktemp)" + rc=0 + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" 2> "$logfile" || rc="$?" + if (( rc != 0 )); then + ynh_exec_warn cat "$logfile" + ynh_secure_remove "$logfile" + return "$rc" + fi +} + # Remove any logs for all the following commands. # # usage: ynh_print_OFF @@ -248,7 +268,7 @@ ynh_script_progression() { # Re-disable xtrace, ynh_handle_getopts_args set it back set +o xtrace # set +x weight=${weight:-1} - + # Always activate time when running inside CI tests if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then time=${time:-1} diff --git a/helpers/helpers.v1.d/logrotate b/helpers/helpers.v1.d/logrotate new file mode 100644 index 000000000..efc1137c1 --- /dev/null +++ b/helpers/helpers.v1.d/logrotate @@ -0,0 +1,103 @@ +#!/bin/bash + +FIRST_CALL_TO_LOGROTATE="true" + +# Use logrotate to manage the logfile +# +# usage: ynh_use_logrotate [--logfile=/log/file] [--specific_user=user/group] +# | arg: -l, --logfile= - absolute path of logfile +# | arg: -u, --specific_user= - run logrotate as the specified user and group. If not specified logrotate is runned as root. +# +# If no `--logfile` is provided, `/var/log/$app` will be used as default. +# `logfile` can point to a directory or a file. +# +# Requires YunoHost version 2.6.4 or higher. +ynh_use_logrotate() { + + # Stupid patch to ignore legacy --non-append and --nonappend + # which was never properly understood and improperly used and kind of bullshit + local all_args=( ${@} ) + for I in $(seq 0 $(($# - 1))) + do + if [[ "${all_args[$I]}" == "--non-append" ]] || [[ "${all_args[$I]}" == "--nonappend" ]] + then + unset all_args[$I] + fi + done + set -- "${all_args[@]}" + + # Argument parsing + local legacy_args=lu + local -A args_array=([l]=logfile= [u]=specific_user=) + local logfile + local specific_user + ynh_handle_getopts_args "$@" + logfile="${logfile:-}" + specific_user="${specific_user:-}" + + set -o noglob + if [[ -z "$logfile" ]]; then + logfile="/var/log/${app}/*.log" + elif [[ "${logfile##*.}" != "log" ]] && [[ "${logfile##*.}" != "txt" ]]; then + logfile="$logfile/*.log" + fi + set +o noglob + + for stuff in $logfile + do + mkdir --parents $(dirname "$stuff") + done + + local su_directive="" + if [[ -n "$specific_user" ]]; then + su_directive="su ${specific_user%/*} ${specific_user#*/}" + fi + + local tempconf="$(mktemp)" + cat << EOF >$tempconf +$logfile { + # Rotate if the logfile exceeds 100Mo + size 100M + # Keep 12 old log maximum + rotate 12 + # Compress the logs with gzip + compress + # Compress the log at the next cycle. So keep always 2 non compressed logs + delaycompress + # Copy and truncate the log to allow to continue write on it. Instead of moving the log. + copytruncate + # Do not trigger an error if the log is missing + missingok + # Do not rotate if the log is empty + notifempty + # Keep old logs in the same dir + noolddir + $su_directive +} +EOF + + if [[ "$FIRST_CALL_TO_LOGROTATE" == "true" ]] + then + cat $tempconf > /etc/logrotate.d/$app + else + cat $tempconf >> /etc/logrotate.d/$app + fi + + FIRST_CALL_TO_LOGROTATE="false" + + # Make sure permissions are correct (otherwise the config file could be ignored and the corresponding logs never rotated) + chmod 644 "/etc/logrotate.d/$app" + mkdir -p "/var/log/$app" + chmod 750 "/var/log/$app" +} + +# Remove the app's logrotate config. +# +# usage: ynh_remove_logrotate +# +# Requires YunoHost version 2.6.4 or higher. +ynh_remove_logrotate() { + if [ -e "/etc/logrotate.d/$app" ]; then + rm "/etc/logrotate.d/$app" + fi +} diff --git a/helpers/helpers.v1.d/mongodb b/helpers/helpers.v1.d/mongodb new file mode 100644 index 000000000..8736aad31 --- /dev/null +++ b/helpers/helpers.v1.d/mongodb @@ -0,0 +1,355 @@ +#!/bin/bash + +# Execute a mongo command +# +# example: ynh_mongo_exec --command='db.getMongo().getDBNames().indexOf("wekan")' +# example: ynh_mongo_exec --command="db.getMongo().getDBNames().indexOf(\"wekan\")" +# +# usage: ynh_mongo_exec [--user=user] [--password=password] [--authenticationdatabase=authenticationdatabase] [--database=database] [--host=host] [--port=port] --command="command" [--eval] +# | arg: -u, --user= - The user name to connect as +# | arg: -p, --password= - The user password +# | arg: -d, --authenticationdatabase= - The authenticationdatabase to connect to +# | arg: -d, --database= - The database to connect to +# | arg: -h, --host= - The host to connect to +# | arg: -P, --port= - The port to connect to +# | arg: -c, --command= - The command to evaluate +# | arg: -e, --eval - Evaluate instead of execute the command. +# +# +ynh_mongo_exec() { + # Declare an array to define the options of this helper. + local legacy_args=upadhPce + local -A args_array=( [u]=user= [p]=password= [a]=authenticationdatabase= [d]=database= [h]=host= [P]=port= [c]=command= [e]=eval ) + local user + local password + local authenticationdatabase + local database + local host + local port + local command + local eval + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + user="${user:-}" + password="${password:-}" + authenticationdatabase="${authenticationdatabase:-}" + database="${database:-}" + host="${host:-}" + port="${port:-}" + eval=${eval:-0} + + # If user is provided + if [ -n "$user" ] + then + user="--username=$user" + + # If password is provided + if [ -n "$password" ] + then + password="--password=$password" + fi + + # If authenticationdatabase is provided + if [ -n "$authenticationdatabase" ] + then + authenticationdatabase="--authenticationDatabase=$authenticationdatabase" + else + authenticationdatabase="--authenticationDatabase=admin" + fi + else + password="" + authenticationdatabase="" + fi + + # If host is provided + if [ -n "$host" ] + then + host="--host=$host" + fi + + # If port is provided + if [ -n "$port" ] + then + port="--port=$port" + fi + + # If eval is not provided + if [ $eval -eq 0 ] + then + # If database is provided + if [ -n "$database" ] + then + database="use $database" + else + database="" + fi + + mongosh --quiet --username $user --password $password --authenticationDatabase $authenticationdatabase --host $host --port $port < ./dump.bson +# +# usage: ynh_mongo_dump_db --database=database +# | arg: -d, --database= - The database name to dump +# | ret: the mongodump output +# +# +ynh_mongo_dump_db() { + # Declare an array to define the options of this helper. + local legacy_args=d + local -A args_array=( [d]=database= ) + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + mongodump --quiet --db="$database" --archive +} + +# Create a user +# +# [internal] +# +# usage: ynh_mongo_create_user --db_user=user --db_pwd=pwd --db_name=name +# | arg: -u, --db_user= - The user name to create +# | arg: -p, --db_pwd= - The password to identify user by +# | arg: -n, --db_name= - Name of the database to grant privilegies +# +# +ynh_mongo_create_user() { + # Declare an array to define the options of this helper. + local legacy_args=unp + local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= ) + local db_user + local db_name + local db_pwd + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Create the user and set the user as admin of the db + ynh_mongo_exec --database="$db_name" --command='db.createUser( { user: "'${db_user}'", pwd: "'${db_pwd}'", roles: [ { role: "readWrite", db: "'${db_name}'" } ] } );' + + # Add clustermonitoring rights + ynh_mongo_exec --database="$db_name" --command='db.grantRolesToUser("'${db_user}'",[{ role: "clusterMonitor", db: "admin" }]);' +} + +# Check if a mongo database exists +# +# usage: ynh_mongo_database_exists --database=database +# | arg: -d, --database= - The database for which to check existence +# | exit: Return 1 if the database doesn't exist, 0 otherwise +# +# +ynh_mongo_database_exists() { + # Declare an array to define the options of this helper. + local legacy_args=d + local -A args_array=([d]=database=) + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if [ $(ynh_mongo_exec --command='db.getMongo().getDBNames().indexOf("'${database}'")' --eval) -lt 0 ] + then + return 1 + else + return 0 + fi +} + +# Restore a database +# +# example: ynh_mongo_restore_db --database=wekan < ./dump.bson +# +# usage: ynh_mongo_restore_db --database=database +# | arg: -d, --database= - The database name to restore +# +# +ynh_mongo_restore_db() { + # Declare an array to define the options of this helper. + local legacy_args=d + local -A args_array=( [d]=database= ) + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + mongorestore --quiet --db="$database" --archive +} + +# Drop a user +# +# [internal] +# +# usage: ynh_mongo_drop_user --db_user=user --db_name=name +# | arg: -u, --db_user= - The user to drop +# | arg: -n, --db_name= - Name of the database +# +# +ynh_mongo_drop_user() { + # Declare an array to define the options of this helper. + local legacy_args=un + local -A args_array=( [u]=db_user= [n]=db_name= ) + local db_user + local db_name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + ynh_mongo_exec --database="$db_name" --command='db.dropUser("'$db_user'", {w: "majority", wtimeout: 5000})' +} + +# Create a database, an user and its password. Then store the password in the app's config +# +# usage: ynh_mongo_setup_db --db_user=user --db_name=name [--db_pwd=pwd] +# | arg: -u, --db_user= - Owner of the database +# | arg: -n, --db_name= - Name of the database +# | arg: -p, --db_pwd= - Password of the database. If not provided, a password will be generated +# +# After executing this helper, the password of the created database will be available in $db_pwd +# It will also be stored as "mongopwd" into the app settings. +# +# +ynh_mongo_setup_db() { + # Declare an array to define the options of this helper. + local legacy_args=unp + local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= ) + local db_user + local db_name + db_pwd="" + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local new_db_pwd=$(ynh_string_random) # Generate a random password + # If $db_pwd is not provided, use new_db_pwd instead for db_pwd + db_pwd="${db_pwd:-$new_db_pwd}" + + # Create the user and grant access to the database + ynh_mongo_create_user --db_user="$db_user" --db_pwd="$db_pwd" --db_name="$db_name" + + # Store the password in the app's config + ynh_app_setting_set --app=$app --key=db_pwd --value=$db_pwd +} + +# Remove a database if it exists, and the associated user +# +# usage: ynh_mongo_remove_db --db_user=user --db_name=name +# | arg: -u, --db_user= - Owner of the database +# | arg: -n, --db_name= - Name of the database +# +# +ynh_mongo_remove_db() { + # Declare an array to define the options of this helper. + local legacy_args=un + local -A args_array=( [u]=db_user= [n]=db_name= ) + local db_user + local db_name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if ynh_mongo_database_exists --database=$db_name; then # Check if the database exists + ynh_mongo_drop_db --database=$db_name # Remove the database + else + ynh_print_warn --message="Database $db_name not found" + fi + + # Remove mongo user if it exists + ynh_mongo_drop_user --db_user=$db_user --db_name=$db_name +} + +# Install MongoDB and integrate MongoDB service in YunoHost +# +# usage: ynh_install_mongo [--mongo_version=mongo_version] +# | arg: -m, --mongo_version= - Version of MongoDB to install +# +# +ynh_install_mongo() { + # Declare an array to define the options of this helper. + local legacy_args=m + local -A args_array=([m]=mongo_version=) + local mongo_version + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + mongo_version="${mongo_version:-$YNH_MONGO_VERSION}" + + ynh_print_info --message="Installing MongoDB Community Edition ..." + local mongo_debian_release=$(ynh_get_debian_release) + + if [[ "$(grep '^flags' /proc/cpuinfo | uniq)" != *"avx"* && "$mongo_version" != "4.4" ]]; then + ynh_print_warn --message="Installing Mongo 4.4 as $mongo_version is not compatible with your cpu (see https://docs.mongodb.com/manual/administration/production-notes/#x86_64)." + mongo_version="4.4" + fi + if [[ "$mongo_version" == "4.4" ]]; then + ynh_print_warn --message="Switched to buster install as Mongo 4.4 is not compatible with $mongo_debian_release." + mongo_debian_release=buster + fi + + ynh_install_extra_app_dependencies --repo="deb http://repo.mongodb.org/apt/debian $mongo_debian_release/mongodb-org/$mongo_version main" --package="mongodb-org mongodb-org-server mongodb-org-tools mongodb-mongosh" --key="https://www.mongodb.org/static/pgp/server-$mongo_version.asc" + mongodb_servicename=mongod + + # Make sure MongoDB is started and enabled + systemctl enable $mongodb_servicename --quiet + systemctl daemon-reload --quiet + ynh_systemd_action --service_name=$mongodb_servicename --action=restart --line_match="aiting for connections" --log_path="/var/log/mongodb/$mongodb_servicename.log" + + # Integrate MongoDB service in YunoHost + yunohost service add $mongodb_servicename --description="MongoDB daemon" --log="/var/log/mongodb/$mongodb_servicename.log" + + # Store mongo_version into the config of this app + ynh_app_setting_set --app=$app --key=mongo_version --value=$mongo_version +} + +# Remove MongoDB +# Only remove the MongoDB service integration in YunoHost for now +# if MongoDB package as been removed +# +# usage: ynh_remove_mongo +# +# +ynh_remove_mongo() { + # Only remove the mongodb service if it is not installed. + if ! ynh_package_is_installed --package="mongodb*" + then + ynh_print_info --message="Removing MongoDB service..." + mongodb_servicename=mongod + # Remove the mongodb service + yunohost service remove $mongodb_servicename + ynh_secure_remove --file="/var/lib/mongodb" + ynh_secure_remove --file="/var/log/mongodb" + fi +} diff --git a/helpers/multimedia b/helpers/helpers.v1.d/multimedia similarity index 99% rename from helpers/multimedia rename to helpers/helpers.v1.d/multimedia index 05479a84a..c860ae49f 100644 --- a/helpers/multimedia +++ b/helpers/helpers.v1.d/multimedia @@ -44,9 +44,9 @@ ynh_multimedia_build_main_dir() { ## Application des droits étendus sur le dossier multimedia. # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: - setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" + setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" || true # 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" + setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" || true # 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" || true } diff --git a/helpers/mysql b/helpers/helpers.v1.d/mysql similarity index 94% rename from helpers/mysql rename to helpers/helpers.v1.d/mysql index eade5804e..af7dec42e 100644 --- a/helpers/mysql +++ b/helpers/helpers.v1.d/mysql @@ -152,6 +152,8 @@ ynh_mysql_create_user() { # Check if a mysql user exists # +# [internal] +# # usage: ynh_mysql_user_exists --user=user # | arg: -u, --user= - the user for which to check existence # | ret: 0 if the user exists, 1 otherwise. @@ -172,6 +174,19 @@ ynh_mysql_user_exists() { fi } +# Check if a mysql database exists +# +# [internal] +# +# usage: ynh_mysql_database_exists database +# | arg: database - the database for which to check existence +# | exit: Return 1 if the database doesn't exist, 0 otherwise +# +ynh_mysql_database_exists() { + local database=$1 + mysqlshow | grep -qE "^|\s+$database\s+|" +} + # Drop a user # # [internal] @@ -186,6 +201,8 @@ ynh_mysql_drop_user() { # Create a database, an user and its password. Then store the password in the app's config # +# [packagingv1] +# # usage: ynh_mysql_setup_db --db_user=user --db_name=name [--db_pwd=pwd] # | arg: -u, --db_user= - Owner of the database # | arg: -n, --db_name= - Name of the database @@ -219,6 +236,8 @@ ynh_mysql_setup_db() { # Remove a database if it exists, and the associated user # +# [packagingv1] +# # usage: ynh_mysql_remove_db --db_user=user --db_name=name # | arg: -u, --db_user= - Owner of the database # | arg: -n, --db_name= - Name of the database @@ -233,7 +252,7 @@ ynh_mysql_remove_db() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - if mysqlshow | grep -q "^| $db_name "; then + if ynh_mysql_database_exists "$db_name"; then ynh_mysql_drop_db $db_name else ynh_print_warn --message="Database $db_name not found" diff --git a/helpers/network b/helpers/helpers.v1.d/network similarity index 99% rename from helpers/network rename to helpers/helpers.v1.d/network index d6c15060a..bed9dd402 100644 --- a/helpers/network +++ b/helpers/helpers.v1.d/network @@ -2,6 +2,8 @@ # Find a free port and return it # +# [packagingv1] +# # usage: ynh_find_port --port=begin_port # | arg: -p, --port= - port to start to search # | ret: the port number @@ -26,6 +28,8 @@ ynh_find_port() { # Test if a port is available # +# [packagingv1] +# # usage: ynh_find_port --port=XYZ # | arg: -p, --port= - port to check # | ret: 0 if the port is available, 1 if it is already used by another process. diff --git a/helpers/nginx b/helpers/helpers.v1.d/nginx similarity index 64% rename from helpers/nginx rename to helpers/helpers.v1.d/nginx index bb0fe0577..600c70a49 100644 --- a/helpers/nginx +++ b/helpers/helpers.v1.d/nginx @@ -44,35 +44,22 @@ ynh_remove_nginx_config() { } -# Move / regen the nginx config in a change url context +# 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() { + + # Make a backup of the original NGINX config file if manually modified + # (nb: this is possibly different from the same instruction called by + # ynh_add_config inside ynh_add_nginx_config because the path may have + # changed if we're changing the domain too...) 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 + ynh_backup_if_checksum_is_different --file="$old_nginx_conf_path" + ynh_delete_file_checksum --file="$old_nginx_conf_path" + ynh_secure_remove --file="$old_nginx_conf_path" - # 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 + # Regen the nginx conf + ynh_add_nginx_config } - diff --git a/helpers/nodejs b/helpers/helpers.v1.d/nodejs similarity index 76% rename from helpers/nodejs rename to helpers/helpers.v1.d/nodejs index e3ccf82dd..de6d7a43e 100644 --- a/helpers/nodejs +++ b/helpers/helpers.v1.d/nodejs @@ -74,6 +74,8 @@ ynh_use_nodejs() { ynh_node_load_PATH="PATH=$node_PATH" # Same var but in lower case to be compatible with ynh_replace_vars... ynh_node_load_path="PATH=$node_PATH" + # Prevent yet another Node and Corepack madness, with Corepack wanting the user to confirm download of Yarn + export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 } # Install a specific version of nodejs @@ -81,7 +83,7 @@ ynh_use_nodejs() { # ynh_install_nodejs will install the version of node provided as argument by using n. # # usage: ynh_install_nodejs --nodejs_version=nodejs_version -# | arg: -n, --nodejs_version= - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0). The crontab will then handle the update of minor versions when needed. +# | arg: -n, --nodejs_version= - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0). # # `n` (Node version management) uses the `PATH` variable to store the path of the version of node it is going to use. # That's how it changes the version @@ -113,7 +115,7 @@ ynh_install_nodejs() { # 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 + cp "$YNH_HELPERS_DIR/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" @@ -142,14 +144,11 @@ ynh_install_nodejs() { fi # Store the ID of this app and the version of node requested for it - echo "$YNH_APP_INSTANCE_NAME:$nodejs_version" | tee --append "$n_install_dir/ynh_app_version" + echo "$app:$nodejs_version" | tee --append "$n_install_dir/ynh_app_version" # Store nodejs_version into the config of this app ynh_app_setting_set --app=$app --key=nodejs_version --value=$nodejs_version - # Build the update script and set the cronjob - ynh_cron_upgrade_node - ynh_use_nodejs } @@ -166,7 +165,7 @@ ynh_remove_nodejs() { nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version) # Remove the line for this app - sed --in-place "/$YNH_APP_INSTANCE_NAME:$nodejs_version/d" "$n_install_dir/ynh_app_version" + sed --in-place "/$app:$nodejs_version/d" "$n_install_dir/ynh_app_version" # If no other app uses this version of nodejs, remove it. if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version"; then @@ -178,62 +177,5 @@ ynh_remove_nodejs() { ynh_secure_remove --file="$n_install_dir" ynh_secure_remove --file="/usr/local/n" sed --in-place "/N_PREFIX/d" /root/.bashrc - rm --force /etc/cron.daily/node_update fi } - -# Set a cron design to update your node versions -# -# [internal] -# -# This cron will check and update all minor node versions used by your apps. -# -# usage: ynh_cron_upgrade_node -# -# Requires YunoHost version 2.7.12 or higher. -ynh_cron_upgrade_node() { - # Build the update script - cat >"$n_install_dir/node_update.sh" <"/etc/cron.daily/node_update" <> $n_install_dir/node_update.log -EOF - - chmod +x "/etc/cron.daily/node_update" -} diff --git a/helpers/permission b/helpers/helpers.v1.d/permission similarity index 100% rename from helpers/permission rename to helpers/helpers.v1.d/permission diff --git a/helpers/php b/helpers/helpers.v1.d/php similarity index 50% rename from helpers/php rename to helpers/helpers.v1.d/php index 417dbbc61..6be509411 100644 --- a/helpers/php +++ b/helpers/helpers.v1.d/php @@ -1,39 +1,50 @@ #!/bin/bash -readonly YNH_DEFAULT_PHP_VERSION=7.4 +readonly YNH_DEFAULT_PHP_VERSION=8.2 # Declare the actual PHP version to use. # A packager willing to use another version of PHP can override the variable into its _common.sh. YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} # Create a dedicated PHP-FPM config # -# usage 1: ynh_add_fpm_config [--phpversion=7.X] [--use_template] [--package=packages] [--dedicated_service] -# | arg: -v, --phpversion= - Version of PHP to use. -# | arg: -t, --use_template - Use this helper in template mode. -# | arg: -p, --package= - Additionnal PHP packages to install -# | arg: -d, --dedicated_service - Use a dedicated PHP-FPM service instead of the common one. +# usage: ynh_add_fpm_config # -# ----------------------------------------------------------------------------- +# Case 1 (recommended) : your provided a snippet conf/extra_php-fpm.conf # -# usage 2: ynh_add_fpm_config [--phpversion=7.X] --usage=usage --footprint=footprint [--package=packages] [--dedicated_service] -# | arg: -v, --phpversion= - Version of PHP to use. -# | arg: -f, --footprint= - Memory footprint of the service (low/medium/high). +# The actual PHP configuration will be automatically generated, +# and your extra_php-fpm.conf will be appended (typically contains PHP upload limits) +# +# The resulting configuration will be deployed to the appropriate place, /etc/php/$phpversion/fpm/pool.d/$app.conf +# +# Performance-related options in the PHP conf, such as : +# pm.max_children, pm.start_servers, pm.min_spare_servers pm.max_spare_servers +# are computed from two parameters called "usage" and "footprint" which can be set to low/medium/high. (cf details below) +# +# If you wish to tweak those, please initialize the settings `fpm_usage` and `fpm_footprint` +# *prior* to calling this helper. Otherwise, "low" will be used as a default for both values. +# +# Otherwise, if you want the user to have control over these, we encourage to create a config panel +# (which should ultimately be standardized by the core ...) +# +# Case 2 (deprecate) : you provided an entire conf/php-fpm.conf +# +# The configuration will be hydrated, replacing __FOOBAR__ placeholders with $foobar values, etc. +# +# The resulting configuration will be deployed to the appropriate place, /etc/php/$phpversion/fpm/pool.d/$app.conf +# +# ---------------------- +# +# fpm_footprint: Memory footprint of the service (low/medium/high). # low - Less than 20 MB of RAM by pool. # medium - Between 20 MB and 40 MB of RAM by pool. # high - More than 40 MB of RAM by pool. -# Or specify exactly the footprint, the load of the service as MB by pool instead of having a standard value. -# To have this value, use the following command and stress the service. -# watch -n0.5 ps -o user,cmd,%cpu,rss -u APP +# N - Or you can specify a quantitative footprint as MB by pool (use watch -n0.5 ps -o user,cmd,%cpu,rss -u APP) # -# | arg: -u, --usage= - Expected usage of the service (low/medium/high). +# fpm_usage: Expected usage of the service (low/medium/high). # low - Personal usage, behind the SSO. # medium - Low usage, few people or/and publicly accessible. # high - High usage, frequently visited website. # -# | arg: -p, --package= - Additionnal PHP packages to install for a specific version of PHP -# | arg: -d, --dedicated_service - Use a dedicated PHP-FPM service instead of the common one. -# -# # The footprint of the service will be used to defined the maximum footprint we can allow, which is half the maximum RAM. # So it will be used to defined 'pm.max_children' # A lower value for the footprint will allow more children for 'pm.max_children'. And so for @@ -59,27 +70,40 @@ YNH_PHP_VERSION=${YNH_PHP_VERSION:-$YNH_DEFAULT_PHP_VERSION} ynh_add_fpm_config() { local _globalphpversion=${phpversion-:} # Declare an array to define the options of this helper. - local legacy_args=vtufpd - local -A args_array=([v]=phpversion= [t]=use_template [u]=usage= [f]=footprint= [p]=package= [d]=dedicated_service) + local legacy_args=vufg + local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [g]=group=) + local group local phpversion - local use_template local usage local footprint - local package - local dedicated_service # Manage arguments with getopts ynh_handle_getopts_args "$@" - package=${package:-} + group=${group:-} # The default behaviour is to use the template. - use_template="${use_template:-1}" + local autogenconf=false usage="${usage:-}" footprint="${footprint:-}" - if [ -n "$usage" ] || [ -n "$footprint" ]; then - use_template=0 + if [ -n "$usage" ] || [ -n "$footprint" ] || [[ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]]; then + autogenconf=true + + # If no usage provided, default to the value existing in setting ... or to low + local fpm_usage_in_setting=$(ynh_app_setting_get --app=$app --key=fpm_usage) + if [ -z "$usage" ] + then + usage=${fpm_usage_in_setting:-low} + ynh_app_setting_set --app=$app --key=fpm_usage --value=$usage + fi + + # If no footprint provided, default to the value existing in setting ... or to low + local fpm_footprint_in_setting=$(ynh_app_setting_get --app=$app --key=fpm_footprint) + if [ -z "$footprint" ] + then + footprint=${fpm_footprint_in_setting:-low} + ynh_app_setting_set --app=$app --key=fpm_footprint --value=$footprint + fi + fi - # Do not use a dedicated service by default - dedicated_service=${dedicated_service:-0} # Set the default PHP-FPM version by default if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then @@ -103,45 +127,17 @@ ynh_add_fpm_config() { fi fi - # Legacy args (packager should just list their php dependency as regular apt dependencies... - if [ -n "$package" ]; then - # Install the additionnal packages from the default repository - ynh_print_warn --message "Argument --package of ynh_add_fpm_config is deprecated and to be removed in the future" - ynh_install_app_dependencies "$package" - fi - - if [ $dedicated_service -eq 1 ]; then - local fpm_service="${app}-phpfpm" - local fpm_config_dir="/etc/php/$phpversion/dedicated-fpm" - else - local fpm_service="php${phpversion}-fpm" - local fpm_config_dir="/etc/php/$phpversion/fpm" - fi + local fpm_service="php${phpversion}-fpm" + local fpm_config_dir="/etc/php/$phpversion/fpm" # Create the directory for FPM pools mkdir --parents "$fpm_config_dir/pool.d" ynh_app_setting_set --app=$app --key=fpm_config_dir --value="$fpm_config_dir" ynh_app_setting_set --app=$app --key=fpm_service --value="$fpm_service" - ynh_app_setting_set --app=$app --key=fpm_dedicated_service --value="$dedicated_service" ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion - # Migrate from mutual PHP service to dedicated one. - if [ $dedicated_service -eq 1 ]; then - local old_fpm_config_dir="/etc/php/$phpversion/fpm" - # If a config file exist in the common pool, move it. - if [ -e "$old_fpm_config_dir/pool.d/$app.conf" ]; then - ynh_print_info --message="Migrate to a dedicated php-fpm service for $app." - # Create a backup of the old file before migration - ynh_backup_if_checksum_is_different --file="$old_fpm_config_dir/pool.d/$app.conf" - # Remove the old PHP config file - ynh_secure_remove --file="$old_fpm_config_dir/pool.d/$app.conf" - # Reload PHP to release the socket and allow the dedicated service to use it - ynh_systemd_action --service_name=php${phpversion}-fpm --action=reload - fi - fi - - if [ $use_template -eq 1 ]; then + if [ $autogenconf == "false" ]; then # Usage 1, use the template in conf/php-fpm.conf local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" # Make sure now that the template indeed exists @@ -149,19 +145,16 @@ ynh_add_fpm_config() { else # Usage 2, generate a PHP-FPM config file with ynh_get_scalable_phpfpm - # Store settings - ynh_app_setting_set --app=$app --key=fpm_footprint --value=$footprint - ynh_app_setting_set --app=$app --key=fpm_usage --value=$usage - # Define the values to use for the configuration of PHP. ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint + local phpfpm_group=$([[ -n "$group" ]] && echo "$group" || echo "$app") local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" echo " [__APP__] user = __APP__ -group = __APP__ +group = __PHPFPM_GROUP__ chdir = __INSTALL_DIR__ @@ -173,19 +166,19 @@ pm = __PHP_PM__ pm.max_children = __PHP_MAX_CHILDREN__ pm.max_requests = 500 request_terminate_timeout = 1d -" >$phpfpm_path +" >"$phpfpm_path" if [ "$php_pm" = "dynamic" ]; then echo " pm.start_servers = __PHP_START_SERVERS__ pm.min_spare_servers = __PHP_MIN_SPARE_SERVERS__ pm.max_spare_servers = __PHP_MAX_SPARE_SERVERS__ -" >>$phpfpm_path +" >>"$phpfpm_path" elif [ "$php_pm" = "ondemand" ]; then echo " pm.process_idle_timeout = 10s -" >>$phpfpm_path +" >>"$phpfpm_path" fi # Concatene the extra config. @@ -197,56 +190,13 @@ pm.process_idle_timeout = 10s local finalphpconf="$fpm_config_dir/pool.d/$app.conf" ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf" - if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ]; then - ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead." - ynh_add_config --template="php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" - fi - - if [ $dedicated_service -eq 1 ]; then - # Create a dedicated php-fpm.conf for the service - local globalphpconf=$fpm_config_dir/php-fpm-$app.conf - - echo "[global] -pid = /run/php/php__PHPVERSION__-fpm-__APP__.pid -error_log = /var/log/php/fpm-php.__APP__.log -syslog.ident = php-fpm-__APP__ -include = __FINALPHPCONF__ -" >$YNH_APP_BASEDIR/conf/php-fpm-$app.conf - - ynh_add_config --template="php-fpm-$app.conf" --destination="$globalphpconf" - - # Create a config for a dedicated PHP-FPM service for the app - echo "[Unit] -Description=PHP __PHPVERSION__ FastCGI Process Manager for __APP__ -After=network.target - -[Service] -Type=notify -PIDFile=/run/php/php__PHPVERSION__-fpm-__APP__.pid -ExecStart=/usr/sbin/php-fpm__PHPVERSION__ --nodaemonize --fpm-config __GLOBALPHPCONF__ -ExecReload=/bin/kill -USR2 \$MAINPID - -[Install] -WantedBy=multi-user.target -" >$YNH_APP_BASEDIR/conf/$fpm_service - - # Create this dedicated PHP-FPM service - ynh_add_systemd_config --service=$fpm_service --template=$fpm_service - # Integrate the service in YunoHost admin panel - yunohost service add $fpm_service --log /var/log/php/fpm-php.$app.log --description "Php-fpm dedicated to $app" - # Configure log rotate - ynh_use_logrotate --logfile=/var/log/php - # Restart the service, as this service is either stopped or only for this app - ynh_systemd_action --service_name=$fpm_service --action=restart - else - # Validate that the new php conf doesn't break php-fpm entirely - if ! php-fpm${phpversion} --test 2>/dev/null; then - php-fpm${phpversion} --test || true - ynh_secure_remove --file="$finalphpconf" - ynh_die --message="The new configuration broke php-fpm?" - fi - ynh_systemd_action --service_name=$fpm_service --action=reload + # Validate that the new php conf doesn't break php-fpm entirely + if ! php-fpm${phpversion} --test 2>/dev/null; then + php-fpm${phpversion} --test || true + ynh_secure_remove --file="$finalphpconf" + ynh_die --message="The new configuration broke php-fpm?" fi + ynh_systemd_action --service_name=$fpm_service --action=reload } # Remove the dedicated PHP-FPM config @@ -257,8 +207,6 @@ WantedBy=multi-user.target ynh_remove_fpm_config() { local fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) local fpm_service=$(ynh_app_setting_get --app=$app --key=fpm_service) - local dedicated_service=$(ynh_app_setting_get --app=$app --key=fpm_dedicated_service) - dedicated_service=${dedicated_service:-0} # Get the version of PHP used by this app local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) @@ -272,69 +220,7 @@ ynh_remove_fpm_config() { fi ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf" - if [ -e $fpm_config_dir/conf.d/20-$app.ini ]; then - ynh_secure_remove --file="$fpm_config_dir/conf.d/20-$app.ini" - fi - - if [ $dedicated_service -eq 1 ]; then - # Remove the dedicated service PHP-FPM service for the app - ynh_remove_systemd_config --service=$fpm_service - # Remove the global PHP-FPM conf - ynh_secure_remove --file="$fpm_config_dir/php-fpm-$app.conf" - # Remove the service from the list of services known by YunoHost - yunohost service remove $fpm_service - elif ynh_package_is_installed --package="php${phpversion}-fpm"; then - ynh_systemd_action --service_name=$fpm_service --action=reload - fi - - # If the PHP version used is not the default version for YunoHost - # The second part with YNH_APP_PURGE is an ugly hack to guess that we're inside the remove script - # (we don't actually care about its value, we just check its not empty hence it exists) - if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ] && dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then - # Remove app dependencies ... but ideally should happen via an explicit call from packager - ynh_remove_app_dependencies - fi -} - -# Install another version of PHP. -# -# [internal] -# -# Legacy, to be remove on bullseye -# -# usage: ynh_install_php --phpversion=phpversion [--package=packages] -# | arg: -v, --phpversion= - Version of PHP to install. -# | arg: -p, --package= - Additionnal PHP packages to install -# -# Requires YunoHost version 3.8.1 or higher. -ynh_install_php() { - # Declare an array to define the options of this helper. - local legacy_args=vp - local -A args_array=([v]=phpversion= [p]=package=) - local phpversion - local package - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - package=${package:-} - - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]; then - ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION" - fi - - ynh_install_app_dependencies "$package" -} - -# Remove the specific version of PHP used by the app. -# -# [internal] -# -# Legacy, to be remove on bullseye -# -# usage: ynh_remove_php -# -# Requires YunoHost version 3.8.1 or higher. -ynh_remove_php () { - ynh_remove_app_dependencies + ynh_systemd_action --service_name=$fpm_service --action=reload } # Define the values to configure PHP-FPM @@ -473,84 +359,3 @@ ynh_get_scalable_phpfpm() { fi fi } - -readonly YNH_DEFAULT_COMPOSER_VERSION=1.10.17 -# Declare the actual composer version to use. -# A packager willing to use another version of composer can override the variable into its _common.sh. -YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} - -# Execute a command with Composer -# -# 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 $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=) - local phpversion - local workdir - local commands - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - 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 \ - -d "$workdir" --no-interaction --no-ansi 2>&1 -} - -# Install and initialize Composer in the given directory -# -# 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 $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=) - local phpversion - local workdir - local install_args - local composerversion - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - 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}" - - curl -sS https://getcomposer.org/installer \ - | COMPOSER_HOME="$workdir/.composer" \ - php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \ - || ynh_die --message="Unable to install Composer." - - # install dependencies - ynh_composer_exec --phpversion="${phpversion}" --workdir="$workdir" --commands="install --no-dev $install_args" \ - || ynh_die --message="Unable to install core dependencies with Composer." -} diff --git a/helpers/postgresql b/helpers/helpers.v1.d/postgresql similarity index 99% rename from helpers/postgresql rename to helpers/helpers.v1.d/postgresql index 796a36214..56fa41a18 100644 --- a/helpers/postgresql +++ b/helpers/helpers.v1.d/postgresql @@ -1,7 +1,7 @@ #!/bin/bash PSQL_ROOT_PWD_FILE=/etc/yunohost/psql -PSQL_VERSION=13 +PSQL_VERSION=15 # Open a connection as a user # @@ -160,6 +160,8 @@ ynh_psql_create_user() { # Check if a psql user exists # +# [packagingv1] +# # usage: ynh_psql_user_exists --user=user # | arg: -u, --user= - the user for which to check existence # | exit: Return 1 if the user doesn't exist, 0 otherwise @@ -222,6 +224,8 @@ ynh_psql_drop_user() { # Create a database, an user and its password. Then store the password in the app's config # +# [packagingv1] +# # usage: ynh_psql_setup_db --db_user=user --db_name=name [--db_pwd=pwd] # | arg: -u, --db_user= - Owner of the database # | arg: -n, --db_name= - Name of the database @@ -257,6 +261,8 @@ ynh_psql_setup_db() { # Remove a database if it exists, and the associated user # +# [packagingv1] +# # usage: ynh_psql_remove_db --db_user=user --db_name=name # | arg: -u, --db_user= - Owner of the database # | arg: -n, --db_name= - Name of the database diff --git a/helpers/helpers.v1.d/redis b/helpers/helpers.v1.d/redis new file mode 100644 index 000000000..545bb8705 --- /dev/null +++ b/helpers/helpers.v1.d/redis @@ -0,0 +1,39 @@ +#!/bin/bash + +# get the first available redis database +# +# usage: ynh_redis_get_free_db +# | returns: the database number to use +ynh_redis_get_free_db() { + local result max db + result=$(redis-cli INFO keyspace) + + # get the num + max=$(cat /etc/redis/redis.conf | grep ^databases | grep -Eow "[0-9]+") + + db=0 + # default Debian setting is 15 databases + for i in $(seq 0 "$max") + do + if ! echo "$result" | grep -q "db$i" + then + db=$i + break 1 + fi + db=-1 + done + + test "$db" -eq -1 && ynh_die --message="No available Redis databases..." + + echo "$db" +} + +# Create a master password and set up global settings +# Please always call this script in install and restore scripts +# +# usage: ynh_redis_remove_db database +# | arg: database - the database to erase +ynh_redis_remove_db() { + local db=$1 + redis-cli -n "$db" flushdb +} diff --git a/helpers/helpers.v1.d/ruby b/helpers/helpers.v1.d/ruby new file mode 100644 index 000000000..24e4b218b --- /dev/null +++ b/helpers/helpers.v1.d/ruby @@ -0,0 +1,306 @@ +#!/bin/bash + +rbenv_install_dir="/opt/rbenv" +ruby_version_path="$rbenv_install_dir/versions" + +# RBENV_ROOT is the directory of rbenv, it needs to be loaded as a environment variable. +export RBENV_ROOT="$rbenv_install_dir" +export rbenv_root="$rbenv_install_dir" + +if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + build_ruby_dependencies="libjemalloc-dev curl build-essential libreadline-dev zlib1g-dev libsqlite3-dev libssl-dev libxml2-dev libxslt-dev autoconf automake bison libtool" + build_pkg_dependencies="${build_pkg_dependencies:-} $build_ruby_dependencies" +fi + +# Load the version of Ruby for an app, and set variables. +# +# ynh_use_ruby has to be used in any app scripts before using Ruby for the first time. +# This helper will provide alias and variables to use in your scripts. +# +# To use gem or Ruby, use the alias `ynh_gem` and `ynh_ruby` +# Those alias will use the correct version installed for the app +# For example: use `ynh_gem install` instead of `gem install` +# +# With `sudo` or `ynh_exec_as`, use instead the fallback variables `$ynh_gem` and `$ynh_ruby` +# And propagate $PATH to sudo with $ynh_ruby_load_path +# Exemple: `ynh_exec_as $app $ynh_ruby_load_path $ynh_gem install` +# +# $PATH contains the path of the requested version of Ruby. +# However, $PATH is duplicated into $ruby_path to outlast any manipulation of $PATH +# You can use the variable `$ynh_ruby_load_path` to quickly load your Ruby version +# in $PATH for an usage into a separate script. +# Exemple: $ynh_ruby_load_path $final_path/script_that_use_gem.sh` +# +# +# Finally, to start a Ruby service with the correct version, 2 solutions +# Either the app is dependent of Ruby or gem, but does not called it directly. +# In such situation, you need to load PATH +# `Environment="__YNH_RUBY_LOAD_PATH__"` +# `ExecStart=__FINALPATH__/my_app` +# You will replace __YNH_RUBY_LOAD_PATH__ with $ynh_ruby_load_path +# +# Or Ruby start the app directly, then you don't need to load the PATH variable +# `ExecStart=__YNH_RUBY__ my_app run` +# You will replace __YNH_RUBY__ with $ynh_ruby +# +# +# one other variable is also available +# - $ruby_path: The absolute path to Ruby binaries for the chosen version. +# +# usage: ynh_use_ruby +# +# Requires YunoHost version 3.2.2 or higher. +ynh_use_ruby () { + ruby_version=$(ynh_app_setting_get --app=$app --key=ruby_version) + + # Get the absolute path of this version of Ruby + ruby_path="$ruby_version_path/$app/bin" + + # Allow alias to be used into bash script + shopt -s expand_aliases + + # Create an alias for the specific version of Ruby and a variable as fallback + ynh_ruby="$ruby_path/ruby" + alias ynh_ruby="$ynh_ruby" + # And gem + ynh_gem="$ruby_path/gem" + alias ynh_gem="$ynh_gem" + + # Load the path of this version of Ruby in $PATH + if [[ :$PATH: != *":$ruby_path"* ]]; then + PATH="$ruby_path:$PATH" + fi + # Create an alias to easily load the PATH + ynh_ruby_load_path="PATH=$PATH" + + # Sets the local application-specific Ruby version + pushd ${install_dir:-$final_path} + $rbenv_install_dir/bin/rbenv local $ruby_version + popd +} + +# Install a specific version of Ruby +# +# ynh_install_ruby will install the version of Ruby provided as argument by using rbenv. +# +# This helper creates a /etc/profile.d/rbenv.sh that configures PATH environment for rbenv +# for every LOGIN user, hence your user must have a defined shell (as opposed to /usr/sbin/nologin) +# +# Don't forget to execute ruby-dependent command in a login environment +# (e.g. sudo --login option) +# When not possible (e.g. in systemd service definition), please use direct path +# to rbenv shims (e.g. $RBENV_ROOT/shims/bundle) +# +# usage: ynh_install_ruby --ruby_version=ruby_version +# | arg: -v, --ruby_version= - Version of ruby to install. +# +# Requires YunoHost version 3.2.2 or higher. +ynh_install_ruby () { + # Declare an array to define the options of this helper. + local legacy_args=v + local -A args_array=( [v]=ruby_version= ) + local ruby_version + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Load rbenv path in PATH + local CLEAR_PATH="$rbenv_install_dir/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Ruby prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Move an existing Ruby binary, to avoid to block rbenv + test -x /usr/bin/ruby && mv /usr/bin/ruby /usr/bin/ruby_rbenv + + # Install or update rbenv + mkdir -p $rbenv_install_dir + rbenv="$(command -v rbenv $rbenv_install_dir/bin/rbenv | grep "$rbenv_install_dir/bin/rbenv" | head -1)" + if [ -n "$rbenv" ]; then + pushd "${rbenv%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/rbenv/rbenv.git"; then + ynh_print_info --message="Updating rbenv..." + git pull -q --tags origin master + ynh_ruby_try_bash_extension + else + ynh_print_info --message="Reinstalling rbenv..." + cd .. + ynh_secure_remove --file=$rbenv_install_dir + mkdir -p $rbenv_install_dir + cd $rbenv_install_dir + git init -q + git remote add -f -t master origin https://github.com/rbenv/rbenv.git > /dev/null 2>&1 + git checkout -q -b master origin/master + ynh_ruby_try_bash_extension + rbenv=$rbenv_install_dir/bin/rbenv + fi + popd + else + ynh_print_info --message="Installing rbenv..." + pushd $rbenv_install_dir + git init -q + git remote add -f -t master origin https://github.com/rbenv/rbenv.git > /dev/null 2>&1 + git checkout -q -b master origin/master + ynh_ruby_try_bash_extension + rbenv=$rbenv_install_dir/bin/rbenv + popd + fi + + mkdir -p "${rbenv_install_dir}/plugins" + + ruby_build="$(command -v "$rbenv_install_dir"/plugins/*/bin/rbenv-install rbenv-install | head -1)" + if [ -n "$ruby_build" ]; then + pushd "${ruby_build%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/rbenv/ruby-build.git"; then + ynh_print_info --message="Updating ruby-build..." + git pull -q origin master + fi + popd + else + ynh_print_info --message="Installing ruby-build..." + git clone -q https://github.com/rbenv/ruby-build.git "${rbenv_install_dir}/plugins/ruby-build" + fi + + rbenv_alias="$(command -v "$rbenv_install_dir"/plugins/*/bin/rbenv-alias rbenv-alias | head -1)" + if [ -n "$rbenv_alias" ]; then + pushd "${rbenv_alias%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/tpope/rbenv-aliases.git"; then + ynh_print_info --message="Updating rbenv-aliases..." + git pull -q origin master + fi + popd + else + ynh_print_info --message="Installing rbenv-aliases..." + git clone -q https://github.com/tpope/rbenv-aliases.git "${rbenv_install_dir}/plugins/rbenv-aliase" + fi + + rbenv_latest="$(command -v "$rbenv_install_dir"/plugins/*/bin/rbenv-latest rbenv-latest | head -1)" + if [ -n "$rbenv_latest" ]; then + pushd "${rbenv_latest%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/momo-lab/xxenv-latest.git"; then + ynh_print_info --message="Updating xxenv-latest..." + git pull -q origin master + fi + popd + else + ynh_print_info --message="Installing xxenv-latest..." + git clone -q https://github.com/momo-lab/xxenv-latest.git "${rbenv_install_dir}/plugins/xxenv-latest" + fi + + # Enable caching + mkdir -p "${rbenv_install_dir}/cache" + + # Create shims directory if needed + mkdir -p "${rbenv_install_dir}/shims" + + # Restore /usr/local/bin in PATH + PATH=$CLEAR_PATH + + # And replace the old Ruby binary + test -x /usr/bin/ruby_rbenv && mv /usr/bin/ruby_rbenv /usr/bin/ruby + + # Install the requested version of Ruby + local final_ruby_version=$(rbenv latest --print $ruby_version) + if ! [ -n "$final_ruby_version" ]; then + final_ruby_version=$ruby_version + fi + ynh_print_info --message="Installing Ruby $final_ruby_version" + RUBY_CONFIGURE_OPTS="--disable-install-doc --with-jemalloc" MAKE_OPTS="-j2" rbenv install --skip-existing $final_ruby_version > /dev/null 2>&1 + + # Store ruby_version into the config of this app + ynh_app_setting_set --app=$app --key=ruby_version --value=$final_ruby_version + + # Remove app virtualenv + if rbenv alias --list | grep --quiet "$app " + then + rbenv alias $app --remove + fi + + # Create app virtualenv + rbenv alias $app $final_ruby_version + + # Cleanup Ruby versions + ynh_cleanup_ruby + + # Set environment for Ruby users + echo "#rbenv +export RBENV_ROOT=$rbenv_install_dir +export PATH=\"$rbenv_install_dir/bin:$PATH\" +eval \"\$(rbenv init -)\" +#rbenv" > /etc/profile.d/rbenv.sh + + # Load the environment + eval "$(rbenv init -)" +} + +# Remove the version of Ruby used by the app. +# +# This helper will also cleanup Ruby versions +# +# usage: ynh_remove_ruby +ynh_remove_ruby () { + local ruby_version=$(ynh_app_setting_get --app=$app --key=ruby_version) + + # Load rbenv path in PATH + local CLEAR_PATH="$rbenv_install_dir/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Ruby prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + rbenv alias $app --remove + + # Remove the line for this app + ynh_app_setting_delete --app=$app --key=ruby_version + + # Cleanup Ruby versions + ynh_cleanup_ruby +} + +# Remove no more needed versions of Ruby used by the app. +# +# This helper will check what Ruby version are no more required, +# and uninstall them +# If no app uses Ruby, rbenv will be also removed. +# +# usage: ynh_cleanup_ruby +ynh_cleanup_ruby () { + + # List required Ruby versions + local installed_apps=$(yunohost app list | grep -oP 'id: \K.*$') + local required_ruby_versions="" + for installed_app in $installed_apps + do + local installed_app_ruby_version=$(ynh_app_setting_get --app=$installed_app --key="ruby_version") + if [[ -n "$installed_app_ruby_version" ]] + then + required_ruby_versions="${installed_app_ruby_version}\n${required_ruby_versions}" + fi + done + + # Remove no more needed Ruby versions + local installed_ruby_versions=$(rbenv versions --bare --skip-aliases | grep -Ev '/') + for installed_ruby_version in $installed_ruby_versions + do + if ! echo ${required_ruby_versions} | grep -q "${installed_ruby_version}" + then + ynh_print_info --message="Removing Ruby-$installed_ruby_version" + $rbenv_install_dir/bin/rbenv uninstall --force $installed_ruby_version + fi + done + + # If none Ruby version is required + if [[ -z "$required_ruby_versions" ]] + then + # Remove rbenv environment configuration + ynh_print_info --message="Removing rbenv" + ynh_secure_remove --file="$rbenv_install_dir" + ynh_secure_remove --file="/etc/profile.d/rbenv.sh" + fi +} + +ynh_ruby_try_bash_extension() { + if [ -x src/configure ]; then + src/configure && make -C src || { + ynh_print_info --message="Optional bash extension failed to build, but things will still work normally." + } + fi +} diff --git a/helpers/setting b/helpers/helpers.v1.d/setting similarity index 78% rename from helpers/setting rename to helpers/helpers.v1.d/setting index a2cf3a93d..33751791b 100644 --- a/helpers/setting +++ b/helpers/helpers.v1.d/setting @@ -18,11 +18,7 @@ ynh_app_setting_get() { ynh_handle_getopts_args "$@" app="${app:-$_globalapp}" - if [[ $key =~ (unprotected|protected|skipped)_ ]]; then - yunohost app setting $app $key - else - ynh_app_setting "get" "$app" "$key" - fi + ynh_app_setting "get" "$app" "$key" } # Set an application setting @@ -45,9 +41,41 @@ ynh_app_setting_set() { ynh_handle_getopts_args "$@" app="${app:-$_globalapp}" - if [[ $key =~ (unprotected|protected|skipped)_ ]]; then - yunohost app setting $app $key -v $value - else + ynh_app_setting "set" "$app" "$key" "$value" +} + +# Set an application setting but only if the "$key" variable ain't set yet +# +# Note that it doesn't just define the setting but ALSO define the $foobar variable +# +# Hence it's meant as a replacement for this legacy overly complex syntax: +# +# if [ -z "${foo:-}" ] +# then +# foo="bar" +# ynh_app_setting_set --key="foo" --value="$foo" +# fi +# +# usage: ynh_app_setting_set_default --app=app --key=key --value=value +# | arg: -a, --app= - the application id +# | arg: -k, --key= - the setting name to set +# | arg: -v, --value= - the default setting value to set +# +# Requires YunoHost version 11.1.16 or higher. +ynh_app_setting_set_default() { + local _globalapp=${app-:} + # Declare an array to define the options of this helper. + local legacy_args=akv + local -A args_array=([a]=app= [k]=key= [v]=value=) + local app + local key + local value + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" + + if [ -z "${!key:-}" ]; then + eval $key=\$value ynh_app_setting "set" "$app" "$key" "$value" fi } @@ -70,11 +98,7 @@ ynh_app_setting_delete() { ynh_handle_getopts_args "$@" app="${app:-$_globalapp}" - if [[ "$key" =~ (unprotected|skipped|protected)_ ]]; then - yunohost app setting $app $key -d - else - ynh_app_setting "delete" "$app" "$key" - fi + ynh_app_setting "delete" "$app" "$key" } # Small "hard-coded" interface to avoid calling "yunohost app" directly each @@ -113,6 +137,8 @@ EOF # Check availability of a web path # +# [packagingv1] +# # usage: ynh_webpath_available --domain=domain --path_url=path # | arg: -d, --domain= - the domain/host of the url # | arg: -p, --path_url= - the web path to check the availability of @@ -134,6 +160,8 @@ ynh_webpath_available() { # Register/book a web path for an app # +# [packagingv1] +# # usage: ynh_webpath_register --app=app --domain=domain --path_url=path # | arg: -a, --app= - the app for which the domain should be registered # | arg: -d, --domain= - the domain/host of the web path diff --git a/helpers/helpers.v1.d/sources b/helpers/helpers.v1.d/sources new file mode 100644 index 000000000..9bb0b1c99 --- /dev/null +++ b/helpers/helpers.v1.d/sources @@ -0,0 +1,300 @@ +#!/bin/bash + +# 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"] [--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' (no trailing `/` for folders) +# | arg: -r, --full_replace= - Remove previous sources before installing new sources (can be 1 or 0, default to 0) +# +# ##### 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 +# +# The helper will: +# - 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 `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 +# +# Requires YunoHost version 2.6.4 or higher. +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= [r]=full_replace=) + local dest_dir + local source_id + local keep + local full_replace + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + keep="${keep:-}" + full_replace="${full_replace:-0}" + + 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 + + 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} + src_in_subdir=${src_in_subdir:-true} + src_format=${src_format:-tar.gz} + src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]') + src_extract=${src_extract:-true} + + 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}/${source_id}" + + # Gotta use this trick with 'dirname' because source_id may contain slashes x_x + mkdir -p $(dirname /var/cache/yunohost/download/${YNH_APP_ID}/${source_id}) + src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${source_id}" + + if [ "$src_format" = "docker" ]; then + src_platform="${src_platform:-"linux/$YNH_ARCH"}" + else + if test -e "$local_src"; then + cp $local_src $src_filename + fi + + [ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?" + + # If the file was prefetched but somehow doesn't match the sum, rm and redownload it + if [ -e "$src_filename" ] && ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status + then + rm -f "$src_filename" + fi + + # Only redownload the file if it wasnt prefetched + if [ ! -e "$src_filename" ] + then + # NB. we have to declare the var as local first, + # otherwise 'local foo=$(false) || echo 'pwet'" does'nt work + # because local always return 0 ... + local out + # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) + out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \ + || ynh_die --message="$out" + fi + + # Check the control sum + 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 + rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ + if [ -n "$keep" ] && [ -e "$dest_dir" ]; then + local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} + mkdir -p $keep_dir + local stuff_to_keep + for stuff_to_keep in $keep; do + if [ -e "$dest_dir/$stuff_to_keep" ]; then + mkdir --parents "$(dirname "$keep_dir/$stuff_to_keep")" + cp --archive "$dest_dir/$stuff_to_keep" "$keep_dir/$stuff_to_keep" + fi + 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" == "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 + "$YNH_HELPERS_DIR/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 + local tmp_dir=$(mktemp --directory) + unzip -quo $src_filename -d "$tmp_dir" + cp --archive $tmp_dir/*/. "$dest_dir" + ynh_secure_remove --file="$tmp_dir" + else + unzip -quo $src_filename -d "$dest_dir" + fi + ynh_secure_remove --file="$src_filename" + else + local strip="" + if [ "$src_in_subdir" != "false" ]; then + if [ "$src_in_subdir" == "true" ]; then + local sub_dirs=1 + else + local sub_dirs="$src_in_subdir" + fi + strip="--strip-components $sub_dirs" + fi + if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]]; then + tar --extract --file=$src_filename --directory="$dest_dir" $strip + else + ynh_die --message="Archive format unrecognized." + fi + ynh_secure_remove --file="$src_filename" + fi + + # Apply patches + if [ -d "$YNH_APP_BASEDIR/sources/patches/" ]; then + local patches_folder=$(realpath $YNH_APP_BASEDIR/sources/patches/) + if (($(find $patches_folder -type f -name "${source_id}-*.patch" 2>/dev/null | wc --lines) > "0")); then + pushd "$dest_dir" + for p in $patches_folder/${source_id}-*.patch; do + echo $p + patch --strip=1 <$p || ynh_print_warn --message="Packagers /!\\ patch $p failed to apply" + done + popd + fi + fi + + # Add supplementary files + if test -e "$YNH_APP_BASEDIR/sources/extra_files/${source_id}"; then + cp --archive $YNH_APP_BASEDIR/sources/extra_files/$source_id/. "$dest_dir" + fi + + # Keep files to be backup/restored at the end of the helper + # Assuming $dest_dir already exists + if [ -n "$keep" ]; then + local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} + local stuff_to_keep + for stuff_to_keep in $keep; do + if [ -e "$keep_dir/$stuff_to_keep" ]; then + mkdir --parents "$(dirname "$dest_dir/$stuff_to_keep")" + + # We add "--no-target-directory" (short option is -T) to handle the special case + # when we "keep" a folder, but then the new setup already contains the same dir (but possibly empty) + # in which case a regular "cp" will create a copy of the directory inside the directory ... + # resulting in something like /var/www/$app/data/data instead of /var/www/$app/data + # cf https://unix.stackexchange.com/q/94831 for a more elaborate explanation on the option + cp --archive --no-target-directory "$keep_dir/$stuff_to_keep" "$dest_dir/$stuff_to_keep" + fi + done + fi + rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ +} diff --git a/helpers/string b/helpers/helpers.v1.d/string similarity index 99% rename from helpers/string rename to helpers/helpers.v1.d/string index dc1658e3d..b674d9a4a 100644 --- a/helpers/string +++ b/helpers/helpers.v1.d/string @@ -91,6 +91,8 @@ ynh_replace_special_string() { # Sanitize a string intended to be the name of a database # +# [packagingv1] +# # usage: ynh_sanitize_dbid --db_name=name # | arg: -n, --db_name= - name to correct/sanitize # | ret: the corrected name diff --git a/helpers/systemd b/helpers/helpers.v1.d/systemd similarity index 94% rename from helpers/systemd rename to helpers/helpers.v1.d/systemd index 761e818ad..765c575ef 100644 --- a/helpers/systemd +++ b/helpers/helpers.v1.d/systemd @@ -128,6 +128,7 @@ ynh_systemd_action() { if [[ -n "${line_match:-}" ]]; then set +x local i=0 + local starttime=$(date +%s) for i in $(seq 1 $timeout); do # Read the log until the sentence is found, that means the app finished to start. Or run until the timeout if [ "$log_path" == "systemd" ]; then @@ -145,6 +146,14 @@ ynh_systemd_action() { if [ $i -eq 30 ]; then echo "(this may take some time)" >&2 fi + # Also check the timeout using actual timestamp, because sometimes for some reason, + # journalctl may take a huge time to run, and we end up waiting literally an entire hour + # instead of 5 min ... + if [[ "$(( $(date +%s) - $starttime))" -gt "$timeout" ]] + then + i=$timeout + break + fi sleep 1 done set -x diff --git a/helpers/user b/helpers/helpers.v1.d/systemuser similarity index 73% rename from helpers/user rename to helpers/helpers.v1.d/systemuser index f5f3ec7bd..4fac34bb4 100644 --- a/helpers/user +++ b/helpers/helpers.v1.d/systemuser @@ -1,61 +1,9 @@ #!/bin/bash -# Check if a YunoHost user exists -# -# usage: ynh_user_exists --username=username -# | arg: -u, --username= - the username to check -# | ret: 0 if the user exists, 1 otherwise. -# -# example: ynh_user_exists 'toto' || echo "User does not exist" -# -# Requires YunoHost version 2.2.4 or higher. -ynh_user_exists() { - # Declare an array to define the options of this helper. - local legacy_args=u - local -A args_array=([u]=username=) - local username - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - yunohost user list --output-as json --quiet | jq -e ".users.\"${username}\"" >/dev/null -} - -# Retrieve a YunoHost user information -# -# usage: ynh_user_get_info --username=username --key=key -# | arg: -u, --username= - the username to retrieve info from -# | arg: -k, --key= - the key to retrieve -# | ret: the value associate to that key -# -# example: mail=$(ynh_user_get_info --username="toto" --key=mail) -# -# Requires YunoHost version 2.2.4 or higher. -ynh_user_get_info() { - # Declare an array to define the options of this helper. - local legacy_args=uk - local -A args_array=([u]=username= [k]=key=) - local username - local key - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - yunohost user info "$username" --output-as json --quiet | jq -r ".$key" -} - -# Get the list of YunoHost users -# -# usage: ynh_user_list -# | ret: one username per line as strings -# -# example: for u in $(ynh_user_list); do ... ; done -# -# Requires YunoHost version 2.4.0 or higher. -ynh_user_list() { - yunohost user list --output-as json --quiet | jq -r ".users | keys[]" -} - # Check if a user exists on the system # +# [packagingv1] +# # usage: ynh_system_user_exists --username=username # | arg: -u, --username= - the username to check # | ret: 0 if the user exists, 1 otherwise. @@ -74,6 +22,8 @@ ynh_system_user_exists() { # Check if a group exists on the system # +# [packagingv1] +# # usage: ynh_system_group_exists --group=group # | arg: -g, --group= - the group to check # | ret: 0 if the group exists, 1 otherwise. diff --git a/helpers/helpers.v1.d/templating b/helpers/helpers.v1.d/templating new file mode 100644 index 000000000..76f319137 --- /dev/null +++ b/helpers/helpers.v1.d/templating @@ -0,0 +1,407 @@ +#!/bin/bash + +# Create a dedicated config file from a template +# +# usage: ynh_add_config --template="template" --destination="destination" +# | arg: -t, --template= - Template config file to use +# | arg: -d, --destination= - Destination of the config file +# | arg: -j, --jinja - Use jinja template instead of the simple `__MY_VAR__` templating format +# +# examples: +# ynh_add_config --template=".env" --destination="$install_dir/.env" # (use the template file "conf/.env" from the app's package) +# ynh_add_config --jinja --template="config.j2" --destination="$install_dir/config" # (use the template file "conf/config.j2" from the app's package) +# +# The template can be by default the name of a file in the conf directory +# of a YunoHost Package, a relative path or an absolute path. +# +# The helper will use the template `template` to generate a config file +# `destination` by replacing the following keywords with global variables +# that should be defined before calling this helper : +# ``` +# __PATH__ by $path_url +# __NAME__ by $app +# __NAMETOCHANGE__ by $app +# __USER__ by $app +# __FINALPATH__ by $final_path +# __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: +# ``` +# __DOMAIN__ by $domain +# __APP__ by $app +# __VAR_1__ by $var_1 +# __VAR_2__ by $var_2 +# ``` +# +# ##### When --jinja is enabled +# +# This option is meant for advanced use-cases where the "simple" templating +# mode ain't enough because you need conditional blocks or loops. +# +# For a full documentation of jinja's syntax you can refer to: +# https://jinja.palletsprojects.com/en/3.1.x/templates/ +# +# Note that in YunoHost context, all variables are from shell variables and therefore are strings +# +# ##### Keeping track of manual changes by the admin +# +# The helper will verify the checksum and backup the destination file +# if it's different before applying the new template. +# +# And it will calculate and store the destination file checksum +# into the app settings when configuration is done. +# +# Requires YunoHost version 4.1.0 or higher. +ynh_add_config() { + # Declare an array to define the options of this helper. + local legacy_args=tdj + local -A args_array=([t]=template= [d]=destination= [j]=jinja) + local template + local destination + local jinja + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + local template_path + jinja="${jinja:-0}" + + if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then + template_path="$YNH_APP_BASEDIR/conf/$template" + elif [ -f "$template" ]; then + template_path=$template + else + ynh_die --message="The provided template $template doesn't exist" + fi + + ynh_backup_if_checksum_is_different --file="$destination" + + # Make sure to set the permissions before we copy the file + # This is to cover a case where an attacker could have + # created a file beforehand to have control over it + # (cp won't overwrite ownership / modes by default...) + touch $destination + chmod 640 $destination + _ynh_apply_default_permissions $destination + + if [[ "$jinja" == 1 ]] + then + # This is ran in a subshell such that the "export" does not "contaminate" the main process + ( + export $(compgen -v) + j2 "$template_path" -f env -o $destination + ) + else + cp -f "$template_path" "$destination" + ynh_replace_vars --file="$destination" + fi + + ynh_store_file_checksum --file="$destination" +} + +# Replace variables in a file +# +# [internal] +# +# usage: ynh_replace_vars --file="file" +# | arg: -f, --file= - File where to replace variables +# +# The helper will replace the following keywords with global variables +# that should be defined before calling this helper : +# __PATH__ by $path_url +# __NAME__ by $app +# __NAMETOCHANGE__ by $app +# __USER__ by $app +# __FINALPATH__ by $final_path +# __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: +# __DOMAIN__ by $domain +# __APP__ by $app +# __VAR_1__ by $var_1 +# __VAR_2__ by $var_2 +# +# Requires YunoHost version 4.1.0 or higher. +ynh_replace_vars() { + # Declare an array to define the options of this helper. + local legacy_args=f + local -A args_array=([f]=file=) + local file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Replace specific YunoHost variables + if test -n "${path_url:-}"; then + # path_url_slash_less is path_url, or a blank value if path_url is only '/' + local path_url_slash_less=${path_url%/} + ynh_replace_string --match_string="__PATH__/" --replace_string="$path_url_slash_less/" --target_file="$file" + ynh_replace_string --match_string="__PATH__" --replace_string="$path_url" --target_file="$file" + fi + if test -n "${app:-}"; then + ynh_replace_string --match_string="__NAME__" --replace_string="$app" --target_file="$file" + 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 + # 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 + ynh_replace_string --match_string="__YNH_NODE_LOAD_PATH__" --replace_string="$ynh_node_load_PATH" --target_file="$file" + fi + + # Replace others variables + + # List other unique (__ __) variables in $file + local uniques_vars=($(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g")) + + set +o xtrace # set +x + + # Do the replacement + local delimit=@ + for one_var in "${uniques_vars[@]}"; do + # Validate that one_var is indeed defined + # -v checks if the variable is defined, for example: + # -v FOO tests if $FOO is defined + # -v $FOO tests if ${!FOO} is defined + # More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964 + [[ -v "${one_var:-}" ]] || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" + + # Escape delimiter in match/replace string + match_string="__${one_var^^}__" + match_string=${match_string//${delimit}/"\\${delimit}"} + replace_string="${!one_var}" + replace_string=${replace_string//\\/\\\\} + replace_string=${replace_string//${delimit}/"\\${delimit}"} + + # Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs) + sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$file" + done + set -o xtrace # set -x +} + +# Get a value from heterogeneous file (yaml, json, php, python...) +# +# usage: ynh_read_var_in_file --file=PATH --key=KEY +# | arg: -f, --file= - the path to the file +# | arg: -k, --key= - the key to get +# | arg: -a, --after= - the line just before the key (in case of multiple lines with the name of the key in the file) +# +# This helpers match several var affectation use case in several languages +# We don't use jq or equivalent to keep comments and blank space in files +# This helpers work line by line, it is not able to work correctly +# if you have several identical keys in your files +# +# Example of line this helpers can managed correctly +# .yml +# title: YunoHost documentation +# email: 'yunohost@yunohost.org' +# .json +# "theme": "colib'ris", +# "port": 8102 +# "some_boolean": false, +# "user": null +# .ini +# some_boolean = On +# action = "Clear" +# port = 20 +# .php +# $user= +# user => 20 +# .py +# USER = 8102 +# user = 'https://donate.local' +# CUSTOM['user'] = 'YunoHost' +# +# Requires YunoHost version 4.3 or higher. +ynh_read_var_in_file() { + # Declare an array to define the options of this helper. + local legacy_args=fka + local -A args_array=([f]=file= [k]=key= [a]=after=) + local file + local key + local after + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + after="${after:-}" + + [[ -f $file ]] || ynh_die --message="File $file does not exists" + + set +o xtrace # set +x + + # Get the line number after which we search for the variable + local line_number=1 + if [[ -n "$after" ]]; then + line_number=$(grep -m1 -n $after $file | cut -d: -f1) + if [[ -z "$line_number" ]]; then + set -o xtrace # set -x + return 1 + fi + fi + + local filename="$(basename -- "$file")" + local ext="${filename##*.}" + local endline=',;' + local assign="=>|:|=" + local comments="#" + local string="\"'" + if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then + endline='#' + fi + if [[ "$ext" =~ ^ini|env$ ]]; then + comments="[;#]" + fi + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + comments="//" + fi + local list='\[\s*['$string']?\w+['$string']?\]' + local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' + var_part+="[$string]?${key}[$string]?" + var_part+='\s*\]?\s*' + var_part+="($assign)" + var_part+='\s*' + + # Extract the part after assignation sign + local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" + if [[ "$expression_with_comment" == "YNH_NULL" ]]; then + set -o xtrace # set -x + echo YNH_NULL + return 0 + fi + + # Remove comments if needed + local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" + + local first_char="${expression:0:1}" + if [[ "$first_char" == '"' ]]; then + echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g' + elif [[ "$first_char" == "'" ]]; then + echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g" + else + echo "$expression" + fi + set -o xtrace # set -x +} + +# Set a value into heterogeneous file (yaml, json, php, python...) +# +# usage: ynh_write_var_in_file --file=PATH --key=KEY --value=VALUE +# | arg: -f, --file= - the path to the file +# | arg: -k, --key= - the key to set +# | arg: -v, --value= - the value to set +# | arg: -a, --after= - the line just before the key (in case of multiple lines with the name of the key in the file) +# +# Requires YunoHost version 4.3 or higher. +ynh_write_var_in_file() { + # Declare an array to define the options of this helper. + local legacy_args=fkva + local -A args_array=([f]=file= [k]=key= [v]=value= [a]=after=) + local file + local key + local value + local after + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + after="${after:-}" + + [[ -f $file ]] || ynh_die --message="File $file does not exists" + + set +o xtrace # set +x + + # Get the line number after which we search for the variable + local after_line_number=1 + if [[ -n "$after" ]]; 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 filename="$(basename -- "$file")" + local ext="${filename##*.}" + local endline=',;' + local assign="=>|:|=" + local comments="#" + local string="\"'" + if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then + endline='#' + fi + if [[ "$ext" =~ ^ini|env$ ]]; then + comments="[;#]" + fi + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + comments="//" + fi + local list='\[\s*['$string']?\w+['$string']?\]' + local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' + var_part+="[$string]?${key}[$string]?" + var_part+='\s*\]?\s*' + var_part+="($assign)" + var_part+='\s*' + + # Extract the part after assignation sign + local expression_with_comment="$((tail +$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 + # \ and sed is quite complex you need 2 \\ to get one in a sed + # So we need \\\\ to go through 2 sed + value="$(echo "$value" | sed 's/"/\\\\"/g')" + sed -ri "${range}s$delimiter"'(^'"${var_part}"'")([^"]|\\")*("[\s;,]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}"'"'"${endline}${delimiter}i" ${file} + elif [[ "$first_char" == "'" ]]; then + # \ and sed is quite complex you need 2 \\ to get one in a sed + # However double quotes implies to double \\ to + # So we need \\\\\\\\ to go through 2 sed and 1 double quotes str + value="$(echo "$value" | sed "s/'/\\\\\\\\'/g")" + sed -ri "${range}s$delimiter(^${var_part}')([^']|\\')*('"'[\s,;]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}'${endline}${delimiter}i" ${file} + else + if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]]; then + value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"' + fi + if [[ "$ext" =~ ^yaml|yml$ ]]; then + value=" $value" + fi + sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file} + fi + set -o xtrace # set -x +} + +# Render templates with Jinja2 +# +# [internal] +# +# Attention : Variables should be exported before calling this helper to be +# accessible inside templates. +# +# usage: ynh_render_template some_template output_path +# | arg: some_template - Template file to be rendered +# | arg: output_path - The path where the output will be redirected to +ynh_render_template() { + local template_path=$1 + local output_path=$2 + mkdir -p "$(dirname $output_path)" + # Taken from https://stackoverflow.com/a/35009576 + python3 -c 'import os, sys, jinja2; sys.stdout.write( + jinja2.Template(sys.stdin.read() + ).render(os.environ));' <$template_path >$output_path +} diff --git a/helpers/helpers.v1.d/utils b/helpers/helpers.v1.d/utils new file mode 100644 index 000000000..0e1c18663 --- /dev/null +++ b/helpers/helpers.v1.d/utils @@ -0,0 +1,453 @@ +#!/bin/bash + +YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} + +# Handle script crashes / failures +# +# [internal] +# +# usage: +# ynh_exit_properly is used only by the helper ynh_abort_if_errors. +# You should not use it directly. +# Instead, add to your script: +# ynh_clean_setup () { +# instructions... +# } +# +# This function provide a way to clean some residual of installation that not managed by remove script. +# +# It prints a warning to inform that the script was failed, and execute the ynh_clean_setup function if used in the app script +# +# Requires YunoHost version 2.6.4 or higher. +ynh_exit_properly() { + local exit_code=$? + + 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 + fi + + trap '' EXIT # Ignore new exit signals + # Do not exit anymore if a command fail or if a variable is empty + set +o errexit # set +e + set +o nounset # set +u + + # Small tempo to avoid the next message being mixed up with other DEBUG messages + sleep 0.5 + + if type -t ynh_clean_setup >/dev/null; then # Check if the function exist in the app script. + ynh_clean_setup # Call the function to do specific cleaning for the app. + fi + + # Exit with error status + # We don't call ynh_die basically to avoid unecessary 10-ish + # debug lines about parsing args and stuff just to exit 1.. + exit 1 +} + +# Exits if an error occurs during the execution of the script. +# +# [packagingv1] +# +# usage: ynh_abort_if_errors +# +# This configure the rest of the script execution such that, if an error occurs +# or if an empty variable is used, the execution of the script stops immediately +# and a call to `ynh_clean_setup` is triggered if it has been defined by your script. +# +# Requires YunoHost version 2.6.4 or higher. +ynh_abort_if_errors() { + set -o errexit # set -e; Exit if a command fail + set -o nounset # set -u; And if a variable is used unset + trap ynh_exit_properly EXIT # Capturing exit signals on shell script +} + +# 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 + +# Curl abstraction to help with POST requests to local pages (such as installation forms) +# +# usage: ynh_local_curl "page_uri" "key1=value1" "key2=value2" ... +# | arg: page_uri - Path (relative to `$path_url`) of the page where POST data will be sent +# | arg: key1=value1 - (Optionnal) POST key and corresponding value +# | arg: key2=value2 - (Optionnal) Another POST key and corresponding value +# | arg: ... - (Optionnal) More POST keys and values +# +# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2" +# +# For multiple calls, cookies are persisted between each call for the same app +# +# `$domain` and `$path_url` should be defined externally (and correspond to the domain.tld and the /path (of the app?)) +# +# Requires YunoHost version 2.6.4 or higher. +ynh_local_curl() { + # Define url of page to curl + local local_page=$(ynh_normalize_url_path $1) + local full_path=$path_url$local_page + + if [ "${path_url}" == "/" ]; then + full_path=$local_page + fi + + local full_page_url=https://localhost$full_path + + # Concatenate all other arguments with '&' to prepare POST data + local POST_data="" + local arg="" + for arg in "${@:2}"; do + POST_data="${POST_data}${arg}&" + done + if [ -n "$POST_data" ]; then + # Add --data arg and remove the last character, which is an unecessary '&' + POST_data="--data ${POST_data::-1}" + fi + + # Wait untils nginx has fully reloaded (avoid curl fail with http2) + sleep 2 + + local cookiefile=/tmp/ynh-$app-cookie.txt + touch $cookiefile + chown root $cookiefile + chmod 700 $cookiefile + + # Temporarily enable visitors if needed... + local visitors_enabled=$(ynh_permission_has_user "main" "visitors" && echo yes || echo no) + if [[ $visitors_enabled == "no" ]]; then + ynh_permission_update --permission "main" --add "visitors" + fi + + # Curl the URL + curl --silent --show-error --insecure --location --header "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" --cookie-jar $cookiefile --cookie $cookiefile + + if [[ $visitors_enabled == "no" ]]; then + ynh_permission_update --permission "main" --remove "visitors" + fi +} + +# Fetch the Debian release codename +# +# [packagingv1] +# +# usage: ynh_get_debian_release +# | ret: The Debian release codename (i.e. jessie, stretch, ...) +# +# Requires YunoHost version 2.7.12 or higher. +ynh_get_debian_release() { + echo $(lsb_release --codename --short) +} + +_acceptable_path_to_delete() { + local file=$1 + + local forbidden_paths=$(ls -d / /* /{var,home,usr}/* /etc/{default,sudoers.d,yunohost,cron*} /etc/yunohost/{apps,domains,hooks.d} /opt/yunohost 2> /dev/null) + + # Legacy : A couple apps still have data in /home/$app ... + if [[ -n "${app:-}" ]] + then + forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app") + fi + + # Use realpath to normalize the path .. + # i.e convert ///foo//bar//..///baz//// to /foo/baz + file=$(realpath --no-symlinks "$file") + if [ -z "$file" ] || grep -q -x -F "$file" <<< "$forbidden_paths"; then + return 1 + else + return 0 + fi +} + +# Remove a file or a directory securely +# +# usage: ynh_secure_remove --file=path_to_remove +# | arg: -f, --file= - File or directory to remove +# +# Requires YunoHost version 2.6.4 or higher. +ynh_secure_remove() { + # Declare an array to define the options of this helper. + local legacy_args=f + local -A args_array=([f]=file=) + local file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + set +o xtrace # set +x + + if [ $# -ge 2 ]; then + ynh_print_warn --message="/!\ Packager ! You provided more than one argument to ynh_secure_remove but it will be ignored... Use this helper with one argument at time." + fi + + if [[ -z "$file" ]]; then + ynh_print_warn --message="ynh_secure_remove called with empty argument, ignoring." + elif [[ ! -e $file ]]; then + ynh_print_info --message="'$file' wasn't deleted because it doesn't exist." + elif ! _acceptable_path_to_delete "$file"; then + ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete." + else + rm --recursive "$file" + fi + + set -o xtrace # set -x +} + +# Read the value of a key in a ynh manifest file +# +# usage: ynh_read_manifest --manifest="manifest.json" --manifest_key="key" +# | arg: -m, --manifest= - Path of the manifest to read +# | arg: -k, --manifest_key= - Name of the key to find +# | ret: the value associate to that key +# +# Requires YunoHost version 3.5.0 or higher. +ynh_read_manifest() { + # Declare an array to define the options of this helper. + local legacy_args=mk + local -A args_array=([m]=manifest= [k]=manifest_key=) + local manifest + local manifest_key + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if [ ! -e "${manifest:-}" ]; then + # If the manifest isn't found, try the common place for backup and restore script. + 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 + + 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` +# +# usage: ynh_app_upstream_version [--manifest="manifest.json"] +# | arg: -m, --manifest= - Path of the manifest to read +# | ret: the version number of the upstream app +# +# If the `manifest` is not specified, the envvar `$YNH_APP_MANIFEST_VERSION` will be used. +# +# The version number in the manifest is defined by `~ynh`. +# +# For example, if the manifest contains `4.3-2~ynh3` the function will return `4.3-2` +# +# Requires YunoHost version 3.5.0 or higher. +ynh_app_upstream_version() { + # Declare an array to define the options of this helper. + local legacy_args=m + local -A args_array=([m]=manifest=) + local manifest + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + manifest="${manifest:-}" + + if [[ "$manifest" != "" ]] && [[ -e "$manifest" ]]; then + version_key_=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") + else + version_key_=$YNH_APP_MANIFEST_VERSION + fi + + echo "${version_key_/~ynh*/}" +} + +# Read package version from the manifest +# +# [internal] +# +# usage: ynh_app_package_version [--manifest="manifest.json"] +# | arg: -m, --manifest= - Path of the manifest to read +# | ret: the version number of the package +# +# The version number in the manifest is defined by `~ynh`. +# +# For example, if the manifest contains `4.3-2~ynh3` the function will return `3` +# +# Requires YunoHost version 3.5.0 or higher. +ynh_app_package_version() { + # Declare an array to define the options of this helper. + local legacy_args=m + local -A args_array=([m]=manifest=) + local manifest + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + version_key_=$YNH_APP_MANIFEST_VERSION + echo "${version_key_/*~ynh/}" +} + +# Checks the app version to upgrade with the existing app version and returns: +# +# usage: ynh_check_app_version_changed +# | ret: `UPGRADE_APP` if the upstream version changed, `UPGRADE_PACKAGE` otherwise. +# +# This helper should be used to avoid an upgrade of an app, or the upstream part +# of it, when it's not needed +# +# Requires YunoHost version 3.5.0 or higher. +ynh_check_app_version_changed() { + local return_value=${YNH_APP_UPGRADE_TYPE} + + if [ "$return_value" == "UPGRADE_SAME" ] || [ "$return_value" == "DOWNGRADE" ]; then + return_value="UPGRADE_APP" + fi + + echo $return_value +} + +# Compare the current package version against another version given as an argument. +# +# usage: ynh_compare_current_package_version --comparison (lt|le|eq|ne|ge|gt) --version +# | arg: --comparison - Comparison type. Could be : `lt` (lower than), `le` (lower or equal), `eq` (equal), `ne` (not equal), `ge` (greater or equal), `gt` (greater than) +# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like `2.3.1~ynh4`) +# | ret: 0 if the evaluation is true, 1 if false. +# +# example: ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 +# +# This helper is usually used when we need to do some actions only for some old package versions. +# +# Generally you might probably use it as follow in the upgrade script : +# ``` +# if ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 +# then +# # Do something that is needed for the package version older than 2.3.2~ynh1 +# fi +# ``` +# +# Requires YunoHost version 3.8.0 or higher. +ynh_compare_current_package_version() { + local legacy_args=cv + declare -Ar args_array=([c]=comparison= [v]=version=) + local version + local comparison + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local current_version=$YNH_APP_CURRENT_VERSION + + # Check the syntax of the versions + if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]]; then + ynh_die --message="Invalid argument for version." + fi + + # Check validity of the comparator + if [[ ! $comparison =~ (lt|le|eq|ne|ge|gt) ]]; then + ynh_die --message="Invalid comparator must be : lt, le, eq, ne, ge, gt" + fi + + # Return the return value of dpkg --compare-versions + dpkg --compare-versions $current_version $comparison $version +} + +# Check if we should enforce sane default permissions (= disable rwx for 'others') +# on file/folders handled with ynh_setup_source and ynh_add_config +# +# [internal] +# +# Having a file others-readable or a folder others-executable(=enterable) +# is a security risk comparable to "chmod 777" +# +# Configuration files may contain secrets. Or even just being able to enter a +# folder may allow an attacker to do nasty stuff (maybe a file or subfolder has +# some write permission enabled for 'other' and the attacker may edit the +# content or create files as leverage for priviledge escalation ...) +# +# The sane default should be to set ownership to $app:$app. +# In specific case, you may want to set the ownership to $app:www-data +# for example if nginx needs access to static files. +# +_ynh_apply_default_permissions() { + local target=$1 + + chmod o-rwx $target + chmod g-w $target + chown -R root:root $target + if ynh_system_user_exists $app; then + chown $app:$app $target + fi + + # 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 grep -qE '^(/etc/cron|/etc/php|/etc/nginx/conf.d|/etc/fail2ban|/etc/systemd/system)' <<< "$target" + then + chmod 400 $target + chown root:root $target + fi +} + +int_to_bool() { + sed -e 's/^1$/True/g' -e 's/^0$/False/g' -e 's/^true$/True/g' -e 's/^false$/False/g' +} + +toml_to_json() { + python3 -c 'import toml, json, sys; print(json.dumps(toml.load(sys.stdin)))' +} + +# Check if a YunoHost user exists +# +# usage: ynh_user_exists --username=username +# | arg: -u, --username= - the username to check +# | ret: 0 if the user exists, 1 otherwise. +# +# example: ynh_user_exists 'toto' || echo "User does not exist" +# +# Requires YunoHost version 2.2.4 or higher. +ynh_user_exists() { + # Declare an array to define the options of this helper. + local legacy_args=u + local -A args_array=([u]=username=) + local username + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + yunohost user list --output-as json --quiet | jq -e ".users.\"${username}\"" >/dev/null +} + +# Retrieve a YunoHost user information +# +# usage: ynh_user_get_info --username=username --key=key +# | arg: -u, --username= - the username to retrieve info from +# | arg: -k, --key= - the key to retrieve +# | ret: the value associate to that key +# +# example: mail=$(ynh_user_get_info --username="toto" --key=mail) +# +# Requires YunoHost version 2.2.4 or higher. +ynh_user_get_info() { + # Declare an array to define the options of this helper. + local legacy_args=uk + local -A args_array=([u]=username= [k]=key=) + local username + local key + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + yunohost user info "$username" --output-as json --quiet | jq -r ".$key" +} + +# Get the list of YunoHost users +# +# usage: ynh_user_list +# | ret: one username per line as strings +# +# example: for u in $(ynh_user_list); do ... ; done +# +# Requires YunoHost version 2.4.0 or higher. +ynh_user_list() { + yunohost user list --output-as json --quiet | jq -r ".users | keys[]" +} diff --git a/helpers/helpers.v1.d/vendor b/helpers/helpers.v1.d/vendor new file mode 120000 index 000000000..9c39cc9f8 --- /dev/null +++ b/helpers/helpers.v1.d/vendor @@ -0,0 +1 @@ +../vendor \ No newline at end of file diff --git a/helpers/helpers.v2.1.d/apt b/helpers/helpers.v2.1.d/apt new file mode 100644 index 000000000..4999f179c --- /dev/null +++ b/helpers/helpers.v2.1.d/apt @@ -0,0 +1,336 @@ +#!/bin/bash + +YNH_APT_INSTALL_DEPENDENCIES_REPLACE="true" + +# Define and install dependencies with a equivs control file +# +# example : ynh_install_app_dependencies dep1 dep2 "dep3|dep4|dep5" +# +# usage: ynh_install_app_dependencies dep [dep [...]] +# | arg: dep - the package name to install in dependence. +# | arg: "dep1|dep2|…" - You can specify alternatives. It will require to install (dep1 or dep2, etc). +# +ynh_apt_install_dependencies() { + + # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below) + local dependencies="$(sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g' <<< "$@" | sed 's/|/ | /')" + local version=$(ynh_read_manifest "version") + local app_ynh_deps="${app//_/-}-ynh-deps" # Replace all '_' by '-', and append -ynh-deps + + # Handle specific versions + if grep '[<=>]' <<< "$dependencies"; then + # Replace version specifications by relationships syntax + # https://www.debian.org/doc/debian-policy/ch-relationships.html + # Sed clarification + # [^(\<=\>] ignore if it begins by ( or < = >. To not apply twice. + # [\<=\>] matches < = or > + # \+ matches one or more occurence of the previous characters, for >= or >>. + # [^,]\+ matches all characters except ',' + # Ex: 'package>=1.0' will be replaced by 'package (>= 1.0)' + dependencies="$(sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g' <<< "$dependencies")" + fi + + # ############################## # + # Specific tweaks related to PHP # + # ############################## # + + # Check for specific php dependencies which requires sury + # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" + # The (?<=php) syntax corresponds to lookbehind ;) + local specific_php_version=$(grep -oP '(?<=php)[0-9.]+(?=-|\>|)' <<< "$dependencies" | sort -u) + + if [[ -n "$specific_php_version" ]] + then + # Cover a small edge case where a packager could have specified "php7.4-pwet php5-gni" which is confusing + [[ $(echo $specific_php_version | wc -l) -eq 1 ]] \ + || ynh_die "Inconsistent php versions in dependencies ... found : $specific_php_version" + + dependencies+=", php${specific_php_version}, php${specific_php_version}-fpm, php${specific_php_version}-common" + + local old_php_version=$(ynh_app_setting_get --key=php_version) + + # If the PHP version changed, remove the old fpm conf + if [ -n "$old_php_version" ] && [ "$old_php_version" != "$specific_php_version" ]; then + if [[ -f "/etc/php/$php_version/fpm/pool.d/$app.conf" ]] + then + ynh_backup_if_checksum_is_different "/etc/php/$php_version/fpm/pool.d/$app.conf" + ynh_config_remove_phpfpm + fi + fi + # Store php_version into the config of this app + ynh_app_setting_set --key=php_version --value=$specific_php_version + + # Set the default php version back as the default version for php-cli. + 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 --key=php_version --value=$YNH_DEFAULT_PHP_VERSION + fi + + # Specific tweak related to Postgresql (cf end of the helper) + local psql_installed="$(_ynh_apt_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)" + + # The first time we run ynh_install_app_dependencies, we will replace the + # entire control file (This is in particular meant to cover the case of + # upgrade script where ynh_install_app_dependencies is called with this + # expected effect) Otherwise, any subsequent call will add dependencies + # to those already present in the equivs control file. + if [[ $YNH_APT_INSTALL_DEPENDENCIES_REPLACE == "true" ]] + then + YNH_APT_INSTALL_DEPENDENCIES_REPLACE="false" + else + local current_dependencies="" + if _ynh_apt_package_is_installed "${app_ynh_deps}" + then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${app_ynh_deps}) " + current_dependencies=${current_dependencies// | /|} + fi + dependencies="$current_dependencies, $dependencies" + fi + + # ################ + # Actual install # + # ################ + + # Prepare the virtual-dependency control file for dpkg-deb --build + local TMPDIR=$(mktemp --directory) + mkdir -p ${TMPDIR}/${app_ynh_deps}/DEBIAN + # For some reason, dpkg-deb insists for folder perm to be 755 and sometimes it's 777 o_O? + chmod -R 755 ${TMPDIR}/${app_ynh_deps} + + cat >${TMPDIR}/${app_ynh_deps}/DEBIAN/control < ./dpkg_log 2>&1 || { cat ./dpkg_log; false; } + LC_ALL=C dpkg --force-depends --install "./${app_ynh_deps}.deb" > ./dpkg_log 2>&1 + ) + + # Then install the missing dependencies with apt install + _ynh_apt_install --fix-broken || { + # If the installation failed + # (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process) + # Parse the list of problematic dependencies from dpkg's log ... + # (relevant lines look like: "foo-ynh-deps depends on bar; however:") + cat $TMPDIR/dpkg_log + local problematic_dependencies="$(grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' $TMPDIR/dpkg_log | tr '\n' ' ')" + # Fake an install of those dependencies to see the errors + # The sed command here is, Print only from 'Reading state info' to the end. + [[ -n "$problematic_dependencies" ]] && _ynh_apt_install $problematic_dependencies --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2 + ynh_die "Unable to install apt dependencies" + } + rm --recursive --force "$TMPDIR" # Remove the temp dir. + + # check if the package is actually installed + _ynh_apt_package_is_installed "${app_ynh_deps}" || ynh_die "Unable to install apt dependencies" + + # Specific tweak related to Postgresql + # -> trigger postgresql regenconf if we may have just installed postgresql + local psql_installed2="$(_ynh_apt_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)" + if [[ "$psql_installed" != "$psql_installed2" ]] + then + yunohost tools regen-conf postgresql + fi + +} + +# Remove fake package and its dependencies +# +# Dependencies will removed only if no other package need them. +# +# usage: ynh_apt_remove_dependencies +ynh_apt_remove_dependencies() { + local app_ynh_deps="${app//_/-}-ynh-deps" # Replace all '_' by '-', and append -ynh-deps + + local current_dependencies="" + if _ynh_apt_package_is_installed "${app_ynh_deps}"; then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${app_ynh_deps}) " + current_dependencies=${current_dependencies// | /|} + fi + + # 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 ${app_ynh_deps} + then + apt-mark unhold ${app_ynh_deps} + fi + + # 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 ${app_ynh_deps} &>/dev/null + then + _ynh_apt autoremove --purge ${app_ynh_deps} + fi +} + +# Install packages from an extra repository properly. +# +# usage: ynh_apt_install_dependencies_from_extra_repository --repo="repo" --package="dep1 dep2" --key=key_url +# | arg: --repo= - Complete url of the extra repository. +# | arg: --package= - The packages to install from this extra repository +# | arg: --key= - url to get the public key. +# +ynh_apt_install_dependencies_from_extra_repository() { + # ============ Argument parsing ============= + local -A args_array=([r]=repo= [p]=package= [k]=key=) + local repo + local package + local key + ynh_handle_getopts_args "$@" + # =========================================== + + # Split the repository into uri, suite and components. + IFS=', ' read -r -a repo_parts <<< "$repo" + index=0 + + # Remove "deb " at the beginning of the repo. + if [[ "${repo_parts[0]}" == "deb" ]]; then + index=1 + fi + uri="${repo_parts[$index]}" ; index=$((index+1)) + suite="${repo_parts[$index]}" ; index=$((index+1)) + + # Get the components + if (( "${#repo_parts[@]}" > 0 )); then + component="${repo_parts[*]:$index}" + fi + + if [[ "$key" == "trusted=yes" ]]; then + trust="[trusted=yes]" + else + trust="" + fi + + # Add the new repo in sources.list.d + mkdir --parents "/etc/apt/sources.list.d" + echo "deb $trust $uri $suite $component" > "/etc/apt/sources.list.d/$app.list" + + # Pin the new repo with the default priority, so it won't be used for upgrades. + # Build $pin from the uri without http and any sub path + local pin="${uri#*://}" + pin="${pin%%/*}" + + # Pin repository + mkdir --parents "/etc/apt/preferences.d" + cat << EOF > "/etc/apt/preferences.d/$app" +Package: * +Pin: origin $pin +Pin-Priority: 995 +EOF + + if [ -n "$key" ] && [[ "$key" != "trusted=yes" ]]; then + mkdir --parents "/etc/apt/trusted.gpg.d" + # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) + wget --timeout 900 --quiet "$key" --output-document=- | gpg --dearmor > /etc/apt/trusted.gpg.d/$app.gpg + fi + + # Update the list of package with the new repo NB: we use -o + # Dir::Etc::sourcelist to only refresh this repo, because + # ynh_apt_install_dependencies will also call an ynh_apt update on its own + # and it's good to limit unecessary requests ... Here we mainly want to + # validate that the url+key is correct before going further + _ynh_apt update -o Dir::Etc::sourcelist="/etc/apt/sources.list.d/$app.list" + + # Install requested dependencies from this extra repository. + # NB: because of the mechanism with $ynh_apt_install_DEPENDENCIES_REPLACE, + # this will usually only *append* to the existing list of dependency, not + # replace the existing $app-ynh-deps + ynh_apt_install_dependencies "$package" + + # Force to upgrade to the last version... + # Without doing apt install, an already installed dep is not upgraded + local apps_auto_installed="$(apt-mark showauto $package)" + _ynh_apt_install "$package" + [ -z "$apps_auto_installed" ] || apt-mark auto $apps_auto_installed + + # Remove this extra repository after packages are installed + ynh_safe_rm "/etc/apt/sources.list.d/$app.list" + ynh_safe_rm "/etc/apt/preferences.d/$app" + ynh_safe_rm "/etc/apt/trusted.gpg.d/$app.gpg" + _ynh_apt update +} + +# ##################### +# Internal misc utils # +# ##################### + +# Check if apt is free to use, or wait, until timeout. +_ynh_wait_dpkg_free() { + local try + set +o xtrace # set +x + # With seq 1 17, timeout will be almost 30 minutes + for try in $(seq 1 17); do + # Check if /var/lib/dpkg/lock is used by another process + if lsof /var/lib/dpkg/lock >/dev/null; then + echo "apt is already in use..." + # Sleep an exponential time at each round + sleep $((try * try)) + else + # Check if dpkg hasn't been interrupted and is fully available. + # See this for more information: https://sources.debian.org/src/apt/1.4.9/apt-pkg/deb/debsystem.cc/#L141-L174 + local dpkg_dir="/var/lib/dpkg/updates/" + + # For each file in $dpkg_dir + while read dpkg_file <&9; do + # Check if the name of this file contains only numbers. + if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$"; then + # If so, that a remaining of dpkg. + ynh_print_warn "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." + set -o xtrace # set -x + return 1 + fi + done 9<<<"$(ls -1 $dpkg_dir)" + set -o xtrace # set -x + return 0 + fi + done + echo "apt still used, but timeout reached !" + set -o xtrace # set -x +} + +# Check either a package is installed or not +_ynh_apt_package_is_installed() { + local package=$1 + dpkg-query --show --showformat='${db:Status-Status}' "$package" 2>/dev/null \ + | grep --quiet "^installed$" &>/dev/null +} + +# Return the installed version of an apt package, if installed +_ynh_apt_package_version() { + if _ynh_apt_package_is_installed "$package"; then + dpkg-query --show --showformat='${Version}' "$package" 2>/dev/null + else + echo '' + fi +} + +# APT wrapper for non-interactive operation +_ynh_apt() { + _ynh_wait_dpkg_free + LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get --assume-yes --quiet -o=Acquire::Retries=3 -o=Dpkg::Use-Pty=0 $@ +} + +# Wrapper around "apt install" with the appropriate options +_ynh_apt_install() { + _ynh_apt --no-remove --option Dpkg::Options::=--force-confdef \ + --option Dpkg::Options::=--force-confold install $@ +} diff --git a/helpers/helpers.v2.1.d/backup b/helpers/helpers.v2.1.d/backup new file mode 100644 index 000000000..a40c4f1f2 --- /dev/null +++ b/helpers/helpers.v2.1.d/backup @@ -0,0 +1,277 @@ +#!/bin/bash + +CAN_BIND=${CAN_BIND:-1} + +# Add a file or a directory to the list of paths to backup +# +# usage: ynh_backup /path/to/stuff +# +# NB : note that this helper does *NOT* perform any copy in itself, it only +# declares stuff to be backuped via a CSV which is later picked up by the core +# +# NB 2 : there is a specific behavior for $data_dir (or childs of $data_dir) and +# /var/log/$app which are *NOT* backedup during safety-backup-before-upgrade, +# OR if the setting "do_not_backup_data" is equals 1 for that app +# +# The rationale is that these directories are usually too heavy to be integrated in every backup +# (think for example about Nextcloud with quite a lot of data, or an app with a lot of media files...) +# +# This is coupled to the fact that $data_dir and the log dir won't be (and +# should NOT) be deleted during remove, unless --purge is used. Hence, if the +# upgrade fails and the script is removed prior to restoring the backup, the +# data/logs are not destroyed. +# +ynh_backup() { + + local target="$1" + local is_data=false + + # If the path starts with /var/log/$app or $data_dir + if ([[ -n "${app:-}" ]] && [[ "$target" == "/var/log/$app*" ]]) || ([[ -n "${data_dir:-}" ]] && [[ "$target" == "$data_dir*" ]]) + then + is_data=true + fi + + if [[ -n "${app:-}" ]] + then + local do_not_backup_data=$(ynh_app_setting_get --key=do_not_backup_data) + fi + + # If backing up core only (used by ynh_backup_before_upgrade), + # don't backup big data items + if [[ "$is_data" == true ]] && ([[ ${do_not_backup_data:-0} -eq 1 ]] || [[ ${BACKUP_CORE_ONLY:-0} -eq 1 ]]); then + if [ $BACKUP_CORE_ONLY -eq 1 ]; then + ynh_print_info "$target will not be saved, because 'BACKUP_CORE_ONLY' is set." + else + ynh_print_info "$target will not be saved, because 'do_not_backup_data' is set." + fi + return 1 + fi + + # ============================================================================== + # Format correctly source and destination paths + # ============================================================================== + # Be sure the source path is not empty + if [ ! -e "$target" ]; then + ynh_print_warn "File or folder '${target}' to be backed up does not exist" + return 1 + fi + + # Transform the source path as an absolute path + # If it's a dir remove the ending / + src_path=$(realpath "$target") + + # Initialize the dest path with the source path relative to "/". + # eg: src_path=/etc/yunohost -> dest_path=etc/yunohost + dest_path="${src_path#/}" + + # Check if dest_path already exists in tmp archive + if [[ -e "${dest_path}" ]]; then + ynh_print_warn "Destination path '${dest_path}' already exist" + return 1 + fi + + # Add the relative current working directory to the destination path + local rel_dir="${YNH_CWD#$YNH_BACKUP_DIR}" + rel_dir="${rel_dir%/}/" + dest_path="${rel_dir}${dest_path}" + dest_path="${dest_path#/}" + # ============================================================================== + + # ============================================================================== + # Write file to backup into backup_list + # ============================================================================== + local src=$(echo "${src_path}" | sed --regexp-extended 's/"/\"\"/g') + local dest=$(echo "${dest_path}" | sed --regexp-extended 's/"/\"\"/g') + echo "\"${src}\",\"${dest}\"" >>"${YNH_BACKUP_CSV}" + + # ============================================================================== + + # Create the parent dir of the destination path + # It's for retro compatibility, some script consider ynh_backup creates this dir + mkdir --parents $(dirname "$YNH_BACKUP_DIR/${dest_path}") +} + +# Return the path in the archive where has been stocked the origin path +# +# [internal] +# +# usage: _get_archive_path ORIGIN_PATH +_get_archive_path() { + # For security reasons we use csv python library to read the CSV + python3 -c " +import sys +import csv +with open(sys.argv[1], 'r') as backup_file: + backup_csv = csv.DictReader(backup_file, fieldnames=['source', 'dest']) + for row in backup_csv: + if row['source']==sys.argv[2].strip('\"'): + print(row['dest']) + sys.exit(0) + raise Exception('Original path for %s not found' % sys.argv[2]) + " "${YNH_BACKUP_CSV}" "$1" + return $? +} + +# Restore a file or a directory from the backup archive +# +# usage: ynh_restore /path/to/stuff +# +# examples: +# ynh_restore "/etc/nginx/conf.d/$domain.d/$app.conf" +# +# If the file or dir to be restored already exists on the system and is lighter +# than 500 Mo, it is backed up in `/var/cache/yunohost/appconfbackup/`. +# Otherwise, the existing file or dir is removed. +# +# if `apps/$app/etc/nginx/conf.d/$domain.d/$app.conf` exists, restore it into +# `/etc/nginx/conf.d/$domain.d/$app.conf` +# otheriwse, search for a match in the csv (eg: conf/nginx.conf) and restore it into +# `/etc/nginx/conf.d/$domain.d/$app.conf` +ynh_restore() { + target="$1" + + local archive_path="$YNH_CWD${target}" + + # If the path starts with /var/log/$app or $data_dir + local is_data=false + if ([[ -n "${app:-}" ]] && [[ "$target" == "/var/log/$app*" ]]) || ([[ -n "${data_dir:-}" ]] && [[ "$target" == "$data_dir*" ]]) + then + is_data=true + fi + + # If archive_path doesn't exist, search for a corresponding path in CSV + if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]; then + if [[ "$is_data" == true ]] + then + ynh_print_info "Skipping $target which doesn't exists in the archive, probably because restoring from a safety-backup-before-upgrade" + # Assume it's not a big deal, we may be restoring a safety-backup-before-upgrade which doesnt contain those + return 0 + else + # (get_archive_path will raise an exception if no match found) + archive_path="$YNH_BACKUP_DIR/$(_get_archive_path \"$target\")" + fi + fi + + # Move the old directory if it already exists + if [[ -e "${target}" ]]; then + # Check if the file/dir size is less than 500 Mo + if [[ $(du --summarize --bytes ${target} | cut --delimiter="/" --fields=1) -le "500000000" ]]; then + local backup_file="/var/cache/yunohost/appconfbackup/${target}.backup.$(date '+%Y%m%d.%H%M%S')" + mkdir --parents "$(dirname "$backup_file")" + mv "${target}" "$backup_file" # Move the current file or directory + else + ynh_safe_rm "${target}" + fi + fi + + # Restore target into target + mkdir --parents $(dirname "$target") + + # Do a copy if it's just a mounting point + if mountpoint --quiet $YNH_BACKUP_DIR; then + if [[ -d "${archive_path}" ]]; then + archive_path="${archive_path}/." + mkdir --parents "$target" + fi + cp --archive "$archive_path" "${target}" + # Do a move if YNH_BACKUP_DIR is already a copy + else + mv "$archive_path" "${target}" + fi + + _ynh_apply_default_permissions "$target" +} + +# Restore all files that were previously backuped in an app backup script +# +# usage: ynh_restore_everything +ynh_restore_everything() { + # Deduce the relative path of $YNH_CWD + local REL_DIR="${YNH_CWD#$YNH_BACKUP_DIR/}" + REL_DIR="${REL_DIR%/}/" + + # For each destination path begining by $REL_DIR + cat ${YNH_BACKUP_CSV} | tr --delete $'\r' | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR.*\"$" \ + | while read line; do + local ARCHIVE_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR\K.*(?=\"$)") + ynh_restore "$ARCHIVE_PATH" + done +} + +_ynh_file_checksum_exists() { + local file=$1 + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + [[ -n "$(ynh_app_setting_get --key=$checksum_setting_name)" ]] +} + +# Calculate and store a file checksum into the app settings +# +# usage: ynh_store_file_checksum /path/to/file +ynh_store_file_checksum() { + set +o xtrace # set +x + local file=$1 + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + + ynh_app_setting_set --key=$checksum_setting_name --value=$(md5sum "$file" | cut --delimiter=' ' --fields=1) + + if ynh_in_ci_tests; 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. + # diff return 1 if the files are different, so the || true + diff --report-identical-files --unified --color=always $backup_file_checksum $file >&2 || true + fi + # Unset the variable, so it wouldn't trig a ynh_store_file_checksum without a ynh_backup_if_checksum_is_different before it. + unset backup_file_checksum + set -o xtrace # set -x +} + +# Verify the checksum and backup the file if it's different +# +# usage: ynh_backup_if_checksum_is_different /path/to/file +# +# This helper is primarily meant to allow to easily backup personalised/manually +# modified config files. +ynh_backup_if_checksum_is_different() { + set +o xtrace # set +x + local file=$1 + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + local checksum_value=$(ynh_app_setting_get --key=$checksum_setting_name) + # backup_file_checksum isn't declare as local, so it can be reuse by ynh_store_file_checksum + backup_file_checksum="" + if [ -n "$checksum_value" ]; then # Proceed only if a value was stored into the app settings + if [ -e $file ] && ! echo "$checksum_value $file" | md5sum --check --status; then # If the checksum is now different + + backup_file_checksum="/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 ynh_in_ci_tests; 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 + set -o xtrace # set -x +} + +# Delete a file checksum from the app settings +# +# usage: ynh_delete_file_checksum /path/to/file +ynh_delete_file_checksum() { + local file=$1 + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + ynh_app_setting_delete --key=$checksum_setting_name +} diff --git a/helpers/helpers.v2.1.d/composer b/helpers/helpers.v2.1.d/composer new file mode 100644 index 000000000..b9608f693 --- /dev/null +++ b/helpers/helpers.v2.1.d/composer @@ -0,0 +1,45 @@ +#!/bin/bash + +# Install and initialize Composer in the given directory +# +# The installed version is defined by `$composer_version` which should be defined +# as global prior to calling this helper. +# +# Will use `$install_dir` as workdir unless `$composer_workdir` exists (but that shouldnt be necessary) +# +# usage: ynh_composer_install +ynh_composer_install() { + local workdir="${composer_workdir:-$install_dir}" + + [[ -n "${composer_version}" ]] || ynh_die "\$composer_version should be defined before calling ynh_composer_install. (In the past, this was called \$YNH_COMPOSER_VERSION)" + + [[ ! -e "$workdir/composer.phar" ]] || ynh_safe_rm $workdir/composer.phar + + local composer_url="https://getcomposer.org/download/$composer_version/composer.phar" + + # 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=$workdir/composer.phar $composer_url 2>&1) \ + || ynh_die "$out" +} + +# Execute a command with Composer +# +# Will use `$install_dir` as workdir unless `$composer_workdir` exists (but that shouldnt be necessary) +# +# You may also define `composer_user=root` prior to call this helper if you +# absolutely need composer to run as root, but this is discouraged... +# +# usage: ynh_composer_exec commands +ynh_composer_exec() { + local workdir="${composer_workdir:-$install_dir}" + + COMPOSER_HOME="$workdir/.composer" \ + COMPOSER_MEMORY_LIMIT=-1 \ + sudo -E -u "${composer_user:-$app}" \ + php${php_version} "$workdir/composer.phar" $@ \ + -d "$workdir" --no-interaction --no-ansi 2>&1 +} diff --git a/helpers/helpers.v2.1.d/config b/helpers/helpers.v2.1.d/config new file mode 100644 index 000000000..73b5d85b3 --- /dev/null +++ b/helpers/helpers.v2.1.d/config @@ -0,0 +1,363 @@ +#!/bin/bash + +_ynh_app_config_get_one() { + local short_setting="$1" + local type="$2" + local bind="$3" + local getter="get__${short_setting}" + # Get value from getter if exists + if type -t $getter 2>/dev/null | grep -q '^function$' 2>/dev/null; then + old[$short_setting]="$($getter)" + formats[${short_setting}]="yaml" + + elif [[ "$bind" == *"("* ]] && type -t "get__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then + old[$short_setting]="$("get__${bind%%(*}" $short_setting $type $bind)" + formats[${short_setting}]="yaml" + + elif [[ "$bind" == "null" ]]; then + old[$short_setting]="YNH_NULL" + + # Get value from app settings or from another file + elif [[ "$type" == "file" ]]; then + if [[ "$bind" == "settings" ]]; then + ynh_die "File '${short_setting}' can't be stored in settings" + fi + old[$short_setting]="$(ls "$(echo $bind | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)" 2>/dev/null || echo YNH_NULL)" + file_hash[$short_setting]="true" + + # Get multiline text from settings or from a full file + elif [[ "$type" == "text" ]]; then + if [[ "$bind" == "settings" ]]; then + old[$short_setting]="$(ynh_app_setting_get $app $short_setting)" + elif [[ "$bind" == *":"* ]]; then + ynh_die "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@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/) 2>/dev/null || echo YNH_NULL)" + fi + + # Get value from a kind of key/value file + else + local bind_after="" + if [[ "$bind" == "settings" ]]; then + bind=":/etc/yunohost/apps/$app/settings.yml" + fi + local bind_key_="$(echo "$bind" | cut -d: -f1)" + bind_key_=${bind_key_:-$short_setting} + if [[ "$bind_key_" == *">"* ]]; then + bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" + bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" + fi + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)" + old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key_}" --after="${bind_after}")" + + fi +} +_ynh_app_config_apply_one() { + local short_setting="$1" + local setter="set__${short_setting}" + local bind="${binds[$short_setting]}" + local type="${types[$short_setting]}" + if [ "${changed[$short_setting]}" == "true" ]; then + # Apply setter if exists + if type -t $setter 2>/dev/null | grep -q '^function$' 2>/dev/null; then + $setter + + elif [[ "$bind" == *"("* ]] && type -t "set__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then + "set__${bind%%(*}" $short_setting $type $bind + + elif [[ "$bind" == "null" ]]; then + return + + # Save in a file + elif [[ "$type" == "file" ]]; then + if [[ "$bind" == "settings" ]]; then + ynh_die "File '${short_setting}' can't be stored in settings" + fi + local bind_file="$(echo "$bind" | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)" + if [[ "${!short_setting}" == "" ]]; then + ynh_backup_if_checksum_is_different "$bind_file" + ynh_safe_rm "$bind_file" + ynh_delete_file_checksum "$bind_file" + ynh_print_info "File '$bind_file' removed" + else + ynh_backup_if_checksum_is_different "$bind_file" + if [[ "${!short_setting}" != "$bind_file" ]]; then + cp "${!short_setting}" "$bind_file" + fi + if _ynh_file_checksum_exists "$bind_file" + then + ynh_store_file_checksum "$bind_file" + fi + ynh_print_info "File '$bind_file' overwritten with ${!short_setting}" + fi + + # Save value in app settings + elif [[ "$bind" == "settings" ]]; then + ynh_app_setting_set --key=$short_setting --value="${!short_setting}" + ynh_print_info "Configuration key '$short_setting' edited in app settings" + + # Save multiline text in a file + elif [[ "$type" == "text" ]]; then + if [[ "$bind" == *":"* ]]; then + ynh_die "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@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)" + ynh_backup_if_checksum_is_different "$bind_file" + echo "${!short_setting}" >"$bind_file" + if _ynh_file_checksum_exists "$bind_file" + then + ynh_store_file_checksum "$bind_file" + fi + ynh_print_info "File '$bind_file' overwritten with the content provided in question '${short_setting}'" + + # Set value into a kind of key/value file + else + local bind_after="" + local bind_key_="$(echo "$bind" | cut -d: -f1)" + if [[ "$bind_key_" == *">"* ]]; then + bind_after="$(echo "${bind_key_}" | cut -d'>' -f1)" + bind_key_="$(echo "${bind_key_}" | cut -d'>' -f2)" + fi + bind_key_=${bind_key_:-$short_setting} + local bind_file="$(echo "$bind" | cut -d: -f2 | sed s@__INSTALL_DIR__@${install_dir:-}@ | sed s/__APP__/$app/)" + + ynh_backup_if_checksum_is_different "$bind_file" + ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}" + if _ynh_file_checksum_exists "$bind_file" + then + ynh_store_file_checksum "$bind_file" + fi + + # We stored the info in settings in order to be able to upgrade the app + ynh_app_setting_set --key=$short_setting --value="${!short_setting}" + ynh_print_info "Configuration key '$bind_key_' edited into $bind_file" + + fi + fi +} +_ynh_app_config_get() { + # From settings + local lines + lines=$( + python3 <" in bind_section: + bind_section = bind_section + bind_panel_file + else: + bind_section = regex + bind_section + bind_panel_file + + for name, param in section.items(): + if not isinstance(param, dict): + continue + + bind = param.get('bind') + + if not bind: + if bind_section: + bind = bind_section + else: + bind = 'settings' + elif bind[-1] == ":" and bind_section and ":" in bind_section: + regex, bind_file = bind_section.split(":") + if ">" in bind: + bind = bind + bind_file + else: + bind = regex + bind + bind_file + if bind == "settings" and param.get('type', 'string') == 'file': + bind = 'null' + + print('|'.join([ + name, + param.get('type', 'string'), + bind + ])) +EOL + ) + for line in $lines; do + # Split line into short_setting, type and bind + IFS='|' read short_setting type bind <<<"$line" + binds[${short_setting}]="$bind" + types[${short_setting}]="$type" + file_hash[${short_setting}]="" + formats[${short_setting}]="" + ynh_app_config_get_one $short_setting $type $bind + done + +} + +_ynh_app_config_apply() { + for short_setting in "${!old[@]}"; do + ynh_app_config_apply_one $short_setting + done +} + +_ynh_app_config_show() { + for short_setting in "${!old[@]}"; do + if [[ "${old[$short_setting]}" != YNH_NULL ]]; then + if [[ "${formats[$short_setting]}" == "yaml" ]]; then + ynh_return "${short_setting}:" + ynh_return "$(echo "${old[$short_setting]}" | sed 's/^/ /g')" + else + ynh_return "${short_setting}: '$(echo "${old[$short_setting]}" | sed "s/'/''/g" | sed ':a;N;$!ba;s/\n/\n\n/g')'" + fi + fi + done +} + +_ynh_app_config_validate() { + # Change detection + ynh_script_progression "Checking what changed in the new configuration..." + local nothing_changed=true + local changes_validated=true + for short_setting in "${!old[@]}"; do + changed[$short_setting]=false + if [ -z ${!short_setting+x} ]; then + # Assign the var with the old value in order to allows multiple + # args validation + declare -g "$short_setting"="${old[$short_setting]}" + continue + fi + if [ ! -z "${file_hash[${short_setting}]}" ]; then + file_hash[old__$short_setting]="" + file_hash[new__$short_setting]="" + if [ -f "${old[$short_setting]}" ]; then + file_hash[old__$short_setting]=$(sha256sum "${old[$short_setting]}" | cut -d' ' -f1) + if [ -z "${!short_setting}" ]; then + changed[$short_setting]=true + nothing_changed=false + fi + fi + if [ -f "${!short_setting}" ]; then + file_hash[new__$short_setting]=$(sha256sum "${!short_setting}" | cut -d' ' -f1) + if [[ "${file_hash[old__$short_setting]}" != "${file_hash[new__$short_setting]}" ]]; then + changed[$short_setting]=true + nothing_changed=false + fi + fi + else + if [[ "${!short_setting}" != "${old[$short_setting]}" ]]; then + changed[$short_setting]=true + nothing_changed=false + fi + fi + done + if [[ "$nothing_changed" == "true" ]]; then + ynh_print_info "Nothing has changed" + exit 0 + fi + + # Run validation if something is changed + ynh_script_progression "Validating the new configuration..." + + for short_setting in "${!old[@]}"; do + [[ "${changed[$short_setting]}" == "false" ]] && continue + local result="" + if type -t validate__$short_setting | grep -q '^function$' 2>/dev/null; then + result="$(validate__$short_setting)" + elif [[ "$bind" == *"("* ]] && type -t "validate__${bind%%(*}" 2>/dev/null | grep -q '^function$' 2>/dev/null; then + "validate__${bind%%(*}" $short_setting + fi + if [ -n "$result" ]; then + # + # Return a yaml such as: + # + # validation_errors: + # some_key: "An error message" + # some_other_key: "Another error message" + # + # We use changes_validated to know if this is + # the first validation error + if [[ "$changes_validated" == true ]]; then + ynh_return "validation_errors:" + fi + ynh_return " ${short_setting}: \"$result\"" + changes_validated=false + fi + done + + # If validation failed, exit the script right now (instead of going into apply) + # Yunohost core will pick up the errors returned via ynh_return previously + if [[ "$changes_validated" == "false" ]]; then + exit 0 + fi + +} + +ynh_app_config_get_one() { + _ynh_app_config_get_one $1 $2 $3 +} + +ynh_app_config_get() { + _ynh_app_config_get +} + +ynh_app_config_show() { + _ynh_app_config_show +} + +ynh_app_config_validate() { + _ynh_app_config_validate +} + +ynh_app_config_apply_one() { + _ynh_app_config_apply_one $1 +} +ynh_app_config_apply() { + _ynh_app_config_apply +} + +ynh_app_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=() + declare -Ag file_hash=() + declare -Ag binds=() + declare -Ag types=() + declare -Ag formats=() + + case $1 in + show) + ynh_app_config_get + ynh_app_config_show + ;; + apply) + max_progression=4 + ynh_script_progression "Reading config panel description and current configuration..." + ynh_app_config_get + + ynh_app_config_validate + + ynh_script_progression "Applying the new configuration..." + ynh_app_config_apply + ynh_script_progression "Configuration of $app completed" + ;; + *) + ynh_app_action_run $1 + esac +} diff --git a/helpers/helpers.v2.1.d/fail2ban b/helpers/helpers.v2.1.d/fail2ban new file mode 100644 index 000000000..6ca379074 --- /dev/null +++ b/helpers/helpers.v2.1.d/fail2ban @@ -0,0 +1,118 @@ +#!/bin/bash + +# Create a dedicated fail2ban config (jail and filter conf files) +# +# usage: ynh_config_add_fail2ban --logpath=log_file --failregex=filter +# | arg: --logpath= - Log file to be checked by fail2ban +# | arg: --failregex= - Failregex to be looked for by fail2ban +# +# If --logpath / --failregex are provided, the helper will generate the appropriate conf using these. +# +# Otherwise, it will assume that the app provided templates, namely +# `../conf/f2b_jail.conf` and `../conf/f2b_filter.conf` +# +# They will typically look like (for example here for synapse): +# ``` +# f2b_jail.conf: +# [__APP__] +# enabled = true +# port = http,https +# filter = __APP__ +# logpath = /var/log/__APP__/logfile.log +# maxretry = 5 +# ``` +# ``` +# f2b_filter.conf: +# [INCLUDES] +# before = common.conf +# [Definition] +# +# # Part of regex definition (just used to make more easy to make the global regex) +# __synapse_start_line = .? \- synapse\..+ \- +# +# # Regex definition. +# failregex = ^%(__synapse_start_line)s INFO \- POST\-(\d+)\- \- \d+ \- Received request\: POST /_matrix/client/r0/login\??%(__synapse_start_line)s INFO \- POST\-\1\- Got login request with identifier: \{u'type': u'm.id.user', u'user'\: u'(.+?)'\}, medium\: None, address: None, user\: u'\5'%(__synapse_start_line)s WARNING \- \- (Attempted to login as @\5\:.+ but they do not exist|Failed password login for user @\5\:.+)$ +# +# ignoreregex = +# ``` +# +# ##### Regarding the the `failregex` option: +# +# regex to match the password failure messages in the logfile. The host must be +# matched by a group named "`host`". The tag "``" can be used for standard +# IP/hostname matching and is only an alias for `(?:::f{4,6}:)?(?P[\w\-.^_]+)` +# +# You can find some more explainations about how to make a regex here : +# https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters +# +# To validate your regex you can test with this command: +# ``` +# fail2ban-regex /var/log/YOUR_LOG_FILE_PATH /etc/fail2ban/filter.d/YOUR_APP.conf +# ``` +ynh_config_add_fail2ban() { + # ============ Argument parsing ============= + local -A args_array=([l]=logpath= [r]=failregex=) + local logpath + local failregex + ynh_handle_getopts_args "$@" + # =========================================== + + # If failregex is provided, Build a config file on-the-fly using $logpath and $failregex + if [[ -n "${failregex:-}" ]]; then + test -n "$logpath" || ynh_die "ynh_config_add_fail2ban expects a logfile path as first argument and received nothing." + + echo " +[__APP__] +enabled = true +port = http,https +filter = __APP__ +logpath = __LOGPATH__ +maxretry = 5 +" >"$YNH_APP_BASEDIR/conf/f2b_jail.conf" + + echo " +[INCLUDES] +before = common.conf +[Definition] +failregex = __FAILREGEX__ +ignoreregex = +" >"$YNH_APP_BASEDIR/conf/f2b_filter.conf" + fi + + ynh_config_add --template="f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" + ynh_config_add --template="f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" + + # if "$logpath" doesn't exist (as if using --use_template argument), assign + # "$logpath" using the one in the previously generated fail2ban conf file + if [ -z "${logpath:-}" ]; then + # the first sed deletes possibles spaces and the second one extract the path + logpath=$(grep "^logpath" "/etc/fail2ban/jail.d/$app.conf" | sed "s/ //g" | sed "s/logpath=//g") + fi + + # Create the folder and logfile if they doesn't exist, + # as fail2ban require an existing logfile before configuration + mkdir -p "/var/log/$app" + if [ ! -f "$logpath" ]; then + touch "$logpath" + fi + # Make sure log folder's permissions are correct + chown -R "$app:$app" "/var/log/$app" + chmod -R u=rwX,g=rX,o= "/var/log/$app" + + ynh_systemctl --service=fail2ban --action=reload --wait_until="(Started|Reloaded) Fail2Ban Service" --log_path=systemd + + local fail2ban_error="$(journalctl --no-hostname --unit=fail2ban | tail --lines=50 | grep "WARNING.*$app.*")" + if [[ -n "$fail2ban_error" ]]; then + ynh_print_warn "Fail2ban failed to load the jail for $app" + ynh_print_warn "${fail2ban_error#*WARNING}" + fi +} + +# Remove the dedicated fail2ban config (jail and filter conf files) +# +# usage: ynh_config_remove_fail2ban +ynh_config_remove_fail2ban() { + ynh_safe_rm "/etc/fail2ban/jail.d/$app.conf" + ynh_safe_rm "/etc/fail2ban/filter.d/$app.conf" + ynh_systemctl --service=fail2ban --action=reload +} diff --git a/helpers/helpers.v2.1.d/getopts b/helpers/helpers.v2.1.d/getopts new file mode 100644 index 000000000..ca517eebd --- /dev/null +++ b/helpers/helpers.v2.1.d/getopts @@ -0,0 +1,187 @@ +#!/bin/bash + +# Internal helper design to allow helpers to use getopts to manage their arguments +# +# [internal] +# +# example: function my_helper() +# { +# local -A args_array=( [a]=arg1= [b]=arg2= [c]=arg3 ) +# local arg1 +# local arg2 +# local arg3 +# ynh_handle_getopts_args "$@" +# +# [...] +# } +# my_helper --arg1 "val1" -b val2 -c +# +# usage: ynh_handle_getopts_args "$@" +# | arg: $@ - Simply "$@" to tranfert all the positionnal arguments to the function +# +# This helper need an array, named "args_array" with all the arguments used by the helper +# that want to use ynh_handle_getopts_args +# Be carreful, this array has to be an associative array, as the following example: +# local -A args_array=( [a]=arg1 [b]=arg2= [c]=arg3 ) +# Let's explain this array: +# a, b and c are short options, -a, -b and -c +# arg1, arg2 and arg3 are the long options associated to the previous short ones. --arg1, --arg2 and --arg3 +# For each option, a short and long version has to be defined. +# Let's see something more significant +# local -A args_array=( [u]=user [f]=finalpath= [d]=database ) +# +# NB: Because we're using 'declare' without -g, the array will be declared as a local variable. +# +# Please keep in mind that the long option will be used as a variable to store the values for this option. +# For the previous example, that means that $finalpath will be fill with the value given as argument for this option. +# +# Also, in the previous example, finalpath has a '=' at the end. That means this option need a value. +# So, the helper has to be call with --finalpath /final/path, --finalpath=/final/path or -f /final/path, the variable $finalpath will get the value /final/path +# If there's many values for an option, -f /final /path, the value will be separated by a ';' $finalpath=/final;/path +# For an option without value, like --user in the example, the helper can be called only with --user or -u. $user will then get the value 1. +# +ynh_handle_getopts_args() { + # Trick to only re-enable debugging if it was set before + local xtrace_enable=$(set +o | grep xtrace) + + # Manage arguments only if there's some provided + set +o xtrace # set +x + if [ $# -eq 0 ]; then + eval "$xtrace_enable" + return + # Validate that the first char is - because it should be something like --option=value or -o ... + elif [[ "${1:0:1}" != "-" ]] + then + ynh_die "It looks like you called the helper using positional arguments instead of keyword arguments ?" + fi + + # Store arguments in an array to keep each argument separated + local arguments=("$@") + + # For each option in the array, reduce to short options for getopts (e.g. for [u]=user, --user will be -u) + # And built parameters string for getopts + # ${!args_array[@]} is the list of all option_flags in the array (An option_flag is 'u' in [u]=user, user is a value) + local getopts_parameters="" + local option_flag="" + for option_flag in "${!args_array[@]}"; do + # Concatenate each option_flags of the array to build the string of arguments for getopts + # Will looks like 'abcd' for -a -b -c -d + # If the value of an option_flag finish by =, it's an option with additionnal values. (e.g. --user bob or -u bob) + # Check the last character of the value associate to the option_flag + if [ "${args_array[$option_flag]: -1}" = "=" ]; then + # For an option with additionnal values, add a ':' after the letter for getopts. + getopts_parameters="${getopts_parameters}${option_flag}:" + else + getopts_parameters="${getopts_parameters}${option_flag}" + fi + # Check each argument given to the function + local arg="" + # ${#arguments[@]} is the size of the array + for arg in $(seq 0 $((${#arguments[@]} - 1))); do + # Escape options' values starting with -. Otherwise the - will be considered as another option. + arguments[arg]="${arguments[arg]//--${args_array[$option_flag]}-/--${args_array[$option_flag]}\\TOBEREMOVED\\-}" + # And replace long option (value of the option_flag) by the short option, the option_flag itself + # (e.g. for [u]=user, --user will be -u) + # Replace long option with = (match the beginning of the argument) + arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]}/-${option_flag} /")" + # And long option without = (match the whole line) + arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]%=}$/-${option_flag} /")" + done + done + + # Read and parse all the arguments + # Use a function here, to use standart arguments $@ and be able to use shift. + parse_arg() { + # Read all arguments, until no arguments are left + while [ $# -ne 0 ]; do + # Initialize the index of getopts + OPTIND=1 + # Parse with getopts only if the argument begin by -, that means the argument is an option + # getopts will fill $parameter with the letter of the option it has read. + local parameter="" + getopts ":$getopts_parameters" parameter || true + + if [ "$parameter" = "?" ]; then + ynh_die "Invalid argument: ${1:-}" + elif [ "$parameter" = ":" ]; then + ynh_die "${1:-} parameter requires an argument." + else + local shift_value=1 + # Use the long option, corresponding to the short option read by getopts, as a variable + # (e.g. for [u]=user, 'user' will be used as a variable) + # Also, remove '=' at the end of the long option + # The variable name will be stored in 'option_var' + local option_var="${args_array[$parameter]%=}" + # If this option doesn't take values + # if there's a '=' at the end of the long option name, this option takes values + if [ "${args_array[$parameter]: -1}" != "=" ]; then + # 'eval ${option_var}' will use the content of 'option_var' + eval ${option_var}=1 + else + # Read all other arguments to find multiple value for this option. + # Load args in a array + local all_args=("$@") + + # If the first argument is longer than 2 characters, + # There's a value attached to the option, in the same array cell + if [ ${#all_args[0]} -gt 2 ]; then + # Remove the option and the space, so keep only the value itself. + all_args[0]="${all_args[0]#-${parameter} }" + + # At this point, if all_args[0] start with "-", then the argument is not well formed + if [ "${all_args[0]:0:1}" == "-" ]; then + ynh_die "Argument \"${all_args[0]}\" not valid! Did you use a single \"-\" instead of two?" + fi + # Reduce the value of shift, because the option has been removed manually + shift_value=$((shift_value - 1)) + fi + + # Declare the content of option_var as a variable. + eval ${option_var}="" + # Then read the array value per value + local i + for i in $(seq 0 $((${#all_args[@]} - 1))); do + # If this argument is an option, end here. + if [ "${all_args[$i]:0:1}" == "-" ]; then + # Ignore the first value of the array, which is the option itself + if [ "$i" -ne 0 ]; then + break + fi + else + # Ignore empty parameters + if [ -n "${all_args[$i]}" ]; then + # Else, add this value to this option + # Each value will be separated by ';' + if [ -n "${!option_var}" ]; then + # If there's already another value for this option, add a ; before adding the new value + eval ${option_var}+="\;" + fi + + # Remove the \ that escape - at beginning of values. + all_args[i]="${all_args[i]//\\TOBEREMOVED\\/}" + + # For the record. + # We're using eval here to get the content of the variable stored itself as simple text in $option_var... + # Other ways to get that content would be to use either ${!option_var} or declare -g ${option_var} + # But... ${!option_var} can't be used as left part of an assignation. + # declare -g ${option_var} will create a local variable (despite -g !) and will not be available for the helper itself. + # So... Stop fucking arguing each time that eval is evil... Go find an other working solution if you can find one! + + eval ${option_var}+='"${all_args[$i]}"' + fi + shift_value=$((shift_value + 1)) + fi + done + fi + fi + + # Shift the parameter and its argument(s) + shift $shift_value + done + } + + # Call parse_arg and pass the modified list of args as an array of arguments. + parse_arg "${arguments[@]}" + + eval "$xtrace_enable" +} diff --git a/helpers/helpers.v2.1.d/go b/helpers/helpers.v2.1.d/go new file mode 100644 index 000000000..bb272d50c --- /dev/null +++ b/helpers/helpers.v2.1.d/go @@ -0,0 +1,195 @@ +#!/bin/bash + +readonly GOENV_INSTALL_DIR="/opt/goenv" +# goenv_ROOT is the directory of goenv, it needs to be loaded as a environment variable. +export GOENV_ROOT="$GOENV_INSTALL_DIR" + +_ynh_load_go_in_path_and_other_tweaks() { + + # Get the absolute path of this version of go + go_dir="$GOENV_INSTALL_DIR/versions/$app/bin" + + # Load the path of this version of go in $PATH + if [[ :$PATH: != *":$go_dir"* ]]; then + PATH="$go_dir:$PATH" + fi + + # Export PATH such that it's available through sudo -E / ynh_exec_as $app + export PATH + + # This is in full lowercase such that it gets replaced in templates + path_with_go="$PATH" + PATH_with_go="$PATH" + + # Sets the local application-specific go version + pushd ${install_dir} + $GOENV_INSTALL_DIR/bin/goenv local $go_version + popd +} + +# Install a specific version of Go using goenv +# +# The installed version is defined by `$go_version` which should be defined as global prior to calling this helper +# +# usage: ynh_go_install +# +# The helper adds the appropriate, specific version of go to the `$PATH` variable (which +# is preserved when calling `ynh_exec_as_app`). Also defines: +# - `$path_with_go` (the value of the modified `$PATH`, but you dont really need it?) +# - `$go_dir` (the directory containing the specific go version) +# +# This helper also creates a /etc/profile.d/goenv.sh that configures PATH environment for goenv +ynh_go_install () { + + [[ -n "${go_version:-}" ]] || ynh_die "\$go_version should be defined prior to calling ynh_go_install" + + # Load goenv path in PATH + local CLEAR_PATH="$GOENV_INSTALL_DIR/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Go prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Move an existing Go binary, to avoid to block goenv + test -x /usr/bin/go && mv /usr/bin/go /usr/bin/go_goenv + + # Install or update goenv + mkdir -p $GOENV_INSTALL_DIR + pushd "$GOENV_INSTALL_DIR" + if ! [ -x "$GOENV_INSTALL_DIR/bin/goenv" ]; then + ynh_print_info "Downloading goenv..." + git init -q + git remote add origin https://github.com/syndbg/goenv.git + else + ynh_print_info "Updating goenv..." + fi + git fetch -q --tags --prune origin + local git_latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)") + git checkout -q "$git_latest_tag" + _ynh_go_try_bash_extension + goenv=$GOENV_INSTALL_DIR/bin/goenv + popd + + # Install or update xxenv-latest + mkdir -p "$GOENV_INSTALL_DIR/plugins/xxenv-latest" + pushd "$GOENV_INSTALL_DIR/plugins/xxenv-latest" + if ! [ -x "$GOENV_INSTALL_DIR/plugins/xxenv-latest/bin/goenv-latest" ]; then + ynh_print_info "Downloading xxenv-latest..." + git init -q + git remote add origin https://github.com/momo-lab/xxenv-latest.git + else + ynh_print_info "Updating xxenv-latest..." + fi + git fetch -q --tags --prune origin + local git_latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)") + git checkout -q "$git_latest_tag" + popd + + # Enable caching + mkdir -p "${GOENV_INSTALL_DIR}/cache" + + # Create shims directory if needed + mkdir -p "${GOENV_INSTALL_DIR}/shims" + + # Restore /usr/local/bin in PATH + PATH=$CLEAR_PATH + + # And replace the old Go binary + test -x /usr/bin/go_goenv && mv /usr/bin/go_goenv /usr/bin/go + + # Install the requested version of Go + local final_go_version=$("$GOENV_INSTALL_DIR/plugins/xxenv-latest/bin/goenv-latest" --print "$go_version") + ynh_print_info "Installation of Go-$final_go_version" + goenv install --quiet --skip-existing "$final_go_version" 2>&1 + + # Store go_version into the config of this app + ynh_app_setting_set --app="$app" --key="go_version" --value="$final_go_version" + go_version=$final_go_version + + # Cleanup Go versions + _ynh_go_cleanup + + # Set environment for Go users + echo "#goenv +export GOENV_ROOT=$GOENV_INSTALL_DIR +export PATH=\"$GOENV_INSTALL_DIR/bin:$PATH\" +eval \"\$(goenv init -)\" +#goenv" > /etc/profile.d/goenv.sh + + # Load the environment + eval "$(goenv init -)" + + _ynh_load_go_in_path_and_other_tweaks +} + +# Remove the version of Go used by the app. +# +# This helper will also cleanup Go versions +# +# usage: ynh_go_remove +ynh_go_remove () { + local go_version=$(ynh_app_setting_get --key="go_version") + + # Load goenv path in PATH + local CLEAR_PATH="$GOENV_INSTALL_DIR/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Go prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Remove the line for this app + ynh_app_setting_delete --key="go_version" + + # Cleanup Go versions + _ynh_go_cleanup +} + +# Remove no more needed versions of Go used by the app. +# +# [internal] +# +# This helper will check what Go version are no more required, +# and uninstall them +# If no app uses Go, goenv will be also removed. +# +# usage: _ynh_go_cleanup +_ynh_go_cleanup () { + + # List required Go versions + local installed_apps=$(yunohost app list --output-as json --quiet | jq -r .apps[].id) + local required_go_versions="" + for installed_app in $installed_apps + do + local installed_app_go_version=$(ynh_app_setting_get --app=$installed_app --key="go_version") + if [[ $installed_app_go_version ]] + then + required_go_versions="${installed_app_go_version}\n${required_go_versions}" + fi + done + + # Remove no more needed Go versions + local installed_go_versions=$(goenv versions --bare --skip-aliases | grep -Ev '/') + for installed_go_version in $installed_go_versions + do + if ! `echo ${required_go_versions} | grep "${installed_go_version}" 1>/dev/null 2>&1` + then + ynh_print_info "Removing of Go-$installed_go_version" + $GOENV_INSTALL_DIR/bin/goenv uninstall --force "$installed_go_version" + fi + done + + # If none Go version is required + if [[ ! $required_go_versions ]] + then + # Remove goenv environment configuration + ynh_print_info "Removing of goenv" + ynh_safe_rm "$GOENV_INSTALL_DIR" + ynh_safe_rm "/etc/profile.d/goenv.sh" + fi +} + +_ynh_go_try_bash_extension() { + if [ -x src/configure ]; then + src/configure && make -C src || { + ynh_print_info "Optional bash extension failed to build, but things will still work normally." + } + fi +} diff --git a/helpers/helpers.v2.1.d/logging b/helpers/helpers.v2.1.d/logging new file mode 100644 index 000000000..a2471cd2d --- /dev/null +++ b/helpers/helpers.v2.1.d/logging @@ -0,0 +1,120 @@ +#!/bin/bash + +# Print a message to stderr and terminate the current script +# +# usage: ynh_die "Some message" +ynh_die() { + set +o xtrace # set +x + if [[ -n "${1:-}" ]] + then + if [[ -n "${YNH_STDRETURN:-}" ]] + then + python3 -c 'import yaml, sys; print(yaml.dump({"error": sys.stdin.read()}))' <<< "${1:-}" >>"$YNH_STDRETURN" + fi + echo "${1:-}" 1>&2 + fi + exit 1 +} + +# Print an "INFO" message +# +# usage: ynh_print_info "Some message" +ynh_print_info() { + echo "$1" >&$YNH_STDINFO +} + +# Print a warning on stderr +# +# usage: ynh_print_warn "Some message" +ynh_print_warn() { + echo "$1" >&2 +} + +# Execute a command and redirect stderr to stdout +# +# usage: ynh_hide_warnings your command and args +# | arg: command - command to execute +# +ynh_hide_warnings() { + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" 2>&1 +} + +# Execute a command and redirect stderr in /dev/null. Print stderr on error. +# +# usage: ynh_exec_and_print_stderr_only_if_error your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_and_print_stderr_only_if_error +ynh_exec_and_print_stderr_only_if_error() { + logfile="$(mktemp)" + rc=0 + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" 2> "$logfile" || rc="$?" + if (( rc != 0 )); then + cat "$logfile" >&2 + ynh_safe_rm "$logfile" + return "$rc" + fi +} + +# Return data to the YunoHost core for later processing +# (to be used by special hooks like app config panel and core diagnosis) +# +# usage: ynh_return somedata +ynh_return() { + echo "$1" >>"$YNH_STDRETURN" +} + +# Initial definitions for ynh_script_progression +increment_progression=0 +previous_weight=0 +max_progression=-1 +# Set the scale of the progression bar +# progress_string(0,1,2) should have the size of the scale. +progress_scale=20 +progress_string2="####################" +progress_string1="++++++++++++++++++++" +progress_string0="...................." + +# Print a progress bar showing the progression of an app script +# +# usage: ynh_script_progression "Some message" +ynh_script_progression() { + set +o xtrace # set +x + + # Compute $max_progression (if we didn't already) + if [ "$max_progression" = -1 ]; then + # Get the number of occurrences of 'ynh_script_progression' in the script. Except those are commented. + local helper_calls= + max_progression="$(grep --count "^[^#]*ynh_script_progression" $0)" + fi + + # Increment each execution of ynh_script_progression in this script by the weight of the previous call. + increment_progression=$(($increment_progression + $previous_weight)) + # Store the weight of the current call in $previous_weight for next call + previous_weight=1 + + # Reduce $increment_progression to the size of the scale + local effective_progression=$(($increment_progression * $progress_scale / $max_progression)) + + # If last is specified, fill immediately the progression_bar + + # Build $progression_bar from progress_string(0,1,2) according to $effective_progression and the weight of the current task + # expected_progression is the progression expected after the current task + local expected_progression="$((($increment_progression + 1) * $progress_scale / $max_progression - $effective_progression))" + + # Hack for the "--last" message + if grep -qw 'completed' <<< "$1"; + then + effective_progression=$progress_scale + expected_progression=0 + fi + # left_progression is the progression not yet done + local left_progression="$(($progress_scale - $effective_progression - $expected_progression))" + # Build the progression bar with $effective_progression, work done, $expected_progression, current work and $left_progression, work to be done. + local progression_bar="${progress_string2:0:$effective_progression}${progress_string1:0:$expected_progression}${progress_string0:0:$left_progression}" + + echo "[$progression_bar] > ${1}" >&$YNH_STDINFO + set -o xtrace # set -x +} diff --git a/helpers/helpers.v2.1.d/logrotate b/helpers/helpers.v2.1.d/logrotate new file mode 100644 index 000000000..e431841c5 --- /dev/null +++ b/helpers/helpers.v2.1.d/logrotate @@ -0,0 +1,75 @@ +#!/bin/bash + +FIRST_CALL_TO_LOGROTATE="true" + +# Add a logrotate configuration to manage log files / log directory +# +# usage: ynh_config_add_logrotate [/path/to/log/file/or/folder] +# +# If not argument is provided, `/var/log/$app/*.log` is used as default. +# +# The configuration is autogenerated by YunoHost +# (ie it doesnt come from a specific app template like nginx or systemd conf) +ynh_config_add_logrotate() { + + local logfile="${1:-}" + + set -o noglob + if [[ -z "$logfile" ]]; then + logfile="/var/log/${app}/*.log" + elif [[ "${logfile##*.}" != "log" ]] && [[ "${logfile##*.}" != "txt" ]]; then + logfile="$logfile/*.log" + fi + set +o noglob + + for stuff in $logfile + do + # Make sure the permissions of the parent dir are correct (otherwise the config file could be ignored and the corresponding logs never rotated) + local dir=$(dirname "$stuff") + mkdir --parents $dir + chmod 750 $dir + chown $app:$app $dir + done + + local tempconf="$(mktemp)" + cat << EOF >$tempconf +$logfile { + # Rotate if the logfile exceeds 100Mo + size 100M + # Keep 12 old log maximum + rotate 12 + # Compress the logs with gzip + compress + # Compress the log at the next cycle. So keep always 2 non compressed logs + delaycompress + # Copy and truncate the log to allow to continue write on it. Instead of moving the log. + copytruncate + # Do not trigger an error if the log is missing + missingok + # Do not rotate if the log is empty + notifempty + # Keep old logs in the same dir + noolddir +} +EOF + + if [[ "$FIRST_CALL_TO_LOGROTATE" == "true" ]] + then + cat $tempconf > /etc/logrotate.d/$app + else + cat $tempconf >> /etc/logrotate.d/$app + fi + + FIRST_CALL_TO_LOGROTATE="false" + + chmod 644 "/etc/logrotate.d/$app" +} + +# Remove the app's logrotate config. +# +# usage:ynh_config_remove_logrotate +ynh_config_remove_logrotate() { + if [ -e "/etc/logrotate.d/$app" ]; then + rm "/etc/logrotate.d/$app" + fi +} diff --git a/helpers/helpers.v2.1.d/mongodb b/helpers/helpers.v2.1.d/mongodb new file mode 100644 index 000000000..3fb851277 --- /dev/null +++ b/helpers/helpers.v2.1.d/mongodb @@ -0,0 +1,274 @@ +#!/bin/bash + +# Execute a mongo command +# +# example: ynh_mongo_exec --command='db.getMongo().getDBNames().indexOf("wekan")' +# example: ynh_mongo_exec --command="db.getMongo().getDBNames().indexOf(\"wekan\")" +# +# usage: ynh_mongo_exec [--database=database] --command="command" +# | arg: --database= - The database to connect to +# | arg: --command= - The command to evaluate +# +# +ynh_mongo_exec() { + # ============ Argument parsing ============= + local -A args_array=( [d]=database= [c]=command= ) + local database + local command + ynh_handle_getopts_args "$@" + database="${database:-}" + # =========================================== + + if [ -n "$database" ] + then + mongosh --quiet < ./dump.bson +# +# usage: ynh_mongo_dump_db --database=database +# | arg: --database= - The database name to dump +# | ret: the mongodump output +# +# +ynh_mongo_dump_db() { + # ============ Argument parsing ============= + local -A args_array=( [d]=database= ) + local database + ynh_handle_getopts_args "$@" + # =========================================== + + mongodump --quiet --db="$database" --archive +} + +# Create a user +# +# [internal] +# +# usage: ynh_mongo_create_user --db_user=user --db_pwd=pwd --db_name=name +# | arg: --db_user= - The user name to create +# | arg: --db_pwd= - The password to identify user by +# | arg: --db_name= - Name of the database to grant privilegies +# +# +ynh_mongo_create_user() { + # ============ Argument parsing ============= + local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= ) + local db_user + local db_name + local db_pwd + ynh_handle_getopts_args "$@" + # =========================================== + + # Create the user and set the user as admin of the db + ynh_mongo_exec --database="$db_name" --command='db.createUser( { user: "'${db_user}'", pwd: "'${db_pwd}'", roles: [ { role: "readWrite", db: "'${db_name}'" } ] } );' + + # Add clustermonitoring rights + ynh_mongo_exec --database="$db_name" --command='db.grantRolesToUser("'${db_user}'",[{ role: "clusterMonitor", db: "admin" }]);' +} + +# Check if a mongo database exists +# +# usage: ynh_mongo_database_exists --database=database +# | arg: --database= - The database for which to check existence +# | exit: Return 1 if the database doesn't exist, 0 otherwise +# +# +ynh_mongo_database_exists() { + # ============ Argument parsing ============= + local -A args_array=([d]=database=) + local database + ynh_handle_getopts_args "$@" + # =========================================== + + if [ $(ynh_mongo_exec --command='db.getMongo().getDBNames().indexOf("'${database}'")') -lt 0 ] + then + return 1 + else + return 0 + fi +} + +# Restore a database +# +# example: ynh_mongo_restore_db --database=wekan < ./dump.bson +# +# usage: ynh_mongo_restore_db --database=database +# | arg: --database= - The database name to restore +# +# +ynh_mongo_restore_db() { + # ============ Argument parsing ============= + local -A args_array=( [d]=database= ) + local database + ynh_handle_getopts_args "$@" + # =========================================== + + mongorestore --quiet --db="$database" --archive +} + +# Drop a user +# +# [internal] +# +# usage: ynh_mongo_drop_user --db_user=user --db_name=name +# | arg: --db_user= - The user to drop +# | arg: --db_name= - Name of the database +# +# +ynh_mongo_drop_user() { + # ============ Argument parsing ============= + local -A args_array=( [u]=db_user= [n]=db_name= ) + local db_user + local db_name + ynh_handle_getopts_args "$@" + # =========================================== + + ynh_mongo_exec --database="$db_name" --command='db.dropUser("'$db_user'", {w: "majority", wtimeout: 5000})' +} + +# Create a database, an user and its password. Then store the password in the app's config +# +# usage: ynh_mongo_setup_db --db_user=user --db_name=name [--db_pwd=pwd] +# | arg: --db_user= - Owner of the database +# | arg: --db_name= - Name of the database +# | arg: --db_pwd= - Password of the database. If not provided, a password will be generated +# +# After executing this helper, the password of the created database will be available in $db_pwd +# It will also be stored as "mongopwd" into the app settings. +# +# +ynh_mongo_setup_db() { + # ============ Argument parsing ============= + local -A args_array=( [u]=db_user= [n]=db_name= [p]=db_pwd= ) + local db_user + local db_name + db_pwd="" + ynh_handle_getopts_args "$@" + # =========================================== + + local new_db_pwd=$(ynh_string_random) # Generate a random password + # If $db_pwd is not provided, use new_db_pwd instead for db_pwd + db_pwd="${db_pwd:-$new_db_pwd}" + + # Create the user and grant access to the database + ynh_mongo_create_user --db_user="$db_user" --db_pwd="$db_pwd" --db_name="$db_name" + + # Store the password in the app's config + ynh_app_setting_set --key=db_pwd --value=$db_pwd +} + +# Remove a database if it exists, and the associated user +# +# usage: ynh_mongo_remove_db --db_user=user --db_name=name +# | arg: --db_user= - Owner of the database +# | arg: --db_name= - Name of the database +# +# +ynh_mongo_remove_db() { + # ============ Argument parsing ============= + local -A args_array=( [u]=db_user= [n]=db_name= ) + local db_user + local db_name + ynh_handle_getopts_args "$@" + # =========================================== + + if ynh_mongo_database_exists --database=$db_name; then # Check if the database exists + ynh_mongo_drop_db --database=$db_name # Remove the database + else + ynh_print_warn "Database $db_name not found" + fi + + # Remove mongo user if it exists + ynh_mongo_drop_user --db_user=$db_user --db_name=$db_name +} + +# Install MongoDB and integrate MongoDB service in YunoHost +# +# The installed version is defined by $mongo_version which should be defined as global prior to calling this helper +# +# usage: ynh_install_mongo +# +ynh_install_mongo() { + + [[ -n "${mongo_version:-}" ]] || ynh_die "\$mongo_version should be defined prior to calling ynh_install_mongo" + + ynh_print_info "Installing MongoDB Community Edition ..." + local mongo_debian_release=$YNH_DEBIAN_VERSION + + if [[ "$(grep '^flags' /proc/cpuinfo | uniq)" != *"avx"* && "$mongo_version" != "4.4" ]]; then + ynh_print_warn "Installing Mongo 4.4 as $mongo_version is not compatible with your cpu (see https://docs.mongodb.com/manual/administration/production-notes/#x86_64)." + mongo_version="4.4" + fi + if [[ "$mongo_version" == "4.4" ]]; then + ynh_print_warn "Switched to buster install as Mongo 4.4 is not compatible with $mongo_debian_release." + mongo_debian_release=buster + fi + + ynh_apt_install_dependencies_from_extra_repository \ + --repo="deb http://repo.mongodb.org/apt/debian $mongo_debian_release/mongodb-org/$mongo_version main" \ + --package="mongodb-org mongodb-org-server mongodb-org-tools mongodb-mongosh" \ + --key="https://www.mongodb.org/static/pgp/server-$mongo_version.asc" + mongodb_servicename=mongod + + # Make sure MongoDB is started and enabled + systemctl enable $mongodb_servicename --quiet + systemctl daemon-reload --quiet + ynh_systemctl --service=$mongodb_servicename --action=restart --wait_until="aiting for connections" --log_path="/var/log/mongodb/$mongodb_servicename.log" + + # Integrate MongoDB service in YunoHost + yunohost service add $mongodb_servicename --description="MongoDB daemon" --log="/var/log/mongodb/$mongodb_servicename.log" + + # Store mongo_version into the config of this app + ynh_app_setting_set --key=mongo_version --value=$mongo_version +} + +# Remove MongoDB +# Only remove the MongoDB service integration in YunoHost for now +# if MongoDB package as been removed +# +# usage: ynh_remove_mongo +# +# +ynh_remove_mongo() { + # Only remove the mongodb service if it is not installed. + if ! _ynh_apt_package_is_installed "mongodb*" + then + ynh_print_info "Removing MongoDB service..." + mongodb_servicename=mongod + # Remove the mongodb service + yunohost service remove $mongodb_servicename + ynh_safe_rm "/var/lib/mongodb" + ynh_safe_rm "/var/log/mongodb" + fi +} diff --git a/helpers/helpers.v2.1.d/multimedia b/helpers/helpers.v2.1.d/multimedia new file mode 100644 index 000000000..630e92ecc --- /dev/null +++ b/helpers/helpers.v2.1.d/multimedia @@ -0,0 +1,89 @@ +#!/bin/bash + +readonly MEDIA_GROUP=multimedia +readonly MEDIA_DIRECTORY=/home/yunohost.multimedia + +# Initialize the multimedia directory system +# +# usage: ynh_multimedia_build_main_dir +ynh_multimedia_build_main_dir() { + + ## Création du groupe multimedia + groupadd -f $MEDIA_GROUP + + ## Création des dossiers génériques + mkdir -p "$MEDIA_DIRECTORY" + mkdir -p "$MEDIA_DIRECTORY/share" + mkdir -p "$MEDIA_DIRECTORY/share/Music" + mkdir -p "$MEDIA_DIRECTORY/share/Picture" + mkdir -p "$MEDIA_DIRECTORY/share/Video" + mkdir -p "$MEDIA_DIRECTORY/share/eBook" + + ## Création des dossiers utilisateurs + for user in $(yunohost user list --output-as json | jq -r '.users | keys[]'); do + mkdir -p "$MEDIA_DIRECTORY/$user" + mkdir -p "$MEDIA_DIRECTORY/$user/Music" + mkdir -p "$MEDIA_DIRECTORY/$user/Picture" + mkdir -p "$MEDIA_DIRECTORY/$user/Video" + mkdir -p "$MEDIA_DIRECTORY/$user/eBook" + ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share" + # Création du lien symbolique dans le home de l'utilisateur. + #link will only be created if the home directory of the user exists and if it's located in '/home' folder + local user_home="$(getent passwd $user | cut -d: -f6 | grep '^/home/')" + if [[ -d "$user_home" ]]; then + ln -sfn "$MEDIA_DIRECTORY/$user" "$user_home/Multimedia" + fi + # Propriétaires des dossiers utilisateurs. + chown -R $user "$MEDIA_DIRECTORY/$user" + done + # Default yunohost hooks for post_user_create,delete will take care + # of creating/deleting corresponding multimedia folders when users + # are created/deleted in the future... + + ## Application des droits étendus sur le dossier multimedia. + # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: + setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" || true + # 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" || true + # 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" || true +} + +# Add a directory in yunohost.multimedia +# +# usage: ynh_multimedia_addfolder --source_dir="source_dir" --dest_dir="dest_dir" +# +# | arg: --source_dir= - Source directory - The real directory which contains your medias. +# | arg: --dest_dir= - Destination directory - The name and the place of the symbolic link, relative to "/home/yunohost.multimedia" +# +# This "directory" will be a symbolic link to a existing directory. +ynh_multimedia_addfolder() { + + # ============ Argument parsing ============= + local -A args_array=([s]=source_dir= [d]=dest_dir=) + local source_dir + local dest_dir + ynh_handle_getopts_args "$@" + # =========================================== + + # Ajout d'un lien symbolique vers le dossier à partager + ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir" + + ## Application des droits étendus sur le dossier ajouté + # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: + setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. + setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. + setfacl -RL -m m::rwx "$source_dir" +} + +# Add an user to the multimedia group, in turn having write permission in multimedia directories +# +# usage: ynh_multimedia_addaccess user_name +# +# | arg: user_name - The name of the user which gain this access. +ynh_multimedia_addaccess() { + groupadd -f $MEDIA_GROUP + usermod -a -G $MEDIA_GROUP $1 +} diff --git a/helpers/helpers.v2.1.d/mysql b/helpers/helpers.v2.1.d/mysql new file mode 100644 index 000000000..bc532a536 --- /dev/null +++ b/helpers/helpers.v2.1.d/mysql @@ -0,0 +1,116 @@ +#!/bin/bash + +# Run SQL instructions in a database ($db_name by default) +# +# usage: ynh_mysql_db_shell [database] <<< "instructions" +# | arg: database= - the database to connect to (by default, $db_name) +# +# examples: +# ynh_mysql_db_shell $db_name <<< "UPDATE ...;" +# ynh_mysql_db_shell < /path/to/file.sql +# +ynh_mysql_db_shell() { + local database=${1:-$db_name} + mysql -B $database +} + +# Create a database and grant optionnaly privilegies to a user +# +# [internal] ... handled by the core / "database resource" +# +# usage: ynh_mysql_create_db db [user [pwd]] +# | arg: db - the database name to create +# | arg: user - the user to grant privilegies +# | arg: pwd - the password to identify user by +# +ynh_mysql_create_db() { + local db=$1 + + local sql="CREATE DATABASE ${db};" + + # grant all privilegies to user + if [[ $# -gt 1 ]]; then + sql+=" GRANT ALL PRIVILEGES ON ${db}.* TO '${2}'@'localhost'" + if [[ -n ${3:-} ]]; then + sql+=" IDENTIFIED BY '${3}'" + fi + sql+=" WITH GRANT OPTION;" + fi + + mysql -B <<< "$sql" +} + +# Drop a database +# +# [internal] ... handled by the core / "database resource" +# +# If you intend to drop the database *and* the associated user, +# consider using ynh_mysql_remove_db instead. +# +# usage: ynh_mysql_drop_db db +# | arg: db - the database name to drop +# +ynh_mysql_drop_db() { + mysql -B <<< "DROP DATABASE ${1};" +} + +# Dump a database +# +# usage: ynh_mysql_dump_db database +# | arg: database - the database name to dump (by default, $db_name) +# | ret: The mysqldump output +# +# example: ynh_mysql_dump_db "roundcube" > ./dump.sql +# +ynh_mysql_dump_db() { + local database=${1:-$db_name} + mysqldump --single-transaction --skip-dump-date --routines "$database" +} + +# Create a user +# +# [internal] ... handled by the core / "database resource" +# +# usage: ynh_mysql_create_user user pwd [host] +# | arg: user - the user name to create +# | arg: pwd - the password to identify user by +# +ynh_mysql_create_user() { + mysql -B <<< "CREATE USER '${1}'@'localhost' IDENTIFIED BY '${2}';" +} + +# Check if a mysql user exists +# +# [internal] +# +# usage: ynh_mysql_user_exists user +# | arg: user - the user for which to check existence +# | ret: 0 if the user exists, 1 otherwise. +ynh_mysql_user_exists() { + local user=$1 + [[ -n "$(mysql -B <<< "SELECT User from mysql.user WHERE User = '$user';")" ]] +} + +# Check if a mysql database exists +# +# [internal] +# +# usage: ynh_mysql_database_exists database +# | arg: database - the database for which to check existence +# | exit: Return 1 if the database doesn't exist, 0 otherwise +# +ynh_mysql_database_exists() { + local database=$1 + mysqlshow | grep -q "^| $database " +} + +# Drop a user +# +# [internal] ... handled by the core / "database resource" +# +# usage: ynh_mysql_drop_user user +# | arg: user - the user name to drop +# +ynh_mysql_drop_user() { + mysql -B <<< "DROP USER '${1}'@'localhost';" +} diff --git a/helpers/helpers.v2.1.d/nginx b/helpers/helpers.v2.1.d/nginx new file mode 100644 index 000000000..75fa0f0d4 --- /dev/null +++ b/helpers/helpers.v2.1.d/nginx @@ -0,0 +1,59 @@ +#!/bin/bash + +# Create a dedicated nginx config +# +# usage: ynh_config_add_nginx +# +# This will use a template in `../conf/nginx.conf` +# See the documentation of `ynh_config_add` for a description of the template +# format and how placeholders are replaced with actual variables. +# +# Additionally, ynh_config_add_nginx will replace: +# - `#sub_path_only` by empty string if `path` is not `'/'` +# - `#root_path_only` by empty string if `path` *is* `'/'` +# +# This allows to enable/disable specific behaviors dependenging on the install +# location +ynh_config_add_nginx() { + + local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" + + ynh_config_add --template="nginx.conf" --destination="$finalnginxconf" + + if [ "${path:-}" != "/" ]; then + ynh_replace --match="^#sub_path_only" --replace="" --file="$finalnginxconf" + else + ynh_replace --match="^#root_path_only" --replace="" --file="$finalnginxconf" + fi + + ynh_store_file_checksum "$finalnginxconf" + + ynh_systemctl --service=nginx --action=reload +} + +# Remove the dedicated nginx config +# +# usage: ynh_config_remove_nginx +ynh_config_remove_nginx() { + ynh_safe_rm "/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_systemctl --service=nginx --action=reload +} + + +# Regen the nginx config in a change url context +# +# usage: ynh_config_change_url_nginx +ynh_config_change_url_nginx() { + + # Make a backup of the original NGINX config file if manually modified + # (nb: this is possibly different from the same instruction called by + # ynh_config_add inside ynh_config_add_nginx because the path may have + # changed if we're changing the domain too...) + local old_nginx_conf_path=/etc/nginx/conf.d/$old_domain.d/$app.conf + ynh_backup_if_checksum_is_different "$old_nginx_conf_path" + ynh_delete_file_checksum "$old_nginx_conf_path" + ynh_safe_rm "$old_nginx_conf_path" + + # Regen the nginx conf + ynh_config_add_nginx +} diff --git a/helpers/helpers.v2.1.d/nodejs b/helpers/helpers.v2.1.d/nodejs new file mode 100644 index 000000000..699288949 --- /dev/null +++ b/helpers/helpers.v2.1.d/nodejs @@ -0,0 +1,124 @@ +#!/bin/bash + +readonly N_INSTALL_DIR="/opt/node_n" +# N_PREFIX is the directory of n, it needs to be loaded as a environment variable. +export N_PREFIX="$N_INSTALL_DIR" + +# [internal] +_ynh_load_nodejs_in_path_and_other_tweaks() { + + # Get the absolute path of this version of node + nodejs_dir="$N_INSTALL_DIR/n/versions/node/$nodejs_version/bin" + + # Load the path of this version of node in $PATH + if [[ :$PATH: != *":$nodejs_dir"* ]]; then + PATH="$nodejs_dir:$PATH" + fi + + # Export PATH such that it's available through sudo -E / ynh_exec_as $app + export PATH + + # This is in full lowercase such that it gets replaced in templates + path_with_nodejs="$PATH" + PATH_with_nodejs="$PATH" + + # Prevent yet another Node and Corepack madness, with Corepack wanting the user to confirm download of Yarn + export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +} + +# Install a specific version of nodejs, using 'n' +# +# The installed version is defined by `$nodejs_version` which should be defined as global prior to calling this helper +# +# usage: ynh_nodejs_install +# +# `n` (Node version management) uses the `PATH` variable to store the path of the version of node it is going to use. +# That's how it changes the version +# +# The helper adds the appropriate, specific version of nodejs to the `$PATH` variable (which +# is preserved when calling ynh_exec_as_app). Also defines: +# - `$path_with_nodejs` to be used in the systemd config (`Environment="PATH=__PATH_WITH_NODEJS__"`) +# - `$nodejs_dir`, the directory containing the specific version of nodejs, which may be used in the systemd config too (e.g. `ExecStart=__NODEJS_DIR__/node foo bar`) +ynh_nodejs_install() { + # Use n, https://github.com/tj/n to manage the nodejs versions + + [[ -n "${nodejs_version:-}" ]] || ynh_die "\$nodejs_version should be defined prior to calling ynh_nodejs_install" + + # Create $N_INSTALL_DIR + mkdir --parents "$N_INSTALL_DIR" + + # Load n path in PATH + CLEAR_PATH="$N_INSTALL_DIR/bin:$PATH" + # Remove /usr/local/bin in PATH in case of node prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Move an existing node binary, to avoid to block n. + 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 + + # Install (or update if YunoHost vendor/ folder updated since last install) n + mkdir -p $N_INSTALL_DIR/bin/ + cp "$YNH_HELPERS_DIR/vendor/n/n" $N_INSTALL_DIR/bin/n + # Tweak for n to understand it's installed in $N_PREFIX + ynh_replace --match="^N_PREFIX=\${N_PREFIX-.*}$" --replace="N_PREFIX=\${N_PREFIX-$N_PREFIX}" --file="$N_INSTALL_DIR/bin/n" + + # Restore /usr/local/bin in PATH + PATH=$CLEAR_PATH + + # And replace the old node binary. + test -x /usr/bin/node_n && mv /usr/bin/node_n /usr/bin/node + test -x /usr/bin/npm_n && mv /usr/bin/npm_n /usr/bin/npm + + # Install the requested version of nodejs + uname=$(uname --machine) + if [[ $uname =~ aarch64 || $uname =~ arm64 ]]; then + n $nodejs_version --arch=arm64 + else + n $nodejs_version + fi + + # Find the last "real" version for this major version of node. + real_nodejs_version=$(find $N_INSTALL_DIR/n/versions/node/$nodejs_version* -maxdepth 0 | sort --version-sort | tail --lines=1) + real_nodejs_version=$(basename $real_nodejs_version) + + # Create a symbolic link for this major version if the file doesn't already exist + if [ ! -e "$N_INSTALL_DIR/n/versions/node/$nodejs_version" ]; then + ln --symbolic --force --no-target-directory \ + $N_INSTALL_DIR/n/versions/node/$real_nodejs_version \ + $N_INSTALL_DIR/n/versions/node/$nodejs_version + fi + + # Store the ID of this app and the version of node requested for it + echo "$YNH_APP_INSTANCE_NAME:$nodejs_version" | tee --append "$N_INSTALL_DIR/ynh_app_version" + + # Store nodejs_version into the config of this app + ynh_app_setting_set --key=nodejs_version --value=$nodejs_version + + _ynh_load_nodejs_in_path_and_other_tweaks +} + +# Remove the version of node used by the app. +# +# usage: ynh_nodejs_remove +# +# This helper will check if another app uses the same version of node. +# - If not, this version of node will be removed. +# - If no other app uses node, n will be also removed. +ynh_nodejs_remove() { + + [[ -n "${nodejs_version:-}" ]] || ynh_die "\$nodejs_version should be defined prior to calling ynh_nodejs_remove" + + # Remove the line for this app + sed --in-place "/$YNH_APP_INSTANCE_NAME:$nodejs_version/d" "$N_INSTALL_DIR/ynh_app_version" + + # If no other app uses this version of nodejs, remove it. + if ! grep --quiet "$nodejs_version" "$N_INSTALL_DIR/ynh_app_version"; then + $N_INSTALL_DIR/bin/n rm $nodejs_version + fi + + # If no other app uses n, remove n + if [ ! -s "$N_INSTALL_DIR/ynh_app_version" ]; then + ynh_safe_rm "$N_INSTALL_DIR" + sed --in-place "/N_PREFIX/d" /root/.bashrc + fi +} diff --git a/helpers/helpers.v2.1.d/permission b/helpers/helpers.v2.1.d/permission new file mode 100644 index 000000000..cccba8256 --- /dev/null +++ b/helpers/helpers.v2.1.d/permission @@ -0,0 +1,310 @@ +#!/bin/bash + +# Create a new permission for the app +# +# Example 1: `ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \ +# --label="My app admin" --show_tile=true` +# +# This example will create a new permission permission with this following effect: +# - A tile named "My app admin" in the SSO will be available for the users alice and bob. This tile will point to the relative url '/admin'. +# - Only the user alice and bob will have the access to theses following url: /admin, domain.tld/admin, /superadmin +# +# +# Example 2: +# +# ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \ +# --label="MyApp API" --protected=true +# +# This example will create a new protected permission. So the admin won't be able to add/remove the visitors group of this permission. +# In case of an API with need to be always public it avoid that the admin break anything. +# With this permission all client will be allowed to access to the url 'domain.tld/api'. +# Note that in this case no tile will be show on the SSO. +# Note that the auth_header parameter is to 'false'. So no authentication header will be passed to the application. +# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case. +# So in this case it's better to disable this option for all API. +# +# +# usage: ynh_permission_create --permission="permission" [--url="url"] [--additional_urls="second-url" [ "third-url" ]] [--auth_header=true|false] +# [--allowed=group1 [ group2 ]] [--label="label"] [--show_tile=true|false] +# [--protected=true|false] +# | arg: --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: --url= - (optional) URL for which access will be allowed/forbidden. Note that if 'show_tile' is enabled, this URL will be the URL of the tile. +# | arg: --additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden +# | arg: --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true +# | arg: --allowed= - (optional) A list of group/user to allow for the permission +# | arg: --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. Default is "APP_LABEL (permission name)". +# | arg: --show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. Defaults to false for the permission different than 'main'. +# | arg: --protected= - (optional) 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'. +# +# [packagingv1] +# +# If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they +# start with '/'. For example: +# / -> domain.tld/app +# /admin -> domain.tld/app/admin +# domain.tld/app/api -> domain.tld/app/api +# +# 'url' or 'additional_urls' can be treated as a PCRE (not lua) regex if it starts with "re:". +# For example: +# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# +# Note that globally the parameter 'url' and 'additional_urls' are same. The only difference is: +# - 'url' is only one url, 'additional_urls' can be a list of urls. There are no limitation of 'additional_urls' +# - 'url' is used for the url of tile in the SSO (if enabled with the 'show_tile' parameter) +# +# +# About the authentication header (auth_header parameter). +# The SSO pass (by default) to the application theses following HTTP header (linked to the authenticated user) to the application: +# - "Auth-User": username +# - "Remote-User": username +# - "Email": user email +# +# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly. +# See https://github.com/YunoHost/issues/issues/1420 for more informations +ynh_permission_create() { + # ============ Argument parsing ============= + local -A args_array=([p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected=) + local permission + local url + local additional_urls + local auth_header + local allowed + local label + local show_tile + local protected + ynh_handle_getopts_args "$@" + url=${url:-} + additional_urls=${additional_urls:-} + auth_header=${auth_header:-} + allowed=${allowed:-} + label=${label:-} + show_tile=${show_tile:-} + protected=${protected:-} + # =========================================== + + if [[ -n $url ]]; then + url=",url='$url'" + fi + + if [[ -n $additional_urls ]]; then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # By example: + # --additional_urls /urlA /urlB + # will be: + # additional_urls=['/urlA', '/urlB'] + additional_urls=",additional_urls=['${additional_urls//;/\',\'}']" + fi + + if [[ -n $auth_header ]]; then + if [ $auth_header == "true" ]; then + auth_header=",auth_header=True" + else + auth_header=",auth_header=False" + fi + fi + + if [[ -n $allowed ]]; then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # By example: + # --allowed alice bob + # will be: + # allowed=['alice', 'bob'] + allowed=",allowed=['${allowed//;/\',\'}']" + fi + + if [[ -n ${label:-} ]]; then + label=",label='$label'" + else + label=",label='$permission'" + fi + + if [[ -n ${show_tile:-} ]]; then + if [ $show_tile == "true" ]; then + show_tile=",show_tile=True" + else + show_tile=",show_tile=False" + fi + fi + + if [[ -n ${protected:-} ]]; then + if [ $protected == "true" ]; then + protected=",protected=True" + else + protected=",protected=False" + fi + fi + + yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission' $url $additional_urls $auth_header $allowed $label $show_tile $protected)" +} + +# Remove a permission for the app (note that when the app is removed all permission is automatically removed) +# +# example: ynh_permission_delete --permission=editors +# +# usage: ynh_permission_delete --permission="permission" +# | arg: --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +ynh_permission_delete() { + # ============ Argument parsing ============= + local -A args_array=([p]=permission=) + local permission + ynh_handle_getopts_args "$@" + # =========================================== + + yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission')" +} + +# Check if a permission exists +# +# usage: ynh_permission_exists --permission=permission +# | arg: --permission= - the permission to check +# | exit: Return 1 if the permission doesn't exist, 0 otherwise +ynh_permission_exists() { + # ============ Argument parsing ============= + local -A args_array=([p]=permission=) + local permission + ynh_handle_getopts_args "$@" + # =========================================== + + yunohost user permission list "$app" --output-as json --quiet \ + | jq -e --arg perm "$app.$permission" '.permissions[$perm]' >/dev/null +} + +# Redefine the url associated to a permission +# +# usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]] +# [--auth_header=true|false] [--clear_urls] +# | arg: --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +# | arg: --url= - (optional) URL for which access will be allowed/forbidden. Note that if you want to remove url you can pass an empty sting as arguments (""). +# | arg: --add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. +# | arg: --remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden +# | arg: --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application +# | arg: --clear_urls - (optional) Clean all urls (url and additional_urls) +ynh_permission_url() { + # ============ Argument parsing ============= + local -A args_array=([p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls) + local permission + local url + local add_url + local remove_url + local auth_header + local clear_urls + ynh_handle_getopts_args "$@" + url=${url:-} + add_url=${add_url:-} + remove_url=${remove_url:-} + auth_header=${auth_header:-} + clear_urls=${clear_urls:-} + # =========================================== + + if [[ -n $url ]]; then + url=",url='$url'" + fi + + if [[ -n $add_url ]]; then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --add_url /urlA /urlB + # will be: + # add_url=['/urlA', '/urlB'] + add_url=",add_url=['${add_url//;/\',\'}']" + fi + + if [[ -n $remove_url ]]; then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --remove_url /urlA /urlB + # will be: + # remove_url=['/urlA', '/urlB'] + remove_url=",remove_url=['${remove_url//;/\',\'}']" + fi + + if [[ -n $auth_header ]]; then + if [ $auth_header == "true" ]; then + auth_header=",auth_header=True" + else + auth_header=",auth_header=False" + fi + fi + + if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ]; then + clear_urls=",clear_urls=True" + fi + + yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission' $url $add_url $remove_url $auth_header $clear_urls)" +} + +# Update a permission for the app +# +# usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]] +# +# | arg: --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: --add= - the list of group or users to enable add to the permission +# | arg: --remove= - the list of group or users to remove from the permission +ynh_permission_update() { + # ============ Argument parsing ============= + local -A args_array=([p]=permission= [a]=add= [r]=remove=) + local permission + local add + local remove + ynh_handle_getopts_args "$@" + add=${add:-} + remove=${remove:-} + # =========================================== + + if [[ -n $add ]]; then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --add alice bob + # will be: + # add=['alice', 'bob'] + add=",add=['${add//';'/"','"}']" + fi + if [[ -n $remove ]]; then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --remove alice bob + # will be: + # remove=['alice', 'bob'] + remove=",remove=['${remove//';'/"','"}']" + fi + + yunohost tools shell -c "from yunohost.permission import user_permission_update; user_permission_update('$app.$permission' $add $remove , force=True)" +} + +# Check if a permission has an user +# +# example: ynh_permission_has_user --permission=main --user=visitors +# +# usage: ynh_permission_has_user --permission=permission --user=user +# | arg: --permission= - the permission to check +# | arg: --user= - the user seek in the permission +# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise +ynh_permission_has_user() { + # ============ Argument parsing ============= + local -A args_array=([p]=permission= [u]=user=) + local permission + local user + ynh_handle_getopts_args "$@" + # =========================================== + + if ! ynh_permission_exists --permission=$permission; then + return 1 + fi + + # Check both allowed and corresponding_users sections in the json + for section in "allowed" "corresponding_users"; do + if yunohost user permission info "$app.$permission" --output-as json --quiet \ + | jq -e --arg user $user --arg section $section '.[$section] | index($user)' >/dev/null; then + return 0 + fi + done + + return 1 +} diff --git a/helpers/helpers.v2.1.d/php b/helpers/helpers.v2.1.d/php new file mode 100644 index 000000000..b7165e010 --- /dev/null +++ b/helpers/helpers.v2.1.d/php @@ -0,0 +1,149 @@ +#!/bin/bash + +# (this is used in the apt helpers, big meh ...) +readonly YNH_DEFAULT_PHP_VERSION=7.4 + +# Create a dedicated PHP-FPM config +# +# usage: ynh_config_add_phpfpm +# +# This will automatically generate an appropriate PHP-FPM configuration for this app. +# +# The resulting configuration will be deployed to the appropriate place: +# `/etc/php/$php_version/fpm/pool.d/$app.conf` +# +# If the app provides a `conf/extra_php-fpm.conf` template, it will be appended +# to the generated configuration. (In the vast majority of cases, this shouldnt +# be necessary) +# +# $php_version should be defined prior to calling this helper, but there should +# be no reason to manually set it, as it is automatically set by the apt +# helpers/resources when installing phpX.Y dependencies (PHP apps should at +# least install phpX.Y-fpm using the `apt` helper/resource) +# +# `$php_group` can be defined as a global (from `_common.sh`) if the worker +# processes should run with a different group than `$app` +# +# Additional "pm" and "php_admin_value" settings which are meant to be possibly +# configurable by admins from a future standard config panel at some point, +# related to performance and availability of the app, for which tweaking may be +# required if the app is used by "plenty" of users and other memory/CPU load +# considerations.... +# +# If you have good reasons to be willing to use different +# defaults than the one set by this helper (while still allowing admin to +# override it) you should use `ynh_app_setting_set_default` +# +# - `$php_upload_max_filezise`: corresponds upload_max_filesize and post_max_size. Defaults to 50M +# - `$php_process_management`: corresponds to "pm" (ondemand, dynamic, static). Defaults to ondemand +# - `$php_max_children`: by default, computed from "total RAM" divided by 40, cf `_default_php_max_children` +# - `$php_memory_limit`: by default, 128M (from global php.ini) +# +# Note that if $php_process_management is set to "dynamic", then these +# variables MUST be defined prior to calling the helper (no default value) ... +# Check PHP-FPM's manual for more info on what these are (: ... +# +# - `$php_start_servers` +# - `$php_min_spare_servers` +# - `$php_max_spare_servers` +# +ynh_config_add_phpfpm() { + + [[ -n "${php_version:-}" ]] || ynh_die "\$php_version should be defined prior to calling ynh_config_add_phpfpm. You should not need to define it manually, it is automatically set by the apt helper when installing the phpX.Y- depenencies" + + # Apps may define $php_group as a global (e.g. from _common.sh) to change this + # (this is not meant to be overridable by users) + local php_group=${php_group:-$app} + + # Meant to be overridable by users from a standard config panel at some point ... + # Apps willing to tweak these should use ynh_setting_set_default_value (in install and upgrade?) + # + local php_upload_max_filesize=${php_upload_max_filesize:-50M} + local php_process_management=${php_process_management:-ondemand} # alternatively 'dynamic' or 'static' + local php_max_children=${php_max_children:-$(_default_php_max_children)} + local php_memory_limit=${php_memory_limit:-128M} # default value is from global php.ini + + local phpfpm_template=$(mktemp) + cat << EOF > $phpfpm_template +[__APP__] + +user = __APP__ +group = __PHP_GROUP__ + +chdir = __INSTALL_DIR__ + +listen = /var/run/php/php__PHP_VERSION__-fpm-__APP__.sock +listen.owner = www-data +listen.group = www-data + +pm = __PHP_PROCESS_MANAGEMENT__ +pm.max_children = __PHP_MAX_CHILDREN__ +pm.max_requests = 500 +request_terminate_timeout = 1d + +EOF + if [ "$php_process_management" = "dynamic" ]; then + cat << EOF >> $phpfpm_template +pm.start_servers = __PHP_START_SERVERS__ +pm.min_spare_servers = __PHP_MIN_SPARE_SERVERS__ +pm.max_spare_servers = __PHP_MAX_SPARE_SERVERS__ +EOF + elif [ "$php_process_management" = "ondemand" ]; then + cat << EOF >> $phpfpm_template +pm.process_idle_timeout = 10s +EOF + fi + + cat << EOF >> $phpfpm_template +php_admin_value[upload_max_filesize] = __PHP_UPLOAD_MAX_FILESIZE__ +php_admin_value[post_max_size] = __PHP_UPLOAD_MAX_FILESIZE__ +php_admin_value[memory_limit] = __PHP_MEMORY_LIMIT__ +EOF + + # Concatene the extra config + if [ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]; then + cat $YNH_APP_BASEDIR/conf/extra_php-fpm.conf >>"$phpfpm_template" + fi + + # Make sure the fpm pool dir exists + mkdir --parents "/etc/php/$php_version/fpm/pool.d" + # And hydrate configuration + ynh_config_add --template="$phpfpm_template" --destination="/etc/php/$php_version/fpm/pool.d/$app.conf" + + # Validate that the new php conf doesn't break php-fpm entirely + if ! php-fpm${php_version} --test 2>/dev/null; then + php-fpm${php_version} --test || true + ynh_safe_rm "/etc/php/$php_version/fpm/pool.d/$app.conf" + ynh_die "The new configuration broke php-fpm?" + fi + + ynh_systemctl --service=php${php_version}-fpm --action=reload +} + +# Remove the dedicated PHP-FPM config +# +# usage: ynh_config_remove_phpfpm +ynh_config_remove_phpfpm() { + ynh_safe_rm "/etc/php/$php_version/fpm/pool.d/$app.conf" + ynh_systemctl --service="php${php_version}-fpm" --action=reload +} + +_default_php_max_children() { + # Get the total of RAM available + local total_ram=$(ynh_get_ram --total) + + # The value of pm.max_children is the total amount of ram divide by 2, + # divide again by 20MB (= a default, classic worker footprint) This is + # designed such that if PHP-FPM start the maximum of children, it won't + # exceed half of the ram. + local php_max_children="$(($total_ram / 40))" + # Make sure we get at least max_children = 1 + if [ $php_max_children -le 0 ]; then + php_max_children=1 + # To not overload the proc, limit the number of children to 4 times the number of cores. + elif [ $php_max_children -gt "$(($(nproc) * 4))" ]; then + php_max_children="$(($(nproc) * 4))" + fi + + echo "$php_max_children" +} diff --git a/helpers/helpers.v2.1.d/postgresql b/helpers/helpers.v2.1.d/postgresql new file mode 100644 index 000000000..2b0b55bf4 --- /dev/null +++ b/helpers/helpers.v2.1.d/postgresql @@ -0,0 +1,124 @@ +#!/bin/bash + +PSQL_ROOT_PWD_FILE=/etc/yunohost/psql +PSQL_VERSION=13 + +# Run SQL instructions in a database ($db_name by default) +# +# usage: ynh_psql_db_shell database <<< "instructions" +# | arg: database - the database to connect to (by default, $db_name) +# +# examples: +# ynh_psql_db_shell $db_name <<< "UPDATE ...;" +# ynh_psql_db_shell < /path/to/file.sql +# +ynh_psql_db_shell() { + local database="${1:-$db_name}" + sudo --login --user=postgres psql "$database" +} + +# Create a database and grant optionnaly privilegies to a user +# +# [internal] ... handled by the core / "database resource" +# +# usage: ynh_psql_create_db db [user] +# | arg: db - the database name to create +# | arg: user - the user to grant privilegies +# +ynh_psql_create_db() { + local db=$1 + local user=${2:-} + + local sql="CREATE DATABASE ${db};" + + # grant all privilegies to user + if [ -n "$user" ]; then + sql+="ALTER DATABASE ${db} OWNER TO ${user};" + sql+="GRANT ALL PRIVILEGES ON DATABASE ${db} TO ${user} WITH GRANT OPTION;" + fi + + sudo --login --user=postgres psql <<< "$sql" +} + +# Drop a database +# +# [internal] ... handled by the core / "database resource" +# +# If you intend to drop the database *and* the associated user, +# consider using ynh_psql_remove_db instead. +# +# usage: ynh_psql_drop_db db +# | arg: db - the database name to drop +# +ynh_psql_drop_db() { + local db=$1 + # First, force disconnection of all clients connected to the database + # https://stackoverflow.com/questions/17449420/postgresql-unable-to-drop-database-because-of-some-auto-connections-to-db + sudo --login --user=postgres psql $db <<< "REVOKE CONNECT ON DATABASE $db FROM public;" + sudo --login --user=postgres psql $db <<< "SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '$db' AND pid <> pg_backend_pid();" + sudo --login --user=postgres dropdb $db +} + +# Dump a database +# +# usage: ynh_psql_dump_db database +# | arg: database - the database name to dump (by default, $db_name) +# | ret: the psqldump output +# +# example: ynh_psql_dump_db 'roundcube' > ./dump.sql +# +ynh_psql_dump_db() { + local database="${1:-$db_name}" + sudo --login --user=postgres pg_dump "$database" +} + +# Create a user +# +# [internal] ... handled by the core / "database resource" +# +# usage: ynh_psql_create_user user pwd +# | arg: user - the user name to create +# | arg: pwd - the password to identify user by +# +ynh_psql_create_user() { + local user=$1 + local pwd=$2 + sudo --login --user=postgres psql <<< "CREATE USER $user WITH ENCRYPTED PASSWORD '$pwd'" +} + +# Check if a psql user exists +# +# [internal] +# +# usage: ynh_psql_user_exists user +# | arg: user= - the user for which to check existence +# | exit: Return 1 if the user doesn't exist, 0 otherwise +# +ynh_psql_user_exists() { + local user=$1 + sudo --login --user=postgres psql -tAc "SELECT rolname FROM pg_roles WHERE rolname='$user';" | grep --quiet "$user" +} + +# Check if a psql database exists +# +# [internal] +# +# usage: ynh_psql_database_exists database +# | arg: database - the database for which to check existence +# | exit: Return 1 if the database doesn't exist, 0 otherwise +# +ynh_psql_database_exists() { + local database=$1 + sudo --login --user=postgres psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database" +} + +# Drop a user +# +# [internal] ... handled by the core / "database resource" +# +# usage: ynh_psql_drop_user user +# | arg: user - the user name to drop +# +ynh_psql_drop_user() { + sudo --login --user=postgres psql <<< "DROP USER ${1};" +} diff --git a/helpers/helpers.v2.1.d/redis b/helpers/helpers.v2.1.d/redis new file mode 100644 index 000000000..310db411b --- /dev/null +++ b/helpers/helpers.v2.1.d/redis @@ -0,0 +1,39 @@ +#!/bin/bash + +# get the first available redis database +# +# usage: ynh_redis_get_free_db +# | returns: the database number to use +ynh_redis_get_free_db() { + local result max db + result=$(redis-cli INFO keyspace) + + # get the num + max=$(cat /etc/redis/redis.conf | grep ^databases | grep -Eow "[0-9]+") + + db=0 + # default Debian setting is 15 databases + for i in $(seq 0 "$max") + do + if ! echo "$result" | grep -q "db$i" + then + db=$i + break 1 + fi + db=-1 + done + + test "$db" -eq -1 && ynh_die "No available Redis databases..." + + echo "$db" +} + +# Create a master password and set up global settings +# Please always call this script in install and restore scripts +# +# usage: ynh_redis_remove_db database +# | arg: database - the database to erase +ynh_redis_remove_db() { + local db=$1 + redis-cli -n "$db" flushdb +} diff --git a/helpers/helpers.v2.1.d/ruby b/helpers/helpers.v2.1.d/ruby new file mode 100644 index 000000000..22b28d9ad --- /dev/null +++ b/helpers/helpers.v2.1.d/ruby @@ -0,0 +1,252 @@ +#!/bin/bash + +readonly RBENV_INSTALL_DIR="/opt/rbenv" + +# RBENV_ROOT is the directory of rbenv, it needs to be loaded as a environment variable. +export RBENV_ROOT="$RBENV_INSTALL_DIR" +export rbenv_root="$RBENV_INSTALL_DIR" + +_ynh_load_ruby_in_path_and_other_tweaks() { + + # Get the absolute path of this version of Ruby + ruby_dir="$RBENV_INSTALL_DIR/versions/$app/bin" + + # Load the path of this version of ruby in $PATH + if [[ :$PATH: != *":$ruby_dir"* ]]; then + PATH="$ruby_dir:$PATH" + fi + + # Export PATH such that it's available through sudo -E / ynh_exec_as $app + export PATH + + # This is in full lowercase such that it gets replaced in templates + path_with_ruby="$PATH" + PATH_with_ruby="$PATH" + + # Sets the local application-specific Ruby version + pushd ${install_dir} + $RBENV_INSTALL_DIR/bin/rbenv local $ruby_version + popd +} + +# Install a specific version of Ruby using rbenv +# +# The installed version is defined by `$ruby_version` which should be defined as global prior to calling this helper +# +# usage: ynh_ruby_install +# +# The helper adds the appropriate, specific version of ruby to the `$PATH` variable (which +# is preserved when calling ynh_exec_as_app). Also defines: +# - `$path_with_ruby` to be used in the systemd config (`Environment="PATH=__PATH_WITH_RUBY__"`) +# - `$ruby_dir`, the directory containing the specific version of ruby, which may be used in the systemd config too (e.g. `ExecStart=__RUBY_DIR__/ruby foo bar`) +# +# This helper also creates a /etc/profile.d/rbenv.sh that configures PATH environment for rbenv +ynh_ruby_install () { + + [[ -n "${ruby_version:-}" ]] || ynh_die "\$ruby_version should be defined prior to calling ynh_ruby_install" + + # Load rbenv path in PATH + local CLEAR_PATH="$RBENV_INSTALL_DIR/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Ruby prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Move an existing Ruby binary, to avoid to block rbenv + test -x /usr/bin/ruby && mv /usr/bin/ruby /usr/bin/ruby_rbenv + + # Install or update rbenv + mkdir -p $RBENV_INSTALL_DIR + rbenv="$(command -v rbenv $RBENV_INSTALL_DIR/bin/rbenv | grep "$RBENV_INSTALL_DIR/bin/rbenv" | head -1)" + if [ -n "$rbenv" ]; then + pushd "${rbenv%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/rbenv/rbenv.git"; then + echo "Updating rbenv..." + git pull -q --tags origin master + _ynh_ruby_try_bash_extension + else + echo "Reinstalling rbenv..." + cd .. + ynh_safe_rm $RBENV_INSTALL_DIR + mkdir -p $RBENV_INSTALL_DIR + cd $RBENV_INSTALL_DIR + git init -q + git remote add -f -t master origin https://github.com/rbenv/rbenv.git > /dev/null 2>&1 + git checkout -q -b master origin/master + _ynh_ruby_try_bash_extension + rbenv=$RBENV_INSTALL_DIR/bin/rbenv + fi + popd + else + echo "Installing rbenv..." + pushd $RBENV_INSTALL_DIR + git init -q + git remote add -f -t master origin https://github.com/rbenv/rbenv.git > /dev/null 2>&1 + git checkout -q -b master origin/master + _ynh_ruby_try_bash_extension + rbenv=$RBENV_INSTALL_DIR/bin/rbenv + popd + fi + + mkdir -p "${RBENV_INSTALL_DIR}/plugins" + + ruby_build="$(command -v "$RBENV_INSTALL_DIR"/plugins/*/bin/rbenv-install rbenv-install | head -1)" + if [ -n "$ruby_build" ]; then + pushd "${ruby_build%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/rbenv/ruby-build.git"; then + echo "Updating ruby-build..." + git pull -q origin master + fi + popd + else + echo "Installing ruby-build..." + git clone -q https://github.com/rbenv/ruby-build.git "${RBENV_INSTALL_DIR}/plugins/ruby-build" + fi + + rbenv_alias="$(command -v "$RBENV_INSTALL_DIR"/plugins/*/bin/rbenv-alias rbenv-alias | head -1)" + if [ -n "$rbenv_alias" ]; then + pushd "${rbenv_alias%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/tpope/rbenv-aliases.git"; then + echo "Updating rbenv-aliases..." + git pull -q origin master + fi + popd + else + echo "Installing rbenv-aliases..." + git clone -q https://github.com/tpope/rbenv-aliases.git "${RBENV_INSTALL_DIR}/plugins/rbenv-aliase" + fi + + rbenv_latest="$(command -v "$RBENV_INSTALL_DIR"/plugins/*/bin/rbenv-latest rbenv-latest | head -1)" + if [ -n "$rbenv_latest" ]; then + pushd "${rbenv_latest%/*/*}" + if git remote -v 2>/dev/null | grep "https://github.com/momo-lab/xxenv-latest.git"; then + echo "Updating xxenv-latest..." + git pull -q origin master + fi + popd + else + echo "Installing xxenv-latest..." + git clone -q https://github.com/momo-lab/xxenv-latest.git "${RBENV_INSTALL_DIR}/plugins/xxenv-latest" + fi + + # Enable caching + mkdir -p "${RBENV_INSTALL_DIR}/cache" + + # Create shims directory if needed + mkdir -p "${RBENV_INSTALL_DIR}/shims" + + # Restore /usr/local/bin in PATH + PATH=$CLEAR_PATH + + # And replace the old Ruby binary + test -x /usr/bin/ruby_rbenv && mv /usr/bin/ruby_rbenv /usr/bin/ruby + + # Install the requested version of Ruby + local final_ruby_version=$(rbenv latest --print $ruby_version) + if ! [ -n "$final_ruby_version" ]; then + final_ruby_version=$ruby_version + fi + echo "Installing Ruby $final_ruby_version" + RUBY_CONFIGURE_OPTS="--disable-install-doc --with-jemalloc" MAKE_OPTS="-j2" rbenv install --skip-existing $final_ruby_version > /dev/null 2>&1 + + # Store ruby_version into the config of this app + ynh_app_setting_set --key=ruby_version --value=$final_ruby_version + ruby_version=$final_ruby_version + + # Remove app virtualenv + if rbenv alias --list | grep --quiet "$app " + then + rbenv alias $app --remove + fi + + # Create app virtualenv + rbenv alias $app $final_ruby_version + + # Cleanup Ruby versions + _ynh_ruby_cleanup + + # Set environment for Ruby users + echo "#rbenv +export RBENV_ROOT=$RBENV_INSTALL_DIR +export PATH=\"$RBENV_INSTALL_DIR/bin:$PATH\" +eval \"\$(rbenv init -)\" +#rbenv" > /etc/profile.d/rbenv.sh + + # Load the environment + eval "$(rbenv init -)" + + _ynh_load_ruby_in_path_and_other_tweaks +} + +# Remove the version of Ruby used by the app. +# +# This helper will also cleanup unused Ruby versions +# +# usage: ynh_ruby_remove +ynh_ruby_remove () { + + [[ -n "${ruby_version:-}" ]] || ynh_die "\$ruby_version should be defined prior to calling ynh_ruby_remove" + + # Load rbenv path in PATH + local CLEAR_PATH="$RBENV_INSTALL_DIR/bin:$PATH" + + # Remove /usr/local/bin in PATH in case of Ruby prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + rbenv alias $app --remove + + # Remove the line for this app + ynh_app_setting_delete --key=ruby_version + + # Cleanup Ruby versions + _ynh_ruby_cleanup +} + +# Remove no more needed versions of Ruby used by the app. +# +# [internal] +# +# This helper will check what Ruby version are no more required, +# and uninstall them +# If no app uses Ruby, rbenv will be also removed. +_ynh_ruby_cleanup () { + + # List required Ruby versions + local installed_apps=$(yunohost app list | grep -oP 'id: \K.*$') + local required_ruby_versions="" + for installed_app in $installed_apps + do + local installed_app_ruby_version=$(ynh_app_setting_get --app=$installed_app --key="ruby_version") + if [[ -n "$installed_app_ruby_version" ]] + then + required_ruby_versions="${installed_app_ruby_version}\n${required_ruby_versions}" + fi + done + + # Remove no more needed Ruby versions + local installed_ruby_versions=$(rbenv versions --bare --skip-aliases | grep -Ev '/') + for installed_ruby_version in $installed_ruby_versions + do + if ! echo ${required_ruby_versions} | grep -q "${installed_ruby_version}" + then + echo "Removing Ruby-$installed_ruby_version" + $RBENV_INSTALL_DIR/bin/rbenv uninstall --force $installed_ruby_version + fi + done + + # If none Ruby version is required + if [[ -z "$required_ruby_versions" ]] + then + # Remove rbenv environment configuration + echo "Removing rbenv" + ynh_safe_rm "$RBENV_INSTALL_DIR" + ynh_safe_rm "/etc/profile.d/rbenv.sh" + fi +} + +_ynh_ruby_try_bash_extension() { + if [ -x src/configure ]; then + src/configure && make -C src 2>&1 || { + ynh_print_info "Optional bash extension failed to build, but things will still work normally." + } + fi +} diff --git a/helpers/helpers.v2.1.d/setting b/helpers/helpers.v2.1.d/setting new file mode 100644 index 000000000..01480331c --- /dev/null +++ b/helpers/helpers.v2.1.d/setting @@ -0,0 +1,139 @@ +#!/bin/bash + +# Get an application setting +# +# usage: ynh_app_setting_get --key=key +# | arg: --app= - the application id (global $app by default) +# | arg: --key= - the setting to get +ynh_app_setting_get() { + # ============ Argument parsing ============= + local _globalapp=${app-:} + local -A args_array=([a]=app= [k]=key=) + local app + local key + ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" + # =========================================== + + ynh_app_setting "get" "$app" "$key" +} + +# Set an application setting +# +# usage: ynh_app_setting_set --key=key --value=value +# | arg: --app= - the application id (global $app by default) +# | arg: --key= - the setting name to set +# | arg: --value= - the setting value to set +ynh_app_setting_set() { + # ============ Argument parsing ============= + local _globalapp=${app-:} + local -A args_array=([a]=app= [k]=key= [v]=value=) + local app + local key + local value + ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" + # =========================================== + + ynh_app_setting "set" "$app" "$key" "$value" +} + +# Set an application setting but only if the "$key" variable ain't set yet +# +# Note that it doesn't just define the setting but ALSO define the $foobar variable +# +# Hence it's meant as a replacement for this legacy overly complex syntax: +# +# if [ -z "${foo:-}" ] +# then +# foo="bar" +# ynh_app_setting_set --key="foo" --value="$foo" +# fi +# +# usage: ynh_app_setting_set_default --key=key --value=value +# | arg: --app= - the application id (global $app by default) +# | arg: --key= - the setting name to set +# | arg: --value= - the default setting value to set +ynh_app_setting_set_default() { + # ============ Argument parsing ============= + local _globalapp=${app-:} + local -A args_array=([a]=app= [k]=key= [v]=value=) + local app + local key + local value + ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" + # =========================================== + + if [ -z "${!key:-}" ]; then + eval $key=\$value + ynh_app_setting "set" "$app" "$key" "$value" + fi +} + +# Delete an application setting +# +# usage: ynh_app_setting_delete --key=key +# | arg: --app= - the application id (global $app by default) +# | arg: --key= - the setting to delete +ynh_app_setting_delete() { + # ============ Argument parsing ============= + local _globalapp=${app-:} + local -A args_array=([a]=app= [k]=key=) + local app + local key + ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" + # =========================================== + + ynh_app_setting "delete" "$app" "$key" +} + +# Small "hard-coded" interface to avoid calling "yunohost app" directly each +# time dealing with a setting is needed (which may be so slow on ARM boards) +# +# [internal] +# +ynh_app_setting() { + # Trick to only re-enable debugging if it was set before + local xtrace_enable=$(set +o | grep xtrace) + set +o xtrace # set +x + ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python3 - <&1) \ + || ynh_die "$out" + fi + + # Check the control sum + if ! echo "${src_sum} ${src_filename}" | sha256sum --check --status + then + local actual_sum="$(sha256sum ${src_filename} | cut --delimiter=' ' --fields=1)" + local actual_size="$(du -hs ${src_filename} | cut --fields=1)" + rm -f ${src_filename} + ynh_die "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 + rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ + if [ -n "$keep" ] && [ -e "$dest_dir" ]; then + local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} + mkdir -p $keep_dir + local stuff_to_keep + for stuff_to_keep in $keep; do + if [ -e "$dest_dir/$stuff_to_keep" ]; then + mkdir --parents "$(dirname "$keep_dir/$stuff_to_keep")" + cp --archive "$dest_dir/$stuff_to_keep" "$keep_dir/$stuff_to_keep" + fi + done + fi + + if [ "$full_replace" -eq 1 ]; then + ynh_safe_rm "$dest_dir" + fi + + # Extract source into the app dir + mkdir --parents "$dest_dir" + + 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 + "$YNH_HELPERS_DIR/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 + local tmp_dir=$(mktemp --directory) + unzip -quo $src_filename -d "$tmp_dir" + cp --archive $tmp_dir/*/. "$dest_dir" + ynh_safe_rm "$tmp_dir" + else + unzip -quo $src_filename -d "$dest_dir" + fi + ynh_safe_rm "$src_filename" + else + local strip="" + if [ "$src_in_subdir" != "false" ]; then + if [ "$src_in_subdir" == "true" ]; then + local sub_dirs=1 + else + local sub_dirs="$src_in_subdir" + fi + strip="--strip-components $sub_dirs" + fi + if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz|tar$ ]]; then + tar --extract --file=$src_filename --directory="$dest_dir" $strip + else + ynh_die "Archive format unrecognized." + fi + ynh_safe_rm "$src_filename" + fi + + # Apply patches + local patches_folder=$(realpath "$YNH_APP_BASEDIR/patches/$source_id") + if [ -d "$patches_folder" ]; then + pushd "$dest_dir" + for patchfile in "$patches_folder/"*.patch; do + echo "Applying $patchfile" + if ! patch --strip=1 < "$patchfile"; then + if ynh_in_ci_tests; then + ynh_die "Patch $patchfile failed to apply!" + else + ynh_print_warn "Warn your packagers /!\\ Patch $patchfile failed to apply" + fi + fi + done + popd + fi + + # Keep files to be backup/restored at the end of the helper + # Assuming $dest_dir already exists + if [ -n "$keep" ]; then + local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} + local stuff_to_keep + for stuff_to_keep in $keep; do + if [ -e "$keep_dir/$stuff_to_keep" ]; then + mkdir --parents "$(dirname "$dest_dir/$stuff_to_keep")" + + # We add "--no-target-directory" (short option is -T) to handle the special case + # when we "keep" a folder, but then the new setup already contains the same dir (but possibly empty) + # in which case a regular "cp" will create a copy of the directory inside the directory ... + # resulting in something like /var/www/$app/data/data instead of /var/www/$app/data + # cf https://unix.stackexchange.com/q/94831 for a more elaborate explanation on the option + cp --archive --no-target-directory "$keep_dir/$stuff_to_keep" "$dest_dir/$stuff_to_keep" + fi + done + fi + rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ + + if [ -n "${install_dir:-}" ] && [ "$dest_dir" == "$install_dir" ]; then + _ynh_apply_default_permissions $dest_dir + fi +} diff --git a/helpers/helpers.v2.1.d/string b/helpers/helpers.v2.1.d/string new file mode 100644 index 000000000..d03fcff19 --- /dev/null +++ b/helpers/helpers.v2.1.d/string @@ -0,0 +1,129 @@ +#!/bin/bash + +# Generate a random string +# +# usage: ynh_string_random [--length=string_length] +# | arg: --length= - the string length to generate (default: 24) +# | arg: --filter= - the kind of characters accepted in the output (default: 'A-Za-z0-9') +# | ret: the generated string +# +# example: pwd=$(ynh_string_random --length=8) +ynh_string_random() { + # ============ Argument parsing ============= + local -A args_array=([l]=length= [f]=filter=) + local length + local filter + ynh_handle_getopts_args "$@" + length=${length:-24} + filter=${filter:-'A-Za-z0-9'} + # =========================================== + + dd if=/dev/urandom bs=1 count=1000 2>/dev/null \ + | tr --complement --delete "$filter" \ + | sed --quiet 's/\(.\{'"$length"'\}\).*/\1/p' +} + +# Substitute/replace a string (or expression) by another in a file +# +# usage: ynh_replace --match=match --replace=replace --file=file +# | arg: --match= - String to be searched and replaced in the file +# | arg: --replace= - String that will replace matches +# | arg: --file= - File in which the string will be replaced. +# +# As this helper is based on sed command, regular expressions and references to +# sub-expressions can be used (see sed manual page for more information) +ynh_replace() { + # ============ Argument parsing ============= + local -A args_array=([m]=match= [r]=replace= [f]=file=) + local match + local replace + local file + ynh_handle_getopts_args "$@" + # =========================================== + set +o xtrace # set +x + + local delimit=$'\001' + # Escape the delimiter if it's in the string. + match=${match//${delimit}/"\\${delimit}"} + replace=${replace//${delimit}/"\\${delimit}"} + + set -o xtrace # set -x + sed --in-place "s${delimit}${match}${delimit}${replace}${delimit}g" "$file" +} + +# Substitute/replace a regex in a file +# +# usage: ynh_replace_regex --match=match --replace=replace --file=file +# | arg: --match= - String to be searched and replaced in the file +# | arg: --replace= - String that will replace matches +# | arg: --file= - File in which the string will be replaced. +# +# This helper will use ynh_replace, but as you can use special +# characters, you can't use some regular expressions and sub-expressions. +ynh_replace_regex() { + # ============ Argument parsing ============= + local -A args_array=([m]=match= [r]=replace= [f]=file=) + local match + local replace + local file + ynh_handle_getopts_args "$@" + # =========================================== + + # Escape any backslash to preserve them as simple backslash. + match=${match//\\/"\\\\"} + replace=${replace//\\/"\\\\"} + + # Escape the & character, who has a special function in sed. + match=${match//&/"\&"} + replace=${replace//&/"\&"} + + ynh_replace --match="$match" --replace="$replace" --file="$file" +} + +# Sanitize a string intended to be the name of a database +# +# [packagingv1] +# +# usage: ynh_sanitize_dbid --db_name=name +# | arg: --db_name= - name to correct/sanitize +# | ret: the corrected name +# +# example: dbname=$(ynh_sanitize_dbid $app) +# +# Underscorify the string (replace - and . by _) +ynh_sanitize_dbid() { + # ============ Argument parsing ============= + local -A args_array=([n]=db_name=) + local db_name + ynh_handle_getopts_args "$@" + # =========================================== + + # We should avoid having - and . in the name of databases. They are replaced by _ + echo ${db_name//[-.]/_} +} + +# Normalize the url path syntax +# +# Handle the slash at the beginning of path and its absence at ending +# Return a normalized url path +# +# examples: +# url_path=$(ynh_normalize_url_path $url_path) +# ynh_normalize_url_path example # -> /example +# ynh_normalize_url_path /example # -> /example +# ynh_normalize_url_path /example/ # -> /example +# ynh_normalize_url_path / # -> / +# +# usage: ynh_normalize_url_path path_to_normalize +ynh_normalize_url_path() { + local path_url=$1 + + test -n "$path_url" || ynh_die "ynh_normalize_url_path expect a URL path as first argument and received nothing." + if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a / + path_url="/$path_url" # Add / at begin of path variable + fi + if [ "${path_url:${#path_url}-1}" == "/" ] && [ ${#path_url} -gt 1 ]; then # If the last character is a / and that not the only character. + path_url="${path_url:0:${#path_url}-1}" # Delete the last character + fi + echo $path_url +} diff --git a/helpers/helpers.v2.1.d/systemd b/helpers/helpers.v2.1.d/systemd new file mode 100644 index 000000000..b8ff84fd1 --- /dev/null +++ b/helpers/helpers.v2.1.d/systemd @@ -0,0 +1,181 @@ +#!/bin/bash + +# Create a dedicated systemd config +# +# usage: ynh_config_add_systemd [--service=service] [--template=template] +# | arg: --service= - Service name (optionnal, `$app` by default) +# | arg: --template= - Name of template file (optionnal, this is 'systemd' by default, meaning `../conf/systemd.service` will be used as template) +# +# This will use the template `../conf/.service`. +# +# See the documentation of `ynh_config_add` for a description of the template +# format and how placeholders are replaced with actual variables. +ynh_config_add_systemd() { + # ============ Argument parsing ============= + local -A args_array=([s]=service= [t]=template=) + local service + local template + ynh_handle_getopts_args "$@" + service="${service:-$app}" + template="${template:-systemd.service}" + # =========================================== + + ynh_config_add --template="$template" --destination="/etc/systemd/system/$service.service" + + systemctl enable $service --quiet + systemctl daemon-reload +} + +# Remove the dedicated systemd config +# +# usage: ynh_config_remove_systemd service +# | arg: service - Service name (optionnal, $app by default) +ynh_config_remove_systemd() { + local service="${1:-$app}" + if [ -e "/etc/systemd/system/$service.service" ]; then + ynh_systemctl --service=$service --action=stop + systemctl disable $service --quiet + ynh_safe_rm "/etc/systemd/system/$service.service" + systemctl daemon-reload + fi +} + +# Start (or other actions) a service, print a log in case of failure and optionnaly wait until the service is completely started +# +# usage: ynh_systemctl [--service=service] [--action=action] [ [--wait_until="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ] +# | arg: --service= - Name of the service to start. Default : `$app` +# | arg: --action= - Action to perform with systemctl. Default: start +# | arg: --wait_until= - The pattern to find in the log to attest the service is effectively fully started. +# | arg: --log_path= - Log file - Path to the log file. Default : `/var/log/$app/$app.log` +# | arg: --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 60 seconds. +# | arg: --length= - Length of the error log displayed for debugging : Default : 20 +ynh_systemctl() { + # ============ Argument parsing ============= + local -A args_array=([n]=service= [a]=action= [w]=wait_until= [p]=log_path= [t]=timeout= [e]=length=) + local service + local action + local wait_until + local length + local log_path + local timeout + ynh_handle_getopts_args "$@" + service="${service:-$app}" + action=${action:-start} + wait_until=${wait_until:-} + length=${length:-20} + log_path="${log_path:-/var/log/$service/$service.log}" + timeout=${timeout:-60} + # =========================================== + + # On CI, use length=100 because it's sometime hell to debug otherwise for super-long output + if ynh_in_ci_tests && [ $length -le 20 ] + then + length=100 + fi + + # Manage case of service already stopped + if [ "$action" == "stop" ] && ! systemctl is-active --quiet $service; then + return 0 + fi + + # Start to read the log + if [[ -n "$wait_until" ]]; then + local templog="$(mktemp)" + # Following the starting of the app in its log + if [ "$log_path" == "systemd" ]; then + # Read the systemd journal + journalctl --unit=$service --follow --since=-0 --quiet >"$templog" & + # Get the PID of the journalctl command + local pid_tail=$! + else + # Read the specified log file + tail --follow=name --retry --lines=0 "$log_path" >"$templog" 2>&1 & + # Get the PID of the tail command + local pid_tail=$! + fi + fi + + # Use reload-or-restart instead of reload. So it wouldn't fail if the service isn't running. + if [ "$action" == "reload" ]; then + 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; then + # Show syslog for this service + journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service >&2 + # If a log is specified for this service, show also the content of this log + if [ -e "$log_path" ]; then + tail --lines=$length "$log_path" >&2 + fi + _ynh_clean_check_starting + return 1 + fi + + # Start the timeout and try to find wait_until + if [[ -n "${wait_until:-}" ]]; then + set +o xtrace # set +x + local i=0 + local starttime=$(date +%s) + for i in $(seq 1 $timeout); do + # Read the log until the sentence is found, that means the app finished to start. Or run until the timeout + if [ "$log_path" == "systemd" ]; then + # 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 --since="$time_start" --quiet --no-pager --no-hostname | grep --extended-regexp --quiet "$wait_until"; then + ynh_print_info "The service $service has correctly executed the action ${action}." + break + fi + else + if grep --extended-regexp --quiet "$wait_until" "$templog"; then + ynh_print_info "The service $service has correctly executed the action ${action}." + break + fi + fi + if [ $i -eq 30 ]; then + echo "(this may take some time)" >&2 + fi + # Also check the timeout using actual timestamp, because sometimes for some reason, + # journalctl may take a huge time to run, and we end up waiting literally an entire hour + # instead of 5 min ... + if [[ "$(( $(date +%s) - $starttime))" -gt "$timeout" ]] + then + i=$timeout + break + fi + sleep 1 + done + set -o xtrace # set -x + if [ $i -ge 3 ]; then + echo "" >&2 + fi + if [ $i -eq $timeout ]; then + ynh_print_warn "The service $service didn't fully executed the action ${action} before the timeout." + ynh_print_warn "Please find here an extract of the end of the log of the service $service:" + journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service >&2 + if [ -e "$log_path" ]; then + ynh_print_warn "===" + tail --lines=$length "$log_path" >&2 + fi + + # If we tried to reload/start/restart the service but systemctl consider it to be still inactive/broken, then handle it as a failure + if ([ "$action" == "reload" ] || [ "$action" == "start" ] || [ "$action" == "restart" ]) && ! systemctl --quiet is-active $service + then + _ynh_clean_check_starting + return 1 + fi + fi + _ynh_clean_check_starting + fi +} + +_ynh_clean_check_starting() { + if [ -n "${pid_tail:-}" ]; then + # Stop the execution of tail. + kill -SIGTERM $pid_tail 2>&1 + fi + if [ -n "${templog:-}" ]; then + ynh_safe_rm "$templog" 2>&1 + fi +} diff --git a/helpers/helpers.v2.1.d/systemuser b/helpers/helpers.v2.1.d/systemuser new file mode 100644 index 000000000..720a6ec28 --- /dev/null +++ b/helpers/helpers.v2.1.d/systemuser @@ -0,0 +1,105 @@ +#!/bin/bash + +# Check if a user exists on the system +# +# usage: ynh_system_user_exists --username=username +# | arg: --username= - the username to check +# | ret: 0 if the user exists, 1 otherwise. +ynh_system_user_exists() { + # ============ Argument parsing ============= + local -A args_array=([u]=username=) + local username + ynh_handle_getopts_args "$@" + # =========================================== + + getent passwd "$username" &>/dev/null +} + +# Check if a group exists on the system +# +# usage: ynh_system_group_exists --group=group +# | arg: --group= - the group to check +# | ret: 0 if the group exists, 1 otherwise. +ynh_system_group_exists() { + # ============ Argument parsing ============= + local -A args_array=([g]=group=) + local group + ynh_handle_getopts_args "$@" + # =========================================== + + getent group "$group" &>/dev/null +} + +# Create a system user +# +# usage: ynh_system_user_create --username=user_name [--home_dir=home_dir] [--use_shell] [--groups="group1 group2"] +# | arg: --username= - Name of the system user that will be create +# | arg: --home_dir= - Path of the home dir for the user. Usually the final path of the app. If this argument is omitted, the user will be created without home +# | arg: --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell +# | arg: --groups - Add the user to system groups. Typically meant to add the user to the ssh.app / sftp.app group (e.g. for borgserver, my_webapp) +# +# Create a nextcloud user with no home directory and /usr/sbin/nologin login shell (hence no login capability) : +# ``` +# ynh_system_user_create --username=nextcloud +# ``` +# Create a discourse user using /var/www/discourse as home directory and the default login shell : +# ``` +# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell +# ``` +ynh_system_user_create() { + # ============ Argument parsing ============= + local -A args_array=([u]=username= [h]=home_dir= [s]=use_shell [g]=groups=) + local username + local home_dir + local use_shell + local groups + ynh_handle_getopts_args "$@" + use_shell="${use_shell:-0}" + home_dir="${home_dir:-}" + groups="${groups:-}" + # =========================================== + + if ! ynh_system_user_exists --username="$username"; then # Check if the user exists on the system + # If the user doesn't exist + if [ -n "$home_dir" ]; then # If a home dir is mentioned + local user_home_dir="--home-dir $home_dir" + else + local user_home_dir="--no-create-home" + fi + if [ $use_shell -eq 1 ]; then # If we want a shell for the user + local shell="" # Use default shell + else + local shell="--shell /usr/sbin/nologin" + fi + useradd $user_home_dir --system --user-group $username $shell || ynh_die "Unable to create $username system account" + fi + + local group + for group in $groups; do + usermod -a -G "$group" "$username" + done +} + +# Delete a system user +# +# usage: ynh_system_user_delete --username=user_name +# | arg: --username= - Name of the system user that will be create +ynh_system_user_delete() { + # ============ Argument parsing ============= + local -A args_array=([u]=username=) + local username + ynh_handle_getopts_args "$@" + # =========================================== + + # Check if the user exists on the system + if ynh_system_user_exists --username="$username"; then + deluser $username + else + ynh_print_warn "The user $username was not found" + fi + + # Check if the group exists on the system + if ynh_system_group_exists --group="$username"; then + delgroup $username + fi +} diff --git a/helpers/helpers.v2.1.d/templating b/helpers/helpers.v2.1.d/templating new file mode 100644 index 000000000..dc7a090a2 --- /dev/null +++ b/helpers/helpers.v2.1.d/templating @@ -0,0 +1,333 @@ +#!/bin/bash + +# Create a dedicated config file from a template +# +# usage: ynh_config_add --template="template" --destination="destination" +# | arg: --template= - Template config file to use +# | arg: --destination= - Destination of the config file +# | arg: --jinja - Use jinja template instead of the simple `__MY_VAR__` templating format +# +# examples: +# ynh_add_config --template=".env" --destination="$install_dir/.env" # (use the template file "conf/.env" from the app's package) +# ynh_add_config --jinja --template="config.j2" --destination="$install_dir/config" # (use the template file "conf/config.j2" from the app's package) +# +# The template can be 1) the name of a file in the `conf` directory of +# the app, 2) a relative path or 3) an absolute path. +# +# This applies a simple templating format which covers a good 95% of cases, +# where patterns like `__FOO__` are replaced by the bash variable `$foo`, for example: +# `__DOMAIN__` by `$domain` +# `__PATH__` by `$path` +# `__APP__` by `$app` +# `__VAR_1__` by `$var_1` +# `__VAR_2__` by `$var_2` +# +# Special case for `__PATH__/` which is replaced by `/` instead of `//` if `$path` is `/` +# +# ##### When --jinja is enabled +# +# This option is meant for advanced use-cases where the "simple" templating +# mode ain't enough because you need conditional blocks or loops. +# +# For a full documentation of jinja's syntax you can refer to: +# https://jinja.palletsprojects.com/en/3.1.x/templates/ +# +# Note that in YunoHost context, all variables are from shell variables and therefore are strings +# +# ##### Keeping track of manual changes by the admin +# +# The helper will verify the checksum and backup the destination file +# if it's different before applying the new template. +# +# And it will calculate and store the destination file checksum +# into the app settings when configuration is done. +ynh_config_add() { + # ============ Argument parsing ============= + local -A args_array=([t]=template= [d]=destination= [j]=jinja) + local template + local destination + local jinja + ynh_handle_getopts_args "$@" + jinja="${jinja:-0}" + # =========================================== + + local template_path + if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then + template_path="$YNH_APP_BASEDIR/conf/$template" + elif [ -f "$template" ]; then + template_path=$template + else + ynh_die "The provided template $template doesn't exist" + fi + + ynh_backup_if_checksum_is_different "$destination" + + # Make sure to set the permissions before we copy the file + # This is to cover a case where an attacker could have + # created a file beforehand to have control over it + # (cp won't overwrite ownership / modes by default...) + touch $destination + chmod 640 $destination + _ynh_apply_default_permissions $destination + + if [[ "$jinja" == 1 ]] + then + # This is ran in a subshell such that the "export" does not "contaminate" the main process + ( + export $(compgen -v) + j2 "$template_path" -f env -o $destination + ) + else + cp -f "$template_path" "$destination" + _ynh_replace_vars "$destination" + fi + + ynh_store_file_checksum "$destination" +} + +# Replace `__FOO__` patterns in file with bash variable `$foo` +# +# [internal] +# +# usage: ynh_replace_vars "/path/to/file" +# | arg: /path/to/file - File where to replace variables +# +# This applies a simple templating format which covers a good 95% of cases, +# where patterns like `__FOO__` are replaced by the bash variable `$foo`, for example: +# `__DOMAIN__` by `$domain` +# `__PATH__` by `$path` +# `__APP__` by `$app` +# `__VAR_1__` by `$var_1` +# `__VAR_2__` by `$var_2` +# +# Special case for `__PATH__/` which is replaced by `/` instead of `//` if `$path` is `/` +_ynh_replace_vars() { + local file=$1 + + # List unique (__ __) variables in $file + local uniques_vars=($(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g")) + + set +o xtrace # set +x + + # Specific trick to make sure that __PATH__/ doesn't end up in "//" if $path=/ + if [[ "${path:-}" == "/" ]] && grep -q '__PATH__/' $file; then + sed --in-place "s@__PATH__/@/@g" "$file" + fi + + # Do the replacement + local delimit=@ + for one_var in "${uniques_vars[@]}"; do + # Validate that one_var is indeed defined + # -v checks if the variable is defined, for example: + # -v FOO tests if $FOO is defined + # -v $FOO tests if ${!FOO} is defined + # More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964 + [[ -v "${one_var:-}" ]] || ynh_die "Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" + + # Escape delimiter in match/replace string + match_string="__${one_var^^}__" + match_string=${match_string//${delimit}/"\\${delimit}"} + replace_string="${!one_var}" + replace_string=${replace_string//\\/\\\\} + replace_string=${replace_string//${delimit}/"\\${delimit}"} + + # Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs) + sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$file" + done + set -o xtrace # set -x +} + +# Get a value from heterogeneous file (yaml, json, php, python...) +# +# usage: ynh_read_var_in_file --file=PATH --key=KEY +# | arg: --file= - the path to the file +# | arg: --key= - the key to get +# | arg: --after= - the line just before the key (in case of multiple lines with the name of the key in the file) +# +# This helpers match several var affectation use case in several languages +# We don't use jq or equivalent to keep comments and blank space in files +# This helpers work line by line, it is not able to work correctly +# if you have several identical keys in your files +# +# Example of line this helpers can managed correctly +# .yml +# title: YunoHost documentation +# email: 'yunohost@yunohost.org' +# .json +# "theme": "colib'ris", +# "port": 8102 +# "some_boolean": false, +# "user": null +# .ini +# some_boolean = On +# action = "Clear" +# port = 20 +# .php +# $user= +# user => 20 +# .py +# USER = 8102 +# user = 'https://donate.local' +# CUSTOM['user'] = 'YunoHost' +ynh_read_var_in_file() { + # ============ Argument parsing ============= + local -A args_array=([f]=file= [k]=key= [a]=after=) + local file + local key + local after + ynh_handle_getopts_args "$@" + after="${after:-}" + # =========================================== + + [[ -f $file ]] || ynh_die "File $file does not exists" + + set +o xtrace # set +x + + # Get the line number after which we search for the variable + local line_number=1 + if [[ -n "$after" ]]; then + line_number=$(grep -m1 -n $after $file | cut -d: -f1) + if [[ -z "$line_number" ]]; then + set -o xtrace # set -x + return 1 + fi + fi + + local filename="$(basename -- "$file")" + local ext="${filename##*.}" + local endline=',;' + local assign="=>|:|=" + local comments="#" + local string="\"'" + if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then + endline='#' + fi + if [[ "$ext" =~ ^ini|env$ ]]; then + comments="[;#]" + fi + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + comments="//" + fi + local list='\[\s*['$string']?\w+['$string']?\]' + local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' + var_part+="[$string]?${key}[$string]?" + var_part+='\s*\]?\s*' + var_part+="($assign)" + var_part+='\s*' + + # Extract the part after assignation sign + local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" + if [[ "$expression_with_comment" == "YNH_NULL" ]]; then + set -o xtrace # set -x + echo YNH_NULL + return 0 + fi + + # Remove comments if needed + local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" + + local first_char="${expression:0:1}" + if [[ "$first_char" == '"' ]]; then + echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g' + elif [[ "$first_char" == "'" ]]; then + echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g" + else + echo "$expression" + fi + set -o xtrace # set -x +} + +# Set a value into heterogeneous file (yaml, json, php, python...) +# +# usage: ynh_write_var_in_file --file=PATH --key=KEY --value=VALUE +# | arg: --file= - the path to the file +# | arg: --key= - the key to set +# | arg: --value= - the value to set +# | arg: --after= - the line just before the key (in case of multiple lines with the name of the key in the file) +ynh_write_var_in_file() { + # ============ Argument parsing ============= + local -A args_array=([f]=file= [k]=key= [v]=value= [a]=after=) + local file + local key + local value + local after + ynh_handle_getopts_args "$@" + after="${after:-}" + # =========================================== + + [[ -f $file ]] || ynh_die "File $file does not exists" + + set +o xtrace # set +x + + # Get the line number after which we search for the variable + local after_line_number=1 + if [[ -n "$after" ]]; 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 filename="$(basename -- "$file")" + local ext="${filename##*.}" + local endline=',;' + local assign="=>|:|=" + local comments="#" + local string="\"'" + if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then + endline='#' + fi + if [[ "$ext" =~ ^ini|env$ ]]; then + comments="[;#]" + fi + if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then + comments="//" + fi + local list='\[\s*['$string']?\w+['$string']?\]' + local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' + var_part+="[$string]?${key}[$string]?" + var_part+='\s*\]?\s*' + var_part+="($assign)" + var_part+='\s*' + + # Extract the part after assignation sign + local expression_with_comment="$((tail +$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 + # \ and sed is quite complex you need 2 \\ to get one in a sed + # So we need \\\\ to go through 2 sed + value="$(echo "$value" | sed 's/"/\\\\"/g')" + sed -ri "${range}s$delimiter"'(^'"${var_part}"'")([^"]|\\")*("[\s;,]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}"'"'"${endline}${delimiter}i" ${file} + elif [[ "$first_char" == "'" ]]; then + # \ and sed is quite complex you need 2 \\ to get one in a sed + # However double quotes implies to double \\ to + # So we need \\\\\\\\ to go through 2 sed and 1 double quotes str + value="$(echo "$value" | sed "s/'/\\\\\\\\'/g")" + sed -ri "${range}s$delimiter(^${var_part}')([^']|\\')*('"'[\s,;]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}'${endline}${delimiter}i" ${file} + else + if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]]; then + value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"' + fi + if [[ "$ext" =~ ^yaml|yml$ ]]; then + value=" $value" + fi + sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file} + fi + set -o xtrace # set -x +} diff --git a/helpers/helpers.v2.1.d/utils b/helpers/helpers.v2.1.d/utils new file mode 100644 index 000000000..aca976c54 --- /dev/null +++ b/helpers/helpers.v2.1.d/utils @@ -0,0 +1,458 @@ +#!/bin/bash + +YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} + +# Handle script crashes / failures +# +# [internal] +# +ynh_exit_properly() { + local exit_code=$? + + 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 + fi + + trap '' EXIT # Ignore new exit signals + # Do not exit anymore if a command fail or if a variable is empty + set +o errexit # set +e + set +o nounset # set +u + + # Small tempo to avoid the next message being mixed up with other DEBUG messages + sleep 0.5 + + # Exit with error status + # We don't call ynh_die basically to avoid unecessary 10-ish + # debug lines about parsing args and stuff just to exit 1.. + exit 1 +} + +# Exits if an error occurs during the execution of the script. +# +# [packagingv1] +# +# usage: ynh_abort_if_errors +# +# This configure the rest of the script execution such that, if an error occurs +# or if an empty variable is used, the execution of the script stops immediately +ynh_abort_if_errors() { + set -o errexit # set -e; Exit if a command fail + set -o nounset # set -u; And if a variable is used unset + trap ynh_exit_properly EXIT # Capturing exit signals on shell script +} + +# When running an app script, auto-enable ynh_abort_if_errors except for remove script +if [[ "${YNH_CONTEXT:-}" != "regenconf" ]] && [[ "${YNH_APP_ACTION}" != "remove" ]] +then + ynh_abort_if_errors +fi + +# Execute a command after sudoing as $app +# +# Note that the $PATH variable is preserved (using --preserve-env=PATH) +# +# usage: ynh_exec_as_app COMMAND [ARG ...] +ynh_exec_as_app() { + sudo --preserve-env=PATH -u "$app" "$@" +} + +# Curl abstraction to help with POST requests to local pages (such as installation forms) +# +# usage: ynh_local_curl "page_uri" "key1=value1" "key2=value2" ... +# | arg: page_uri - Path (relative to `$path`) of the page where POST data will be sent +# | arg: key1=value1 - (Optionnal) POST key and corresponding value +# | arg: key2=value2 - (Optionnal) Another POST key and corresponding value +# | arg: ... - (Optionnal) More POST keys and values +# +# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2" +# +# For multiple calls, cookies are persisted between each call for the same app +# +# `$domain` and `$path` should be defined externally (and correspond to the domain.tld and the /path (of the app?)) +ynh_local_curl() { + # Define url of page to curl + local local_page=$(ynh_normalize_url_path $1) + local full_path=$path$local_page + + if [ "${path}" == "/" ]; then + full_path=$local_page + fi + + local full_page_url=https://localhost$full_path + + # Concatenate all other arguments with '&' to prepare POST data + local POST_data="" + local arg="" + for arg in "${@:2}"; do + POST_data="${POST_data}${arg}&" + done + if [ -n "$POST_data" ]; then + # Add --data arg and remove the last character, which is an unecessary '&' + POST_data="--data ${POST_data::-1}" + fi + + # Wait untils nginx has fully reloaded (avoid curl fail with http2) + sleep 2 + + local cookiefile=/tmp/ynh-$app-cookie.txt + touch $cookiefile + chown root $cookiefile + chmod 700 $cookiefile + + # Temporarily enable visitors if needed... + local visitors_enabled=$(ynh_permission_has_user --permission="main" --user="visitors" && echo yes || echo no) + if [[ $visitors_enabled == "no" ]]; then + ynh_permission_update --permission="main" --add="visitors" + fi + + # Curl the URL + curl --silent --show-error --insecure --location --header "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" --cookie-jar $cookiefile --cookie $cookiefile + + if [[ $visitors_enabled == "no" ]]; then + ynh_permission_update --permission="main" --remove="visitors" + fi +} + +_acceptable_path_to_delete() { + local file=$1 + + local forbidden_paths=$(ls -d / /* /{var,home,usr}/* /etc/{default,sudoers.d,yunohost,cron*} /etc/yunohost/{apps,domains,hooks.d} /opt/yunohost 2> /dev/null) + + # Legacy : A couple apps still have data in /home/$app ... + if [[ -n "${app:-}" ]] + then + forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app") + fi + + # Use realpath to normalize the path .. + # i.e convert ///foo//bar//..///baz//// to /foo/baz + file=$(realpath --no-symlinks "$file") + if [ -z "$file" ] || grep -q -x -F "$file" <<< "$forbidden_paths"; then + return 1 + else + return 0 + fi +} + +# Remove a file or a directory, checking beforehand that it's not a disastrous location to rm such as entire /var or /home +# +# usage: ynh_safe_rm path_to_remove +ynh_safe_rm() { + local target="$1" + set +o xtrace # set +x + + if [ $# -ge 2 ]; then + ynh_print_warn "/!\ Packager ! You provided more than one argument to ynh_safe_rm but it will be ignored... Use this helper with one argument at time." + fi + + if [[ -z "$target" ]]; then + ynh_print_warn "ynh_safe_rm called with empty argument, ignoring." + elif [[ ! -e "$target" ]] && [[ ! -L "$target" ]]; then + ynh_print_info "'$target' wasn't deleted because it doesn't exist." + elif ! _acceptable_path_to_delete "$target"; then + ynh_print_warn "Not deleting '$target' because it is not an acceptable path to delete." + else + rm --recursive "$target" + fi + + set -o xtrace # set -x +} + +# Read the value of a key in the app's manifest +# +# usage: ynh_read_manifest "key" +# | arg: key - Name of the key to find +# | ret: the value associate to that key +ynh_read_manifest() { + cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq ".$1" --raw-output +} + +# Return the app upstream version, deduced from `$YNH_APP_MANIFEST_VERSION` and strippig the `~ynhX` part +# +# usage: ynh_app_upstream_version +# | ret: the version number of the upstream app +# +# For example, if the manifest contains `4.3-2~ynh3` the function will return `4.3-2` +ynh_app_upstream_version() { + echo "${YNH_APP_MANIFEST_VERSION/~ynh*/}" +} + +# Return 0 if the "upstream" part of the version changed, or 1 otherwise (ie only the ~ynh suffix changed) +# +# usage: if ynh_app_upstream_version_changed; then ... +ynh_app_upstream_version_changed() { + # "UPGRADE_PACKAGE" means only the ~ynh prefix changed + [[ "$YNH_APP_UPGRADE_TYPE" == "UPGRADE_PACKAGE" ]] && return 1 || return 0 +} + +# Compare the current package version is strictly lower than another version given as an argument +# +# example: if ynh_app_upgrading_from_version_before 2.3.2~ynh1; then ... +ynh_app_upgrading_from_version_before() { + local version=$1 + [[ $version =~ '~ynh' ]] || ynh_die "Invalid argument for version, should include the ~ynhX prefix" + + dpkg --compare-versions $YNH_APP_CURRENT_VERSION lt $version +} + +# Compare the current package version is lower or equal to another version given as an argument +# +# example: if ynh_app_upgrading_from_version_before_or_equal_to 2.3.2~ynh1; then ... +ynh_app_upgrading_from_version_before_or_equal_to() { + local version=$1 + [[ $version =~ '~ynh' ]] || ynh_die "Invalid argument for version, should include the ~ynhX prefix" + + dpkg --compare-versions $YNH_APP_CURRENT_VERSION le $version +} + +# Apply sane permissions for files installed by ynh_setup_source and ynh_config_add. +# +# [internal] +# +# * Anything below $install_dir is chown $app:$app and chmod o-rwx,g-w +# * The rest is considered as system configuration and chown root, chmod 400 +# +_ynh_apply_default_permissions() { + local target=$1 + + is_in_dir() { + # Returns false if parent is empty + [ -n "$2" ] || return 1 + local child=$(realpath "$1" 2>/dev/null) + local parent=$(realpath "$2" 2>/dev/null) + [[ "${child}" =~ ^$parent ]] + } + + # App files can have files of their own + if ynh_system_user_exists --username="$app"; then + # If this is a file in $install_dir or $data_dir : it should be owned and read+writable by $app only + if [ -f "$target" ] && (is_in_dir "$target" "${install_dir:-}" || is_in_dir "$target" "${data_dir:-}" || is_in_dir "$target" "/etc/$app") + then + chmod 600 "$target" + chown "$app:$app" "$target" + return + fi + # If this is the install dir (so far this is the only way this helper is called with a directory) + if [ "$target" == "${install_dir:-}" ] + then + # Read the group from the install_dir manifest resource + local group="$(ynh_read_manifest 'resources.install_dir.group' | sed 's/null//g' | sed "s/__APP__/$app/g" | cut -f1 -d:)" + if [[ -z "$group" ]] + then + # We set the group to www-data for webapps that do serve static assets, which therefore need to be readable by nginx ... + # The fact that the app needs this is infered by the existence of an nginx.conf and the presence of "alias" or "root" directive + if grep -q '^\s*alias\s\|^\s*root\s' "$YNH_APP_BASEDIR/conf/nginx.conf" 2>/dev/null; + then + group="www-data" + # Or default to "$app" + else + group="$app" + fi + fi + # Files inside should be owned by $app with rw-r----- (+x for folders or files that already have +x) + # The group needs read/dirtraversal (in particular if it's www-data) + chmod -R u=rwX,g=rX,o=--- "$target" + chown -R "$app:$group" "$target" + return + fi + fi + + # Other files are considered system + chmod 400 "$target" + chown root:root "$target" +} + +int_to_bool() { + sed -e 's/^1$/True/g' -e 's/^0$/False/g' -e 's/^true$/True/g' -e 's/^false$/False/g' +} + +toml_to_json() { + python3 -c 'import toml, json, sys; print(json.dumps(toml.load(sys.stdin)))' +} + +# Validate an IP address +# +# usage: ynh_validate_ip --family=family --ip_address=ip_address +# | ret: 0 for valid ip addresses, 1 otherwise +# +# example: ynh_validate_ip 4 111.222.333.444 +ynh_validate_ip() { + # ============ Argument parsing ============= + local -A args_array=([f]=family= [i]=ip_address=) + local family + local ip_address + ynh_handle_getopts_args "$@" + # =========================================== + + [ "$family" == "4" ] || [ "$family" == "6" ] || return 1 + + # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298 + python3 /dev/stdin </dev/null || ynh_die "There is no \"$app\" system user" + + # Make sure the app has an install_dir setting + local install_dir=$(ynh_app_setting_get --app=$app --key=install_dir) + [ -n "$install_dir" ] || ynh_die "$app has no install_dir setting (does it use packaging format >=2?)" + + # 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 + + # Activate the Python environment, if it exists + if [ -f $install_dir/venv/bin/activate ] + then + # set -/+a enables and disables new variables being automatically exported. Needed when using `source`. + set -a + source $install_dir/venv/bin/activate + 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/helpers.v2.1.d/vendor b/helpers/helpers.v2.1.d/vendor new file mode 120000 index 000000000..9c39cc9f8 --- /dev/null +++ b/helpers/helpers.v2.1.d/vendor @@ -0,0 +1 @@ +../vendor \ No newline at end of file diff --git a/helpers/helpers.v2.d b/helpers/helpers.v2.d new file mode 120000 index 000000000..e2614c897 --- /dev/null +++ b/helpers/helpers.v2.d @@ -0,0 +1 @@ +helpers.v1.d \ No newline at end of file diff --git a/helpers/logrotate b/helpers/logrotate deleted file mode 100644 index 45f66d443..000000000 --- a/helpers/logrotate +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash - -# Use logrotate to manage the logfile -# -# usage: ynh_use_logrotate [--logfile=/log/file] [--nonappend] [--specific_user=user/group] -# | arg: -l, --logfile= - absolute path of logfile -# | arg: -n, --nonappend - (optional) Replace the config file instead of appending this new config. -# | arg: -u, --specific_user= - run logrotate as the specified user and group. If not specified logrotate is runned as root. -# -# If no `--logfile` is provided, `/var/log/$app` will be used as default. -# `logfile` can point to a directory or a file. -# -# It's possible to use this helper multiple times, each config will be added to -# the same logrotate config file. Unless you use the option `--non-append` -# -# 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=lnu - local -A args_array=([l]=logfile= [n]=nonappend [u]=specific_user=) - local logfile - local nonappend - local specific_user - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - logfile="${logfile:-}" - nonappend="${nonappend:-0}" - specific_user="${specific_user:-}" - - # LEGACY CODE - PRE GETOPTS - if [ $# -gt 0 ] && [ "$(echo ${1:0:1})" != "-" ]; then - # If the given logfile parameter already exists as a file, or if it ends up with ".log", - # we just want to manage a single file - if [ -f "$1" ] || [ "$(echo ${1##*.})" == "log" ]; then - local logfile=$1 - # Otherwise we assume we want to manage a directory and all its .log file inside - else - local logfile=$1/*.log - fi - fi - # LEGACY CODE - - local customtee="tee --append" - if [ "$nonappend" -eq 1 ]; then - customtee="tee" - fi - if [ -n "$logfile" ]; then - if [ ! -f "$1" ] && [ "$(echo ${logfile##*.})" != "log" ]; then # Keep only the extension to check if it's a logfile - local logfile="$logfile/*.log" # Else, uses the directory and all logfile into it. - fi - else - logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log - fi - local su_directive="" - if [[ -n $specific_user ]]; then - su_directive=" # Run logorotate as specific user - group - su ${specific_user%/*} ${specific_user#*/}" - fi - - cat >./${app}-logrotate </dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) -} - -# Remove the app's logrotate config. -# -# usage: ynh_remove_logrotate -# -# Requires YunoHost version 2.6.4 or higher. -ynh_remove_logrotate() { - if [ -e "/etc/logrotate.d/$app" ]; then - rm "/etc/logrotate.d/$app" - fi -} diff --git a/helpers/utils b/helpers/utils deleted file mode 100644 index 489c5c261..000000000 --- a/helpers/utils +++ /dev/null @@ -1,1088 +0,0 @@ -#!/bin/bash - -YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)} - -# Handle script crashes / failures -# -# [internal] -# -# usage: -# ynh_exit_properly is used only by the helper ynh_abort_if_errors. -# You should not use it directly. -# Instead, add to your script: -# ynh_clean_setup () { -# instructions... -# } -# -# This function provide a way to clean some residual of installation that not managed by remove script. -# -# It prints a warning to inform that the script was failed, and execute the ynh_clean_setup function if used in the app script -# -# Requires YunoHost version 2.6.4 or higher. -ynh_exit_properly() { - local exit_code=$? - - 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 - fi - - trap '' EXIT # Ignore new exit signals - # Do not exit anymore if a command fail or if a variable is empty - set +o errexit # set +e - set +o nounset # set +u - - # Small tempo to avoid the next message being mixed up with other DEBUG messages - sleep 0.5 - - if type -t ynh_clean_setup >/dev/null; then # Check if the function exist in the app script. - ynh_clean_setup # Call the function to do specific cleaning for the app. - fi - - # Exit with error status - # We don't call ynh_die basically to avoid unecessary 10-ish - # debug lines about parsing args and stuff just to exit 1.. - exit 1 -} - -# Exits if an error occurs during the execution of the script. -# -# usage: ynh_abort_if_errors -# -# This configure the rest of the script execution such that, if an error occurs -# or if an empty variable is used, the execution of the script stops immediately -# and a call to `ynh_clean_setup` is triggered if it has been defined by your script. -# -# Requires YunoHost version 2.6.4 or higher. -ynh_abort_if_errors() { - set -o errexit # set -e; Exit if a command fail - set -o nounset # set -u; And if a variable is used unset - trap ynh_exit_properly EXIT # Capturing exit signals on shell script -} - -# 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"] [--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=Sha256 sum -# SOURCE_FORMAT=tar.gz -# SOURCE_IN_SUBDIR=false -# SOURCE_FILENAME=example.tar.gz -# SOURCE_EXTRACT=(true|false) -# SOURCE_PLATFORM=linux/arm64/v8 -# ``` -# -# The helper will: -# - 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 `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 -# -# Requires YunoHost version 2.6.4 or higher. -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= [r]=full_replace=) - local dest_dir - local source_id - local keep - local full_replace - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - keep="${keep:-}" - full_replace="${full_replace:-0}" - - 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 - - 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} - src_in_subdir=${src_in_subdir:-true} - src_format=${src_format:-tar.gz} - src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]') - src_extract=${src_extract:-true} - - 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}/${source_id}" - - # Gotta use this trick with 'dirname' because source_id may contain slashes x_x - mkdir -p $(dirname /var/cache/yunohost/download/${YNH_APP_ID}/${source_id}) - src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${source_id}" - - 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 ?" - - # If the file was prefetched but somehow doesn't match the sum, rm and redownload it - if [ -e "$src_filename" ] && ! echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status - then - rm -f "$src_filename" - fi - - # Only redownload the file if it wasnt prefetched - if [ ! -e "$src_filename" ] - then - # NB. we have to declare the var as local first, - # otherwise 'local foo=$(false) || echo 'pwet'" does'nt work - # because local always return 0 ... - local out - # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) - out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \ - || ynh_die --message="$out" - fi - - # Check the control sum - 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 - rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ - if [ -n "$keep" ] && [ -e "$dest_dir" ]; then - local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} - mkdir -p $keep_dir - local stuff_to_keep - for stuff_to_keep in $keep; do - if [ -e "$dest_dir/$stuff_to_keep" ]; then - mkdir --parents "$(dirname "$keep_dir/$stuff_to_keep")" - cp --archive "$dest_dir/$stuff_to_keep" "$keep_dir/$stuff_to_keep" - fi - 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" == "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 - local tmp_dir=$(mktemp --directory) - unzip -quo $src_filename -d "$tmp_dir" - cp --archive $tmp_dir/*/. "$dest_dir" - ynh_secure_remove --file="$tmp_dir" - else - unzip -quo $src_filename -d "$dest_dir" - fi - ynh_secure_remove --file="$src_filename" - else - local strip="" - if [ "$src_in_subdir" != "false" ]; then - if [ "$src_in_subdir" == "true" ]; then - local sub_dirs=1 - else - local sub_dirs="$src_in_subdir" - fi - strip="--strip-components $sub_dirs" - fi - if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]]; then - tar --extract --file=$src_filename --directory="$dest_dir" $strip - else - ynh_die --message="Archive format unrecognized." - fi - ynh_secure_remove --file="$src_filename" - fi - - # Apply patches - if [ -d "$YNH_APP_BASEDIR/sources/patches/" ]; then - local patches_folder=$(realpath $YNH_APP_BASEDIR/sources/patches/) - if (($(find $patches_folder -type f -name "${source_id}-*.patch" 2>/dev/null | wc --lines) > "0")); then - ( - cd "$dest_dir" - for p in $patches_folder/${source_id}-*.patch; do - echo $p - patch --strip=1 <$p - done - ) || ynh_die --message="Unable to apply patches" - fi - fi - - # Add supplementary files - if test -e "$YNH_APP_BASEDIR/sources/extra_files/${source_id}"; then - cp --archive $YNH_APP_BASEDIR/sources/extra_files/$source_id/. "$dest_dir" - fi - - # Keep files to be backup/restored at the end of the helper - # Assuming $dest_dir already exists - if [ -n "$keep" ]; then - local keep_dir=/var/cache/yunohost/files_to_keep_during_setup_source/${YNH_APP_ID} - local stuff_to_keep - for stuff_to_keep in $keep; do - if [ -e "$keep_dir/$stuff_to_keep" ]; then - mkdir --parents "$(dirname "$dest_dir/$stuff_to_keep")" - cp --archive "$keep_dir/$stuff_to_keep" "$dest_dir/$stuff_to_keep" - fi - done - fi - rm -rf /var/cache/yunohost/files_to_keep_during_setup_source/ -} - -# Curl abstraction to help with POST requests to local pages (such as installation forms) -# -# usage: ynh_local_curl "page_uri" "key1=value1" "key2=value2" ... -# | arg: page_uri - Path (relative to `$path_url`) of the page where POST data will be sent -# | arg: key1=value1 - (Optionnal) POST key and corresponding value -# | arg: key2=value2 - (Optionnal) Another POST key and corresponding value -# | arg: ... - (Optionnal) More POST keys and values -# -# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2" -# -# For multiple calls, cookies are persisted between each call for the same app -# -# `$domain` and `$path_url` should be defined externally (and correspond to the domain.tld and the /path (of the app?)) -# -# Requires YunoHost version 2.6.4 or higher. -ynh_local_curl() { - # Define url of page to curl - local local_page=$(ynh_normalize_url_path $1) - local full_path=$path_url$local_page - - if [ "${path_url}" == "/" ]; then - full_path=$local_page - fi - - local full_page_url=https://localhost$full_path - - # Concatenate all other arguments with '&' to prepare POST data - local POST_data="" - local arg="" - for arg in "${@:2}"; do - POST_data="${POST_data}${arg}&" - done - if [ -n "$POST_data" ]; then - # Add --data arg and remove the last character, which is an unecessary '&' - POST_data="--data ${POST_data::-1}" - fi - - # Wait untils nginx has fully reloaded (avoid curl fail with http2) - sleep 2 - - local cookiefile=/tmp/ynh-$app-cookie.txt - touch $cookiefile - chown root $cookiefile - chmod 700 $cookiefile - - # Temporarily enable visitors if needed... - local visitors_enabled=$(ynh_permission_has_user "main" "visitors" && echo yes || echo no) - if [[ $visitors_enabled == "no" ]]; then - ynh_permission_update --permission "main" --add "visitors" - fi - - # Curl the URL - curl --silent --show-error --insecure --location --header "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" --cookie-jar $cookiefile --cookie $cookiefile - - if [[ $visitors_enabled == "no" ]]; then - ynh_permission_update --permission "main" --remove "visitors" - fi -} - -# Create a dedicated config file from a template -# -# usage: ynh_add_config --template="template" --destination="destination" -# | arg: -t, --template= - Template config file to use -# | arg: -d, --destination= - Destination of the config file -# -# examples: -# 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 -# of a YunoHost Package, a relative path or an absolute path. -# -# The helper will use the template `template` to generate a config file -# `destination` by replacing the following keywords with global variables -# that should be defined before calling this helper : -# ``` -# __PATH__ by $path_url -# __NAME__ by $app -# __NAMETOCHANGE__ by $app -# __USER__ by $app -# __FINALPATH__ by $final_path -# __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: -# ``` -# __DOMAIN__ by $domain -# __APP__ by $app -# __VAR_1__ by $var_1 -# __VAR_2__ by $var_2 -# ``` -# -# The helper will verify the checksum and backup the destination file -# if it's different before applying the new template. -# -# And it will calculate and store the destination file checksum -# into the app settings when configuration is done. -# -# Requires YunoHost version 4.1.0 or higher. -ynh_add_config() { - # Declare an array to define the options of this helper. - local legacy_args=tdv - local -A args_array=([t]=template= [d]=destination=) - local template - local destination - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - local template_path - - if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then - template_path="$YNH_APP_BASEDIR/conf/$template" - elif [ -f "$template" ]; then - template_path=$template - else - ynh_die --message="The provided template $template doesn't exist" - fi - - ynh_backup_if_checksum_is_different --file="$destination" - - # Make sure to set the permissions before we copy the file - # This is to cover a case where an attacker could have - # created a file beforehand to have control over it - # (cp won't overwrite ownership / modes by default...) - touch $destination - chown root:root $destination - chmod 640 $destination - - cp -f "$template_path" "$destination" - - _ynh_apply_default_permissions $destination - - ynh_replace_vars --file="$destination" - - ynh_store_file_checksum --file="$destination" -} - -# Replace variables in a file -# -# [internal] -# -# usage: ynh_replace_vars --file="file" -# | arg: -f, --file= - File where to replace variables -# -# The helper will replace the following keywords with global variables -# that should be defined before calling this helper : -# __PATH__ by $path_url -# __NAME__ by $app -# __NAMETOCHANGE__ by $app -# __USER__ by $app -# __FINALPATH__ by $final_path -# __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: -# __DOMAIN__ by $domain -# __APP__ by $app -# __VAR_1__ by $var_1 -# __VAR_2__ by $var_2 -# -# Requires YunoHost version 4.1.0 or higher. -ynh_replace_vars() { - # Declare an array to define the options of this helper. - local legacy_args=f - local -A args_array=([f]=file=) - local file - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - # Replace specific YunoHost variables - if test -n "${path_url:-}"; then - # path_url_slash_less is path_url, or a blank value if path_url is only '/' - local path_url_slash_less=${path_url%/} - ynh_replace_string --match_string="__PATH__/" --replace_string="$path_url_slash_less/" --target_file="$file" - ynh_replace_string --match_string="__PATH__" --replace_string="$path_url" --target_file="$file" - fi - if test -n "${app:-}"; then - ynh_replace_string --match_string="__NAME__" --replace_string="$app" --target_file="$file" - 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 - # 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 - ynh_replace_string --match_string="__YNH_NODE_LOAD_PATH__" --replace_string="$ynh_node_load_PATH" --target_file="$file" - fi - - # Replace others variables - - # List other unique (__ __) variables in $file - local uniques_vars=($(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g")) - - # Do the replacement - local delimit=@ - for one_var in "${uniques_vars[@]}"; do - # Validate that one_var is indeed defined - # -v checks if the variable is defined, for example: - # -v FOO tests if $FOO is defined - # -v $FOO tests if ${!FOO} is defined - # More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964 - [[ -v "${one_var:-}" ]] || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" - - # Escape delimiter in match/replace string - match_string="__${one_var^^}__" - match_string=${match_string//${delimit}/"\\${delimit}"} - replace_string="${!one_var}" - replace_string=${replace_string//\\/\\\\} - replace_string=${replace_string//${delimit}/"\\${delimit}"} - - # Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs) - sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$file" - done -} - -# Get a value from heterogeneous file (yaml, json, php, python...) -# -# usage: ynh_read_var_in_file --file=PATH --key=KEY -# | arg: -f, --file= - the path to the file -# | arg: -k, --key= - the key to get -# -# This helpers match several var affectation use case in several languages -# We don't use jq or equivalent to keep comments and blank space in files -# This helpers work line by line, it is not able to work correctly -# if you have several identical keys in your files -# -# Example of line this helpers can managed correctly -# .yml -# title: YunoHost documentation -# email: 'yunohost@yunohost.org' -# .json -# "theme": "colib'ris", -# "port": 8102 -# "some_boolean": false, -# "user": null -# .ini -# some_boolean = On -# action = "Clear" -# port = 20 -# .php -# $user= -# user => 20 -# .py -# USER = 8102 -# user = 'https://donate.local' -# CUSTOM['user'] = 'YunoHost' -# -# Requires YunoHost version 4.3 or higher. -ynh_read_var_in_file() { - # Declare an array to define the options of this helper. - local legacy_args=fka - local -A args_array=([f]=file= [k]=key= [a]=after=) - local file - local key - local after - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - after="${after:-}" - - [[ -f $file ]] || ynh_die --message="File $file does not exists" - - set +o xtrace # set +x - - # Get the line number after which we search for the variable - local line_number=1 - if [[ -n "$after" ]]; then - line_number=$(grep -m1 -n $after $file | cut -d: -f1) - if [[ -z "$line_number" ]]; then - set -o xtrace # set -x - return 1 - fi - fi - - local filename="$(basename -- "$file")" - local ext="${filename##*.}" - local endline=',;' - local assign="=>|:|=" - local comments="#" - local string="\"'" - if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then - endline='#' - fi - if [[ "$ext" =~ ^ini|env$ ]]; then - comments="[;#]" - fi - if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then - comments="//" - fi - local list='\[\s*['$string']?\w+['$string']?\]' - local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' - var_part+="[$string]?${key}[$string]?" - var_part+='\s*\]?\s*' - var_part+="($assign)" - var_part+='\s*' - - # Extract the part after assignation sign - local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" - if [[ "$expression_with_comment" == "YNH_NULL" ]]; then - set -o xtrace # set -x - echo YNH_NULL - return 0 - fi - - # Remove comments if needed - local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" - - local first_char="${expression:0:1}" - if [[ "$first_char" == '"' ]]; then - echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g' - elif [[ "$first_char" == "'" ]]; then - echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g" - else - echo "$expression" - fi - set -o xtrace # set -x -} - -# Set a value into heterogeneous file (yaml, json, php, python...) -# -# usage: ynh_write_var_in_file --file=PATH --key=KEY --value=VALUE -# | arg: -f, --file= - the path to the file -# | arg: -k, --key= - the key to set -# | arg: -v, --value= - the value to set -# -# Requires YunoHost version 4.3 or higher. -ynh_write_var_in_file() { - # Declare an array to define the options of this helper. - local legacy_args=fkva - local -A args_array=([f]=file= [k]=key= [v]=value= [a]=after=) - local file - local key - local value - local after - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - after="${after:-}" - - [[ -f $file ]] || ynh_die --message="File $file does not exists" - - set +o xtrace # set +x - - # Get the line number after which we search for the variable - local after_line_number=1 - if [[ -n "$after" ]]; 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 filename="$(basename -- "$file")" - local ext="${filename##*.}" - local endline=',;' - local assign="=>|:|=" - local comments="#" - local string="\"'" - if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then - endline='#' - fi - if [[ "$ext" =~ ^ini|env$ ]]; then - comments="[;#]" - fi - if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then - comments="//" - fi - local list='\[\s*['$string']?\w+['$string']?\]' - local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' - var_part+="[$string]?${key}[$string]?" - var_part+='\s*\]?\s*' - var_part+="($assign)" - var_part+='\s*' - - # Extract the part after assignation sign - local expression_with_comment="$((tail +$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 - # \ and sed is quite complex you need 2 \\ to get one in a sed - # So we need \\\\ to go through 2 sed - value="$(echo "$value" | sed 's/"/\\\\"/g')" - sed -ri "${range}s$delimiter"'(^'"${var_part}"'")([^"]|\\")*("[\s;,]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}"'"'"${endline}${delimiter}i" ${file} - elif [[ "$first_char" == "'" ]]; then - # \ and sed is quite complex you need 2 \\ to get one in a sed - # However double quotes implies to double \\ to - # So we need \\\\\\\\ to go through 2 sed and 1 double quotes str - value="$(echo "$value" | sed "s/'/\\\\\\\\'/g")" - sed -ri "${range}s$delimiter(^${var_part}')([^']|\\')*('"'[\s,;]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}'${endline}${delimiter}i" ${file} - else - if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]]; then - value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"' - fi - if [[ "$ext" =~ ^yaml|yml$ ]]; then - value=" $value" - fi - sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file} - fi - set -o xtrace # set -x -} - -# Render templates with Jinja2 -# -# [internal] -# -# Attention : Variables should be exported before calling this helper to be -# accessible inside templates. -# -# usage: ynh_render_template some_template output_path -# | arg: some_template - Template file to be rendered -# | arg: output_path - The path where the output will be redirected to -ynh_render_template() { - local template_path=$1 - local output_path=$2 - mkdir -p "$(dirname $output_path)" - # Taken from https://stackoverflow.com/a/35009576 - python3 -c 'import os, sys, jinja2; sys.stdout.write( - jinja2.Template(sys.stdin.read() - ).render(os.environ));' <$template_path >$output_path -} - -# Fetch the Debian release codename -# -# usage: ynh_get_debian_release -# | ret: The Debian release codename (i.e. jessie, stretch, ...) -# -# Requires YunoHost version 2.7.12 or higher. -ynh_get_debian_release() { - echo $(lsb_release --codename --short) -} - -_acceptable_path_to_delete() { - local file=$1 - - local forbidden_paths=$(ls -d / /* /{var,home,usr}/* /etc/{default,sudoers.d,yunohost,cron*}) - - # Legacy : A couple apps still have data in /home/$app ... - if [[ -n "${app:-}" ]] - then - forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app") - fi - - # Use realpath to normalize the path .. - # i.e convert ///foo//bar//..///baz//// to /foo/baz - file=$(realpath --no-symlinks "$file") - if [ -z "$file" ] || grep -q -x -F "$file" <<< "$forbidden_paths"; then - return 1 - else - return 0 - fi -} - -# Remove a file or a directory securely -# -# usage: ynh_secure_remove --file=path_to_remove -# | arg: -f, --file= - File or directory to remove -# -# Requires YunoHost version 2.6.4 or higher. -ynh_secure_remove() { - # Declare an array to define the options of this helper. - local legacy_args=f - local -A args_array=([f]=file=) - local file - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - set +o xtrace # set +x - - if [ $# -ge 2 ]; then - ynh_print_warn --message="/!\ Packager ! You provided more than one argument to ynh_secure_remove but it will be ignored... Use this helper with one argument at time." - fi - - if [[ -z "$file" ]]; then - ynh_print_warn --message="ynh_secure_remove called with empty argument, ignoring." - elif [[ ! -e $file ]]; then - ynh_print_info --message="'$file' wasn't deleted because it doesn't exist." - elif ! _acceptable_path_to_delete "$file"; then - ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete." - else - rm --recursive "$file" - fi - - set -o xtrace # set -x -} - -# Read the value of a key in a ynh manifest file -# -# usage: ynh_read_manifest --manifest="manifest.json" --key="key" -# | arg: -m, --manifest= - Path of the manifest to read -# | arg: -k, --key= - Name of the key to find -# | ret: the value associate to that key -# -# Requires YunoHost version 3.5.0 or higher. -ynh_read_manifest() { - # Declare an array to define the options of this helper. - local legacy_args=mk - local -A args_array=([m]=manifest= [k]=manifest_key=) - local manifest - local manifest_key - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - if [ ! -e "${manifest:-}" ]; then - # If the manifest isn't found, try the common place for backup and restore script. - 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 - - 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` -# -# usage: ynh_app_upstream_version [--manifest="manifest.json"] -# | arg: -m, --manifest= - Path of the manifest to read -# | ret: the version number of the upstream app -# -# If the `manifest` is not specified, the envvar `$YNH_APP_MANIFEST_VERSION` will be used. -# -# The version number in the manifest is defined by `~ynh`. -# -# For example, if the manifest contains `4.3-2~ynh3` the function will return `4.3-2` -# -# Requires YunoHost version 3.5.0 or higher. -ynh_app_upstream_version() { - # Declare an array to define the options of this helper. - local legacy_args=m - local -A args_array=([m]=manifest=) - local manifest - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - manifest="${manifest:-}" - - if [[ "$manifest" != "" ]] && [[ -e "$manifest" ]]; then - version_key_=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") - else - version_key_=$YNH_APP_MANIFEST_VERSION - fi - - echo "${version_key_/~ynh*/}" -} - -# Read package version from the manifest -# -# usage: ynh_app_package_version [--manifest="manifest.json"] -# | arg: -m, --manifest= - Path of the manifest to read -# | ret: the version number of the package -# -# The version number in the manifest is defined by `~ynh`. -# -# For example, if the manifest contains `4.3-2~ynh3` the function will return `3` -# -# Requires YunoHost version 3.5.0 or higher. -ynh_app_package_version() { - # Declare an array to define the options of this helper. - local legacy_args=m - local -A args_array=([m]=manifest=) - local manifest - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - version_key_=$YNH_APP_MANIFEST_VERSION - echo "${version_key_/*~ynh/}" -} - -# Checks the app version to upgrade with the existing app version and returns: -# -# usage: ynh_check_app_version_changed -# | ret: `UPGRADE_APP` if the upstream version changed, `UPGRADE_PACKAGE` otherwise. -# -# This helper should be used to avoid an upgrade of an app, or the upstream part -# of it, when it's not needed -# -# You can force an upgrade, even if the package is up to date, with the `--force` (or `-F`) argument : -# ``` -# sudo yunohost app upgrade --force -# ``` -# Requires YunoHost version 3.5.0 or higher. -ynh_check_app_version_changed() { - local return_value=${YNH_APP_UPGRADE_TYPE} - - if [ "$return_value" == "UPGRADE_FULL" ] || [ "$return_value" == "UPGRADE_FORCED" ] || [ "$return_value" == "DOWNGRADE_FORCED" ]; then - return_value="UPGRADE_APP" - fi - - echo $return_value -} - -# Compare the current package version against another version given as an argument. -# -# usage: ynh_compare_current_package_version --comparison (lt|le|eq|ne|ge|gt) --version -# | arg: --comparison - Comparison type. Could be : `lt` (lower than), `le` (lower or equal), `eq` (equal), `ne` (not equal), `ge` (greater or equal), `gt` (greater than) -# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like `2.3.1~ynh4`) -# | ret: 0 if the evaluation is true, 1 if false. -# -# example: ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 -# -# This helper is usually used when we need to do some actions only for some old package versions. -# -# Generally you might probably use it as follow in the upgrade script : -# ``` -# if ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 -# then -# # Do something that is needed for the package version older than 2.3.2~ynh1 -# fi -# ``` -# -# Requires YunoHost version 3.8.0 or higher. -ynh_compare_current_package_version() { - local legacy_args=cv - declare -Ar args_array=([c]=comparison= [v]=version=) - local version - local comparison - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - local current_version=$YNH_APP_CURRENT_VERSION - - # Check the syntax of the versions - if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]]; then - ynh_die --message="Invalid argument for version." - fi - - # Check validity of the comparator - if [[ ! $comparison =~ (lt|le|eq|ne|ge|gt) ]]; then - ynh_die --message="Invalid comparator must be : lt, le, eq, ne, ge, gt" - fi - - # Return the return value of dpkg --compare-versions - dpkg --compare-versions $current_version $comparison $version -} - -# Check if we should enforce sane default permissions (= disable rwx for 'others') -# on file/folders handled with ynh_setup_source and ynh_add_config -# -# [internal] -# -# Having a file others-readable or a folder others-executable(=enterable) -# is a security risk comparable to "chmod 777" -# -# Configuration files may contain secrets. Or even just being able to enter a -# folder may allow an attacker to do nasty stuff (maybe a file or subfolder has -# some write permission enabled for 'other' and the attacker may edit the -# content or create files as leverage for priviledge escalation ...) -# -# The sane default should be to set ownership to $app:$app. -# In specific case, you may want to set the ownership to $app:www-data -# for example if nginx needs access to static files. -# -_ynh_apply_default_permissions() { - local target=$1 - - local ynh_requirement=$(ynh_read_manifest --manifest_key="requirements.yunohost" | tr -d '<>= ') - - 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 - if ynh_system_user_exists $app; then - chown $app:$app $target - fi - fi - - # Crons should be owned by root otherwise they probably don't run - if echo "$target" | grep -q '^/etc/cron' - then - chmod 400 $target - chown root:root $target - fi -} - -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 index 82579b059..986360f1a 100644 --- a/helpers/vendor/docker-image-extract/LICENSE +++ b/helpers/vendor/docker-image-extract/LICENSE @@ -1,21 +1,19 @@ -MIT License +Copyright (c) 2020-2023, Jeremy Lin -Copyright (c) 2021 Emmanuel Frecon +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/helpers/vendor/docker-image-extract/README.md b/helpers/vendor/docker-image-extract/README.md index 6f6cb5074..4c4fa301f 100644 --- a/helpers/vendor/docker-image-extract/README.md +++ b/helpers/vendor/docker-image-extract/README.md @@ -1 +1 @@ -This is taken from https://github.com/efrecon/docker-image-extract +This is taken from https://github.com/jjlin/docker-image-extract, under MIT license. \ No newline at end of file diff --git a/helpers/vendor/docker-image-extract/docker-image-extract b/helpers/vendor/docker-image-extract/docker-image-extract index 4842a8e04..b5dfdb7a7 100755 --- a/helpers/vendor/docker-image-extract/docker-image-extract +++ b/helpers/vendor/docker-image-extract/docker-image-extract @@ -2,7 +2,7 @@ # # This script pulls and extracts all files from an image in Docker Hub. # -# Copyright (c) 2020-2022, Jeremy Lin +# Copyright (c) 2020-2023, Jeremy Lin # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -103,6 +103,17 @@ if [ $# -eq 0 ]; then exit 1 fi +if [ -e "${OUT_DIR}" ]; then + if [ -d "${OUT_DIR}" ]; then + echo "WARNING: Output dir already exists. If it contains a previous extracted image," + echo "there may be errors when trying to overwrite files with read-only permissions." + echo + else + echo "ERROR: Output dir already exists, but is not a directory." + exit 1 + fi +fi + have_curl() { command -v curl >/dev/null } @@ -173,16 +184,20 @@ fetch() { # https://docs.docker.com/docker-hub/api/latest/#tag/repositories manifest_list_url="https://hub.docker.com/v2/repositories/${image}/tags/${ref}" -# If we're pulling the image for the default platform, or the ref is already -# a SHA-256 image digest, then we don't need to look up anything. -if [ "${PLATFORM}" = "${PLATFORM_DEFAULT}" ] || [ -z "${ref##sha256:*}" ]; then +# If the ref is already a SHA-256 image digest, then we don't need to look up anything. +if [ -z "${ref##sha256:*}" ]; then digest="${ref}" else echo "Getting multi-arch manifest list..." + NL=' +' digest=$(fetch "${manifest_list_url}" | # Break up the single-line JSON output into separate lines by adding # newlines before and after the chars '[', ']', '{', and '}'. - sed -e 's/\([][{}]\)/\n\1\n/g' | + # This uses the \${NL} syntax because some BSD variants of sed don't + # support \n syntax in the replacement string, but instead require + # a literal newline preceded by a backslash. + sed -e 's/\([][{}]\)/\'"${NL}"'\1\'"${NL}"'/g' | # Extract the "images":[...] list. sed -n '/"images":/,/]/ p' | # Each image's details are now on a separate line, e.g. @@ -205,13 +220,13 @@ else break fi done) -fi -if [ -n "${digest}" ]; then - echo "Platform ${PLATFORM} resolved to '${digest}'..." -else - echo "No image digest found. Verify that the image, ref, and platform are valid." - exit 1 + if [ -n "${digest}" ]; then + echo "Platform ${PLATFORM} resolved to '${digest}'..." + else + echo "No image digest found. Verify that the image, ref, and platform are valid." + exit 1 + fi fi # https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate @@ -226,10 +241,21 @@ blobs_base_url="https://registry-1.docker.io/v2/${image}/blobs" echo "Getting API token..." token=$(fetch "${api_token_url}" | extract 'token') auth_header="Authorization: Bearer $token" -v2_header="Accept: application/vnd.docker.distribution.manifest.v2+json" + +# https://github.com/distribution/distribution/blob/main/docs/spec/manifest-v2-2.md +docker_manifest_v2="application/vnd.docker.distribution.manifest.v2+json" + +# https://github.com/opencontainers/image-spec/blob/main/manifest.md +oci_manifest_v1="application/vnd.oci.image.manifest.v1+json" + +# Docker Hub can return either type of manifest format. Most images seem to +# use the Docker format for now, but the OCI format will likely become more +# common as features that require that format become enabled by default +# (e.g., https://github.com/docker/build-push-action/releases/tag/v3.3.0). +accept_header="Accept: ${docker_manifest_v2},${oci_manifest_v1}" echo "Getting image manifest for $image:$ref..." -layers=$(fetch "${manifest_url}" "${auth_header}" "${v2_header}" | +layers=$(fetch "${manifest_url}" "${auth_header}" "${accept_header}" | # Extract `digest` values only after the `layers` section appears. sed -n '/"layers":/,$ p' | extract 'digest') @@ -259,4 +285,4 @@ for layer in $layers; do IFS="${OLD_IFS}" done -echo "Image contents extracted into ${OUT_DIR}." +echo "Image contents extracted into ${OUT_DIR}." \ No newline at end of file diff --git a/helpers/vendor/n/n b/helpers/vendor/n/n index 2a877c45b..86b6a0fa9 100755 --- a/helpers/vendor/n/n +++ b/helpers/vendor/n/n @@ -61,7 +61,7 @@ function n_grep() { # Setup and state # -VERSION="v9.1.0" +VERSION="v9.2.3" N_PREFIX="${N_PREFIX-/usr/local}" N_PREFIX=${N_PREFIX%/} @@ -135,6 +135,7 @@ g_target_node= DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec) ARCH= SHOW_VERBOSE_LOG="true" +OFFLINE=false # ANSI escape codes # https://en.wikipedia.org/wiki/ANSI_escape_code @@ -393,6 +394,7 @@ Options: -q, --quiet Disable curl output. Disable log messages processing "auto" and "engine" labels. -d, --download Download if necessary, and don't make active -a, --arch Override system architecture + --offline Resolve target version against cached downloads instead of internet lookup --all ls-remote displays all matches instead of last 20 --insecure Turn off certificate checking for https requests (may be needed from behind a proxy server) --use-xz/--no-use-xz Override automatic detection of xz support and enable/disable use of xz compressed node downloads. @@ -784,6 +786,9 @@ install() { exit fi fi + if [[ "$OFFLINE" == "true" ]]; then + abort "version unavailable offline" + fi log installing "${g_mirror_folder_name}-v$version" @@ -848,7 +853,7 @@ function do_get() { function do_get_index() { if command -v curl &> /dev/null; then # --silent to suppress progress et al - curl --silent --compressed "${CURL_OPTIONS[@]}" "$@" + curl --silent "${CURL_OPTIONS[@]}" "$@" elif command -v wget &> /dev/null; then wget "${WGET_OPTIONS[@]}" "$@" else @@ -1103,6 +1108,7 @@ function get_package_engine_version() { verbose_log "target" "${version}" else command -v npx &> /dev/null || abort "an active version of npx is required to use complex 'engine' ranges from package.json" + [[ "$OFFLINE" != "true" ]] || abort "offline: an internet connection is required for looking up complex 'engine' ranges from package.json" verbose_log "resolving" "${range}" local version_per_line="$(n lsr --all)" local versions_one_line=$(echo "${version_per_line}" | tr '\n' ' ') @@ -1199,6 +1205,8 @@ function get_latest_resolved_version() { # Just numbers, already resolved, no need to lookup first. simple_version="${simple_version#v}" g_target_node="${simple_version}" + elif [[ "$OFFLINE" == "true" ]]; then + g_target_node=$(display_local_versions "${version}") else # Complicated recognising exact version, KISS and lookup. g_target_node=$(N_MAX_REMOTE_MATCHES=1 display_remote_versions "$version") @@ -1232,6 +1240,56 @@ function display_match_limit(){ fi } +# +# Synopsis: display_local_versions version +# + +function display_local_versions() { + local version="$1" + local match='.' + verbose_log "offline" "matching cached versions" + + # Transform some labels before processing further. + if is_node_support_version "${version}"; then + version="$(display_latest_node_support_alias "${version}")" + match_count=1 + elif [[ "${version}" = "auto" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_auto_version || return 2 + version="${g_target_node}" + elif [[ "${version}" = "engine" ]]; then + # suppress stdout logging so lsr layout same as usual for scripting + get_engine_version || return 2 + version="${g_target_node}" + fi + + if [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match='^node/.' + elif is_exact_numeric_version "${version}"; then + # Quote any dots in version so they are literal for expression + match="^node/${version//\./\.}" + elif is_numeric_version "${version}"; then + version="${version#v}" + # Quote any dots in version so they are literal for expression + match="${version//\./\.}" + # Avoid 1.2 matching 1.23 + match="^node/${match}[^0-9]" + # elif is_lts_codename "${version}"; then + # see if demand + elif is_download_folder "${version}"; then + match="^${version}/" + # elif is_download_version "${version}"; then + # see if demand + else + abort "invalid version '$1' for offline matching" + fi + + display_versions_paths \ + | n_grep -E "${match}" \ + | tail -n 1 \ + | sed 's|node/||' +} + # # Synopsis: display_remote_versions version # @@ -1367,7 +1425,11 @@ uninstall_installed() { function show_permission_suggestions() { echo "Suggestions:" echo "- run n with sudo, or" - echo "- define N_PREFIX to a writeable location, or" + if [[ "${N_CACHE_PREFIX}" == "${N_PREFIX}" ]]; then + echo "- define N_PREFIX to a writeable location, or" + else + echo "- define N_PREFIX and N_CACHE_PREFIX to writeable locations, or" + fi } # @@ -1498,34 +1560,42 @@ function show_diagnostics() { 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 + printf "\nChecking prefix folders...\n" + if [[ ! -e "${N_PREFIX}" ]]; then + echo "Folder does not exist: ${N_PREFIX}" + echo "- This folder will be created when you do an install." + fi + if [[ "${N_PREFIX}" != "${N_CACHE_PREFIX}" && ! -e "${N_CACHE_PREFIX}" ]]; then + echo "Folder does not exist: ${N_CACHE_PREFIX}" + echo "- This folder will be created when you do an install." + fi + if [[ -e "${N_PREFIX}" && -e "${N_CACHE_PREFIX}" ]]; then echo "good" fi + if [[ -e "${N_CACHE_PREFIX}" ]]; then + printf "\nChecking permissions for cache folder...\n" + # Using knowledge cache path ends in /n/versions in following check. + if [[ ! -e "${CACHE_DIR}" && (( -e "${N_CACHE_PREFIX}/n" && ! -w "${N_CACHE_PREFIX}/n" ) || ( ! -e "${N_CACHE_PREFIX}/n" && ! -w "${N_CACHE_PREFIX}" )) ]]; then + echo_red "You do not have write permission to create: ${CACHE_DIR}" + show_permission_suggestions + echo "- make a folder you own:" + echo " sudo mkdir -p \"${CACHE_DIR}\"" + echo " sudo chown $(whoami) \"${CACHE_DIR}\"" + elif [[ ! -e "${CACHE_DIR}" ]]; then + echo "Cache folder does not exist: ${CACHE_DIR}" + echo "- This is normal if you have not done an install yet, as cache is only created when needed." + elif [[ ! -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 + 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 @@ -1534,13 +1604,20 @@ function show_diagnostics() { echo_red "You do not have write permission to: ${N_PREFIX}/${subdir}" break fi + if [[ ! -e "${N_PREFIX}/${subdir}" && ! -w "${N_PREFIX}" ]]; then + install_writeable="false" + echo_red "You do not have write permission to create: ${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)" + echo " cd \"${N_PREFIX}\"" + echo " sudo mkdir -p bin lib include share" + echo " sudo chown -R $(whoami) bin lib include share" fi fi @@ -1577,6 +1654,7 @@ while [[ $# -ne 0 ]]; do -h|--help|help) display_help; exit ;; -q|--quiet) set_quiet ;; -d|--download) DOWNLOAD="true" ;; + --offline) OFFLINE="true" ;; --insecure) set_insecure ;; -p|--preserve) N_PRESERVE_NPM="true" N_PRESERVE_COREPACK="true" ;; --no-preserve) N_PRESERVE_NPM="" N_PRESERVE_COREPACK="" ;; diff --git a/hooks/backup/27-data_xmpp b/hooks/backup/27-data_xmpp deleted file mode 100644 index 253aacdc2..000000000 --- a/hooks/backup/27-data_xmpp +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Exit hook on subcommand error or unset variable -set -eu - -# Source YNH helpers -source /usr/share/yunohost/helpers - -# Backup destination -backup_dir="${1}/data/xmpp" - -[[ ! -d /var/lib/metronome ]] || ynh_backup /var/lib/metronome "${backup_dir}/var_lib_metronome" --not_mandatory -[[ ! -d /var/xmpp-upload ]] || ynh_backup /var/xmpp-upload/ "${backup_dir}/var_xmpp-upload" --not_mandatory diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost index d0e6fb783..fccaeecf3 100755 --- a/hooks/conf_regen/01-yunohost +++ b/hooks/conf_regen/01-yunohost @@ -2,15 +2,167 @@ set -e -do_init_regen() { - if [[ $EUID -ne 0 ]]; then - echo "You must be root to run this script" 1>&2 - exit 1 +base_folder_and_perm_init() { + + ############################# + # Base yunohost conf folder # + ############################# + + mkdir -p /etc/yunohost + # NB: x permission for 'others' is important for ssl-cert (and maybe mdns), otherwise slapd will fail to start because can't access the certs + chmod 755 /etc/yunohost + + ################ + # Logs folders # + ################ + + mkdir -p /var/log/yunohost + chown root:root /var/log/yunohost + chmod 750 /var/log/yunohost + + ################## + # Portal folders # + ################## + + getent passwd ynh-portal &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group ynh-portal + + mkdir -p /etc/yunohost/portal + chmod 500 /etc/yunohost/portal + chown ynh-portal:ynh-portal /etc/yunohost/portal + + mkdir -p /usr/share/yunohost/portal/customassets + chmod 775 /usr/share/yunohost/portal/customassets + chown root:root /usr/share/yunohost/portal/customassets + + touch /var/log/yunohost-portalapi.log + chown ynh-portal:root /var/log/yunohost-portalapi.log + chmod 600 /var/log/yunohost-portalapi.log + + ############################### + # Sessions folder and secrets # + ############################### + + # Portal + mkdir -p /var/cache/yunohost-portal/sessions + chown ynh-portal:www-data /var/cache/yunohost-portal + chmod 510 /var/cache/yunohost-portal + chown ynh-portal:www-data /var/cache/yunohost-portal/sessions + chmod 710 /var/cache/yunohost-portal/sessions + + # Webadmin + mkdir -p /var/cache/yunohost/sessions + chown root:root /var/cache/yunohost/sessions + chmod 700 /var/cache/yunohost/sessions + + if test -e /etc/yunohost/installed + then + # Initialize session secrets + # Obviously we only do this in the post_regen, ie during the postinstall, because we don't want every pre-installed instance to have the same secret + if [ ! -e /etc/yunohost/.admin_cookie_secret ]; then + dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 64 > /etc/yunohost/.admin_cookie_secret + fi + chown root:root /etc/yunohost/.admin_cookie_secret + chmod 400 /etc/yunohost/.admin_cookie_secret + + if [ ! -e /etc/yunohost/.ssowat_cookie_secret ]; then + # NB: we need this to be exactly 32 char long, because it is later used as a key for AES256 + dd if=/dev/urandom bs=1 count=1000 2>/dev/null | tr --complement --delete 'A-Za-z0-9' | head -c 32 > /etc/yunohost/.ssowat_cookie_secret + fi + chown ynh-portal:root /etc/yunohost/.ssowat_cookie_secret + chmod 400 /etc/yunohost/.ssowat_cookie_secret fi + ################## + # Domain folders # + ################## + + mkdir -p /etc/yunohost/domains + chown root /etc/yunohost/domains + chmod 700 /etc/yunohost/domains + + ############### + # App folders # + ############### + + mkdir -p /etc/yunohost/apps + chown root /etc/yunohost/apps + chmod 700 /etc/yunohost/apps + + ##################### + # Apps data folders # + ##################### + + mkdir -p /home/yunohost.app + chmod 755 /home/yunohost.app + + ################ + # Certs folder # + ################ + + mkdir -p /etc/yunohost/certs + chown -R root:ssl-cert /etc/yunohost/certs + chmod 750 /etc/yunohost/certs + # We do this with find because there could be a lot of them... + find /etc/yunohost/certs/ -type f -exec chmod 640 {} \; + find /etc/yunohost/certs/ -type d -exec chmod 750 {} \; + + ################## + # Backup folders # + ################## + + mkdir -p /home/yunohost.backup/archives + chmod 770 /home/yunohost.backup + chmod 770 /home/yunohost.backup/archives + + if test -e /etc/yunohost/installed + then + # The admins group only exist after the postinstall + chown root:admins /home/yunohost.backup + chown root:admins /home/yunohost.backup/archives + else + chown root:root /home/yunohost.backup + chown root:root /home/yunohost.backup/archives + fi + + ######## + # Misc # + ######## + + mkdir -p /etc/yunohost/hooks.d + chown root /etc/yunohost/hooks.d + chmod 700 /etc/yunohost/hooks.d + + mkdir -p /var/cache/yunohost/repo + chown root:root /var/cache/yunohost + chmod 700 /var/cache/yunohost + + [ ! -e /var/www/.well-known/ynh-diagnosis/ ] || chmod 775 /var/www/.well-known/ynh-diagnosis/ + + if test -e /etc/yunohost/installed + then + setfacl -m g:all_users:--- /var/www + setfacl -m g:all_users:--- /var/log/nginx + setfacl -m g:all_users:--- /etc/yunohost + setfacl -m g:all_users:--- /etc/ssowat + fi +} + +do_init_regen() { + cd /usr/share/yunohost/conf/yunohost - [[ -d /etc/yunohost ]] || mkdir -p /etc/yunohost + base_folder_and_perm_init + + # Empty ssowat json persistent conf + echo "{}" >'/etc/ssowat/conf.json.persistent' + chmod 644 /etc/ssowat/conf.json.persistent + chown root:root /etc/ssowat/conf.json.persistent + echo "{}" >'/etc/ssowat/conf.json' + chmod 644 /etc/ssowat/conf.json + chown root:root /etc/ssowat/conf.json + + # Empty service conf + touch /etc/yunohost/services.yml # set default current_host [[ -f /etc/yunohost/current_host ]] \ @@ -24,39 +176,9 @@ do_init_regen() { [[ -d /etc/skel/media ]] \ || (mkdir -p /media && ln -s /media /etc/skel/media) - # Cert folders - mkdir -p /etc/yunohost/certs - chown -R root:ssl-cert /etc/yunohost/certs - chmod 750 /etc/yunohost/certs - - # App folders - mkdir -p /etc/yunohost/apps - chmod 700 /etc/yunohost/apps - mkdir -p /home/yunohost.app - chmod 755 /home/yunohost.app - - # Domain settings - mkdir -p /etc/yunohost/domains - chmod 700 /etc/yunohost/domains - - # Backup folders - mkdir -p /home/yunohost.backup/archives - chmod 750 /home/yunohost.backup/archives - chown root:root /home/yunohost.backup/archives # This is later changed to root:admins once the admins group exists - - # Empty ssowat json persistent conf - echo "{}" >'/etc/ssowat/conf.json.persistent' - chmod 644 /etc/ssowat/conf.json.persistent - chown root:root /etc/ssowat/conf.json.persistent - - # Empty service conf - touch /etc/yunohost/services.yml - - mkdir -p /var/cache/yunohost/repo - chown root:root /var/cache/yunohost - chmod 700 /var/cache/yunohost - + # YunoHost services cp yunohost-api.service /etc/systemd/system/yunohost-api.service + cp yunohost-portal-api.service /etc/systemd/system/yunohost-portal-api.service cp yunohost-firewall.service /etc/systemd/system/yunohost-firewall.service cp yunoprompt.service /etc/systemd/system/yunoprompt.service @@ -64,6 +186,10 @@ do_init_regen() { systemctl enable yunohost-api.service --quiet systemctl start yunohost-api.service + + systemctl enable yunohost-portal-api.service + systemctl start yunohost-portal-api.service + # Yunohost-firewall is enabled only during postinstall, not init, not 100% sure why cp dpkg-origins /etc/dpkg/origins/yunohost @@ -97,7 +223,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 @@ -155,6 +281,7 @@ HandleLidSwitchExternalPower=ignore EOF cp yunohost-api.service ${pending_dir}/etc/systemd/system/yunohost-api.service + cp yunohost-portal-api.service ${pending_dir}/etc/systemd/system/yunohost-portal-api.service cp yunohost-firewall.service ${pending_dir}/etc/systemd/system/yunohost-firewall.service cp yunoprompt.service ${pending_dir}/etc/systemd/system/yunoprompt.service cp proc-hidepid.service ${pending_dir}/etc/systemd/system/proc-hidepid.service @@ -162,57 +289,53 @@ EOF mkdir -p ${pending_dir}/etc/dpkg/origins/ cp dpkg-origins ${pending_dir}/etc/dpkg/origins/yunohost + # Remove legacy hackish/clumsy nodejs autoupdate which ends up filling up space with ambiguous upgrades >_> + touch "/etc/cron.daily/node_update" } do_post_regen() { regen_conf_files=$1 - ###################### - # Enfore permissions # - ###################### + # Re-mkdir / apply permission to all basic folders etc + base_folder_and_perm_init - chmod 770 /home/yunohost.backup - chmod 770 /home/yunohost.backup/archives - chmod 700 /var/cache/yunohost - chown root:admins /home/yunohost.backup - chown root:admins /home/yunohost.backup/archives - chown root:root /var/cache/yunohost + # Legacy log tree structure + if [ ! -e /var/log/yunohost/operations ] + then + mkdir -p /var/log/yunohost/operations + fi + if [ -d /var/log/yunohost/categories/operation ] && [ ! -L /var/log/yunohost/categories/operation ] + then + # (we use find -type f instead of mv /folder/* to make sure to also move hidden files which are not included in globs by default) + find /var/log/yunohost/categories/operation/ -type f -print0 | xargs -0 -I {} mv {} /var/log/yunohost/operations/ + # Attempt to delete the old dir (because we want it to be a symlink) or just rename it if it can't be removed (not empty) for some reason + rmdir /var/log/yunohost/categories/operation || mv /var/log/yunohost/categories/operation /var/log/yunohost/categories/operation.old + ln -s /var/log/yunohost/operations /var/log/yunohost/categories/operation + fi - # NB: x permission for 'others' is important for ssl-cert (and maybe mdns), otherwise slapd will fail to start because can't access the certs - chmod 755 /etc/yunohost + # Make sure conf files why may be created by apps are owned and writable only by root + find /etc/systemd/system/*.service -type f | xargs -r chown root:root + find /etc/systemd/system/*.service -type f | xargs -r chmod 0644 - # Certs - # We do this with find because there could be a lot of them... - chown -R root:ssl-cert /etc/yunohost/certs - chmod 750 /etc/yunohost/certs - find /etc/yunohost/certs/ -type f -exec chmod 640 {} \; - find /etc/yunohost/certs/ -type d -exec chmod 750 {} \; + if ls -l /etc/php/*/fpm/pool.d/*.conf 2>/dev/null + then + chown root:root /etc/php/*/fpm/pool.d/*.conf + chmod 644 /etc/php/*/fpm/pool.d/*.conf + fi find /etc/cron.*/yunohost-* -type f -exec chmod 755 {} \; find /etc/cron.d/yunohost-* -type f -exec chmod 644 {} \; find /etc/cron.*/yunohost-* -type f -exec chown root:root {} \; - setfacl -m g:all_users:--- /var/www - setfacl -m g:all_users:--- /var/log/nginx - setfacl -m g:all_users:--- /etc/yunohost - setfacl -m g:all_users:--- /etc/ssowat for USER in $(yunohost user list --quiet --output-as json | jq -r '.users | .[] | .username'); do [ ! -e "/home/$USER" ] || setfacl -m g:all_users:--- /home/$USER done - # Domain settings - mkdir -p /etc/yunohost/domains - # Misc configuration / state files 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 - [[ ! -e /etc/yunohost/hooks.d ]] || (chown root /etc/yunohost/hooks.d && chmod 700 /etc/yunohost/hooks.d) - [[ ! -e /etc/yunohost/apps ]] || (chown root /etc/yunohost/apps && chmod 700 /etc/yunohost/apps) - [[ ! -e /etc/yunohost/domains ]] || (chown root /etc/yunohost/domains && chmod 700 /etc/yunohost/domains) - # Create ssh.app and sftp.app groups if they don't exist yet grep -q '^ssh.app:' /etc/group || groupadd ssh.app grep -q '^sftp.app:' /etc/group || groupadd sftp.app @@ -225,6 +348,7 @@ do_post_regen() { systemctl restart ntp } fi + [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || { systemctl daemon-reload @@ -232,6 +356,7 @@ do_post_regen() { } [[ ! "$regen_conf_files" =~ "yunohost-firewall.service" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "yunohost-portal-api.service" ]] || systemctl daemon-reload if [[ "$regen_conf_files" =~ "yunoprompt.service" ]]; then systemctl daemon-reload @@ -244,6 +369,9 @@ do_post_regen() { systemctl $action proc-hidepid --quiet --now fi + systemctl enable yunohost-portal-api.service --quiet + systemctl is-active yunohost-portal-api --quiet || systemctl start yunohost-portal-api.service + # Change dpkg vendor # see https://wiki.debian.org/Derivatives/Guidelines#Vendor if readlink -f /etc/dpkg/origins/default | grep -q debian; diff --git a/hooks/conf_regen/03-ssh b/hooks/conf_regen/03-ssh index d0351b4e5..747389727 100755 --- a/hooks/conf_regen/03-ssh +++ b/hooks/conf_regen/03-ssh @@ -9,17 +9,16 @@ do_pre_regen() { cd /usr/share/yunohost/conf/ssh + # Support different strategy for security configurations + export compatibility="$(jq -r '.ssh_compatibility' <<< "$YNH_SETTINGS")" + export port="$(jq -r '.ssh_port' <<< "$YNH_SETTINGS")" + export password_authentication="$(jq -r '.ssh_password_authentication' <<< "$YNH_SETTINGS" | int_to_bool)" + export ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null || true) + # do not listen to IPv6 if unavailable [[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false - - ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null || true) - - # Support different strategy for security configurations - 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 9ba61863b..b44d9d649 100755 --- a/hooks/conf_regen/06-slapd +++ b/hooks/conf_regen/06-slapd @@ -8,10 +8,6 @@ config="/usr/share/yunohost/conf/slapd/config.ldif" db_init="/usr/share/yunohost/conf/slapd/db_init.ldif" do_init_regen() { - if [[ $EUID -ne 0 ]]; then - echo "You must be root to run this script" 1>&2 - exit 1 - fi do_pre_regen "" diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt index 93ff053b8..84955ae64 100755 --- a/hooks/conf_regen/10-apt +++ b/hooks/conf_regen/10-apt @@ -11,7 +11,7 @@ do_pre_regen() { # Add sury mkdir -p ${pending_dir}/etc/apt/sources.list.d/ - echo "deb https://packages.sury.org/php/ $(lsb_release --codename --short) main" > "${pending_dir}/etc/apt/sources.list.d/extra_php_version.list" + echo "deb [signed-by=/etc/apt/trusted.gpg.d/extra_php_version.gpg] https://packages.sury.org/php/ $(lsb_release --codename --short) main" > "${pending_dir}/etc/apt/sources.list.d/extra_php_version.list" # Ban some packages from sury echo " @@ -23,19 +23,33 @@ Pin-Priority: 500" >>"${pending_dir}/etc/apt/preferences.d/extra_php_version" for package in $packages_to_refuse_from_sury; do echo " Package: $package -Pin: origin \"packages.sury.org\" +Pin: origin \"packages.sury.org\" Pin-Priority: -1" >>"${pending_dir}/etc/apt/preferences.d/extra_php_version" done + # Add yarn + echo "deb [signed-by=/etc/apt/trusted.gpg.d/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > "${pending_dir}/etc/apt/sources.list.d/yarn.list" + + # Ban everything from Yarn except Yarn + echo " +Package: * +Pin: origin \"dl.yarnpkg.com\" +Pin-Priority: -1 + +Package: yarn +Pin: origin \"dl.yarnpkg.com\" +Pin-Priority: 500" >>"${pending_dir}/etc/apt/preferences.d/yarn" + + # Ban apache2, bind9 echo " # PLEASE READ THIS WARNING AND DON'T EDIT THIS FILE -# You are probably reading this file because you tried to install apache2 or +# You are probably reading this file because you tried to install apache2 or # bind9. These 2 packages conflict with YunoHost. # Installing apache2 will break nginx and break the entire YunoHost ecosystem -# on your server, therefore don't remove those lines! +# on your server, therefore don't remove those lines! # You have been warned. @@ -62,6 +76,10 @@ Pin-Priority: -1 do_post_regen() { regen_conf_files=$1 + # Purge expired keys (such as sury 95BD4743) + EXPIRED_KEYS="$(LC_ALL='en_US.UTF-8' apt-key list 2>/dev/null | grep -A1 'expired:' | grep -v 'expired\|^-' | sed 's/\s//g')" + for KEY in $EXPIRED_KEYS; do apt-key del $KEY 2>/dev/null; done + # Add sury key # We do this only at the post regen and if the key doesn't already exists, because we don't want the regenconf to fuck everything up if the regenconf runs while the network is down if [[ ! -s /etc/apt/trusted.gpg.d/extra_php_version.gpg ]] @@ -69,6 +87,12 @@ do_post_regen() { wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg" fi + # Similar to Sury + if [[ ! -s /etc/apt/trusted.gpg.d/yarn.gpg ]] + then + wget --timeout 900 --quiet "https://dl.yarnpkg.com/debian/pubkey.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/yarn.gpg" + fi + # Make sure php7.4 is the default version when using php in cli if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION then diff --git a/hooks/conf_regen/12-metronome b/hooks/conf_regen/12-metronome deleted file mode 100755 index b039ace31..000000000 --- a/hooks/conf_regen/12-metronome +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -set -e - -if ! dpkg --list | grep -q 'ii *metronome ' -then - echo 'metronome is not installed, skipping' - exit 0 -fi - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/conf/metronome - - # create directories for pending conf - metronome_dir="${pending_dir}/etc/metronome" - metronome_conf_dir="${metronome_dir}/conf.d" - mkdir -p "$metronome_conf_dir" - - # retrieve variables - main_domain=$(cat /etc/yunohost/current_host) - - # install main conf file - cat metronome.cfg.lua \ - | sed "s/{{ main_domain }}/${main_domain}/g" \ - >"${metronome_dir}/metronome.cfg.lua" - - # 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" - done - - # remove old domain conf files - conf_files=$(ls -1 /etc/metronome/conf.d \ - | awk '/^[^\.]+\.[^\.]+.*\.cfg\.lua$/ { print $1 }') - for file in $conf_files; do - domain=${file%.cfg.lua} - [[ $YNH_DOMAINS =~ $domain ]] \ - || touch "${metronome_conf_dir}/${file}" - done -} - -do_post_regen() { - regen_conf_files=$1 - - # retrieve variables - main_domain=$(cat /etc/yunohost/current_host) - - # create metronome directories for domains - for domain in $YNH_MAIN_DOMAINS; do - mkdir -p "/var/lib/metronome/${domain//./%2e}/pep" - # http_upload directory must be writable by metronome and readable by nginx - mkdir -p "/var/xmpp-upload/${domain}/upload" - # sgid bit allows that file created in that dir will be owned by www-data - # despite the fact that metronome ain't in the www-data group - chmod g+s "/var/xmpp-upload/${domain}/upload" - done - - # fix some permissions - [ ! -e '/var/xmpp-upload' ] || chown -R metronome:www-data "/var/xmpp-upload/" - [ ! -e '/var/xmpp-upload' ] || chmod 750 "/var/xmpp-upload/" - - # metronome should be in ssl-cert group to let it access SSL certificates - usermod -aG ssl-cert metronome - chown -R metronome: /var/lib/metronome/ - chown -R metronome: /etc/metronome/conf.d/ - - 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 28d9e90fb..e4683ff92 100755 --- a/hooks/conf_regen/15-nginx +++ b/hooks/conf_regen/15-nginx @@ -4,25 +4,20 @@ set -e . /usr/share/yunohost/helpers -do_init_regen() { - if [[ $EUID -ne 0 ]]; then - echo "You must be root to run this script" 1>&2 - exit 1 - fi +do_base_regen() { - cd /usr/share/yunohost/conf/nginx - - nginx_dir="/etc/nginx" + pending_dir=$1 + nginx_dir="${pending_dir}/etc/nginx" nginx_conf_dir="${nginx_dir}/conf.d" mkdir -p "$nginx_conf_dir" # install plain conf files - cp plain/* "$nginx_conf_dir" + cp acme-challenge.conf.inc "$nginx_conf_dir" + cp global.conf "$nginx_conf_dir" + cp ssowat.conf "$nginx_conf_dir" + cp yunohost_http_errors.conf.inc "$nginx_conf_dir" + cp yunohost_sso.conf.inc "$nginx_conf_dir" - # probably run with init: just disable default site, restart NGINX and exit - rm -f "${nginx_dir}/sites-enabled/default" - - export compatibility="intermediate" ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf" ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" @@ -30,6 +25,17 @@ do_init_regen() { mkdir -p $nginx_conf_dir/default.d/ cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/ +} + +do_init_regen() { + + cd /usr/share/yunohost/conf/nginx + + export compatibility="intermediate" + do_base_regen "" + + # probably run with init: just disable default site, restart NGINX and exit + rm -f "${nginx_dir}/sites-enabled/default" # Restart nginx if conf looks good, otherwise display error and exit unhappy nginx -t 2>/dev/null || { @@ -53,27 +59,21 @@ do_pre_regen() { nginx_conf_dir="${nginx_dir}/conf.d" mkdir -p "$nginx_conf_dir" - # install / update plain conf files - cp plain/* "$nginx_conf_dir" - # remove the panel overlay if this is specified in settings - panel_overlay=$(yunohost settings get 'misc.portal.ssowat_panel_overlay_enabled' | int_to_bool) - if [ "$panel_overlay" == "False" ]; then - echo "#" >"${nginx_conf_dir}/yunohost_panel.conf.inc" + export webadmin_allowlist_enabled="$(jq -r '.webadmin_allowlist_enabled' <<< "$YNH_SETTINGS" | int_to_bool)" + if [ "$webadmin_allowlist_enabled" == "True" ]; then + export webadmin_allowlist="$(jq -r '.webadmin_allowlist' <<< "$YNH_SETTINGS" | sed 's/^null$//g')" fi - # retrieve variables - main_domain=$(cat /etc/yunohost/current_host) - # Support different strategy for security configurations - export redirect_to_https="$(yunohost settings get 'security.nginx.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" + export redirect_to_https="$(jq -r '.nginx_redirect_to_https' <<< "$YNH_SETTINGS" | int_to_bool)" + export compatibility="$(jq -r '.nginx_compatibility' <<< "$YNH_SETTINGS" | int_to_bool)" + export experimental="$(jq -r '.security_experimental_enabled' <<< "$YNH_SETTINGS" | int_to_bool)" + + do_base_regen "${pending_dir}" 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" @@ -86,12 +86,6 @@ 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" @@ -109,15 +103,8 @@ do_pre_regen() { done - 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.webadmin_allowlist) - fi - ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" - ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" - ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf" - mkdir -p $nginx_conf_dir/default.d/ - cp "redirect_to_admin.conf" $nginx_conf_dir/default.d/ + # Legacy file to remove, but we can't really remove it because it may be included by app confs... + echo "# The old yunohost panel/tile/button doesn't exists anymore" > "$nginx_conf_dir"/yunohost_panel.conf.inc # remove old domain conf files conf_files=$(ls -1 /etc/nginx/conf.d \ @@ -144,6 +131,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 3a2aead5d..694080302 100755 --- a/hooks/conf_regen/19-postfix +++ b/hooks/conf_regen/19-postfix @@ -22,19 +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.postfix_compatibility')" + export compatibility="$(jq -r '.postfix_compatibility' <<< "$YNH_SETTINGS")" # 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="" - export relay_enabled="$(yunohost settings get 'email.smtp.smtp_relay_enabled' | int_to_bool)" + export relay_enabled="$(jq -r '.smtp_relay_enabled' <<< "$YNH_SETTINGS" | 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')" + relay_host="$(jq -r '.smtp_relay_host' <<< "$YNH_SETTINGS")" + relay_port="$(jq -r '.smtp_relay_port' <<< "$YNH_SETTINGS")" + relay_user="$(jq -r '.smtp_relay_user' <<< "$YNH_SETTINGS")" + relay_password="$(jq -r '.smtp_relay_password' <<< "$YNH_SETTINGS")" # Avoid to display "Relay account paswword" to other users touch ${postfix_dir}/sasl_passwd @@ -56,7 +56,7 @@ do_pre_regen() { >"${default_dir}/postsrsd" # adapt it for IPv4-only hosts - ipv6="$(yunohost settings get 'email.smtp.smtp_allow_ipv6' | int_to_bool)" + ipv6="$(jq -r '.smtp_allow_ipv6' <<< "$YNH_SETTINGS" | int_to_bool)" if [ "$ipv6" == "False" ] || [ ! -f /proc/net/if_inet6 ]; then sed -i \ 's/ \[::ffff:127.0.0.0\]\/104 \[::1\]\/128//g' \ @@ -80,6 +80,8 @@ do_post_regen() { postmap -F hash:/etc/postfix/sni + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="postfix")' + [[ -z "$regen_conf_files" ]] \ || { systemctl restart postfix && systemctl restart postsrsd; } diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot index 49ff0c9ba..6421cfafa 100755 --- a/hooks/conf_regen/25-dovecot +++ b/hooks/conf_regen/25-dovecot @@ -16,7 +16,7 @@ do_pre_regen() { cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf" cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve" - export pop3_enabled="$(yunohost settings get 'email.pop3.pop3_enabled' | int_to_bool)" + export pop3_enabled="$(jq -r '.pop3_enabled' <<< "$YNH_SETTINGS" | int_to_bool)" export main_domain=$(cat /etc/yunohost/current_host) export domain_list="$(yunohost domain list --features mail_in mail_out --output-as json | jq -r ".domains[]" | tr '\n' ' ')" @@ -42,7 +42,7 @@ do_post_regen() { # create vmail user id vmail >/dev/null 2>&1 \ - || adduser --system --ingroup mail --uid 500 vmail --home /var/vmail --no-create-home + || { mkdir -p /var/vmail; adduser --system --ingroup mail --uid 500 vmail --home /var/vmail --no-create-home; } # Delete legacy home for vmail that existed in the past but was empty, poluting /home/ [ ! -e /home/vmail ] || rmdir --ignore-fail-on-non-empty /home/vmail @@ -53,6 +53,8 @@ do_post_regen() { chown root:mail /var/mail chmod 1775 /var/mail + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="dovecot")' + [ -z "$regen_conf_files" ] && exit 0 # compile sieve script diff --git a/hooks/conf_regen/30-opendkim b/hooks/conf_regen/30-opendkim new file mode 100755 index 000000000..30a8927f4 --- /dev/null +++ b/hooks/conf_regen/30-opendkim @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/opendkim + + install -D -m 644 opendkim.conf "${pending_dir}/etc/opendkim.conf" +} + +do_post_regen() { + mkdir -p /etc/dkim + + # Create / empty those files because we're force-regenerating them + echo "" > /etc/dkim/keytable + echo "" > /etc/dkim/signingtable + + # create DKIM key for domains + 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 + # handle 2048... + opendkim-genkey --domain="$domain" \ + --selector=mail --directory=/etc/dkim -b 1024 + mv /etc/dkim/mail.private "$domain_key" + mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" + } + + echo "mail._domainkey.${domain} ${domain}:mail:${domain_key}" >> /etc/dkim/keytable + echo "*@$domain mail._domainkey.${domain}" >> /etc/dkim/signingtable + done + + chown -R opendkim /etc/dkim/ + chmod 700 /etc/dkim/ + + systemctl restart opendkim +} + +do_$1_regen ${@:2} diff --git a/hooks/conf_regen/31-rspamd b/hooks/conf_regen/31-rspamd deleted file mode 100755 index 6807ce0cd..000000000 --- a/hooks/conf_regen/31-rspamd +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -set -e - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/conf/rspamd - - install -D -m 644 metrics.local.conf \ - "${pending_dir}/etc/rspamd/local.d/metrics.conf" - install -D -m 644 dkim_signing.conf \ - "${pending_dir}/etc/rspamd/local.d/dkim_signing.conf" - install -D -m 644 rspamd.sieve \ - "${pending_dir}/etc/dovecot/global_script/rspamd.sieve" -} - -do_post_regen() { - - ## - ## DKIM key generation - ## - - # create DKIM directory with proper permission - mkdir -p /etc/dkim - chown _rspamd /etc/dkim - - # create DKIM key for domains - 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 - # handle 2048... - opendkim-genkey --domain="$domain" \ - --selector=mail --directory=/etc/dkim -b 1024 - mv /etc/dkim/mail.private "$domain_key" - mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" - } - done - - # fix DKIM keys permissions - chown _rspamd /etc/dkim/*.mail.key - chmod 400 /etc/dkim/*.mail.key - - [ ! -e /var/log/rspamd ] || chown -R _rspamd:_rspamd /var/log/rspamd - - regen_conf_files=$1 - [ -z "$regen_conf_files" ] && exit 0 - - # compile sieve script - [[ "$regen_conf_files" =~ rspamd\.sieve ]] && { - sievec /etc/dovecot/global_script/rspamd.sieve - chown -R vmail:mail /etc/dovecot/global_script - systemctl restart dovecot - } - - # Restart rspamd due to the upgrade - # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html - systemctl -q restart rspamd.service -} - -do_$1_regen ${@:2} diff --git a/hooks/conf_regen/43-dnsmasq b/hooks/conf_regen/43-dnsmasq index 648a128c2..90e3ed2d7 100755 --- a/hooks/conf_regen/43-dnsmasq +++ b/hooks/conf_regen/43-dnsmasq @@ -62,7 +62,8 @@ do_post_regen() { regen_conf_files=$1 # Force permission (to cover some edge cases where root's umask is like 027 and then dnsmasq cant read this file) - chown 644 /etc/resolv.dnsmasq.conf + chown root /etc/resolv.dnsmasq.conf + chmod 644 /etc/resolv.dnsmasq.conf # Fuck it, those domain/search entries from dhclient are usually annoying # lying shit from the ISP trying to MiTM diff --git a/hooks/conf_regen/52-fail2ban b/hooks/conf_regen/52-fail2ban index d463892c7..0789556c4 100755 --- a/hooks/conf_regen/52-fail2ban +++ b/hooks/conf_regen/52-fail2ban @@ -14,16 +14,23 @@ do_pre_regen() { mkdir -p "${fail2ban_dir}/jail.d" cp yunohost.conf "${fail2ban_dir}/filter.d/yunohost.conf" + cp yunohost-portal.conf "${fail2ban_dir}/filter.d/yunohost-portal.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.ssh_port')" + export ssh_port="$(jq -r '.ssh_port' <<< "$YNH_SETTINGS")" 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/27-data_xmpp b/hooks/restore/27-data_xmpp deleted file mode 100644 index f07ac6a33..000000000 --- a/hooks/restore/27-data_xmpp +++ /dev/null @@ -1,11 +0,0 @@ -backup_dir="$1/data/xmpp" - -if [[ -e $backup_dir/var_lib_metronome/ ]] -then - cp -a $backup_dir/var_lib_metronome/. /var/lib/metronome -fi - -if [[ -e $backup_dir/var_xmpp-upload ]] -then - cp -a $backup_dir/var_xmpp-upload/. /var/xmpp-upload -fi diff --git a/locales/ar.json b/locales/ar.json index 2d3e1381e..2a5f46aa1 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -20,7 +20,7 @@ "ask_main_domain": "النطاق الرئيسي", "ask_new_admin_password": "كلمة السر الإدارية الجديدة", "ask_password": "كلمة السر", - "backup_applying_method_copy": "جارٍ نسخ كافة الملفات المراد نسخها احتياطيا …", + "backup_applying_method_copy": "جارٍ نسخ كافة الملفات المراد نسخها احتياطيا…", "backup_applying_method_tar": "جارٍ إنشاء ملف TAR للنسخة الاحتياطية…", "backup_created": "تم إنشاء النسخة الإحتياطية: {name}", "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", @@ -37,9 +37,8 @@ "domain_exists": "اسم النطاق موجود سلفًا", "domains_available": "النطاقات المتوفرة :", "done": "تم", - "downloading": "عملية التنزيل جارية …", + "downloading": "عملية التنزيل جارية…", "dyndns_ip_updated": "لقد تم تحديث عنوان الإيبي الخاص بك على نظام أسماء النطاقات الديناميكي", - "dyndns_key_generating": "جارٍ إنشاء مفتاح DNS ... قد يستغرق الأمر بعض الوقت.", "dyndns_key_not_found": "لم يتم العثور على مفتاح DNS الخاص باسم النطاق هذا", "extracting": "عملية فك الضغط جارية…", "installation_complete": "إكتملت عملية التنصيب", @@ -65,7 +64,7 @@ "unlimit": "دون تحديد الحصة", "updating_apt_cache": "جارٍ جلب قائمة حُزم النظام المحدّثة المتوفرة…", "upgrade_complete": "اكتملت عملية الترقية و التحديث", - "upgrading_packages": "عملية ترقية الحُزم جارية …", + "upgrading_packages": "عملية ترقية الحُزم جارية…", "upnp_disabled": "تم تعطيل UPnP", "user_created": "تم إنشاء المستخدم", "user_deleted": "تم حذف المستخدم", @@ -73,7 +72,7 @@ "user_unknown": "المستخدم {user} مجهول", "user_update_failed": "لا يمكن تحديث المستخدم {user}: {error}", "user_updated": "تم تحديث معلومات المستخدم", - "yunohost_installing": "عملية تنصيب واي يونوهوست جارية …", + "yunohost_installing": "عملية تنصيب واي يونوهوست جارية…", "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب بشكل جيد. فضلًا قم بتنفيذ الأمر 'yunohost tools postinstall'", "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", "service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP", @@ -108,7 +107,6 @@ "service_description_rspamd": "يقوم بتصفية البريد المزعج و إدارة ميزات أخرى للبريد", "service_description_yunohost-firewall": "يُدير فتح وإغلاق منافذ الاتصال إلى الخدمات", "aborting": "إلغاء.", - "app_not_upgraded": "", "app_start_install": "جارٍ تثبيت {app}…", "app_start_remove": "جارٍ حذف {app}…", "app_start_restore": "جارٍ استرجاع {app}…", @@ -257,5 +255,8 @@ "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}' موجود مِن قَبل" + "user_already_exists": "المستخدم '{user}' موجود مِن قَبل", + "backup_archive_name_unknown": "أرشيف نسخ احتياطي محلي غير معروف باسم '{name}'", + "custom_app_url_required": "يجب عليك تقديم عنوان URL لتحديث تطبيقك المخصص {app}", + "backup_copying_to_organize_the_archive": "نسخ {size} ميغا بايت لتنظيم الأرشيف" } \ No newline at end of file diff --git a/locales/ca.json b/locales/ca.json index 821e5c3eb..697b4555d 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -4,7 +4,7 @@ "app_already_installed": "{app} ja està instal·lada", "app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a `app changeurl` si està disponible.", "app_already_up_to_date": "{app} ja està actualitzada", - "app_argument_choice_invalid": "Utilitzeu una de les opcions «{choices}» per l'argument «{name}» en lloc de «{value}»", + "app_argument_choice_invalid": "Trieu un valor vàlid per a l'argument «{name}»: «{value}»\" no es troba entre les opcions disponibles ({choices})", "app_argument_invalid": "Escolliu un valor vàlid per l'argument «{name}»: {error}", "app_argument_required": "Es necessita l'argument '{name}'", "app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain}{path}'), no hi ha res per fer.", @@ -18,12 +18,12 @@ "app_not_correctly_installed": "{app} sembla estar mal instal·lada", "app_not_installed": "No s'ha trobat {app} en la llista d'aplicacions instal·lades: {all_apps}", "app_not_properly_removed": "{app} no s'ha pogut suprimir correctament", - "app_removed": "{app} ha estat suprimida", - "app_requirements_checking": "Verificació dels paquets requerits per {app}...", + "app_removed": "{app} ha estat desinstal·lada", + "app_requirements_checking": "Verificació dels requisits per a {app}…", "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", - "app_upgrade_app_name": "Actualitzant {app}...", + "app_upgrade_app_name": "Actualitzant {app}…", "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}", @@ -32,44 +32,44 @@ "ask_password": "Contrasenya", "backup_abstract_method": "Encara està per implementar aquest mètode de còpia de seguretat", "backup_app_failed": "No s'ha pogut fer la còpia de seguretat de {app}", - "backup_applying_method_copy": "Còpia de tots els fitxers a la còpia de seguretat...", - "backup_applying_method_custom": "Crida del mètode de còpia de seguretat personalitzat \"{method}\"...", - "backup_applying_method_tar": "Creació de l'arxiu TAR de la còpia de seguretat...", + "backup_applying_method_copy": "Còpia de tots els fitxers a la còpia de seguretat…", + "backup_applying_method_custom": "Crida del mètode de còpia de seguretat personalitzat «{method}»…", + "backup_applying_method_tar": "Creació de l'arxiu TAR de la còpia de seguretat…", "backup_archive_app_not_found": "No s'ha pogut trobar {app} en l'arxiu de la còpia de seguretat", "backup_archive_broken_link": "No s'ha pogut accedir a l'arxiu de la còpia de seguretat (enllaç invàlid cap a {path})", - "backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb aquest nom.", + "backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb el nom «{name}».", "backup_archive_name_unknown": "Còpia de seguretat local \"{name}\" desconeguda", "backup_archive_open_failed": "No s'ha pogut obrir l'arxiu de la còpia de seguretat", "backup_archive_system_part_not_available": "La part «{part}» del sistema no està disponible en aquesta copia de seguretat", - "backup_archive_writing_error": "No es poden afegir els arxius «{source}» (anomenats en l'arxiu «{dest}») a l'arxiu comprimit de la còpia de seguretat «{archive}»", + "backup_archive_writing_error": "No es poden afegir els arxius «{source}» (anomenats en l'arxiu «{dest}») a l'arxiu comprimit de la còpia de seguretat «{archive}»", "backup_ask_for_copying_if_needed": "Voleu fer la còpia de seguretat utilitzant {size}MB temporalment? (S'utilitza aquest mètode ja que alguns dels fitxers no s'han pogut preparar utilitzar un mètode més eficient.)", "backup_cant_mount_uncompress_archive": "No es pot carregar l'arxiu descomprimit com a protegit contra escriptura", "backup_cleaning_failed": "No s'ha pogut netejar el directori temporal de la còpia de seguretat", "backup_copying_to_organize_the_archive": "Copiant {size}MB per organitzar l'arxiu", "backup_couldnt_bind": "No es pot lligar {src} amb {dest}.", - "backup_created": "S'ha creat la còpia de seguretat", + "backup_created": "S'ha creat la còpia de seguretat: {name}", "aborting": "Avortant.", "app_not_upgraded": "L'aplicació «{failed_app}» no s'ha pogut actualitzar, i com a conseqüència s'ha cancel·lat l'actualització de les següents aplicacions: {apps}", - "app_start_install": "instal·lant {app}...", - "app_start_remove": "Eliminant {app}...", - "app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per «{app}»...", - "app_start_restore": "Recuperant {app}...", + "app_start_install": "instal·lant {app}…", + "app_start_remove": "Eliminant {app}…", + "app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per «{app}»…", + "app_start_restore": "Recuperant {app}…", "app_upgrade_several_apps": "S'actualitzaran les següents aplicacions: {apps}", "ask_new_domain": "Nou domini", "ask_new_path": "Nou camí", - "backup_actually_backuping": "Creant un arxiu de còpia de seguretat a partir dels fitxers recuperats...", + "backup_actually_backuping": "Creant un arxiu de còpia de seguretat a partir dels fitxers recuperats…", "backup_creation_failed": "No s'ha pogut crear l'arxiu de la còpia de seguretat", "backup_csv_addition_failed": "No s'han pogut afegir fitxers per a fer-ne la còpia de seguretat al fitxer CSV", "backup_csv_creation_failed": "No s'ha pogut crear el fitxer CSV necessari per a la restauració", "backup_custom_backup_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa «backup»", "backup_custom_mount_error": "El mètode de còpia de seguretat personalitzat ha fallat a l'etapa «mount»", "backup_delete_error": "No s'ha pogut suprimir «{path}»", - "backup_deleted": "S'ha suprimit la còpia de seguretat", + "backup_deleted": "S'ha suprimit la còpia de seguretat: {name}", "backup_hook_unknown": "Script de còpia de seguretat «{hook}» desconegut", "backup_method_copy_finished": "La còpia de la còpia de seguretat ha acabat", "backup_method_custom_finished": "El mètode de còpia de seguretat personalitzat \"{method}\" ha acabat", "backup_method_tar_finished": "S'ha creat l'arxiu de còpia de seguretat TAR", - "backup_mount_archive_for_restore": "Preparant l'arxiu per la restauració...", + "backup_mount_archive_for_restore": "Preparant l'arxiu per la restauració…", "good_practices_about_user_password": "Esteu a punt de definir una nova contrasenya d'usuari. 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).", "password_listed": "Aquesta contrasenya és una de les més utilitzades en el món. Si us plau utilitzeu-ne una més única.", "password_too_simple_1": "La contrasenya ha de tenir un mínim de 8 caràcters", @@ -82,7 +82,7 @@ "backup_output_directory_not_empty": "Heu d'escollir un directori de sortida buit", "backup_output_directory_required": "Heu d'especificar un directori de sortida per la còpia de seguretat", "backup_output_symlink_dir_broken": "El directori del arxiu «{path}» es un enllaç simbòlic trencat. Pot ser heu oblidat muntar, tornar a muntar o connectar el mitja d'emmagatzematge al que apunta.", - "backup_running_hooks": "Executant els scripts de la còpia de seguretat...", + "backup_running_hooks": "Executant els scripts de la còpia de seguretat…", "backup_system_part_failed": "No s'ha pogut fer la còpia de seguretat de la part \"{part}\" del sistema", "backup_unable_to_organize_files": "No s'ha pogut utilitzar el mètode ràpid per organitzar els fitxers dins de l'arxiu", "backup_with_no_backup_script_for_app": "L'aplicació «{app}» no té un script de còpia de seguretat. Serà ignorat.", @@ -96,19 +96,19 @@ "certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini «{domain}»", "certmanager_cert_renew_success": "S'ha renovat correctament el certificat Let's Encrypt pel domini «{domain}»", "certmanager_cert_signing_failed": "No s'ha pogut firmar el nou certificat", - "certmanager_certificate_fetching_or_enabling_failed": "Sembla que utilitzar el nou certificat per {domain} ha fallat...", + "certmanager_certificate_fetching_or_enabling_failed": "Sembla que utilitzar el nou certificat per {domain} ha fallat…", "certmanager_domain_cert_not_selfsigned": "El certificat pel domini {domain} no és auto-signat Esteu segur de voler canviar-lo? (Utilitzeu «--force» per fer-ho)", - "certmanager_domain_dns_ip_differs_from_public_ip": "Els registres DNS pel domini «{domain}» són diferents a l'adreça IP d'aquest servidor. Mireu la categoria «registres DNS» (bàsic) al diagnòstic per a més informació. Si heu modificat recentment el registre A, si us plau espereu a que es propagui (hi ha eines per verificar la propagació disponibles a internet). (Si sabeu el que esteu fent, podeu utilitzar «--no-checks» per desactivar aquestes comprovacions.)", - "certmanager_domain_http_not_working": "El domini {domain} sembla que no és accessible via HTTP. Verifiqueu la categoria «Web» en el diagnòstic per a més informació. (Si sabeu el que esteu fent, utilitzeu «--no-checks» per deshabilitar les comprovacions.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Les entrades DNS pel domini «{domain}» són diferents a l'adreça IP d'aquest servidor. Mireu la categoria «registres DNS» (bàsic) al diagnòstic per a més informació. Si heu modificat recentment el registre A, si us plau espereu a que es propagui (hi ha eines per verificar la propagació disponibles a internet). (Si sabeu el que esteu fent, podeu utilitzar «--no-checks» per desactivar aquestes comprovacions.)", + "certmanager_domain_http_not_working": "El domini {domain} sembla que no és accessible via HTTP. Verifiqueu la categoria «Web» en el diagnòstic per a més informació. (Si sabeu el que esteu fent, utilitzeu «--no-checks» per deshabilitar aquestes comprovacions.)", "certmanager_hit_rate_limit": "S'han emès massa certificats recentment per aquest mateix conjunt de dominis {domain}. Si us plau torneu-ho a intentar més tard. Consulteu https://letsencrypt.org/docs/rate-limits/ per obtenir més detalls", - "certmanager_no_cert_file": "No s'ha pogut llegir l'arxiu del certificat pel domini {domain} (fitxer: {file})", + "certmanager_no_cert_file": "No s'ha pogut llegir l'arxiu del certificat pel domini {domain} (fitxer: {file})", "certmanager_self_ca_conf_file_not_found": "No s'ha trobat el fitxer de configuració per l'autoritat del certificat auto-signat (fitxer: {file})", "certmanager_unable_to_parse_self_CA_name": "No s'ha pogut analitzar el nom de l'autoritat del certificat auto-signat (fitxer: {file})", - "confirm_app_install_warning": "Atenció: Aquesta aplicació funciona, però no està ben integrada amb YunoHost. Algunes característiques com la autenticació única i la còpia de seguretat/restauració poden no estar disponibles. Voleu instal·lar-la de totes maneres? [{answers}] ", - "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema... Si accepteu el risc, escriviu «{answers}»", - "confirm_app_install_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}»", + "confirm_app_install_warning": "Atenció: Aquesta aplicació funciona, però no està ben integrada a YunoHost. Algunes característiques com la autenticació única i la còpia de seguretat/restauració poden no estar disponibles. Voleu instal·lar-la de totes maneres? [{answers}] ", + "confirm_app_install_danger": "PERILL! Aquesta aplicació encara és experimental (si no és que no funciona directament)! No hauríeu d'instal·lar-la a no ser que sapigueu el que feu. No obtindreu CAP AJUDA si l'aplicació no funciona o trenca el sistema… Si accepteu el risc, escriviu «{answers}»", + "confirm_app_install_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}", - "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat... Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", + "dpkg_is_broken": "No es pot fer això en aquest instant perquè dpkg/APT (els gestors de paquets del sistema) sembla estar mal configurat… Podeu intentar solucionar-ho connectant-vos per SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a» i/o «sudo dpkg --audit».", "domain_cannot_remove_main": "No es pot eliminar «{domain}» ja que és el domini principal, primer s'ha d'establir un nou domini principal utilitzant «yunohost domain main-domain -n »; aquí hi ha una llista dels possibles dominis: {other_domains}", "domain_cert_gen_failed": "No s'ha pogut generar el certificat", "domain_created": "S'ha creat el domini", @@ -119,23 +119,19 @@ "app_action_cannot_be_ran_because_required_services_down": "Aquests serveis necessaris haurien d'estar funcionant per poder executar aquesta acció: {services} Intenteu reiniciar-los per continuar (i possiblement investigar perquè estan aturats).", "domain_dns_conf_is_just_a_recommendation": "Aquesta ordre mostra la configuració *recomanada*. En cap cas fa la configuració del DNS. És la vostra responsabilitat configurar la zona DNS en el vostre registrar en acord amb aquesta recomanació.", "domain_dyndns_already_subscribed": "Ja us heu subscrit a un domini DynDNS", - "domain_dyndns_root_unknown": "Domini DynDNS principal desconegut", "domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (podria no passar res).", "domain_uninstall_app_first": "Aquestes aplicacions encara estan instal·lades en el vostre domini:\n{apps}\n\nDesinstal·leu-les utilitzant l'ordre «yunohost app remove id_de_lapplicació» o moveu-les a un altre domini amb «yunohost app change-url id_de_lapplicació» abans d'eliminar el domini", "domains_available": "Dominis disponibles:", "done": "Fet", - "downloading": "Descarregant...", + "downloading": "Descarregant…", "dyndns_could_not_check_available": "No s'ha pogut verificar la disponibilitat de {domain} a {provider}.", "dyndns_ip_update_failed": "No s'ha pogut actualitzar l'adreça IP al DynDNS", "dyndns_ip_updated": "S'ha actualitzat l'adreça IP al DynDNS", - "dyndns_key_generating": "S'està generant la clau DNS... això pot trigar una estona.", "dyndns_key_not_found": "No s'ha trobat la clau DNS pel domini", "dyndns_no_domain_registered": "No hi ha cap domini registrat amb DynDNS", - "dyndns_registered": "S'ha registrat el domini DynDNS", - "dyndns_registration_failed": "No s'ha pogut registrar el domini DynDNS: {error}", "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...", + "extracting": "Extracció en curs…", "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", @@ -189,22 +185,22 @@ "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}»", "mailbox_used_space_dovecot_down": "S'ha d'engegar el servei de correu Dovecot, per poder obtenir l'espai utilitzat per la bústia de correu", - "mail_unavailable": "Aquesta adreça de correu està reservada i ha de ser atribuïda automàticament el primer usuari", + "mail_unavailable": "Aquesta adreça de correu està reservada per al grup d'administradors", "main_domain_change_failed": "No s'ha pogut canviar el domini principal", "main_domain_changed": "S'ha canviat el domini principal", "migrations_list_conflict_pending_done": "No es pot utilitzar «--previous» i «--done» al mateix temps.", - "migrations_loading_migration": "Carregant la migració {id}...", + "migrations_loading_migration": "Carregant la migració {id}…", "migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}", "migrations_no_migrations_to_run": "No hi ha cap migració a fer", - "migrations_skip_migration": "Saltant migració {id}...", + "migrations_skip_migration": "Saltant migració {id}…", "migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations run».", "migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».", "not_enough_disk_space": "No hi ha prou espai en «{path}»", "pattern_backup_archive_name": "Ha de ser un nom d'arxiu vàlid amb un màxim de 30 caràcters, compost per caràcters alfanumèrics i -_. exclusivament", "pattern_domain": "Ha de ser un nom de domini vàlid (ex.: el-meu-domini.cat)", "pattern_email": "Ha de ser una adreça de correu vàlida, sense el símbol «+» (ex.: algu@domini.cat)", - "pattern_firstname": "Ha de ser un nom vàlid", - "pattern_lastname": "Ha de ser un cognom vàlid", + "pattern_firstname": "Ha de ser un nom vàlid (almenys tres caràcters)", + "pattern_lastname": "Ha de ser un cognom vàlid (almenys tres caràcters)", "pattern_mailbox_quota": "Ha de ser una mida amb el sufix b/k/M/G/T o 0 per no tenir quota", "pattern_password": "Ha de tenir un mínim de 3 caràcters", "pattern_port_or_range": "Ha de ser un número de port vàlid (i.e. 0-65535) o un interval de ports (ex. 100:200)", @@ -224,23 +220,23 @@ "regenconf_up_to_date": "La configuració ja està al dia per la categoria «{category}»", "regenconf_updated": "S'ha actualitzat la configuració per la categoria «{category}»", "regenconf_would_be_updated": "La configuració hagués estat actualitzada per la categoria «{category}»", - "regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»...", + "regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»…", "regenconf_failed": "No s'ha pogut regenerar la configuració per la/les categoria/es : {categories}", - "regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»...", + "regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»…", "restore_already_installed_app": "Una aplicació amb la ID «{app}» ja està instal·lada", "app_restore_failed": "No s'ha pogut restaurar {app}: {error}", "restore_cleaning_failed": "No s'ha pogut netejar el directori temporal de restauració", "restore_complete": "Restauració completada", "restore_confirm_yunohost_installed": "Esteu segur de voler restaurar un sistema ja instal·lat? [{answers}]", - "restore_extracting": "Extracció dels fitxers necessaris de l'arxiu...", + "restore_extracting": "Extracció dels fitxers necessaris de l'arxiu…", "restore_failed": "No s'ha pogut restaurar el sistema", "restore_hook_unavailable": "El script de restauració «{part}» no està disponible en el sistema i tampoc és en l'arxiu", "restore_may_be_not_enough_disk_space": "Sembla que no hi ha prou espai disponible en el sistema (lliure: {free_space} B, espai necessari: {needed_space} B, marge de seguretat: {margin} B)", "restore_not_enough_disk_space": "No hi ha prou espai disponible (espai: {free_space} B, espai necessari: {needed_space} B, marge de seguretat: {margin} B)", "restore_nothings_done": "No s'ha restaurat res", "restore_removing_tmp_dir_failed": "No s'ha pogut eliminar un directori temporal antic", - "restore_running_app_script": "Restaurant l'aplicació «{app}»...", - "restore_running_hooks": "Execució dels hooks de restauració...", + "restore_running_app_script": "Restaurant l'aplicació «{app}»…", + "restore_running_hooks": "Execució dels hooks de restauració…", "restore_system_part_failed": "No s'ha pogut restaurar la part «{part}» del sistema", "root_password_desynchronized": "S'ha canviat la contrasenya d'administració, però YunoHost no ha pogut propagar-ho cap a la contrasenya root!", "server_shutdown": "S'aturarà el servidor", @@ -285,16 +281,16 @@ "ssowat_conf_generated": "S'ha regenerat la configuració SSOwat", "system_upgraded": "S'ha actualitzat el sistema", "system_username_exists": "El nom d'usuari ja existeix en la llista d'usuaris de sistema", - "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema)... Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", + "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema)… Podeu intentar resoldre el problema connectant-vos amb SSH i executant `sudo apt install --fix-broken` i/o `sudo dpkg --configure -a`.", "unbackup_app": "{app} no es guardarà", "unexpected_error": "Hi ha hagut un error inesperat: {error}", "unlimit": "Sense quota", "unrestore_app": "{app} no es restaurarà", "update_apt_cache_failed": "No s'ha pogut actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list, que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}", "update_apt_cache_warning": "Hi ha hagut errors al actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}", - "updating_apt_cache": "Obtenció de les actualitzacions disponibles per als paquets del sistema...", + "updating_apt_cache": "Obtenció de les actualitzacions disponibles per als paquets del sistema…", "upgrade_complete": "Actualització acabada", - "upgrading_packages": "Actualitzant els paquets...", + "upgrading_packages": "Actualitzant els paquets…", "upnp_dev_not_found": "No s'ha trobat cap dispositiu UPnP", "upnp_disabled": "S'ha desactivat UPnP", "upnp_enabled": "S'ha activat UPnP", @@ -309,7 +305,7 @@ "user_updated": "S'ha canviat la informació de l'usuari", "yunohost_already_installed": "YunoHost ja està instal·lat", "yunohost_configured": "YunoHost està configurat", - "yunohost_installing": "Instal·lació de YunoHost...", + "yunohost_installing": "Instal·lació de YunoHost…", "yunohost_not_installed": "YunoHost no està instal·lat correctament. Executeu «yunohost tools postinstall»", "backup_permission": "Permís de còpia de seguretat per {app}", "group_created": "S'ha creat el grup «{group}»", @@ -344,7 +340,7 @@ "migrations_must_provide_explicit_targets": "Heu de proporcionar objectius explícits al utilitzar «--skip» o «--force-rerun»", "migrations_no_such_migration": "No hi ha cap migració anomenada «{id}»", "migrations_pending_cant_rerun": "Aquestes migracions encara estan pendents, així que no es poden tornar a executar: {ids}", - "migrations_running_forward": "Executant la migració {id}...", + "migrations_running_forward": "Executant la migració {id}…", "migrations_success_forward": "Migració {id} completada", "apps_already_up_to_date": "Ja estan actualitzades totes les aplicacions", "dyndns_provider_unreachable": "No s'ha pogut connectar amb el proveïdor DynDNS {provider}: o el vostre YunoHost no està ben connectat a Internet o el servidor dynette està caigut.", @@ -368,13 +364,13 @@ "permission_already_up_to_date": "No s'ha actualitzat el permís perquè la petició d'afegir/eliminar ja corresponent a l'estat actual.", "permission_currently_allowed_for_all_users": "El permís ha el té el grup de tots els usuaris (all_users) a més d'altres grups. Segurament s'hauria de revocar el permís a «all_users» o eliminar els altres grups als que s'ha atribuït.", "permission_require_account": "El permís {permission} només té sentit per als usuaris que tenen un compte, i per tant no es pot activar per als visitants.", - "app_remove_after_failed_install": "Eliminant l'aplicació després que hagi fallat la instal·lació...", + "app_remove_after_failed_install": "Eliminant l'aplicació després que hagi fallat la instal·lació…", "diagnosis_basesystem_ynh_main_version": "El servidor funciona amb YunoHost {main_version} ({repo})", "diagnosis_ram_low": "El sistema només té {available} ({available_percent}%) de memòria RAM disponibles d'un total de {total}. Aneu amb compte.", "diagnosis_swap_none": "El sistema no té swap. Hauríeu de considerar afegir un mínim de {recommended} de swap per evitar situacions en les que el sistema es queda sense memòria.", "diagnosis_regenconf_manually_modified": "El fitxer de configuració {file} sembla haver estat modificat manualment.", "diagnosis_security_vulnerable_to_meltdown_details": "Per arreglar-ho, hauríeu d'actualitzar i reiniciar el sistema per tal de carregar el nou nucli de linux (o contactar amb el proveïdor del servidor si no funciona). Vegeu https://meltdownattack.com/ per a més informació.", - "diagnosis_http_could_not_diagnose": "No s'ha pogut diagnosticar si el domini és accessible des de l'exterior.", + "diagnosis_http_could_not_diagnose": "No s'ha pogut diagnosticar si el domini és accessible des de l'exterior amb IPv{ipversion}.", "diagnosis_http_could_not_diagnose_details": "Error: {error}", "domain_cannot_remove_main_add_new_one": "No es pot eliminar «{domain}» ja que és el domini principal i únic domini, primer s'ha d'afegir un altre domini utilitzant «yunohost domain add », i després fer-lo el domini principal amb «yunohost domain main-domain -n » i després es pot eliminar el domini «{domain}» utilitzant «yunohost domain remove {domain}».", "diagnosis_basesystem_host": "El servidor funciona amb Debian {debian_version}", @@ -405,9 +401,9 @@ "diagnosis_dns_missing_record": "Segons la configuració DNS recomanada, hauríeu d'afegir un registre DNS amb la següent informació.
Tipus: {type}
Nom: {name}
Valor: {value}", "diagnosis_dns_discrepancy": "La configuració DNS següent sembla que no segueix la configuració recomanada:
Tipus: {type}
Nom: {name}
Valor actual: {current}
Valor esperat: {value}", "diagnosis_services_bad_status": "El servei {service} està {status} :(", - "diagnosis_diskusage_verylow": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) només té disponibles {free} ({free_percent}%). Hauríeu de considerar alliberar una mica d'espai!", - "diagnosis_diskusage_low": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) només té disponibles {free} ({free_percent}%). Aneu amb compte.", - "diagnosis_diskusage_ok": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) encara té {free} ({free_percent}%) lliures!", + "diagnosis_diskusage_verylow": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) només té {free} ({free_percent}%) d'espai disponible (d'un total de {total}). Hauríeu de considerar alliberar una mica d'espai!", + "diagnosis_diskusage_low": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) només té {free} ({free_percent}%) d'espai disponible (d'un total de {total}). Aneu amb compte.", + "diagnosis_diskusage_ok": "El lloc d'emmagatzematge {mountpoint} (en l'aparell {device}) encara té {free} ({free_percent}%) lliures (d'un total de {total})!", "diagnosis_ram_verylow": "El sistema només té {available} ({available_percent}%) de memòria RAM disponibles! (d'un total de {total})", "diagnosis_ram_ok": "El sistema encara té {available} ({available_percent}%) de memòria RAM disponibles d'un total de {total}.", "diagnosis_swap_notsomuch": "El sistema només té {total} de swap. Hauríeu de considerar tenir un mínim de {recommended} per evitar situacions en les que el sistema es queda sense memòria.", @@ -422,28 +418,28 @@ "diagnosis_description_systemresources": "Recursos del sistema", "diagnosis_description_ports": "Exposició dels ports", "diagnosis_description_regenconf": "Configuració del sistema", - "diagnosis_ports_could_not_diagnose": "No s'ha pogut diagnosticar si els ports són accessibles des de l'exterior.", + "diagnosis_ports_could_not_diagnose": "No s'ha pogut diagnosticar si els ports són accessibles des de l'exterior amb IPv{ipversion}.", "diagnosis_ports_could_not_diagnose_details": "Error: {error}", "diagnosis_ports_unreachable": "El port {port} no és accessible des de l'exterior.", "diagnosis_ports_ok": "El port {port} és accessible des de l'exterior.", "diagnosis_http_ok": "El domini {domain} és accessible per mitjà de HTTP des de fora de la xarxa local.", "diagnosis_http_unreachable": "Sembla que el domini {domain} no és accessible a través de HTTP des de fora de la xarxa local.", - "diagnosis_unknown_categories": "Les següents categories són desconegudes: {categories}", + "diagnosis_unknown_categories": "Les categories següents són desconegudes: {categories}", "apps_catalog_init_success": "S'ha iniciat el sistema de catàleg d'aplicacions!", - "apps_catalog_updating": "S'està actualitzant el catàleg d'aplicacions...", + "apps_catalog_updating": "S'està actualitzant el catàleg d'aplicacions…", "apps_catalog_failed_to_download": "No s'ha pogut descarregar el catàleg d'aplicacions {apps_catalog}: {error}", "apps_catalog_obsolete_cache": "La memòria cau del catàleg d'aplicacions és buida o obsoleta.", "apps_catalog_update_success": "S'ha actualitzat el catàleg d'aplicacions!", - "diagnosis_mail_outgoing_port_25_blocked": "Sembla que el port de sortida 25 està bloquejat. Hauríeu d'intentar desbloquejar-lo al panell de configuració del proveïdor d'accés a internet (o allotjador). Mentrestant, el servidor no podrà enviar correus a altres servidors.", + "diagnosis_mail_outgoing_port_25_blocked": "El servidor de correu SMTP no pot enviar correus a altres servidor perquè el port 25 està bloquejat en IPv{ipversion}.", "diagnosis_description_mail": "Correu electrònic", "app_upgrade_script_failed": "Hi ha hagut un error en el script d'actualització de l'aplicació", "diagnosis_services_bad_status_tip": "Podeu intentar reiniciar el servei, i si no funciona, podeu mirar els registres a la pàgina web d'administració (des de la línia de comandes, ho podeu fer utilitzant yunohost service restart {service} i yunohost service log {service}).", "diagnosis_ports_forwarding_tip": "Per arreglar aquest problema, segurament s'ha de configurar el reenviament de ports en el router tal i com s'explica a https://yunohost.org/isp_box_config", - "diagnosis_http_bad_status_code": "Sembla que una altra màquina (potser el router) a respost en lloc del vostre servidor.
1. La causa més probable per a aquest problema és que el port 80 (i 443) no reenvien correctament cap al vostre servidor.
2. En configuracions més complexes: assegureu-vos que no hi ha cap tallafoc o reverse-proxy interferint.", + "diagnosis_http_bad_status_code": "Sembla que una altra màquina (potser el router) a respost en lloc del vostre servidor.
1. La causa més probable per a aquest problema és que el port 80 (i 443) no reenvien correctament cap al vostre servidor.
2. En configuracions més complexes: assegureu-vos que no hi ha cap tallafoc o reverse-proxy interferint.", "diagnosis_no_cache": "Encara no hi ha memòria cau pel diagnòstic de la categoria «{category}»", - "diagnosis_http_timeout": "S'ha exhaurit el temps d'esperar intentant connectar amb el servidor des de l'exterior.
1. La causa més probable per a aquest problema és que el port 80 (i 443) no reenvien correctament cap al vostre servidor.
2. També us hauríeu d'assegurar que el servei nginx estigui funcionant
3. En configuracions més complexes: assegureu-vos que no hi ha cap tallafoc o reverse-proxy interferint.", + "diagnosis_http_timeout": "S'ha exhaurit el temps d'esperar intentant connectar amb el servidor des de l'exterior.
1. La causa més probable per a aquest problema és que el port 80 (i 443) no reenvien correctament cap al vostre servidor.
2. També us hauríeu d'assegurar que el servei nginx estigui funcionant
3. En configuracions més complexes: assegureu-vos que no hi ha cap tallafoc o reverse-proxy interferint.", "diagnosis_http_connection_error": "Error de connexió: no s'ha pogut connectar amb el domini demanat, segurament és inaccessible.", - "yunohost_postinstall_end_tip": "S'ha completat la post-instal·lació. Per acabar la configuració, considereu:\n - afegir un primer usuari a través de la secció «Usuaris» a la pàgina web d'administració (o emprant «yunohost user create » a la línia d'ordres);\n - diagnosticar possibles problemes a través de la secció «Diagnòstics» a la pàgina web d'administració (o emprant «yunohost diagnosis run» a la línia d'ordres);\n - llegir les seccions «Finalizing your setup» i «Getting to know YunoHost» a la documentació per administradors: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "S'ha completat la post-instal·lació. Per acabar la configuració, considereu:\n - diagnosticar possibles problemes a través de la secció «Diagnòstics» a la pàgina web d'administració (o emprant «yunohost diagnosis run» a la línia d'ordres);\n - llegir les seccions «Finalizing your setup» i «Getting to know YunoHost» a la documentació per administradors: https://yunohost.org/admindoc.", "diagnosis_services_running": "El servei {service} s'està executant!", "diagnosis_services_conf_broken": "La configuració pel servei {service} està trencada!", "diagnosis_ports_needed_by": "És necessari exposar aquest port per a les funcions {category} (servei {service})", @@ -451,14 +447,14 @@ "diagnosis_never_ran_yet": "Sembla que el servidor s'ha configurat recentment i encara no hi cap informe de diagnòstic per mostrar. S'ha d'executar un diagnòstic complet primer, ja sigui des de la pàgina web d'administració o utilitzant la comanda «yunohost diagnosis run» al terminal.", "diagnosis_description_web": "Web", "diagnosis_basesystem_hardware": "L'arquitectura del maquinari del servidor és {virt} {arch}", - "group_already_exist_on_system_but_removing_it": "El grup {group} ja existeix en els grups del sistema, però YunoHost l'eliminarà...", + "group_already_exist_on_system_but_removing_it": "El grup {group} ja existeix en els grups del sistema, però YunoHost l'eliminarà…", "certmanager_warning_subdomain_dns_record": "El subdomini «{subdomain}» no resol a la mateixa adreça IP que «{domain}». Algunes funcions no estaran disponibles fins que no s'hagi arreglat i s'hagi regenerat el certificat.", "domain_cannot_add_xmpp_upload": "No podeu afegir dominis començant per «xmpp-upload.». Aquest tipus de nom està reservat per a la funció de pujada de XMPP integrada a YunoHost.", "diagnosis_display_tip": "Per veure els problemes que s'han trobat, podeu anar a la secció de Diagnòstic a la pàgina web d'administració, o utilitzar « yunohost diagnostic show --issues --human-readable» a la línia de comandes.", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alguns proveïdors no permeten desbloquejar el port de sortida 25 perquè no els hi importa la Neutralitat de la Xarxa.
- Alguns d'ells ofereixen l'alternativa d'utilitzar un relay de servidor de correu electrònic tot i que implica que el relay serà capaç d'espiar el tràfic de correus electrònics.
- Una alternativa respectuosa amb la privacitat és utilitzar una VPN *amb una IP pública dedicada* per sortejar aquest tipus de limitació. Vegeu https://yunohost.org/#/vpn_advantage
- També podeu considerar canviar-vos a un proveïdor més respectuós de la neutralitat de la xarxa", - "diagnosis_ip_global": "IP global: {global}", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alguns proveïdors no permeten desbloquejar el port de sortida 25 perquè no els hi importa la Neutralitat de la Xarxa.
- Alguns d'ells ofereixen l'alternativa d'utilitzar un relay de servidor de correu electrònic tot i que implica que el relay serà capaç d'espiar el tràfic de correus electrònics.
- Una alternativa respectuosa amb la privacitat és utilitzar una VPN *amb una IP pública dedicada* per sortejar aquestos tipus de limitacions. Vegeu https://yunohost.org/vpn_advantage
- També podeu considerar canviar-vos a un proveïdor més respectuós de la neutralitat de la xarxa", + "diagnosis_ip_global": "IP global: {global}", "diagnosis_ip_local": "IP local: {local}", - "diagnosis_dns_point_to_doc": "Consulteu la documentació a https://yunohost.org/dns_config si necessiteu ajuda per configurar els registres DNS.", + "diagnosis_dns_point_to_doc": "Consulteu la documentació a https://yunohost.org/dns_config si necessiteu ajuda per configurar els registres DNS.", "diagnosis_mail_outgoing_port_25_ok": "El servidor de correu electrònic SMTP pot enviar correus electrònics (el port de sortida 25 no està bloquejat).", "diagnosis_mail_outgoing_port_25_blocked_details": "Primer heu d'intentar desbloquejar el port 25 en la interfície del vostre router o en la interfície del vostre allotjador. (Alguns proveïdors d'allotjament demanen enviar un tiquet de suport en aquests casos).", "diagnosis_mail_ehlo_ok": "El servidor de correu electrònic SMTP és accessible des de l'exterior i per tant pot rebre correus electrònics!", @@ -481,20 +477,20 @@ "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.", - "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.", - "diagnosis_mail_blacklist_website": "Després d'haver identificat perquè estàveu llistats i haver arreglat el problema, no dubteu en demanar que la vostra IP o domini sigui eliminat de {blacklist_website}", + "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 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 ser marcats com a correu brossa.", + "diagnosis_mail_blacklist_website": "Després d'haver identificat perquè estàveu llistats i arreglat el problema, no dubteu a demanar que la vostra IP o domini sigui eliminat de {blacklist_website}", "diagnosis_ports_partially_unreachable": "El port {port} no és accessible des de l'exterior amb IPv{failed}.", "diagnosis_http_partially_unreachable": "El domini {domain} sembla que no és accessible utilitzant HTTP des de l'exterior de la xarxa local amb IPv{failed}, tot i que funciona amb IPv{passed}.", - "diagnosis_mail_fcrdns_nok_details": "Hauríeu d'intentar configurar primer el DNS invers amb {ehlo_domain} en la interfície del router o en la interfície del vostre allotjador. (Alguns allotjadors requereixen que obris un informe de suport per això).", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Alguns proveïdors no permeten configurar el DNS invers (o aquesta funció pot no funcionar…). Si teniu problemes a causa d'això, considereu les solucions següents:
- Alguns proveïdors d'accés a internet (ISP) donen l'alternativa de utilitzar un relay de servidor de correu electrònic tot i que implica que el relay podrà espiar el trànsit de correus electrònics.
- Una alternativa respectuosa amb la privacitat és utilitzar una VPN *amb una IP pública dedicada* per sobrepassar aquest tipus de limitacions. Mireu https://yunohost.org/#/vpn_advantage
- O es pot canviar a un proveïdor diferent", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Alguns proveïdors no permeten configurar el vostre DNS invers (o la funció no els hi funciona…). Si el vostre DNS invers està correctament configurat per IPv4, podeu intentar deshabilitar l'ús de IPv6 per a enviar correus electrònics utilitzant yunohost settings set smtp.allow_ipv6 -v off. Nota: aquesta última solució implica que no podreu enviar o rebre correus electrònics cap a els pocs servidors que hi ha que només tenen IPv-6.", + "diagnosis_mail_fcrdns_nok_details": "Hauríeu d'intentar primer configurar el DNS invers amb {ehlo_domain} en la interfície del router o en la interfície del vostre allotjador. (Alguns proveïdors d'allotjament requereixen que obris un informe de suport per això).", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Alguns proveïdors no permeten configurar el DNS invers (o aquesta funció pot no funcionar…). Si teniu problemes a causa d'això, considereu les solucions següents:
- Alguns proveïdors d'accés a internet (ISP) donen l'alternativa de utilitzar un relay de servidor de correu electrònic tot i que implica que el relay podrà espiar el trànsit de correus electrònics.
- Una alternativa respectuosa amb la privacitat és utilitzar una VPN *amb una IP pública dedicada* per sobrepassar aquest tipus de limitacions. Mireu https://yunohost.org/vpn_advantage
- O es pot canviar a un proveïdor diferent", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Alguns proveïdors no permeten configurar el vostre DNS invers (o la funció no els hi funciona…). Si el vostre DNS invers està correctament configurat per IPv4, podeu intentar deshabilitar l'ús de IPv6 per a enviar correus electrònics utilitzant yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Nota: aquesta última solució implica que no podreu enviar o rebre correus electrònics cap a els pocs servidors que hi ha que només tenen IPv-6.", "diagnosis_http_hairpinning_issue_details": "Això és probablement a causa del router del vostre proveïdor d'accés a internet. El que fa, que gent de fora de la xarxa local pugui accedir al servidor sense problemes, però no la gent de dins la xarxa local (com vostè probablement) quan s'utilitza el nom de domini o la IP global. Podreu segurament millorar la situació fent una ullada a https://yunohost.org/dns_local_network", - "backup_archive_cant_retrieve_info_json": "No s'ha pogut carregar la informació de l'arxiu «{archive}»... No s'ha pogut obtenir el fitxer info.json (o no és un fitxer json vàlid).", + "backup_archive_cant_retrieve_info_json": "No s'ha pogut carregar la informació de l'arxiu «{archive}»… No s'ha pogut obtenir el fitxer info.json (o no és un fitxer json vàlid).", "backup_archive_corrupted": "Sembla que l'arxiu de la còpia de seguretat «{archive}» està corromput : {error}", - "certmanager_domain_not_diagnosed_yet": "Encara no hi ha cap resultat de diagnòstic per al domini {domain}. Torneu a executar el diagnòstic per a les categories «Registres DNS» i «Web» en la secció de diagnòstic per comprovar que el domini està preparat per a Let's Encrypt. (O si sabeu el que esteu fent, utilitzant «--no-checks» per deshabilitar les comprovacions.)", - "diagnosis_ip_no_ipv6_tip": "Utilitzar una IPv6 no és obligatori per a que funcioni el servidor, però és millor per la salut d'Internet en conjunt. La IPv6 hauria d'estar configurada automàticament pel sistema o pel proveïdor si està disponible. Si no és el cas, pot ser necessari configurar alguns paràmetres més de forma manual tal i com s'explica en la documentació disponible aquí: https://yunohost.org/#/ipv6. Si no podeu habilitar IPv6 o us sembla massa tècnic, podeu ignorar aquest avís sense problemes.", + "certmanager_domain_not_diagnosed_yet": "Encara no hi ha cap resultat de diagnòstic per al domini {domain}. Torneu a executar el diagnòstic per a les categories «Registres DNS» i «Web» en la secció de diagnòstic per comprovar que el domini està preparat per a Let's Encrypt. (O si sabeu el que esteu fent, utilitzant «--no-checks» per deshabilitar aquestes comprovacions.)", + "diagnosis_ip_no_ipv6_tip": "Utilitzar una IPv6 no és obligatori per a que funcioni el servidor, però és millor per la salut d'Internet en conjunt. La IPv6 hauria d'estar configurada automàticament pel sistema o pel proveïdor si està disponible. Si no és el cas, pot ser necessari configurar alguns paràmetres més de forma manual tal i com s'explica en la documentació disponible aquí: https://yunohost.org/ipv6. Si no podeu habilitar IPv6 o us sembla massa tècnic, podeu ignorar aquest avís sense problemes.", "diagnosis_domain_expiration_not_found": "No s'ha pogut comprovar la data d'expiració d'alguns dominis", "diagnosis_domain_not_found_details": "El domini {domain} no existeix en la base de dades WHOIS o ha expirat!", "diagnosis_domain_expiration_not_found_details": "La informació WHOIS pel domini {domain} sembla que no conté informació sobre la data d'expiració?", @@ -516,8 +512,8 @@ "pattern_email_forward": "Ha de ser una adreça de correu vàlida, s'accepta el símbol «+» (per exemple, algu+etiqueta@exemple.cat)", "invalid_number": "Ha de ser una xifra", "invalid_regex": "Regex no vàlid: «{regex}»", - "global_settings_setting_smtp_relay_password": "Tramesa de la contrasenya d'amfitrió SMTP", - "global_settings_setting_smtp_relay_user": "Tramesa de compte d'usuari SMTP", + "global_settings_setting_smtp_relay_password": "Contrasenya de retransmissió SMTP", + "global_settings_setting_smtp_relay_user": "Usuari de retransmissió SMTP", "global_settings_setting_smtp_relay_port": "Port de tramesa SMTP", "diagnosis_processes_killed_by_oom_reaper": "El sistema ha matat alguns processos recentment perquè s'ha quedat sense memòria. Això acostuma a ser un símptoma de falta de memòria en el sistema o d'un procés que consumeix massa memòria. Llista dels processos que s'han matat:\n{kills_summary}", "diagnosis_package_installed_from_sury_details": "Alguns paquets s'han instal·lat per equivocació des d'un repositori de tercers anomenat Sury. L'equip de YunoHost a millorat l'estratègia per a gestionar aquests paquets, però s'espera que algunes configuracions que han instal·lat aplicacions PHP7.3 a Stretch puguin tenir algunes inconsistències. Per a resoldre aquesta situació, hauríeu d'intentar executar la següent ordre: {cmd_to_fix}", @@ -526,7 +522,7 @@ "app_manifest_install_ask_is_public": "Aquesta aplicació hauria de ser visible per a visitants anònims?", "app_manifest_install_ask_admin": "Escolliu l'usuari administrador per aquesta aplicació", "app_manifest_install_ask_password": "Escolliu la contrasenya d'administració per aquesta aplicació", - "app_manifest_install_ask_path": "Escolliu la ruta en la que s'hauria d'instal·lar aquesta aplicació", + "app_manifest_install_ask_path": "Escolliu la ruta de l'URL (després del domini) on s'ha d'instal·lar aquesta aplicació", "app_manifest_install_ask_domain": "Escolliu el domini en el que s'hauria d'instal·lar aquesta aplicació", "app_label_deprecated": "Aquesta ordre està desestimada! Si us plau utilitzeu la nova ordre «yunohost user permission update» per gestionar l'etiqueta de l'aplicació.", "app_argument_password_no_default": "Hi ha hagut un error al analitzar l'argument de la contrasenya «{name}»: l'argument de contrasenya no pot tenir un valor per defecte per raons de seguretat", @@ -537,7 +533,7 @@ "postinstall_low_rootfsspace": "El sistema de fitxers arrel té un total de menys de 10 GB d'espai, el que es preocupant! És molt probable que us quedeu sense espai ràpidament! Es recomana tenir un mínim de 16 GB per al sistema de fitxers arrel. Si voleu instal·lar YunoHost tot i aquest avís, torneu a executar la postinstal·lació amb --force-diskspace", "diagnosis_rootfstotalspace_critical": "El sistema de fitxers arrel només té {space} en total i és preocupant! És molt probable que us quedeu sense espai ràpidament! Es recomanar tenir un mínim de 16 GB per al sistema de fitxers arrel.", "diagnosis_rootfstotalspace_warning": "El sistema de fitxers arrel només té {space} en total. Això no hauria de causar cap problema, però haureu de parar atenció ja que us podrieu quedar sense espai ràpidament… Es recomanar tenir un mínim de 16 GB per al sistema de fitxers arrel.", - "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_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.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ó", @@ -546,7 +542,244 @@ "global_settings_setting_admin_strength": "Robustesa de la contrasenya d'administrador", "global_settings_setting_user_strength": "Robustesa de la contrasenya de l'usuari", "global_settings_setting_postfix_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor Postfix. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", - "global_settings_setting_ssh_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_ssh_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor SSH. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat). Visita https://infosec.mozilla.org/guidelines/openssh (anglés) per mes informació.", "global_settings_setting_smtp_allow_ipv6_help": "Permet l'ús de IPv6 per rebre i enviar correus electrònics", - "global_settings_setting_smtp_relay_enabled_help": "L'amfitrió de tramesa SMTP que s'ha d'utilitzar per enviar correus electrònics en lloc d'aquesta instància de YunoHost. És útil si esteu en una de les següents situacions: el port 25 està bloquejat per el vostre proveïdor d'accés a internet o proveïdor de servidor privat virtual, si teniu una IP residencial llistada a DUHL, si no podeu configurar el DNS invers o si el servidor no està directament exposat a internet i voleu utilitzar-ne un altre per enviar correus electrònics." + "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.", + "config_validate_date": "Hauria de ser una data vàlida com en el format AAAA-MM-DD", + "config_validate_email": "Hauria de ser un correu electrònic vàlid", + "app_failed_to_upgrade_but_continue": "L'aplicació {failed_app} no s'ha pogut actualitzar. Continueu amb les properes actualitzacions tal com se sol·licita. Executeu «yunohost log show {operation_logger_name}» per veure el registre d'errors", + "app_not_enough_disk": "Aquesta aplicació requereix {required} espai lliure.", + "app_not_upgraded_broken_system": "L'aplicació «{failed_app}» no s'ha pogut actualitzar i ha posat el sistema en un estat trencat i, com a conseqüència, s'han cancel·lat les actualitzacions de les aplicacions següents: {apps}", + "apps_failed_to_upgrade": "No s'han pogut actualitzar aquestes aplicacions: {apps}", + "app_arch_not_supported": "Aquesta aplicació només es pot instal·lar a les arquitectures {required}, però l'arquitectura del vostre servidor és {current}", + "app_resource_failed": "No s'ha pogut subministrar, desaprovisionar o actualitzar recursos per a {app}: {error}", + "app_yunohost_version_not_supported": "Aquesta aplicació requereix YunoHost >= {required} però la versió instal·lada actual és {current}", + "config_action_failed": "No s'ha pogut executar l'acció «{action}»: {error}", + "config_apply_failed": "No s'ha pogut aplicar la configuració nova: {error}", + "config_validate_color": "Hauria de ser un color hexadecimal RGB vàlid", + "app_config_unable_to_apply": "No s'han pogut aplicar els valors del tauler de configuració.", + "app_failed_to_download_asset": "No s'ha pogut baixar el recurs «{source_id}» ({url}) per a {app}: {out}", + "app_not_upgraded_broken_system_continue": "L'aplicació «{failed_app}» no s'ha pogut actualitzar i ha posat el sistema en un estat trencat (per tant, s'ignora --continue-on-failure) i, com a conseqüència, s'han cancel·lat les actualitzacions de les aplicacions següents: {apps}", + "config_forbidden_readonly_type": "El tipus «{type}» no es pot establir com a només lectura; utilitzeu un altre tipus per representar aquest valor (identificador d'argument rellevant: «{id}»).", + "app_manifest_install_ask_init_admin_permission": "Qui hauria de tenir accés a les funcions d'administració d'aquesta aplicació? (Això es pot canviar més endavant)", + "admins": "Administradors", + "all_users": "Tots els usuaris de YunoHost", + "app_action_failed": "No s'ha pogut executar l'acció {action} per a l'aplicació {app}", + "app_manifest_install_ask_init_main_permission": "Qui hauria de tenir accés a aquesta aplicació? (Això es pot canviar més endavant)", + "ask_admin_fullname": "Nom complet de l'administrador", + "certmanager_cert_install_failed": "La instal·lació del certificat de Let's Encrypt ha fallat per a {domains}", + "certmanager_cert_install_failed_selfsigned": "La instal·lació del certificat autofirmat ha fallat per a {domains}", + "certmanager_cert_renew_failed": "La renovació del certificat de Let's Encrypt ha fallat per a {domains}", + "config_action_disabled": "No s'ha pogut executar l'acció «{action}» perquè està desactivada, assegureu-vos de complir les seves limitacions. ajuda: {help}", + "config_forbidden_keyword": "La paraula clau «{keyword}» està reservada, no podeu crear ni utilitzar un tauler de configuració amb una pregunta amb aquest identificador.", + "config_no_panel": "No s'ha trobat cap tauler de configuració.", + "config_validate_time": "Hauria de ser una hora vàlida com HH:MM", + "config_validate_url": "Hauria de ser un URL web vàlid", + "app_change_url_failed": "No s'ha pogut canviar l'URL per a {app}: {error}", + "app_change_url_require_full_domain": "{app} no es pot moure a aquest URL nou perquè requereix un domini complet (és a dir, amb el camí = /)", + "app_change_url_script_failed": "S'ha produït un error a l'script de canvi d'URL", + "app_config_unable_to_read": "No s'han pogut llegir els valors del tauler de configuració.", + "ask_admin_username": "Nom d'usuari de l'administrador", + "ask_fullname": "Nom complet", + "config_unknown_filter_key": "La clau de filtre «{filter_key}» és incorrecta.", + "ask_dyndns_recovery_password_explain_unavailable": "Aquest domini DynDNS ja està registrat. Si sou la persona que va registrar originalment aquest domini, podeu introduir la contrasenya de recuperació per recuperar aquest domini.", + "app_not_enough_ram": "Aquesta aplicació requereix RAM {required} per instal·lar/actualitzar, però només {current} està disponible ara mateix.", + "ask_dyndns_recovery_password_explain": "Si us plau, trieu una contrasenya de recuperació per al vostre domini DynDNS, en cas que hàgiu de restablir-la més tard.", + "ask_dyndns_recovery_password": "Contrasenya de recuperació de DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Introduïu la contrasenya de recuperació d'aquest domini DynDNS.", + "config_cant_set_value_on_section": "No podeu establir un sol valor en una secció sencera de configuració.", + "diagnosis_http_special_use_tld": "El domini {domain} es basa en un domini de primer nivell (TLD) d'ús especial com ara .local o .test i, per tant, no s'espera que estigui exposat fora de la xarxa local.", + "diagnosis_apps_deprecated_practices": "La versió instal·lada d'aquesta aplicació encara utilitza algunes pràctiques d'empaquetament molt antigues i obsoletes. Realment hauríeu de considerar actualitzar-lo.", + "diagnosis_high_number_auth_failures": "Recentment, hi ha hagut un nombre massa alt d'errors d'autenticació. És possible que vulgueu assegurar-vos que fail2ban s'està executant i està configurat correctament, o bé utilitzar un port personalitzat per a SSH tal com s'explica a https://yunohost.org/security.", + "apps_failed_to_upgrade_line": "\n * {app_id} (per veure el registre corresponent, feu «yunohost log show {operation_logger_name}»)", + "diagnosis_description_apps": "Aplicacions", + "diagnosis_using_yunohost_testing": "apt (el gestor de paquets del sistema) està configurat actualment per instal·lar qualsevol actualització de «prova» per al nucli de YunoHost.", + "domain_config_acme_eligible_explain": "Aquest domini no sembla preparat per a un certificat Let's Encrypt. Comproveu la vostra configuració de DNS i la visibilitat del servidor HTTP. La secció \"Registres DNS\" i \"Web\" a la pàgina de diagnòstic us poden ajudar a entendre què està mal configurat.", + "domain_config_api_protocol": "Protocol API", + "domain_dns_push_already_up_to_date": "Registres ja actualitzats, res a fer.", + "confirm_notifications_read": "ADVERTÈNCIA: hauríeu de comprovar les notificacions de l'aplicació anteriors abans de continuar, és possible que hi hagi coses importants a saber. [{answers}]", + "diagnosis_sshd_config_inconsistent_details": "Executeu yunohost settings set security.ssh.port -v YOUR_SSH_PORT per a definir el port SSH, i executeu yunohost tools regen-conf ssh --dry-run --with-diff i yunohost tools regen-conf ssh --force per a reinicialitzar la vostra configuració perquè s'ajusti a les recomanacions del Yunohost.", + "disk_space_not_sufficient_install": "No queda prou espai al disc per instal·lar aquesta aplicació", + "domain_config_mail_in": "Correus entrants", + "domain_config_mail_out": "Correus sortints", + "domain_dns_push_managed_in_parent_domain": "La funció de configuració automàtica de DNS es gestiona al domini principal {parent_domain}.", + "diagnosis_apps_broken": "Aquesta aplicació està actualment marcada com a trencada al catàleg d'aplicacions de YunoHost. Pot ser un problema temporal mentre els responsables intenten solucionar el problema. Mentrestant, l'actualització d'aquesta aplicació està desactivada.", + "diagnosis_apps_not_in_app_catalog": "Aquesta aplicació no es troba al catàleg d'aplicacions de YunoHost. Si hi era en el passat i s'ha eliminat, hauríeu de considerar la desinstal·lació d'aquesta aplicació, ja que no rebrà actualitzacions i pot comprometre la integritat i la seguretat del vostre sistema.", + "danger": "Perill:", + "diagnosis_dns_specialusedomain": "El domini {domain} es basa en un domini de primer nivell (TLD) d'ús especial com ara .local o .test i, per tant, no s'espera que tingui registres DNS reals.", + "domain_dns_push_failed_to_authenticate": "No s'ha pogut autenticar a l'API del registrador per al domini «{domain}». El més probable és que les credencials siguin incorrectes (error: {error})", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 normalment l'hauria de configurar automàticament el sistema o el vostre proveïdor si està disponible. En cas contrari, és possible que hàgiu de configurar algunes coses manualment tal com s'explica a la documentació aquí: https://yunohost.org/ipv6.", + "diagnosis_using_yunohost_testing_details": "Probablement això està bé si sabeu què feu, però presteu atenció a les notes de la versió abans d'instal·lar les actualitzacions de YunoHost! Si voleu desactivar les actualitzacions de «prova», hauríeu d'eliminar la paraula clau testing de /etc/apt/sources.list.d/yunohost.list.", + "disk_space_not_sufficient_update": "No queda prou espai al disc per actualitzar aquesta aplicació", + "domain_config_auth_application_key": "Clau d'aplicació", + "domain_config_auth_application_secret": "Clau secreta d'aplicació", + "domain_config_auth_consumer_key": "Clau del consumidor", + "domain_config_auth_entrypoint": "Punt d'entrada de l'API", + "domain_config_cert_renew_help": "El certificat es renovarà automàticament durant els darrers 15 dies de validesa. Podeu renovar-lo manualment si voleu (no es recomana).", + "domain_config_cert_summary": "Estat del certificat", + "domain_config_cert_validity": "Validesa", + "domain_config_default_app": "Aplicació per defecte", + "domain_config_xmpp": "Missatgeria instantània (XMPP)", + "domain_dns_conf_special_use_tld": "Aquest domini es basa en un domini de primer nivell (TLD) d'ús especial com ara .local o .test i, per tant, no s'espera que tingui registres DNS reals.", + "domain_dns_push_partial_failure": "Registres DNS parcialment actualitzats: s'han notificat alguns avisos/errors.", + "domain_config_auth_secret": "Secret d'autenticació", + "domain_dns_push_failed_to_list": "No s'han pogut llistar les entrades actuals mitjançant l'API del registrador: {error}", + "diagnosis_apps_allgood": "Totes les aplicacions instal·lades respecten les pràctiques bàsiques d'empaquetament", + "diagnosis_apps_issue": "S'ha trobat un problema per a l'aplicació {app}", + "diagnosis_using_stable_codename": "apt (el gestor de paquets del sistema) està configurat actualment per instal·lar paquets des del nom en codi «stable», en lloc del nom en clau de la versió actual de Debian.", + "domain_config_auth_key": "Clau d'autenticació", + "domain_config_cert_install": "Instal·la el certificat Let's Encrypt", + "domain_config_cert_issuer": "Autoritat de certificació", + "domain_config_cert_no_checks": "Ignorar les comprovacions de diagnòstic", + "domain_config_cert_renew": "Renova el certificat Let's Encrypt", + "domain_config_cert_summary_letsencrypt": "Genial! Esteu utilitzant un certificat de Let's Encrypt vàlid!", + "domain_config_cert_summary_ok": "D'acord, el certificat actual sembla bo!", + "domain_config_cert_summary_selfsigned": "ADVERTIMENT: el certificat actual està signat per ell mateix. Els navegadors mostraran un avís esgarrifós als nous visitants!", + "diagnosis_apps_bad_quality": "Aquesta aplicació està actualment marcada com a trencada al catàleg d'aplicacions de YunoHost. Pot ser un problema temporal mentre els responsables intenten solucionar el problema. Mentrestant, l'actualització d'aquesta aplicació està desactivada.", + "diagnosis_using_stable_codename_details": "Això sol ser causat per una configuració incorrecta del vostre proveïdor d'allotjament. Això és perillós, perquè tan bon punt la propera versió de Debian es converteixi en la nova «stable», apt voldrà actualitzar tots els paquets del sistema sense passar per un procediment de migració adequat. Es recomana arreglar-ho editant la font d'apt per al dipòsit de Debian bàsic i substituir la paraula clau stable pel nom en clau de la versió bullseye. El fitxer de configuració corresponent hauria de ser /etc/apt/sources.list, o un fitxer a /etc/apt/sources.list.d/.", + "confirm_app_insufficient_ram": "PERILL! Aquesta aplicació requereix {required} de RAM per instal·lar/actualitzar, però només n'hi ha {current} disponibles ara mateix. Fins i tot si aquesta aplicació es pot executar, el seu procés d'instal·lació/actualització requereix una gran quantitat de memòria RAM, de manera que el servidor es pot congelar i fallar miserablement. Si esteu disposat a córrer aquest risc de totes maneres, escriviu «{answers}»", + "diagnosis_apps_outdated_ynh_requirement": "La versió instal·lada d'aquesta aplicació només requereix yunohost >= 2.x o 3.x, la qual cosa acostuma a indicar que no està al dia amb les pràctiques d'empaquetament i els «ajudants» recomanats. Realment hauríeu de considerar actualitzar-la.", + "domain_cannot_add_muc_upload": "No podeu afegir dominis que comencin per 'muc.'. Aquest tipus de nom està reservat per a la funció de xat multiusuari XMPP integrada a YunoHost.", + "domain_config_acme_eligible": "Elegibilitat (per a l') ACME", + "domain_config_auth_token": "Token d'autenticació", + "domain_config_cert_summary_abouttoexpire": "El certificat actual està a punt de caducar. Aviat s'hauria de renovar automàticament.", + "domain_config_cert_summary_expired": "CRÍTIC: el certificat actual no és vàlid! HTTPS no funcionarà en absolut!", + "domain_config_default_app_help": "Es redirigirà automàticament a aquesta aplicació en obrir aquest domini. Si no s'especifica cap aplicació, es redirigeix al formulari d'inici de sessió del portal de l'usuari.", + "domain_config_xmpp_help": "NB: algunes funcions XMPP requeriran que actualitzeu els vostres registres DNS i que regenereu el vostre certificat Let's Encrypt perquè estigui activat", + "domain_dns_push_failed": "L'actualització dels registres DNS ha fallat estrepitosament.", + "domain_dns_push_not_applicable": "La funció de configuració automàtica de DNS no és aplicable al domini {domain}. Hauríeu de configurar manualment els vostres registres DNS seguint la documentació a https://yunohost.org/dns_config.", + "migration_0023_not_enough_space": "Allibereu espai a {path} per executar la migració.", + "log_app_config_set": "Aplica la configuració a l'aplicació «{}»", + "log_domain_dns_push": "Envia registres DNS per al domini «{}»", + "global_settings_setting_nginx_compatibility": "Compatibilitat NGINX", + "migration_0021_still_on_buster_after_main_upgrade": "Alguna cosa ha fallat durant l'actualització principal, sembla que el sistema encara està a Debian Buster", + "migration_ldap_rollback_success": "El sistema s'ha revertit.", + "migration_0024_rebuild_python_venv_in_progress": "Ara s'està intentant reconstruir el virtualenv de Python per a «{app}»", + "migration_description_0023_postgresql_11_to_13": "Migra bases de dades de PostgreSQL 11 a 13", + "migration_description_0025_global_settings_to_configpanel": "Migra l'antiga nomenclatura de configuració global a la nova", + "migration_ldap_backup_before_migration": "Creant una còpia de seguretat de la configuració de les aplicacions i la base de dades LDAP abans de la migració real.", + "migration_ldap_can_not_backup_before_migration": "La còpia de seguretat del sistema no s'ha pogut completar abans que la migració fallés. Error: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "No s'ha pogut migrar… s'està intentant revertir el sistema.", + "password_confirmation_not_the_same": "La contrasenya i la confirmació no coincideixen", + "group_mailalias_add": "L'àlies de correu electrònic «{mail}» s'afegirà al grup «{group}»", + "group_mailalias_remove": "L'àlies de correu electrònic «{mail}» s'eliminarà del grup «{group}»", + "group_user_add": "L'usuari «{user}» s'afegirà al grup «{group}»", + "ldap_server_down": "No es pot arribar al servidor LDAP", + "log_settings_reset_all": "Restableix tots els paràmetres", + "log_settings_set": "Aplica la configuració", + "migration_0021_system_not_fully_up_to_date": "El vostre sistema no està completament actualitzat. Realitzeu una actualització regular abans d'executar la migració.", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 està instal·lat, però no PostgreSQL 13!? Alguna cosa estranya podria haver passat al vostre sistema :(…", + "migration_description_0021_migrate_to_bullseye": "Actualitzeu el sistema a Debian Bullseye i YunoHost 11.x", + "user_import_failed": "L'operació d'importació dels usuaris ha fallat completament", + "migration_0023_postgresql_11_not_installed": "PostgreSQL no estava instal·lat al vostre sistema. Res a fer.", + "ldap_server_is_down_restart_it": "El servei LDAP està inactiu, intenteu reiniciar-lo…", + "dyndns_set_recovery_password_invalid_password": "No s'ha pogut establir la contrasenya de recuperació: la contrasenya no és prou forta", + "dyndns_set_recovery_password_unknown_domain": "No s'ha pogut establir la contrasenya de recuperació: domini no registrat", + "dyndns_unsubscribe_already_unsubscribed": "El domini ja està cancel·lat", + "dyndns_unsubscribe_denied": "No s'ha pogut cancel·lar la subscripció del domini: credencials no vàlides", + "dyndns_unsubscribe_failed": "No s'ha pogut cancel·lar la subscripció al domini DynDNS: {error}", + "dyndns_unsubscribed": "S'ha cancel·lat la subscripció al domini DynDNS", + "global_settings_setting_ssh_password_authentication": "Autenticació de contrasenya", + "global_settings_setting_ssh_port": "Port SSH", + "global_settings_setting_pop3_enabled": "Activa POP3", + "global_settings_setting_portal_theme_help": "Més informació sobre la creació de temes de portal personalitzats a https://yunohost.org/theming", + "global_settings_setting_postfix_compatibility": "Compatibilitat Postfix", + "global_settings_setting_security_experimental_enabled_help": "Activa les funcions de seguretat experimentals (no l'habilites si no saps què estàs fent!)", + "global_settings_setting_smtp_relay_host": "Amfitrió de retransmissió SMTP", + "global_settings_setting_smtp_relay_enabled": "Activa la retransmissió SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilitat SSH", + "global_settings_setting_ssh_password_authentication_help": "Permet l'autenticació de contrasenya per a SSH", + "global_settings_setting_user_strength_help": "Aquests requisits només s'apliquen en inicialitzar o canviar la contrasenya", + "global_settings_setting_webadmin_allowlist_enabled": "Activa la lista d'IPs permeses de Webadmin", + "visitors": "Visitants", + "group_user_remove": "L'usuari «{user}» s'eliminarà del grup «{group}»", + "invalid_number_min": "Ha de ser superior a {min}", + "migration_0021_start": "Començant la migració", + "global_settings_setting_webadmin_allowlist_help": "Adreces IP permeses per accedir a Webadmin. La notació CIDR està permesa.", + "app_corrupt_source": "YunoHost ha pogut baixar el recurs «{source_id}» ({url}) per a {app}, però el recurs no coincideix amb la suma de comprovació esperada. Això podria significar que s'ha produït una fallada temporal de la xarxa al vostre servidor, O el responsable de manteniment (o un actor maliciós?) ha canviat d'alguna manera l'actiu i els empaquetadors de YunoHost han d'investigar i actualitzar el manifest de l'aplicació per reflectir aquest canvi.\n Suma de comprovació esperada de sha256: {expected_sha256}\n Suma de comprovació sha256 baixada: {computed_sha256}\n Mida del fitxer baixat: {size}", + "migration_0021_yunohost_upgrade": "S'està iniciant l'actualització de YunoHost…", + "migration_0024_rebuild_python_venv_broken_app": "S'ha omès {app} perquè virtualenv no es pot reconstruir fàcilment per a aquesta aplicació. En comptes d'això, hauríeu de solucionar la situació forçant l'actualització d'aquesta aplicació mitjançant «yunohost app upgrade --force {app}».", + "pattern_fullname": "Ha de ser un nom complet vàlid (almenys tres caràcters)", + "dyndns_subscribe_failed": "No s'ha pogut subscriure el domini DynDNS: {error}", + "invalid_number_max": "Ha de ser inferior a {max}", + "log_domain_config_set": "Actualitza la configuració del domini «{}»", + "migration_0021_patching_sources_list": "Corregint les llistes de fonts…", + "domain_dns_push_success": "Registres DNS actualitzats!", + "domain_dns_pushing": "S'estan enviant els registres DNS…", + "domain_dns_registrar_experimental": "Fins ara, la interfície amb l'API de **{registrar}** no ha estat provada i revisada adequadament per la comunitat YunoHost. El suport és **molt experimental**: aneu amb compte!", + "domain_dns_registrar_managed_in_parent_domain": "Aquest domini és un subdomini de {parent_domain_link}. La configuració del registrador de DNS s'ha de gestionar al tauler de configuració de {parent_domain}.", + "domain_dns_registrar_not_supported": "YunoHost no ha pogut detectar automàticament el registrador que gestiona aquest domini. Hauríeu de configurar manualment les vostres entrades DNS seguint la documentació a https://yunohost.org/dns .", + "domain_dns_registrar_supported": "YunoHost va detectar automàticament que aquest domini el gestiona el registrador **{registrar}**. Si voleu, YunoHost configurarà automàticament aquesta zona DNS, si li proporcioneu les credencials API adequades. Podeu trobar documentació sobre com obtenir les vostres credencials de l'API en aquesta pàgina: https://yunohost.org/registar_api_{registrar}. (També podeu configurar manualment les vostres entrades DNS seguint la documentació a https://yunohost.org/dns )", + "domain_dns_registrar_yunohost": "Aquest domini és un nohost.me / nohost.st / ynh.fr i, per tant, la seva configuració DNS la gestiona automàticament YunoHost sense cap altra configuració. (vegeu l'ordre «yunohost dyndns update»)", + "domain_registrar_is_not_configured": "El registrador encara no està configurat per al domini {domain}.", + "domain_remove_confirm_apps_removal": "Si suprimiu aquest domini, s'eliminaran aquestes aplicacions:\n {apps}\n\n Estàs segur que vols fer-ho? [{answers}]", + "domain_unknown": "Domini «{domain}» desconegut", + "dyndns_no_recovery_password": "No s'ha especificat cap contrasenya de recuperació! En cas que perdeu el control d'aquest domini, haureu de contactar amb un administrador de l'equip de YunoHost!", + "dyndns_set_recovery_password_denied": "No s'ha pogut establir la contrasenya de recuperació: clau no vàlida", + "dyndns_set_recovery_password_failed": "No s'ha pogut establir la contrasenya de recuperació: {error}", + "global_settings_setting_portal_theme": "Tema del portal", + "global_settings_setting_smtp_allow_ipv6": "Permet IPv6", + "global_settings_setting_ssh_port_help": "Es prefereix un port inferior a 1024 per evitar intents d'usurpació per part de serveis que no són administradors a la màquina remota. També hauríeu d'evitar utilitzar un port que ja s'utilitza, com ara 80 o 443.", + "log_backup_create": "Crea un arxiu de còpia de seguretat", + "service_description_postgresql": "Emmagatzema dades de l'aplicació (base de dades SQL)", + "service_description_yunomdns": "Us permet arribar al vostre servidor mitjançant 'yunohost.local' a la vostra xarxa local", + "dyndns_set_recovery_password_success": "S'ha establert la contrasenya de recuperació!", + "dyndns_subscribed": "Domini DynDNS subscrit", + "global_settings_reset_success": "Restableix la configuració global", + "global_settings_setting_backup_compress_tar_archives": "Comprimir còpies de seguretat", + "global_settings_setting_dns_exposure_help": "Nota: això només afecta la configuració de DNS recomanada i les comprovacions de diagnòstic. Això no afecta les configuracions del sistema.", + "global_settings_setting_nginx_redirect_to_https": "Força HTTPS", + "global_settings_setting_passwordless_sudo": "Permet als administradors utilitzar «sudo» sense tornar a escriure les seves contrasenyes", + "global_settings_setting_pop3_enabled_help": "Habilita el protocol POP3 per al servidor de correu", + "global_settings_setting_security_experimental_enabled": "Característiques de seguretat experimentals", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permet que només algunes IPs accedeixin a Webadmin.", + "group_no_change": "No hi ha res a canviar per al grup «{group}»", + "group_update_aliases": "S'estan actualitzant els àlies per al grup «{group}»", + "invalid_shell": "Shell no vàlid: {shell}", + "ldap_attribute_already_exists": "L'atribut LDAP «{attribute}» ja existeix amb el valor «{value}»", + "log_resource_snippet": "Aprovisionament/desprovisionament/actualització d'un recurs", + "migration_0021_cleaning_up": "Netejar la memòria cau i els paquets que ja no són útils…", + "migration_0021_modified_files": "Tingueu en compte que s'ha trobat que els fitxers següents s'han modificat manualment i es poden sobreescriure després de l'actualització: {manually_modified_files}", + "migration_0024_rebuild_python_venv_disclaimer_base": "Després de l'actualització, algunes aplicacions de Python s'han de reconstruir parcialment per convertir-se a la nova versió de Python distribuïda amb Debian (en termes tècnics: cal recrear el que s'anomena «virtualenv»). Mentrestant, aquestes aplicacions Python poden no funcionar. YunoHost pot intentar reconstruir el virtualenv per a alguns d'ells, tal com es detalla a continuació. Per a altres aplicacions, o si l'intent de reconstrucció falla, haureu de forçar manualment una actualització d'aquestes aplicacions.", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Els virtualenvs no es poden reconstruir automàticament per a aquestes aplicacions. Heu de forçar una actualització per a aquestes, que es pot fer des de la línia d'ordres amb: «yunohost app upgrade --force APP»: {ignored_apps}", + "migration_description_0022_php73_to_php74_pools": "Migra els fitxers de configuració «pool» php7.3-fpm a php7.4", + "migration_description_0024_rebuild_python_venv": "Repara l'aplicació Python després de la migració", + "other_available_options": "… i {n} altres opcions disponibles no es mostren", + "permission_cant_add_to_all_users": "El permís {permission} no es pot afegir a tots els usuaris.", + "dyndns_too_many_requests": "El servei dyndns de YunoHost ha rebut massa sol·licituds de la vostra part, espereu una hora més o menys abans de tornar-ho a provar.", + "global_settings_setting_admin_strength_help": "Aquests requisits només s'apliquen en inicialitzar o canviar la contrasenya", + "global_settings_setting_dns_exposure": "Versions IP a tenir en compte per a la configuració i el diagnòstic de DNS", + "global_settings_setting_nginx_redirect_to_https_help": "Redirigeix les sol·licituds HTTP a HTTPs de manera predeterminada (NO HO DESACTIVEU tret que sapigueu realment què esteu fent!)", + "global_settings_setting_root_access_explain": "Als sistemes Linux, «root» és l'administrador absolut. En el context de YunoHost, l'inici de sessió SSH «root» directe està desactivat per defecte, excepte des de la xarxa local del servidor. Els membres del grup «admins» poden utilitzar l'ordre sudo per actuar com a root des de la línia d'ordres. Tanmateix, pot ser útil tenir una contrasenya de root (robusta) per depurar el sistema si per algun motiu els administradors habituals ja no poden iniciar sessió.", + "global_settings_setting_root_password": "Nova contrasenya de root", + "global_settings_setting_root_password_confirm": "Nova contrasenya de root (confirmeu)", + "global_settings_setting_ssowat_panel_overlay_enabled": "Habiliteu el petit quadrat de drecera del portal «YunoHost» a les aplicacions", + "global_settings_setting_webadmin_allowlist": "Llista d'IPs permeses de Webadmin", + "invalid_credentials": "La contrasenya o el nom d'usuari no són vàlids", + "log_dyndns_unsubscribe": "Cancel·la la subscripció a un subdomini de YunoHost «{}»", + "log_settings_reset": "Restableix la configuració", + "log_user_import": "Importa usuaris", + "migration_0021_general_warning": "Tingueu en compte que aquesta migració és una operació delicada. L'equip de YunoHost ha fet tot el possible per revisar-la i provar-la, però la migració encara podria trencar parts del sistema o de les seves aplicacions.\n\n Per tant, es recomana:\n - Feu una còpia de seguretat de qualsevol dada o aplicació crítica. Més informació a https://yunohost.org/backup ;\n - Tingueu paciència després d'iniciar la migració: depenent de la vostra connexió a Internet i el maquinari, l'actualització completa pot prendre fins a unes hores.", + "migration_0021_main_upgrade": "S'està iniciant l'actualització principal…", + "migration_0021_not_buster2": "La distribució actual de Debian no és Buster! Si ja heu executat la migració Buster->Bullseye, aquest error és simptomàtic del fet que el procediment de migració no acabà correctament (en cas contrari, YunoHost l'hauria marcat com a completada). Es recomana investigar què va passar amb l'equip de suport, que necessitarà el registre **complet** de la migració, que es pot trobar a Eines > Registres a Webadmin.", + "migration_0021_not_enough_free_space": "L'espai lliure és bastant baix a /var/! Hauríeu de tenir almenys 1 GB lliure per executar aquesta migració.", + "migration_0021_patch_yunohost_conflicts": "S'està aplicant el pegat per solucionar el problema del conflicte…", + "root_password_changed": "la contrasenya de root s'ha canviat", + "tools_upgrade": "Actualitzant paquets del sistema", + "user_import_success": "Els usuaris s'han importat correctament", + "migration_0021_problematic_apps_warning": "Tingueu en compte que s'han detectat les següents aplicacions instal·lades possiblement problemàtiques. Sembla que no s'han instal·lat des del catàleg d'aplicacions de YunoHost o no estan marcades com a «funcionant». En conseqüència, no es pot garantir que encara funcionin després de l'actualització: {problematic_apps}", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "S'intentarà reconstruir el virtualenv per a les aplicacions següents (Nota: l'operació pot trigar una mica!): {rebuild_apps}", + "migration_0024_rebuild_python_venv_failed": "No s'ha pogut reconstruir el virtualenv de Python per a {app}. És possible que l'aplicació no funcioni mentre no es resolgui. Hauríeu d'arreglar la situació forçant l'actualització d'aquesta aplicació mitjançant «yunohost app upgrade --force {app}».", + "migration_description_0026_new_admins_group": "Migra al nou sistema d'«administradors múltiples»", + "password_too_long": "Si us plau, trieu una contrasenya de menys de 127 caràcters", + "restore_backup_too_old": "Aquest arxiu de còpia de seguretat no es pot restaurar perquè prové d'una versió de YunoHost massa antiga.", + "service_not_reloading_because_conf_broken": "No s'està tornant a carregar/reiniciar el servei «{name}» perquè la seva configuració està trencada: {errors}", + "tools_upgrade_failed": "No s'han pogut actualitzar els paquets: {packages_list}", + "user_import_bad_file": "El vostre fitxer CSV no té el format correcte, s'ignorarà per evitar possibles pèrdues de dades", + "user_import_bad_line": "Línia {line} incorrecta: {details}", + "user_import_missing_columns": "Falten les columnes següents: {columns}", + "user_import_nothing_to_do": "No cal importar cap usuari", + "user_import_partial_failed": "L'operació d'importació dels usuaris ha fallat parcialment", + "domain_dns_push_record_failed": "No s'ha pogut {action} el registre {type}/{name}: {error}", + "registrar_infos": "Informació del registrador" } \ No newline at end of file diff --git a/locales/cs.json b/locales/cs.json index 680d54743..6d0160c3f 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -41,7 +41,7 @@ "group_cannot_edit_visitors": "Skupina 'visitors' nemůže být upravena. Jde o speciální skupinu představující anonymní (neregistrované na YunoHost) návštěvníky", "group_creation_failed": "Nelze založit skupinu '{group}': {error}", "group_created": "Skupina '{group}' vytvořena", - "group_already_exist_on_system_but_removing_it": "Skupina {group} se již nalézá v systémových skupinách, ale YunoHost ji odstraní...", + "group_already_exist_on_system_but_removing_it": "Skupina {group} se již nalézá v systémových skupinách, ale YunoHost ji odstraní…", "group_already_exist_on_system": "Skupina {group} se již nalézá v systémových skupinách", "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).", @@ -54,7 +54,7 @@ "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_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ů." diff --git a/locales/de.json b/locales/de.json index b61d0a431..957bd7dc2 100644 --- a/locales/de.json +++ b/locales/de.json @@ -19,38 +19,34 @@ "ask_password": "Passwort", "backup_app_failed": "Konnte keine Sicherung für {app} erstellen", "backup_archive_app_not_found": "{app} konnte in keiner Datensicherung gefunden werden", - "backup_archive_name_exists": "Datensicherung mit dem selben Namen existiert bereits.", + "backup_archive_name_exists": "Eine Datensicherung mit dem Namen '{name}' existiert bereits.", "backup_archive_name_unknown": "Unbekanntes lokale Datensicherung mit Namen '{name}' gefunden", "backup_archive_open_failed": "Kann Sicherungsarchiv nicht öfnen", "backup_cleaning_failed": "Temporäres Sicherungsverzeichnis konnte nicht geleert werden", - "backup_created": "Datensicherung komplett", + "backup_created": "Datensicherung vollständig: {name}", "backup_delete_error": "Pfad '{path}' konnte nicht gelöscht werden", - "backup_deleted": "Backup wurde entfernt", + "backup_deleted": "Backup gelöscht: {name}", "backup_hook_unknown": "Der Datensicherungshook '{hook}' unbekannt", "backup_nothings_done": "Keine Änderungen zur Speicherung", "backup_output_directory_forbidden": "Wähle ein anderes Ausgabeverzeichnis. Datensicherungen können nicht in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var oder in Unterordnern von /home/yunohost.backup/archives erstellt werden", "backup_output_directory_not_empty": "Der gewählte Ausgabeordner sollte leer sein", "backup_output_directory_required": "Für die Datensicherung muss ein Zielverzeichnis angegeben werden", - "backup_running_hooks": "Datensicherunghook wird ausgeführt...", - "custom_app_url_required": "Du musst eine URL angeben, um deine benutzerdefinierte App {app} zu aktualisieren", + "backup_running_hooks": "Datensicherunghook wird ausgeführt…", + "custom_app_url_required": "Sie müssen eine URL angeben, um Ihre benutzerdefinierte App {app} zu aktualisieren", "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_created": "Domäne erstellt", - "domain_creation_failed": "Konnte Domäne nicht erzeugen", + "domain_creation_failed": "Konnte Domäne {domain} nicht erzeugen: {error}", "domain_deleted": "Domain wurde gelöscht", "domain_deletion_failed": "Domain {domain}: {error} konnte nicht gelöscht werden", "domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domäne registriert", - "domain_dyndns_root_unknown": "Unbekannte DynDNS Hauptdomain", "domain_exists": "Die Domäne existiert bereits", "domain_uninstall_app_first": "Diese Applikationen sind noch auf deiner Domäne installiert; \n{apps}\n\nBitte deinstalliere sie mit dem Befehl 'yunohost app remove the_app_id' oder verschiebe sie mit 'yunohost app change-url the_app_id'", "done": "Erledigt", - "downloading": "Wird heruntergeladen...", + "downloading": "Wird heruntergeladen…", "dyndns_ip_update_failed": "Konnte die IP-Adresse für DynDNS nicht aktualisieren", "dyndns_ip_updated": "Deine IP-Adresse wurde bei DynDNS aktualisiert", - "dyndns_key_generating": "Generierung des DNS-Schlüssels..., das könnte eine Weile dauern.", - "dyndns_registered": "DynDNS Domain registriert", - "dyndns_registration_failed": "DynDNS Domain konnte nicht registriert werden: {error}", "dyndns_unavailable": "Die Domäne {domain} ist nicht verfügbar.", - "extracting": "Wird entpackt...", + "extracting": "Wird entpackt…", "field_invalid": "Feld '{}' ist unbekannt", "firewall_reload_failed": "Firewall konnte nicht neu geladen werden", "firewall_reloaded": "Firewall neu geladen", @@ -67,11 +63,11 @@ "mail_forward_remove_failed": "Die Weiterleitungs-E-Mail '{mail}' konnte nicht gelöscht werden", "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden", "main_domain_changed": "Die Hauptdomain wurde geändert", - "pattern_backup_archive_name": "Es muss ein gültiger Dateiname mit maximal 30 Zeichen sein, nur alphanumerische Zeichen und -_.", + "pattern_backup_archive_name": "Muss ein gültiger Dateiname mit maximal 30 Zeichen sein, ausschließlich alphanumerische Zeichen und -_.", "pattern_domain": "Muss ein gültiger Domainname sein (z.B. meine-domain.org)", "pattern_email": "Es muss sich um eine gültige E-Mail-Adresse handeln, ohne '+'-Symbol (z. B. name@domäne.de)", - "pattern_firstname": "Muss ein gültiger Vorname sein", - "pattern_lastname": "Muss ein gültiger Nachname sein", + "pattern_firstname": "Muss ein gültiger Vorname sein (mindestens 3 Zeichen)", + "pattern_lastname": "Muss ein gültiger Nachname sein (mindestens 3 Zeichen)", "pattern_mailbox_quota": "Es muss eine Größe mit dem Suffix b/k/M/G/T sein oder 0 um kein Kontingent zu haben", "pattern_password": "Muss mindestens drei Zeichen lang sein", "pattern_port_or_range": "Muss ein valider Port (z.B. 0-65535) oder ein Bereich (z.B. 100:200) sein", @@ -79,14 +75,14 @@ "port_already_closed": "Der Port {port} wurde bereits für {ip_version} Verbindungen geschlossen", "port_already_opened": "Der Port {port} wird bereits von {ip_version} benutzt", "restore_already_installed_app": "Eine Applikation mit der ID '{app}' ist bereits installiert", - "restore_cleaning_failed": "Das temporäre Dateiverzeichnis für Systemrestaurierung konnte nicht gelöscht werden", + "restore_cleaning_failed": "Das temporäre Dateiverzeichnis für die Systemwiederherstellung konnte nicht gelöscht werden", "restore_complete": "Vollständig wiederhergestellt", - "restore_confirm_yunohost_installed": "Möchtest du die Wiederherstellung wirklich starten? [{answers}]", + "restore_confirm_yunohost_installed": "Möchten Sie die Wiederherstellung wirklich starten? [{answers}]", "restore_failed": "System konnte nicht wiederhergestellt werden", - "restore_hook_unavailable": "Das Wiederherstellungsskript für '{part}' steht weder in deinem System noch im Archiv zur Verfügung", + "restore_hook_unavailable": "Das Wiederherstellungsskript für '{part}' steht weder in Ihrem System noch im Archiv zur Verfügung", "restore_nothings_done": "Nichts wurde wiederhergestellt", - "restore_running_app_script": "App '{app}' wird wiederhergestellt...", - "restore_running_hooks": "Wiederherstellung wird gestartet...", + "restore_running_app_script": "App '{app}' wird wiederhergestellt…", + "restore_running_hooks": "Wiederherstellung wird gestartet…", "service_add_failed": "Der Dienst '{service}' konnte nicht hinzugefügt werden", "service_added": "Der Dienst '{service}' wurde erfolgreich hinzugefügt", "service_already_started": "Der Dienst '{service}' läuft bereits", @@ -127,22 +123,22 @@ "user_updated": "Kontoinformationen wurden aktualisiert", "yunohost_already_installed": "YunoHost ist bereits installiert", "yunohost_configured": "YunoHost ist nun konfiguriert", - "yunohost_installing": "YunoHost wird installiert...", + "yunohost_installing": "YunoHost wird installiert…", "yunohost_not_installed": "YunoHost ist nicht oder unvollständig installiert worden. Bitte 'yunohost tools postinstall' ausführen", "app_not_properly_removed": "{app} wurde nicht ordnungsgemäß entfernt", "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 Voraussetzungen für {app}...", + "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:", "dyndns_key_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden", "dyndns_no_domain_registered": "Keine Domain mit DynDNS registriert", "mailbox_used_space_dovecot_down": "Der Dovecot-Mailbox-Dienst muss aktiv sein, wenn du den von der Mailbox belegten Speicher abrufen willst", - "certmanager_attempt_to_replace_valid_cert": "Du versuchst gerade eine richtiges und gültiges Zertifikat der Domain {domain} zu überschreiben! (Benutze --force , um diese Nachricht zu umgehen)", - "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain} ist kein selbstsigniertes Zertifikat. Bist du sich sicher, dass du es ersetzen willst? (Benutze dafür '--force')", - "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain} ist fehlgeschlagen...", + "certmanager_attempt_to_replace_valid_cert": "Sie versuchen gerade ein gutes und gültiges Zertifikat der Domäne {domain} zu überschreiben! (Benutzen Sie --force , um diese Nachricht zu umgehen)", + "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domäne {domain} ist kein selbstsigniertes Zertifikat. Sind Sie sicher, dass Sie es ersetzen möchten? (Verwenden Sie dafür '--force')", + "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain} ist fehlgeschlagen…", "certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain '{domain}' wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!", "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)", "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.)", @@ -156,18 +152,18 @@ "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 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_acme_not_configured_for_domain": "Die ACME-Challenge für {domain} kann momentan nicht ausgeführt werden, weil in Ihrer nginx-Konfiguration das entsprechende Code-Snippet fehlt… Bitte stellen Sie sicher, dass Ihre nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file})", "domain_hostname_failed": "Neuer Hostname wurde nicht gesetzt. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", - "app_already_installed_cant_change_url": "Diese Applikation ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.", + "app_already_installed_cant_change_url": "Diese Applikation ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfen Sie ob `app changeurl` verfügbar ist.", "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain} {path}'). Es gibt nichts zu tun.", "app_already_up_to_date": "{app} ist bereits aktuell", "backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt", - "backup_applying_method_tar": "Erstellen des Backup-tar Archives...", - "backup_applying_method_copy": "Kopiere alle Dateien ins Backup...", + "backup_applying_method_tar": "Erstellen des Backup-tar Archives…", + "backup_applying_method_copy": "Kopiere alle Dateien ins Backup…", "app_change_url_no_script": "Die Applikation '{app_name}' unterstützt bisher keine URL-Modifikation. Vielleicht sollte sie aktualisiert werden.", "app_location_unavailable": "Diese URL ist nicht verfügbar oder wird von einer installierten Applikation genutzt:\n{apps}", - "backup_applying_method_custom": "Rufe die benutzerdefinierte Backup-Methode '{method}' auf...", + "backup_applying_method_custom": "Rufe die benutzerdefinierte Backup-Methode '{method}' auf…", "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}", @@ -177,15 +173,15 @@ "domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt dir die *empfohlene* Konfiguration. Er konfiguriert *nicht* das DNS für dich. Es liegt in deiner Verantwortung, die DNS-Zone bei deinem DNS-Registrar nach dieser Empfehlung zu konfigurieren.", "dpkg_lock_not_available": "Dieser Befehl kann momentan nicht ausgeführt werden, da anscheinend ein anderes Programm die Sperre von dpkg (dem Systempaket-Manager) verwendet", "confirm_app_install_thirdparty": "Warnung! Diese Applikation ist nicht Teil des App-Katalogs von YunoHost. Die Installation von Drittanbieter Applikationen kann die Integrität und Sicherheit Ihres Systems gefährden. Sie sollten sie NICHT installieren, wenn Sie nicht wissen, was Sie tun. Es wird KEIN SUPPORT geleistet, wenn diese Applikation nicht funktioniert oder Ihr System beschädigt! Wenn Sie dieses Risiko trotzdem eingehen wollen, geben Sie '{answers}' ein", - "confirm_app_install_danger": "WARNUNG! Diese Applikation ist noch experimentell (wenn nicht sogar ausdrücklich nicht funktionsfähig)! Du solltest sie wahrscheinlich NICHT installieren, es sei denn, du weißt, was du tust. Es wird keine Unterstützung angeboten, falls diese Applikation nicht funktionieren oder dein System beschädigen sollte... Falls du bereit bist, dieses Risiko einzugehen, tippe '{answers}'", + "confirm_app_install_danger": "WARNUNG! Diese Applikation ist noch experimentell (wenn nicht sogar ausdrücklich nicht funktionsfähig)! Sie sollten sie wahrscheinlich NICHT installieren, es sei denn, Sie wissen, was Sie tun. Es wird keine Unterstützung angeboten, falls diese Applikation nicht funktionieren oder Ihr System beschädigen sollte… Falls Sie bereit sind, dieses Risiko einzugehen, tippen Sie '{answers}'", "confirm_app_install_warning": "Warnung: Diese Applikation funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single-Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers}] ", "backup_with_no_restore_script_for_app": "{app} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.", "backup_with_no_backup_script_for_app": "Die App {app} hat kein Sicherungsskript. Ignoriere es.", "backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden", "backup_system_part_failed": "Der Systemteil '{part}' konnte nicht gesichert werden", "backup_permission": "Sicherungsberechtigung für {app}", - "backup_output_symlink_dir_broken": "Dein Archivverzeichnis '{path}' ist ein fehlerhafter Symlink. Vielleicht hast du vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.", - "backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten...", + "backup_output_symlink_dir_broken": "Ihr Archivverzeichnis '{path}' ist ein fehlerhafter Symlink. Vielleicht haben Sie vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.", + "backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten…", "backup_method_tar_finished": "Tar-Backup-Archiv erstellt", "backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method}' beendet", "backup_method_copy_finished": "Sicherungskopie beendet", @@ -193,21 +189,21 @@ "backup_custom_backup_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Sicherung\" ein Fehler aufgetreten", "backup_csv_creation_failed": "Die zur Wiederherstellung erforderliche CSV-Datei kann nicht erstellt werden", "backup_couldnt_bind": "{src} konnte nicht an {dest} angebunden werden.", - "backup_ask_for_copying_if_needed": "Möchtest du die Sicherung mit {size}MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)", - "backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien...", + "backup_ask_for_copying_if_needed": "Möchten Sie die Datensicherung mit {size}MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)", + "backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien…", "ask_new_path": "Neuer Pfad", "ask_new_domain": "Neue Domain", "app_upgrade_some_app_failed": "Einige Applikationen können nicht aktualisiert werden", - "app_upgrade_app_name": "{app} wird jetzt aktualisiert...", + "app_upgrade_app_name": "{app} wird jetzt aktualisiert…", "app_upgrade_several_apps": "Die folgenden Apps werden aktualisiert: {apps}", - "app_start_restore": "{app} wird wiederhergestellt...", - "app_start_backup": "Sammeln von Dateien, die für {app} gesichert werden sollen...", - "app_start_remove": "{app} wird entfernt...", - "app_start_install": "{app} wird installiert...", + "app_start_restore": "{app} wird wiederhergestellt…", + "app_start_backup": "Sammeln von Dateien, die für {app} gesichert werden sollen…", + "app_start_remove": "{app} wird entfernt…", + "app_start_install": "{app} wird installiert…", "app_not_upgraded": "Die App '{failed_app}' konnte nicht aktualisiert werden. Infolgedessen wurden die folgenden App-Upgrades abgebrochen: {apps}", "app_make_default_location_already_used": "Die App \"{app}\" kann nicht als Standard für die Domain \"{domain}\" festgelegt werden. Sie wird bereits von \"{other_app}\" verwendet", "aborting": "Breche ab.", - "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuche, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).", + "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuchen Sie, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).", "already_up_to_date": "Nichts zu tun. Alles ist bereits auf dem neusten Stand.", "app_action_broke_system": "Diese Aktion scheint diese wichtigen Dienste unterbrochen zu haben: {services}", "apps_already_up_to_date": "Alle Apps sind bereits aktuell", @@ -216,24 +212,24 @@ "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.", "group_created": "Gruppe '{group}' angelegt", - "group_creation_failed": "Konnte Gruppe '{group}' nicht anlegen", + "group_creation_failed": "Konnte Gruppe '{group}' nicht anlegen: {error}", "group_unknown": "Die Gruppe '{group}' ist unbekannt", "group_updated": "Gruppe '{group}' erneuert", "group_update_failed": "Kann Gruppe '{group}' nicht aktualisieren: {error}", "log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende 'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen", "log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen", - "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.", + "dpkg_is_broken": "Sie können dies gerade nicht machen, weil dpkg/APT (der Paketmanager des Systems) in einem defekten Zustand zu sein scheint… Sie können versuchen, dieses Problem zu lösen, indem Sie sich über SSH mit dem Server verbinden und `sudo apt install --fix-broken` und/oder `sudo dpkg --configure -a` und/oder `sudo dpkg --audit`ausführen.", "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log show {name}'", "log_app_remove": "Entferne die Applikation '{}'", "log_app_install": "Installiere die Applikation '{}'", "log_app_upgrade": "Upgrade der Applikation '{}'", - "good_practices_about_admin_password": "Du bist nun dabei, ein neues Administratorpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", + "good_practices_about_admin_password": "Sie sind nun dabei, ein neues Administratorpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''", "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log share {name}', um Hilfe zu erhalten", "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht", "log_app_change_url": "Ändere die URL der Applikation '{}'", - "good_practices_about_user_password": "Du bist nun dabei, ein neues Nutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", + "good_practices_about_user_password": "Sie sind nun dabei, ein neues Benutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - es ist jedoch empfehlenswert, ein längeres Passwort (z.B. eine Passphrase) und/oder verschiedene Arten von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_link_to_failed_log": "Der Vorgang konnte nicht abgeschlossen werden '{desc}'. Bitte gib das vollständige Protokoll dieser Operation mit Klicken Sie hier an, um Hilfe zu erhalten", "backup_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden", "backup_csv_addition_failed": "Es konnten keine Dateien zur Sicherung in die CSV-Datei hinzugefügt werden", @@ -242,15 +238,15 @@ "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.", "app_install_failed": "Installation von {app} fehlgeschlagen: {error}", "app_install_script_failed": "Im Installationsscript ist ein Fehler aufgetreten", - "app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation...", + "app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation…", "app_upgrade_script_failed": "Es ist ein Fehler im App-Upgrade-Skript aufgetreten", "diagnosis_basesystem_host": "Server läuft unter Debian {debian_version}", "diagnosis_basesystem_kernel": "Server läuft unter Linux-Kernel {kernel_version}", "diagnosis_basesystem_ynh_single_version": "{package} Version: {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Server läuft YunoHost {main_version} ({repo})", - "diagnosis_basesystem_ynh_inconsistent_versions": "Du verwendest inkonsistente Versionen der YunoHost-Pakete... wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Sie verwenden inkonsistente Versionen der YunoHost-Pakete… wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.", "apps_catalog_init_success": "App-Katalogsystem initialisiert!", - "apps_catalog_updating": "Aktualisierung des Applikationskatalogs...", + "apps_catalog_updating": "Aktualisierung des Applikationskatalogs…", "apps_catalog_failed_to_download": "Der {apps_catalog} App-Katalog kann nicht heruntergeladen werden: {error}", "apps_catalog_obsolete_cache": "Der Cache des App-Katalogs ist leer oder veraltet.", "apps_catalog_update_success": "Der Apps-Katalog wurde aktualisiert!", @@ -264,25 +260,25 @@ "diagnosis_ip_no_ipv6": "Der Server verfügt nicht über eine funktionierende IPv6-Adresse.", "diagnosis_ip_not_connected_at_all": "Der Server scheint überhaupt nicht mit dem Internet verbunden zu sein!?", "diagnosis_failed_for_category": "Diagnose fehlgeschlagen für die Kategorie '{category}': {error}", - "diagnosis_cache_still_valid": "(Cache noch gültig für {category} Diagnose. Es wird keine neue Diagnose durchgeführt!)", + "diagnosis_cache_still_valid": "(Der Cache für die {category} Diagnose ist noch gültig. Es wird keine neue Diagnose durchgeführt!)", "diagnosis_cant_run_because_of_dep": "Kann Diagnose für {category} nicht ausführen während wichtige Probleme zu {dep} noch nicht behoben sind.", - "diagnosis_found_errors_and_warnings": "Habe {errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", - "diagnosis_ip_broken_dnsresolution": "Domänennamen-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert vielleicht eine Firewall die DNS-Anfragen?", + "diagnosis_found_errors_and_warnings": "{errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", + "diagnosis_ip_broken_dnsresolution": "Domänennamen-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren… Blockiert vielleicht eine Firewall die DNS-Anfragen?", "diagnosis_ip_broken_resolvconf": "Domänen-Namensauflösung scheint nicht zu funktionieren, was daran liegen könnte, dass in /etc/resolv.conf kein Eintrag auf 127.0.0.1 zeigt.", "diagnosis_ip_weird_resolvconf_details": "Die Datei /etc/resolv.conf muss ein Symlink auf /etc/resolvconf/run/resolv.conf sein, welcher auf 127.0.0.1 (dnsmasq) zeigt. Falls du die DNS-Resolver manuell konfigurieren möchtest, bearbeite bitte /etc/resolv.dnsmasq.conf.", "diagnosis_dns_good_conf": "DNS Einträge korrekt konfiguriert für die Domäne {domain} (Kategorie {category})", "diagnosis_ignored_issues": "(+ {nb_ignored} ignorierte(s) Problem(e))", "diagnosis_basesystem_hardware": "Server Hardware Architektur ist {virt} {arch}", - "diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", + "diagnosis_found_errors": "{errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", "diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.", "diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!", "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber sei vorsichtig wenn du deine eigene /etc/resolv.conf verwendest.", - "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, kannst du zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues --human-readable' in der Kommandozeile ausführen.", + "diagnosis_display_tip": "Damit Sie die gefundenen Probleme anschauen können, gehen Sie zum Diagnose-Bereich des Admin-Panels, oder führen Sie 'yunohost diagnosis show --issues --human-readable' in der Kommandozeile aus.", "backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}", - "backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).", - "app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Am besten solltest du dein System aktualisieren.", + "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 sollten Sie Ihr System aktualisieren.", "certmanager_domain_not_diagnosed_yet": "Für die Domäne {domain} gibt es noch keine Diagnose-Resultate. Bitte wiederholen Sie die Diagnose für die Kategorien 'DNS-Einträge' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domäne für Let's Encrypt bereit ist. (Wenn Sie wissen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen.)", - "mail_unavailable": "Diese E-Mail Adresse ist reserviert und wird dem ersten Konto automatisch zugewiesen", + "mail_unavailable": "Diese E-Mail-Adresse ist für die Administratoren-Gruppe reserviert", "diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!", "diagnosis_services_running": "Dienst {service} läuft!", "diagnosis_domain_expires_in": "{domain} läuft in {days} Tagen ab.", @@ -290,31 +286,31 @@ "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", - "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls könntest Du mittels yunohost dyndns update --force ein Update erzwingen.", + "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von YunoHost verwaltet werden. Andernfalls können Sie mittels yunohost dyndns update --force ein Update erzwingen.", "diagnosis_dns_point_to_doc": "Bitte schauen Sie in der Dokumentation unter https://yunohost.org/dns_config nach, wenn Sie Hilfe bei der Konfiguration der DNS-Einträge benötigen.", "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", - "diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration solltest du einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", + "diagnosis_dns_missing_record": "Gemäss der empfohlenen DNS-Konfiguration sollten Sie einen DNS-Eintrag mit den folgenden Informationen hinzufügen.
Typ: {type}
Name: {name}
Wert: {value}", "diagnosis_dns_bad_conf": "Einige DNS-Einträge für die Domäne {domain} fehlen oder sind nicht korrekt (Kategorie {category})", "diagnosis_ip_local": "Lokale IP: {local}", "diagnosis_ip_global": "Globale IP: {global}", - "diagnosis_ip_no_ipv6_tip": "Ein funktionierendes IPv6 ist für den Betrieb Ihres Servers nicht zwingend erforderlich, aber es ist besser für das Funktionieren des Internets als Ganzes. IPv6 sollte normalerweise automatisch vom System oder Ihrem Provider konfiguriert werden, wenn es verfügbar ist. Andernfalls müssen Sie möglicherweise einige Dinge manuell konfigurieren, wie in der Dokumentation hier beschrieben: https://yunohost.org/#/ipv6. Wenn Sie IPv6 nicht aktivieren können oder wenn es Ihnen zu technisch erscheint, können Sie diese Warnung auch getrost ignorieren.", + "diagnosis_ip_no_ipv6_tip": "Ein funktionierendes IPv6 ist für den Betrieb Ihres Servers nicht zwingend erforderlich, aber es ist besser für das Funktionieren des Internets als Ganzes. IPv6 sollte normalerweise automatisch vom System oder Ihrem Provider konfiguriert werden, wenn es verfügbar ist. Andernfalls müssen Sie möglicherweise einige Dinge manuell konfigurieren, wie in der Dokumentation hier beschrieben: https://yunohost.org/ipv6. Wenn Sie IPv6 nicht aktivieren können oder wenn es Ihnen zu technisch erscheint, können Sie diese Warnung auch getrost ignorieren.", "diagnosis_services_bad_status_tip": "Du kannst versuchen, den Dienst neu zu starten, und wenn das nicht funktioniert, schaue dir die (Dienst-)Logs in der Verwaltung an (In der Kommandozeile kannst du dies mit yunohost service restart {service} und yunohost service log {service} tun).", "diagnosis_services_bad_status": "Der Dienst {service} ist {status} :(", - "diagnosis_diskusage_verylow": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Du solltest ernsthaft in Betracht ziehen, etwas Seicherplatz frei zu machen!", + "diagnosis_diskusage_verylow": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Sie sollten ernsthaft in Betracht ziehen, etwas Seicherplatz frei zu machen!", "diagnosis_http_ok": "Die Domäne {domain} ist über HTTP von außerhalb des lokalen Netzwerks erreichbar.", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, weil Ihnen die Netzneutralität nichts bedeutet.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine Alternative, welche die Privatsphäre berücksichtigt, wäre die Verwendung eines VPN *mit einer öffentlichen dedizierten IP* um solche Einschränkungen zu umgehen. Schauen Sie unter https://yunohost.org/#/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, weil Ihnen die Netzneutralität nichts bedeutet.
- Einige davon bieten als Alternative an, ein Mailserver-Relay zu verwenden, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine Alternative, welche die Privatsphäre berücksichtigt, wäre die Verwendung eines VPN *mit einer öffentlichen dedizierten IP* um solche Einschränkungen zu umgehen. Schauen Sie unter https://yunohost.org/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", "diagnosis_http_timeout": "Wartezeit wurde beim Versuch überschritten, von Aussen eine Verbindung zu Ihrem Server aufzubauen. Er scheint nicht erreichbar zu sein.
1. Die häufigste Ursache für dieses Problem ist, dass die Ports 80 und 433 nicht richtig zu Ihrem Server weitergeleitet werden.
2. Sie sollten zudem sicherstellen, dass der Dienst nginx läuft.
3. In komplexeren Umgebungen: Stellen Sie sicher, dass keine Firewall oder Reverse-Proxy stört .", "service_reloaded_or_restarted": "Der Dienst '{service}' wurde erfolgreich neu geladen oder gestartet", "service_restarted": "Der Dienst '{service}' wurde neu gestartet", - "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain}\" löst nicht zur gleichen IP Adresse auf wie \"{domain}\". Einige Funktionen sind nicht verfügbar bis du dies behebst und die Zertifikate neu erzeugst.", + "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain}\" löst nicht zur gleichen IP Adresse auf wie \"{domain}\". Einige Funktionen werden nicht verfügbar sein bis Sie dies behoben und die Zertifikate neu erzeugt haben.", "diagnosis_ports_ok": "Port {port} ist von Aussen erreichbar.", "diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})", "diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen, den ausgehenden Port 25 in Ihrer Router-Konfigurationsoberfläche oder in der Konfigurationsoberfläche Ihres Hosting-Anbieters zu öffnen. (Bei einigen Hosting-Anbietern kann es sein, dass man von Ihnen verlangt, dass Sie dafür ein Support-Ticket erstellen).", - "diagnosis_mail_ehlo_ok": "Der SMTP-Server ist von von außen erreichbar und darum auch in der Lage E-Mails zu empfangen!", + "diagnosis_mail_ehlo_ok": "Der SMTP-Server ist von Aussen erreichbar und darum auch in der Lage E-Mails zu empfangen!", "diagnosis_mail_ehlo_bad_answer": "Ein nicht-SMTP-Dienst antwortete auf Port 25 per IPv{ipversion}", "diagnosis_swap_notsomuch": "Das System hat nur {total} Swap. Du solltest dir überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", "diagnosis_swap_ok": "Das System hat {total} Swap!", - "diagnosis_swap_tip": "Bitte beachte, dass das Betreiben der Swap-Partition auf einer SD-Karte oder SSD die Lebenszeit dieser drastisch reduziert.", + "diagnosis_swap_tip": "Bitte wahren Sie Vorsicht und Aufmerksamkeit, dass das Betreiben der Swap-Partition auf einer SD-Karte oder einer SSD die Lebenszeit dieses Geräts drastisch reduzieren kann.", "diagnosis_mail_outgoing_port_25_ok": "Der SMTP-Server ist in der Lage E-Mails zu versenden (der ausgehende Port 25 ist nicht blockiert).", "diagnosis_mail_outgoing_port_25_blocked": "Der SMTP-Server kann keine E-Mails an andere Server senden, weil der ausgehende Port 25 per IPv{ipversion} blockiert ist. Du kannst versuchen, diesen in der Konfigurations-Oberfläche deines Internet-Anbieters (oder Hosters) zu öffnen.", "diagnosis_mail_ehlo_unreachable": "Der SMTP-Server ist von außen nicht erreichbar per IPv{ipversion}. Er wird nicht in der Lage sein E-Mails zu empfangen.", @@ -351,8 +347,8 @@ "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": "Reverse-DNS-Eintrag ist nicht korrekt konfiguriert für IPv{ipversion}. Einige E-Mails könnten eventuell nicht zugestellt oder als Spam markiert werden.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Einige Provider werden es Ihnen vermutlich nicht erlauben, den Reverse-DNS-Eintrag zu konfigurieren (oder vielleicht ist diese Funktion beschädigt...). Falls Sie Ihren Reverse-DNS-Eintrag für IPv4 korrekt konfiguriert haben, können Sie versuchen, die Verwendung von IPv6 für das Versenden von E-Mails auszuschalten, indem Sie den Befehl yunohost settings set smtp.allow_ipv6 -v off ausführen. Bemerkung: Die Folge dieser letzten Lösung ist, dass Sie mit Servern, welche nur über IPv6 verfügen, keine E-Mails mehr versenden oder empfangen können.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Einige Anbieter werden es nicht zulassen, den Reverse-DNS zu konfigurieren (oder diese Funktion ist defekt...). Falls du deswegen auf Probleme stoßen solltest, ziehe folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schaue hier nach https://yunohost.org/#/vpn_advantage
- Schließlich ist es auch möglich zu einem anderen Anbieter zu wechseln", + "diagnosis_mail_fcrdns_nok_alternatives_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 Provider werden Ihnen nicht erlauben, den Reverse-DNS zu konfigurieren (oder deren Funktionalität ist defekt…). Falls Sie deswegen auf Probleme stossen sollten, ziehen Sie folgende Lösungen in Betracht:
- Manche ISPs stellen als Alternative die Benutzung eines Mail-Server-Relays zur Verfügung, was jedoch mit sich zieht, dass das Relay Ihren E-Mail-Verkehr ausspionieren könnte.
- Eine privatsphärenfreundlichere Alternative ist die Benutzung eines VPN *mit einer dedizierten öffentlichen IP* um Einschränkungen dieser Art zu umgehen. Schauen Sie hier nach https://yunohost.org/vpn_advantage
- Schließlich ist es auch möglich, zu einem anderen Provider zu wechseln", "diagnosis_mail_queue_unavailable_details": "Fehler: {error}", "diagnosis_mail_queue_unavailable": "Die Anzahl der anstehenden Nachrichten in der Warteschlange kann nicht abgefragt werden", "diagnosis_mail_queue_ok": "{nb_pending} anstehende E-Mails in der Warteschlange", @@ -362,12 +358,12 @@ "diagnosis_http_partially_unreachable": "Die Domäne {domain} scheint von aussen via HTTP per IPv{failed} nicht erreichbar zu sein, obwohl es per IPv{passed} funktioniert.", "diagnosis_http_unreachable": "Die Domäne {domain} scheint von aussen per HTTP nicht erreichbar zu sein.", "diagnosis_http_connection_error": "Verbindungsfehler: konnte nicht zur angeforderten Domäne verbinden, es ist sehr wahrscheinlich, dass sie nicht erreichbat ist.", - "diagnosis_http_could_not_diagnose_details": "Fehler: {error}", + "diagnosis_http_could_not_diagnose_details": "Fehler: {error}", "diagnosis_http_could_not_diagnose": "Konnte nicht diagnostizieren, ob die Domäne von aussen per IPv{ipversion} erreichbar ist.", - "diagnosis_ports_partially_unreachable": "Port {port} ist von aussen per IPv{failed} nicht erreichbar.", - "diagnosis_ports_unreachable": "Port {port} ist von aussen nicht erreichbar.", + "diagnosis_ports_partially_unreachable": "Port {port} ist von Aussen her per IPv{failed} nicht erreichbar.", + "diagnosis_ports_unreachable": "Port {port} ist von Aussen her nicht erreichbar.", "diagnosis_ports_could_not_diagnose_details": "Fehler: {error}", - "diagnosis_security_vulnerable_to_meltdown_details": "Um dieses Problem zu beheben, solltest du dein System upgraden und neustarten um den neuen Linux-Kernel zu laden (oder deinen Server-Anbieter kontaktieren, falls das nicht funktionieren sollte). Besuche https://meltdownattack.com/ für weitere Informationen.", + "diagnosis_security_vulnerable_to_meltdown_details": "Um dieses Problem zu beheben, solltest Sie Ihr System upgraden und neustarten um den neuen Linux-Kernel zu laden (oder Ihren Server-Anbieter kontaktieren, falls das nicht funktionieren sollte). Besuchen Sie https://meltdownattack.com/ für weitere Informationen.", "diagnosis_ports_could_not_diagnose": "Konnte nicht diagnostizieren, ob die Ports von aussen per IPv{ipversion} erreichbar sind.", "diagnosis_description_regenconf": "Systemkonfiguration", "diagnosis_description_mail": "E-Mail", @@ -377,29 +373,29 @@ "diagnosis_description_dnsrecords": "DNS-Einträge", "diagnosis_description_ip": "Internetkonnektivität", "diagnosis_description_basesystem": "Grundsystem", - "diagnosis_security_vulnerable_to_meltdown": "Es scheint, als ob du durch die kritische Meltdown-Sicherheitslücke verwundbar bist", + "diagnosis_security_vulnerable_to_meltdown": "Es scheint als ob Sie durch die kritische Meltdown-Verwundbarkeit verwundbar sind", "diagnosis_regenconf_manually_modified": "Die Konfigurationsdatei {file} scheint manuell verändert worden zu sein.", - "diagnosis_regenconf_allgood": "Alle Konfigurationsdateien stimmen mit der empfohlenen Konfiguration überein!", + "diagnosis_regenconf_allgood": "Alle Konfigurationsdateien sind in Übereinstimmung mit der empfohlenen Konfiguration!", "diagnosis_package_installed_from_sury": "Einige System-Pakete sollten gedowngradet werden", "diagnosis_ports_forwarding_tip": "Um dieses Problem zu beheben, musst du höchstwahrscheinlich die Port-Weiterleitung auf deinem Internet-Router einrichten wie in https://yunohost.org/isp_box_config beschrieben", - "diagnosis_regenconf_manually_modified_details": "Das ist wahrscheinlich OK wenn du weißt, was du tust! YunoHost wird in Zukunft diese Datei nicht mehr automatisch updaten... Aber sei bitte vorsichtig, da die zukünftigen Upgrades von YunoHost wichtige empfohlene Änderungen enthalten könnten. Wenn du möchtest, kannst du die Unterschiede mit yunohost tools regen-conf {category} --dry-run --with-diff inspizieren und mit yunohost tools regen-conf {category} --force auf das Zurücksetzen die empfohlene Konfiguration erzwingen", + "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 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", "diagnosis_mail_queue_too_big": "Zu viele anstehende Nachrichten in der Warteschlange ({nb_pending} emails)", "diagnosis_package_installed_from_sury_details": "Einige Pakete wurden versehentlich von einem Drittanbieter-Repository namens Sury installiert. Das YunoHost-Team hat die Strategie für den Umgang mit diesen Paketen verbessert, aber es ist zu erwarten, dass einige Setups, die PHP7.3-Anwendungen installiert haben, während sie noch auf Stretch waren, einige verbleibende Inkonsistenzen aufweisen. Um diese Situation zu beheben, sollten Sie versuchen, den folgenden Befehl auszuführen: {cmd_to_fix}", - "domain_cannot_add_xmpp_upload": "Eine hinzugefügte Domain darf nicht mit 'xmpp-upload.' beginnen. Dieser Name ist für das XMPP-Upload-Feature von YunoHost reserviert.", + "domain_cannot_add_xmpp_upload": "Sie können keine Domänen hinzufügen, welche mit 'xmpp-upload.' beginnen. Diese Art von Namen ist für die in YunoHost integrierte XMPP-Upload-Feature reserviert.", "group_cannot_be_deleted": "Die Gruppe {group} kann nicht manuell entfernt werden.", "group_cannot_edit_primary_group": "Die Gruppe '{group}' kann nicht manuell bearbeitet werden. Es ist die primäre Gruppe, welche dazu gedacht ist, nur ein spezifisches Konto zu enthalten.", - "diagnosis_processes_killed_by_oom_reaper": "Das System hat einige Prozesse beendet, weil ihm der Arbeitsspeicher ausgegangen ist. Das passiert normalerweise, wenn das System ingesamt nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein einzelner Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: \n{kills_summary}", + "diagnosis_processes_killed_by_oom_reaper": "Das System hat ein paar Prozesse abgewürgt, da ihm der Speicher ausgegangen ist. Dies ist typischerweise sympomatisch eines ungenügenden Vorhandenseins des Arbeitsspeichers oder eines einzelnen Prozesses, der zu viel Speicher verbraucht. Zusammenfassung der abgewürgtenProzesse: \n{kills_summary}", "diagnosis_description_ports": "Geöffnete Ports", - "additional_urls_already_added": "Zusätzliche URL '{url}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission}'", - "additional_urls_already_removed": "Zusätzliche URL '{url}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission}'", + "additional_urls_already_added": "Die zusätzliche URL '{url}' wurde bereits hinzugefügt für die Berechtigung '{permission}'", + "additional_urls_already_removed": "Die zusätzliche URL '{url}' wurde bereits entfernt für die Berechtigung '{permission}'", "app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutze den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.", - "diagnosis_http_hairpinning_issue_details": "Das liegt wahrscheinlich an deinem Router. Dadurch können Personen von ausserhalb deines Netzwerkes, aber nicht von innerhalb deines lokalen Netzwerkes (wie wahrscheinlich du selbst), auf deinen Server zugreifen, wenn dazu die Domäne oder öffentliche IP verwendet wird. Du kannst das Problem eventuell beheben, indem du ein einen Blick auf https://yunohost.org/dns_local_network wirfst", + "diagnosis_http_hairpinning_issue_details": "Das liegt wahrscheinlich an Ihrem Router. Dadurch können Personen von ausserhalb deines Netzwerkes, aber nicht von innerhalb deines lokalen Netzwerkes (wie wahrscheinlich Sie selbst), auf Ihren Server zugreifen, wenn dazu die Domäne oder öffentliche IP verwendet wird. Sie können das Problem eventuell beheben, indem Sie einen Blick auf https://yunohost.org/dns_local_network werfen", "diagnosis_http_nginx_conf_not_up_to_date": "Die Konfiguration von Nginx scheint für diese Domäne manuell geändert worden zu sein. Dies hindert YunoHost daran festzustellen, ob es über HTTP erreichbar ist.", - "diagnosis_http_bad_status_code": "Es sieht so aus als ob ein anderes Gerät (vielleicht dein Router/Modem) anstelle deines Servers antwortet.
1. Der häufigste Grund hierfür ist, dass Port 80 (und 443) nicht korrekt zu deinem Server weiterleiten.
2. Bei komplexeren Setups: prüfe ob deine Firewall oder Reverse-Proxy die Verbindung stören.", + "diagnosis_http_bad_status_code": "Es sieht so aus als ob ein anderes Gerät (vielleicht dein Router/Modem) anstelle Ihres Servers antwortet.
1. Der häufigste Grund hierfür ist, dass Port 80 (und 443) nicht korrekt zu deinem Server weiterleiten.
2. Bei komplexeren Setups: prüfen Sie ob Ihre Firewall oder Reverse-Proxy die Verbindung stören.", "diagnosis_never_ran_yet": "Es sieht so aus, als wäre dieser Server erst kürzlich eingerichtet worden und es gibt noch keinen Diagnosebericht, der angezeigt werden könnte. Sie sollten zunächst eine vollständige Diagnose durchführen, entweder über die Web-Oberfläche oder mit \"yunohost diagnosis run\" von der Kommandozeile aus.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein, um die Unterschiede anzuzeigen. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", "diagnosis_backports_in_sources_list": "Sie haben vermutlich apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie, was Sie tun.", @@ -408,13 +404,13 @@ "group_user_already_in_group": "Konto {user} ist bereits in der Gruppe {group}", "group_cannot_edit_visitors": "Die Gruppe \"Besucher\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe und repräsentiert anonyme Besucher", "group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Konten in YunoHost zu halten", - "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber YunoHost wird sie entfernen...", + "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber YunoHost wird sie entfernen…", "group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen", "group_already_exist": "Die Gruppe {group} existiert bereits", - "global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort", - "global_settings_setting_smtp_relay_user": "SMTP Relay Benutzer Account", + "global_settings_setting_smtp_relay_password": "SMTP-Relais-Passwort", + "global_settings_setting_smtp_relay_user": "SMTP-Relais-Benutzeraccount", "global_settings_setting_smtp_relay_port": "SMTP Relay Port", - "domain_cannot_remove_main_add_new_one": "Sie können '{domain}' nicht entfernen, da es die Hauptdomäne und Ihre einzige Domäne ist. Sie müssen zuerst eine andere Domäne mit 'yunohost domain add ' hinzufügen, dann als Hauptdomäne mit 'yunohost domain main-domain -n ' festlegen und dann können Sie die Domäne '{domain}' mit 'yunohost domain remove {domain}' entfernen'.'", + "domain_cannot_remove_main_add_new_one": "Sie können '{domain}' nicht entfernen, da es die Hauptdomäne und Ihre einzige Domäne ist. Sie müssen zuerst eine andere Domäne mit 'yunohost domain add ' hinzufügen, dann als Hauptdomäne mit 'yunohost domain main-domain -n ' festlegen und dann können Sie die Domäne '{domain}' mit 'yunohost domain remove {domain}' entfernen'.", "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", "log_backup_restore_app": "Wiederherstellen von '{}' aus einem Sicherungsarchiv", @@ -446,22 +442,22 @@ "log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration", "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation", "domain_remove_confirm_apps_removal": "Wenn du diese Domäne löschst, werden folgende Applikationen entfernt:\n{apps}\n\nBist du sicher? [{answers}]", - "migrations_pending_cant_rerun": "Diese Migrationen sind immer noch anstehend und können deshalb nicht erneut durchgeführt werden: {ids}", - "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht anstehend und können deshalb nicht übersprungen werden: {ids}", + "migrations_pending_cant_rerun": "Diese Migrationen sind immer noch ausstehend und können deshalb nicht erneut durchgeführt werden: {ids}", + "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht ausstehend und können deshalb nicht übersprungen werden: {ids}", "migrations_success_forward": "Migration {id} abgeschlossen", "migrations_dependencies_not_satisfied": "Führe diese Migrationen aus: '{dependencies_id}', bevor du {id} migrierst.", "migrations_failed_to_load_migration": "Konnte Migration nicht laden {id}: {error}", "migrations_list_conflict_pending_done": "Du kannst '--previous' und '--done' nicht gleichzeitig benützen.", "migrations_already_ran": "Diese Migrationen wurden bereits durchgeführt: {ids}", - "migrations_loading_migration": "Lade Migrationen {id}...", + "migrations_loading_migration": "Lade Migrationen {id}…", "migrations_migration_has_failed": "Migration {id} gescheitert mit der Ausnahme {exception}: Abbruch", "migrations_must_provide_explicit_targets": "Du musst konkrete Ziele angeben, wenn du '--skip' oder '--force-rerun' verwendest", "migrations_need_to_accept_disclaimer": "Um die Migration {id} durchzuführen, musst du folgenden Hinweis akzeptieren:\n---\n{disclaimer}\n---\nWenn du nach dem Lesen die Migration durchführen möchtest, wiederhole bitte den Befehl mit der Option '--accept-disclaimer'.", "migrations_no_migrations_to_run": "Keine Migrationen durchzuführen", "migrations_exclusive_options": "'--auto', '--skip' und '--force-rerun' sind Optionen, die sich gegenseitig ausschliessen.", "migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'", - "migrations_running_forward": "Durchführen der Migrationen {id}...", - "migrations_skip_migration": "Überspringe Migrationen {id}...", + "migrations_running_forward": "Durchführen der Migrationen {id}…", + "migrations_skip_migration": "Überspringe Migrationen {id}…", "password_too_simple_2": "Das Passwort muss mindestens 8 Zeichen lang sein und Gross- sowie Kleinbuchstaben enthalten", "password_listed": "Dieses Passwort zählt zu den meistgenutzten Passwörtern der Welt. Bitte wähle ein anderes, einzigartigeres Passwort.", "operation_interrupted": "Wurde die Operation manuell unterbrochen?", @@ -481,7 +477,7 @@ "regenconf_file_copy_failed": "Die neue Konfigurationsdatei '{new}' kann nicht nach '{conf}' kopiert werden", "regenconf_file_backed_up": "Die Konfigurationsdatei '{conf}' wurde unter '{backup}' gespeichert", "permission_require_account": "Berechtigung {permission} ist nur für Personen mit Konto sinnvoll und kann daher nicht für Gäste aktiviert werden.", - "permission_protected": "Die Berechtigung ist geschützt. Du kannst die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.", + "permission_protected": "Die Berechtigung {permission} ist geschützt. Du kannst die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.", "permission_updated": "Berechtigung '{permission}' aktualisiert", "permission_update_failed": "Die Berechtigung '{permission}' kann nicht aktualisiert werden : {error}", "permission_not_found": "Berechtigung '{permission}' nicht gefunden", @@ -498,39 +494,39 @@ "regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'", "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet.", "regenconf_updated": "Konfiguration aktualisiert für '{category}'", - "regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an...", + "regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an…", "regenconf_failed": "Konnte die Konfiguration für die Kategorie(n) {categories} nicht neu erstellen", - "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre...", + "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre…", "regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden", "restore_system_part_failed": "Die Systemteile '{part}' konnten nicht wiederhergestellt werden", "restore_removing_tmp_dir_failed": "Ein altes, temporäres Directory konnte nicht entfernt werden", "restore_not_enough_disk_space": "Nicht genug Speicher (Speicher: {free_space} B, benötigter Speicher: {needed_space} B, Sicherheitspuffer: {margin} B)", - "restore_may_be_not_enough_disk_space": "Dein System scheint nicht genug Speicherplatz zu haben (frei: {free_space} B, benötigter Platz: {needed_space} B, Sicherheitspuffer: {margin} B)", - "restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus...", + "restore_may_be_not_enough_disk_space": "Ihr System scheint nicht genug Speicherplatz zu haben (frei: {free_space} B, benötigter Platz: {needed_space} B, Sicherheitspuffer: {margin} B)", + "restore_extracting": "Auspacken der benötigten Dateien aus dem Archiv…", "restore_already_installed_apps": "Folgende Apps können nicht wiederhergestellt werden, weil sie schon installiert sind: {apps}", - "regex_with_only_domain": "Du kannst regex nicht als Domain verwenden, sondern nur als Pfad", + "regex_with_only_domain": "Sie können regex nicht als Domain verwenden, sondern nur als Pfad", "root_password_desynchronized": "Das Admin-Passwort wurde geändert, aber YunoHost konnte dies nicht auf das Root-Passwort übertragen!", - "regenconf_need_to_explicitly_specify_ssh": "Die SSH-Konfiguration wurde manuell modifiziert, aber du musst explizit die Kategorie 'SSH' mit --force spezifizieren, um die Änderungen tatsächlich anzuwenden.", + "regenconf_need_to_explicitly_specify_ssh": "Die SSH-Konfiguration wurde manuell modifiziert, aber Sie müssen explizit die Kategorie 'SSH' mit --force spezifizieren, um die Änderungen tatsächlich anzuwenden.", "log_backup_create": "Erstelle ein Backup-Archiv", - "diagnosis_sshd_config_inconsistent": "Es sieht aus, als ob der SSH-Port manuell geändert wurde in /etc/ssh/ssh_config. Seit YunoHost 4.2 ist eine neue globale Einstellung 'security.ssh.port' verfügbar um zu verhindern, dass die Konfiguration manuell verändert wird.", + "diagnosis_sshd_config_inconsistent": "Es scheint wie wenn der SSH-Port in /etc/ssh/sshd_config manuell verändert wurde. Seit YunoHost 4.2 ist eine neue globale Einstellung 'security.ssh.ssh_port' verfügbar, um zu verhindern, dass die Konfiguration händisch verändert wird.", "diagnosis_sshd_config_insecure": "Die SSH-Konfiguration wurde scheinbar manuell geändert und ist unsicher, weil sie keine 'AllowGroups'- oder 'AllowUsers' -Direktiven für die Beschränkung des Zugriffs durch autorisierte Benutzer enthält.", "backup_create_size_estimation": "Das Archiv wird etwa {size} an Daten enthalten.", "app_restore_script_failed": "Im Wiederherstellungsskript der Applikation ist ein Fehler aufgetreten", "app_restore_failed": "Konnte {app} nicht wiederherstellen: {error}", "migration_ldap_rollback_success": "Das System wurde zurückgesetzt.", - "migration_ldap_migration_failed_trying_to_rollback": "Migrieren war nicht möglich... Versuch, ein Rollback des Systems durchzuführen.", + "migration_ldap_migration_failed_trying_to_rollback": "Migrieren war nicht möglich… Versuch, ein Rollback des Systems durchzuführen.", "migration_ldap_backup_before_migration": "Vor der eigentlichen Migration ein Backup der LDAP-Datenbank und der Applikations-Einstellungen erstellen.", - "global_settings_setting_ssowat_panel_overlay_enabled": "Das SSOwat-Overlay-Panel aktivieren", - "diagnosis_sshd_config_inconsistent_details": "Bitte führe yunohost settings set security.ssh.port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und prüfe yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um deine Konfiguration auf die YunoHost-Empfehlung zurückzusetzen.", - "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", + "global_settings_setting_ssowat_panel_overlay_enabled": "Das 'YunoHost'-Portalverknüpfungsquadrätchen bei den Apps aktivieren", + "diagnosis_sshd_config_inconsistent_details": "Bitte führen Sie yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT aus, um den SSH-Port festzulegen, und überprüfen Sie yunohost tools regen-conf ssh --dry-run --with-diff und yunohost tools regen-conf ssh --force um Ihre Konfiguration auf die YunoHost-Empfehlung zurückzusetzen.", + "regex_incompatible_with_tile": "/!\\ Packagers! Für Berechtigung '{permission}' ist show_tile auf 'true' gesetzt und deshalb können Sie keine regex-URL als Hauptdomäne setzen", "permission_cant_add_to_all_users": "Die Berechtigung {permission} kann nicht für allen Konten hinzugefügt werden.", "migration_ldap_can_not_backup_before_migration": "Die Sicherung des Systems konnte nicht abgeschlossen werden, bevor die Migration fehlschlug. Fehler: {error}", "service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet", "service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)", - "service_description_dnsmasq": "Verarbeitet die Auflösung des Domainnamens (DNS)", + "service_description_dnsmasq": "Verwaltet die Auflösung des Domainnamens (DNS)", "restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.", "service_description_slapd": "Speichert Konten, Domänen und verbundene Informationen", - "service_description_rspamd": "Spamfilter und andere E-Mail-Merkmale", + "service_description_rspamd": "Spamfilter und andere E-Mail-Funktionen", "service_description_redis-server": "Eine spezialisierte Datenbank für den schnellen Datenzugriff, die Aufgabenwarteschlange und die Kommunikation zwischen Programmen", "service_description_postfix": "Wird benutzt, um E-Mails zu senden und zu empfangen", "service_description_nginx": "Stellt Daten aller Websiten auf dem Server bereit", @@ -545,10 +541,10 @@ "server_shutdown": "Der Server wird heruntergefahren", "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.", + "this_action_broke_dpkg": "Diese Aktion hat unkonfigurierte Pakete verursacht, welche durch dpkg/apt (die Paketverwaltungen dieses Systems) zurückgelassen wurden… Du kannst versuchen dieses Problem zu lösen, indem du 'sudo apt install --fix-broken' und/oder 'sudo dpkg --configure -a' ausführst.", "update_apt_cache_failed": "Kann den Cache von APT (Debians Paketmanager) nicht aktualisieren. Hier ist ein Auszug aus den sources.list-Zeilen, die helfen könnten, das Problem zu identifizieren:\n{sourceslist}", "unknown_main_domain_path": "Unbekannte Domäne oder Pfad für '{app}'. Sie müssen eine Domäne und einen Pfad angeben, um eine URL für die Genehmigung angeben zu können.", - "yunohost_postinstall_end_tip": "Post-install ist fertig! Um das Setup abzuschliessen, wird empfohlen:\n - ein erstes Konto über den Bereich 'Konto' im Adminbereich hinzuzufügen (oder mit 'yunohost user create ' in der Kommandezeile);\n - mögliche Fehler zu diagnostizieren über den Bereich 'Diagnose' im Adminbereich (oder mit 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch zu lesen: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "Post-Installation ist fertig! Um das Setup abzuschliessen, wird folgendes empfohlen:\n - mögliche Fehler diagnostizieren im Bereich 'Diagnose' des Adminbereichs (oder mittels 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch lesen: https://yunohost.org/admindoc.", "user_already_exists": "Das Konto '{user}' ist bereits vorhanden", "update_apt_cache_warning": "Beim Versuch den Cache für APT (Debians Paketmanager) zu aktualisieren, ist etwas schief gelaufen. Hier ist ein Dump der Zeilen aus sources.list, die Ihnen vielleicht dabei helfen, das Problem zu identifizieren:\n{sourceslist}", "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktualisieren", @@ -558,7 +554,7 @@ "config_apply_failed": "Anwenden der neuen Konfiguration fehlgeschlagen: {error}", "config_validate_date": "Sollte ein zulässiges Datum in folgendem Format sein: YYYY-MM-DD", "config_validate_email": "Sollte eine zulässige eMail sein", - "config_forbidden_keyword": "Das Schlüsselwort '{keyword}' ist reserviert. Du kannst kein Konfigurationspanel mit einer Frage erstellen, die diese ID verwendet.", + "config_forbidden_keyword": "Das Schlüsselwort '{keyword}' ist reserviert. Sie können kein Konfigurationspanel mit einer Frage erstellen, das diese ID verwendet.", "config_no_panel": "Kein Konfigurationspanel gefunden.", "config_validate_color": "Sollte eine zulässige RGB hexadezimal Farbe sein", "diagnosis_apps_issue": "Ein Problem für die App {app} ist aufgetreten", @@ -569,7 +565,7 @@ "diagnosis_apps_not_in_app_catalog": "Diese Applikation steht nicht im Applikationskatalog von YunoHost. Sie sollten in Betracht ziehen, sie zu deinstallieren, weil sie keine Aktualisierungen mehr erhält und die Integrität und die Sicherheit Ihres Systems kompromittieren könnte.", "diagnosis_apps_outdated_ynh_requirement": "Die installierte Version dieser Applikation erfordert nur YunoHost >=2.x oder 3.x, was darauf hinweisen könnte, dass die Applikation nicht nach aktuell empfohlenen Paketierungspraktiken und mit aktuellen Helpern erstellt worden ist. Sie sollten wirklich in Betracht ziehen, sie zu aktualisieren.", "diagnosis_description_apps": "Applikationen", - "config_cant_set_value_on_section": "Du kannst einen einzelnen Wert nicht auf einen gesamten Konfigurationsbereich anwenden.", + "config_cant_set_value_on_section": "Sie können einen einzelnen Wert nicht auf einen gesamten Konfigurationsbereich anwenden.", "diagnosis_apps_deprecated_practices": "Die installierte Version dieser Applikation verwendet gewisse veraltete Paketierungspraktiken. Sie sollten sie wirklich aktualisieren.", "app_config_unable_to_apply": "Konnte die Werte des Konfigurations-Panels nicht anwenden.", "app_config_unable_to_read": "Konnte die Werte des Konfigurations-Panels nicht auslesen.", @@ -584,7 +580,7 @@ "domain_config_auth_secret": "Authentifizierungsgeheimnis", "domain_config_api_protocol": "API-Protokoll", "domain_unknown": "Domäne '{domain}' unbekannt", - "ldap_server_is_down_restart_it": "Der LDAP-Dienst ist nicht erreichbar, versuche ihn neu zu starten...", + "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", "user_import_missing_columns": "Die folgenden Spalten fehlen: {columns}", "user_import_nothing_to_do": "Es muss kein Konto importiert werden", @@ -624,25 +620,25 @@ "domain_dns_push_failed_to_authenticate": "Die Authentifizierung bei der API des Registrars für die Domäne '{domain}' ist fehlgeschlagen. Wahrscheinlich sind die Anmeldedaten falsch? (Fehler: {error})", "log_domain_config_set": "Konfiguration für die Domäne '{}' aktualisieren", "log_domain_dns_push": "DNS-Einträge für die Domäne '{}' übertragen", - "service_description_yunomdns": "Ermöglicht es dir, deinen Server über 'yunohost.local' in deinem lokalen Netzwerk zu erreichen", + "service_description_yunomdns": "Ermöglicht es Ihnen, den Server über 'yunohost.local' in Ihrem lokalen Netzwerk zu erreichen", "migration_0021_start": "Beginnen von Migration zu Bullseye", - "migration_0021_patching_sources_list": "Aktualisieren der sources.lists...", - "migration_0021_main_upgrade": "Starte Hauptupdate...", + "migration_0021_patching_sources_list": "Aktualisieren der sources.lists…", + "migration_0021_main_upgrade": "Starte Hauptupdate…", "migration_0021_still_on_buster_after_main_upgrade": "Irgendetwas ist während des Haupt-Upgrades schief gelaufen, das System scheint immer noch auf Debian Buster zu laufen", - "migration_0021_yunohost_upgrade": "Start des YunoHost Kern-Upgrades...", + "migration_0021_yunohost_upgrade": "Start des YunoHost Kern-Upgrades…", "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...", + "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…", "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", "tools_upgrade_failed": "Pakete konnten nicht aktualisiert werden: {packages_list}", "domain_config_default_app": "Standard-Applikation", "migration_0023_postgresql_11_not_installed": "PostgreSQL wurde nicht auf Ihrem System installiert. Es ist nichts zu tun.", - "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 ist installiert, aber nicht PostgreSQL 13!? Irgendetwas Seltsames könnte auf Ihrem System passiert sein. :( ...", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 ist installiert, aber nicht PostgreSQL 13!? Irgendetwas Seltsames könnte auf Ihrem System passiert sein. :(…", "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)", @@ -654,10 +650,10 @@ "global_settings_setting_admin_strength": "Stärke des Admin-Passworts", "global_settings_setting_user_strength": "Stärke des Anmeldepassworts", "global_settings_setting_postfix_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Postfix-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", - "global_settings_setting_ssh_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_ssh_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den SSH-Server. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte). Bei Bedarf können Sie in https://infosec.mozilla.org/guidelines/openssh die Informationen nachlesen.", "global_settings_setting_ssh_password_authentication_help": "Passwort-Authentifizierung für SSH zulassen", "global_settings_setting_ssh_port": "SSH-Port", - "global_settings_setting_webadmin_allowlist_help": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.", + "global_settings_setting_webadmin_allowlist_help": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. CIDR Notation ist erlaubt.", "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.", @@ -677,7 +673,7 @@ "config_forbidden_readonly_type": "Der Typ '{type}' kann nicht auf Nur-Lesen eingestellt werden. Verwenden Sie bitte einen anderen Typ, um diesen Wert zu generieren (relevante ID des Arguments: '{id}').", "diagnosis_using_stable_codename": "apt (Paketmanager des Systems) ist gegenwärtig konfiguriert um die Pakete des Code-Namens 'stable' zu installieren, anstelle die des Code-Namen der aktuellen Debian-Version (bullseye).", "domain_config_acme_eligible": "Geeignet für ACME", - "diagnosis_using_stable_codename_details": "Dies wird meistens durch eine fehlerhafte Konfiguration seitens des Hosting-Providers verursacht. Dies stellt eine Gefahr dar, weil sobald die nächste Debian-Version zum neuen 'stable' wird, wird apt alle System-Pakete aktualisieren wollen, ohne eine ordnungsgemässe Migration zu durchlaufen. Es wird sehr empfohlen dies zu berichtigen, indem Sie die Datei der apt-Quellen des Debian-Basis-Repositorys entsprechend anpassen indem Sie das stable-Keyword durch bullseye ersetzen. Die zugehörige Konfigurationsdatei sollte /etc/apt/sources.list oder eine Datei im Verzeichnis /etc/apt/sources.list.d/sein.", + "diagnosis_using_stable_codename_details": "Dies hat meistens eine fehlerhafte Konfiguration seitens Hosting-Provider zur Ursache. Dies stellt eine Gefahr dar, da sobald die nächste Debian-Version zum neuen 'stable' wird, führt apt eine Aktualisierung aller System-Pakete durch, ohne eine ordnungsgemässe Migration zu durchlaufen. Es wird dringlich darauf hingewiesen, dies zu berichtigen indem Sie die Datei der apt-Quellen des Debian-Base-Repositorys entsprechend anpassen indem Sie das stable-Keyword durch bullseye ersetzen. Die zugehörige Konfigurationsdatei sollte /etc/apt/sources.list oder eine Datei im Verzeichnis /etc/apt/sources.list.d/sein.", "diagnosis_using_yunohost_testing": "apt (der Paketmanager des Systems) ist aktuell so konfiguriert, dass die 'testing'-Upgrades für YunoHost core installiert werden.", "diagnosis_using_yunohost_testing_details": "Dies ist wahrscheinlich OK, wenn Sie wissen, was Sie tun. Aber beachten Sie bitte die Release-Notes bevor sie zukünftige YunoHost-Upgrades installieren! Wenn Sie die 'testing'-Upgrades deaktivieren möchten, sollten sie das testing-Schlüsselwort aus /etc/apt/sources.list.d/yunohost.list entfernen.", "global_settings_setting_security_experimental_enabled": "Experimentelle Sicherheitsfunktionen", @@ -704,5 +700,86 @@ "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}" + "app_resource_failed": "Automatische Ressourcen-Allokation (provisioning), die Unterbindung des Zugriffts auf Ressourcen (deprovisioning) oder die Aktualisierung der Ressourcen für {app} schlug fehl: {error}", + "domain_config_cert_validity": "Validität", + "confirm_notifications_read": "WARNUNG: Sie sollten die App-Benachrichtigungen anschauen bevor sie weitermachen. Es könnte da Dinge geben, die gut zu wissen sein könnten. [{answers}]", + "domain_cannot_add_muc_upload": "Domänen, welche mit 'muc.' beginnen, können/dürfen Sie nicht hinzufügen. Dieser Namens-Typ ist reserviert für das in YunoHost integrierte XMPP-Multiuser-Chat-Feature.", + "domain_config_cert_summary_selfsigned": "WARNUNG: Aktuelles Zertifikat ist selbstssigniert. Browser werden neuen Besuchern eine furchteinflössende Warnung anzeigen!", + "app_failed_to_download_asset": "Konnte die Ressource '{source_id}' ({url}) für {app} nicht herunterladen: {out}", + "apps_failed_to_upgrade_line": "\n * {app_id} (um den zugehörigen Log anzuzeigen, führen Sie ein 'yunohost log show {operation_logger_name}' aus)", + "confirm_app_insufficient_ram": "GEFAHR! Diese App braucht {required} RAM um zu installieren/upgraden wobei momentan aber nur {current} vorhanden sind. Auch wenn diese App laufen könnte, würde ihr Installations- bzw. ihr Upgrade-Prozess eine grosse Menge an RAM brauchen, so dass Ihr Server anhalten und schrecklich versagen würde. Wenn Sie dieses Risiko einfach hinnehmen möchten, tippen Sie '{answers}'", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 sollte, sofern verfügbar, üblicherweise automatisch durch das System oder Ihren Provider konfiguriert werden. Andernfalls kann es notwendig sein, dass Sie ein paar Dinge selbst, händisch konfigurieren, wie es die Dokumentation erklärt: https://yunohost.org/ipv6.", + "app_corrupt_source": "YunoHost konnte die Ressource '{source_id}' ({url}) für {app} herunterladen, aber die Ressource stimmt mit der erwarteten Checksum nicht überein. Dies könnte entweder bedeuten, dass Ihr Server einfach ein vorübergehendes Netzwerkproblem hatte ODER dass der Upstream-Betreuer (oder ein schädlicher/arglistiger Akteur) die Ressource auf eine bestimmte Art verändert hat und dass die YunoHost-Paketierer das App-Manifest untersuchen und so aktualisieren müssen, dass es diese Veränderung berücksichtigt.\n Erwartete sha256-Prüfsumme: {expected_sha256}\n Heruntergeladene sha256-Prüfsumme: {computed_sha256}\n Heruntergeladene Dateigrösse: {size}", + "global_settings_reset_success": "Reinitialisieren der globalen Einstellungen", + "global_settings_setting_root_password_confirm": "Neues root-Passwort (Bestätigung)", + "global_settings_setting_ssh_compatibility": "SSH-Kompatibilität", + "group_mailalias_remove": "Der E-Mail-Alias '{mail}' wird von der Gruppe '{group}' entfernt", + "group_user_add": "Der Benutzer '{user}' wird der Gruppe '{group}' hinzugefügt werden", + "global_settings_setting_nginx_compatibility": "NGINX-Kompatibilität", + "global_settings_setting_passwordless_sudo": "Erlauben Sie Administratoren 'sudo' zu benützen, ohne das Passwort erneut einzugeben", + "global_settings_setting_smtp_allow_ipv6": "Autorisiere das IPv6", + "domain_config_default_app_help": "Personen werden automatisch zu dieser App weitergeleitet, wenn sie diese Domäne öffnen. Wenn keine App spezifiziert wurde, werden Personen zum Benutzerportal-Login-Formular weitergeleitet.", + "domain_config_xmpp_help": "NB: ein paar XMPP-Features werden voraussetzen, dass Sie Ihre DNS-Einträge aktualisieren und Ihr Lets-Encrypt-Zertifikat neu erstellen, um eingeschaltet zu werden", + "global_settings_setting_smtp_relay_host": "Adresse des SMTP-Relais", + "global_settings_setting_nginx_redirect_to_https": "HTTPS erzwingen", + "group_no_change": "Nichts zu ändern für die Gruppe '{group}'", + "global_settings_setting_admin_strength_help": "Diese Parameter werden nur bei einer Initiailisierung oder einer Passwortänderung anwandt", + "global_settings_setting_backup_compress_tar_archives": "Datensicherungen komprimieren", + "global_settings_setting_dns_exposure_help": "NB: Dies beinflusst nur die vorgeschlagenen DNS-Konfigurations- und -Diagnose-Überprüfungen. Dies beeinflusst keine Systemkonfigurationen.", + "global_settings_setting_pop3_enabled": "POP3 einschalten", + "global_settings_setting_pop3_enabled_help": "POP3-Protokoll für den Mail-Server aktivieren", + "global_settings_setting_portal_theme_help": "Weitere Informationen dazu, wie Custom-Portal-Themes kreiert werden können unter https://yunohost.org/theming", + "global_settings_setting_postfix_compatibility": "Postfix-Kompatibilität", + "global_settings_setting_root_password": "Neues root-Passwort", + "global_settings_setting_user_strength_help": "Diese Parameter werden nur bei einer Initialisierung des oder Änderung des Passworts angewandt", + "global_settings_setting_webadmin_allowlist": "Allowlist für die Webadmin-IPs", + "global_settings_setting_webadmin_allowlist_enabled": "Webadmin-IP-Allowlist aktivieren", + "group_update_aliases": "Aktualisieren der Aliase für die Gruppe '{group}'", + "global_settings_setting_portal_theme": "Portal-Theme", + "global_settings_setting_smtp_relay_enabled": "Aktiviere das SMTP-Relais", + "global_settings_setting_dns_exposure": "Bei DNS-Konfiguration und -Diagnose zu berücksichtigende IP-Versionen", + "global_settings_setting_root_access_explain": "Auf Linux-Systemen ist 'root' der absolute Administrator. Im Kontext von YunoHost ist der direkte 'root'-SSH-Login standardmässig deaktiviert - ausgenommen des lokalen Netzwerks des Servers. Mitglieder der 'admins'-Gruppe sind in der Lage mit dem 'sudo'-Befehl in der Kommandozeile (CLI) als root zu agieren. Nun kann es hilfreich sein, ein (robustes) root-Passwort zu haben um das System zu debuggen oder für den Fall, dass sich die regulären Administratoren nicht mehr einloggen können.", + "global_settings_setting_ssh_password_authentication": "Authentifizieren mit Passwort", + "group_mailalias_add": "Der E-Mail-Alias '{mail}' wird der Gruppe '{group}' hinzugefügt", + "group_user_remove": "Der Benutzer '{user}' wird von der Gruppe '{group}' entfernt werden", + "invalid_credentials": "Ungültiges Passwort oder Benutzername", + "invalid_shell": "Ungültiger Shell: {shell}", + "migration_description_0025_global_settings_to_configpanel": "Migrieren der Legacy-Global-Einstellungsnomenklatur zur neuen, modernen Nomenklatur", + "root_password_changed": "Das root-Passwort wurde geändert", + "password_too_long": "Bitte wählen Sie ein Passwort aus, das weniger als 127 Zeichen hat", + "registrar_infos": "Registrar-Informationen (Herausgeber der Domainnamen/Domänennamen)", + "migration_0024_rebuild_python_venv_in_progress": "Probiere die Erneuerung der Python-virtualenv für `{app}`", + "migration_description_0024_rebuild_python_venv": "Reparieren der Python-Applikation nach Bullseye-Migration", + "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Erneuerung der virtualenv wird für die folgenden Applikationen versucht (NB: die Operation kann einige Zeit in Anspruch nehmen!): {rebuild_apps}", + "log_resource_snippet": "Provisioning/Deprovisioning/Aktualisieren einer Ressource", + "log_settings_reset_all": "alle Parameter rücksetzen", + "log_settings_set": "Parameter anwenden", + "password_confirmation_not_the_same": "Das Passwort und die Bestätigung stimmen nicht überein", + "log_settings_reset": "Einstellungen rücksetzen", + "migration_0024_rebuild_python_venv_broken_app": "Ignoriere {app} weil virtualenv für diese Applikation nicht auf einfache Weise neu gebaut werden kann. Stattdessen sollten Sie die Situation berichtigen indem Sie das Aktualisieren der Applikation erzwingen indem Sie `yunohost app upgrade --force {app}`nutzen.", + "migration_0024_rebuild_python_venv_failed": "Fehler aufgetreten bei der Erneuerung der Python-virtualenv für Applikation {app}. Die Applikation kann nicht mehr funktionieren solange die Situation nicht behoben worden ist. Sie sollten diesen Umstand durch eine erzwungene Aktualisierung für diese Applikation beheben indem Sie `yunohost app upgrade --force {app}`benützen.", + "migration_description_0026_new_admins_group": "Migrieren in das neue 'Multiple-Administratoren'-Managementsystem (mehrere Benutzer können in der Gruppe 'Administratoren' präsent sein, mit allen Rechten von Administratoren auf der ganzen YunoHost-Instanz)", + "pattern_fullname": "Muss ein gültiger voller Name sein (mindestens 3 Zeichen)", + "migration_0021_not_buster2": "Die aktuelle Debian-Distribution ist nicht Buster! Wenn Sie bereits eine Buster->Bullseye-Migration durchgeführt haben, dann ist dieser Fehler symptomatisch für den Umstand, dass das Migrationsprozedere nicht zu 100% erfolgreich war (andernfalls hätte Yunohost es als vollständig gekennzeichnet). Es ist empfehlenswert, sich der Geschehnisse zusammen mit dem Support-Team anzunehmen, das einen Bedarf an einem **vollständigen** Log der Migration haben wird, das in Werkzeuge > Logs im Adminpanel auffindbar ist.", + "migration_0024_rebuild_python_venv_disclaimer_base": "Der Aktualisierung zu Debian Bullseye folgend ist es nötig, dass ein paar Python-Applikationen partiell neu gebaut und in die neue, mit Debian mitgelieferte Python-Version konvertiert werden. (in technischen Begrifflichkeiten: das, was wir die 'virtualenv' nennen, muss erneuert werden). In der Zwischenzeit kann es sein, dass diese Python-Applikationen nicht funktionieren. YunoHost kann versuchen die virtualenv für ein paar davon zu erneuern, wie untenstehend detailliert beschrieben wird. Für die anderen Applikationen, oder für den Fall, dass die Erneuerung fehlschlägt, werden eine erzwungene Aktualisierung für diese Applikationen durchführen müssen.", + "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs können für diese Applikationen nicht automatisch erneuert werden. Für diejenigen werden Sie eine erzwungene Aktualisierung durchführen müssen, was in der Kommandozeile bewerkstelligt werden kann mit: `yunohost app upgrade --force APP`: {ignored_apps}", + "ask_dyndns_recovery_password_explain_unavailable": "Diese DynDNS-Domain ist bereits registriert. Wenn Sie die Person sind, die diese Domain ursprünglich registriert hat, können Sie das Wiederherstellungspasswort eingeben, um diese Domäne wiederherzustellen.", + "ask_dyndns_recovery_password": "DynDNS Wiederherstellungspasswort", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Bitte geben Sie das Wiederherstellungspasswort für Ihre DynDNS-Domain ein.", + "dyndns_no_recovery_password": "Es wurde kein Wiederherstellungspasswort spezifiziert! Wenn Sie die Kontrolle über diese Domäne verlieren, werden Sie einen Administrator des YunoHost-Teams kontaktieren müssen!", + "dyndns_too_many_requests": "Der DynDNS-Service von YunoHost hat zu viele Anfragen von Ihnen erhalten, warten Sie ungefähr 1 Stunde bevor Sie erneut versuchen.", + "dyndns_set_recovery_password_invalid_password": "Konnte Wiederherstellungspasswort nicht einstellen: Passwort ist nicht stark genug", + "log_dyndns_unsubscribe": "Von einer YunoHost-Subdomain abmelden '{}'", + "ask_dyndns_recovery_password_explain": "Bitte wählen Sie ein Passwort zur Wiederherstellung ihrer DynDNS, für den Fall, dass Sie sie später zurücksetzen müssen.", + "dyndns_subscribed": "DynDNS-Domäne registriert", + "dyndns_subscribe_failed": "Konnte DynDNS-Domäne nicht registrieren: {error}", + "dyndns_unsubscribe_failed": "Konnte die DynDNS-Domäne nicht abmelden: {error}", + "dyndns_unsubscribed": "DynDNS-Domäne abgemeldet", + "dyndns_unsubscribe_denied": "Konnte Domäne nicht abmelden: ungültige Anmeldedaten", + "dyndns_unsubscribe_already_unsubscribed": "Domäne ist bereits abgemeldet", + "dyndns_set_recovery_password_denied": "Konnte Wiederherstellungspasswort nicht einstellen: ungültiges Passwort", + "dyndns_set_recovery_password_unknown_domain": "Konnte Wiederherstellungspasswort nicht einstellen: Domäne nicht registriert", + "dyndns_set_recovery_password_failed": "Konnte Wiederherstellungspasswort nicht einstellen: {error}", + "dyndns_set_recovery_password_success": "Wiederherstellungspasswort eingestellt!", + "global_settings_setting_ssh_port_help": "Ein Port unter 1024 wird bevorzugt, um Kaperversuche durch Nicht-Administratordienste auf dem Remote-Computer zu verhindern. Sie sollten auch vermeiden, einen bereits verwendeten Port zu verwenden, z. B. 80 oder 443." } \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index ea51b4184..ef2aa2d82 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,8 +1,8 @@ { "aborting": "Aborting.", "action_invalid": "Invalid action '{action}'", - "additional_urls_already_added": "Additionnal URL '{url}' already added in the additional URL for permission '{permission}'", - "additional_urls_already_removed": "Additionnal URL '{url}' already removed in the additional URL for permission '{permission}'", + "additional_urls_already_added": "Additional URL '{url}' already added in the additional URL for permission '{permission}'", + "additional_urls_already_removed": "Additional URL '{url}' already removed in the additional URL for permission '{permission}'", "admin_password": "Administration password", "admins": "Admins", "all_users": "All YunoHost users", @@ -16,8 +16,6 @@ "app_arch_not_supported": "This app can only be installed on architectures {required} but your server architecture is {current}", "app_argument_choice_invalid": "Pick a valid value for argument '{name}': '{value}' is not among the available choices ({choices})", "app_argument_invalid": "Pick a valid value for the argument '{name}': {error}", - "app_argument_password_no_default": "Error while parsing password argument '{name}': password argument can't have a default value for security reasons", - "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.", @@ -26,7 +24,7 @@ "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_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 perhaps update the app manifest to take this change into account.\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", @@ -54,20 +52,20 @@ "app_not_upgraded_broken_system": "The app '{failed_app}' failed to upgrade and put the system in a broken state, and as a consequence the following apps' upgrades have been cancelled: {apps}", "app_not_upgraded_broken_system_continue": "The app '{failed_app}' failed to upgrade and put the system in a broken state (so --continue-on-failure is ignored), and as a consequence the following apps' upgrades have been cancelled: {apps}", "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", - "app_remove_after_failed_install": "Removing the app after installation failure...", + "app_remove_after_failed_install": "Removing the app after installation failure…", "app_removed": "{app} uninstalled", - "app_requirements_checking": "Checking requirements for {app}...", + "app_requirements_checking": "Checking requirements for {app}…", "app_resource_failed": "Provisioning, deprovisioning, or updating resources for {app} failed: {error}", "app_restore_failed": "Could not restore {app}: {error}", "app_restore_script_failed": "An error occured inside the app restore script", "app_sources_fetch_failed": "Could not fetch source files, is the URL correct?", - "app_start_backup": "Collecting files to be backed up for {app}...", - "app_start_install": "Installing {app}...", - "app_start_remove": "Removing {app}...", - "app_start_restore": "Restoring {app}...", + "app_start_backup": "Collecting files to be backed up for {app}…", + "app_start_install": "Installing {app}…", + "app_start_remove": "Removing {app}…", + "app_start_restore": "Restoring {app}…", "app_unknown": "Unknown app", "app_unsupported_remote_type": "Unsupported remote type used for the app", - "app_upgrade_app_name": "Now upgrading {app}...", + "app_upgrade_app_name": "Now upgrading {app}…", "app_upgrade_failed": "Could not upgrade {app}: {error}", "app_upgrade_script_failed": "An error occurred inside the app upgrade script", "app_upgrade_several_apps": "The following apps will be upgraded: {apps}", @@ -76,32 +74,35 @@ "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...", + "apps_catalog_updating": "Updating application catalog…", "apps_failed_to_upgrade": "Those applications failed to upgrade:{apps}", "apps_failed_to_upgrade_line": "\n * {app_id} (to see corresponding log do a 'yunohost log show {operation_logger_name}')", "ask_admin_fullname": "Admin full name", "ask_admin_username": "Admin username", + "ask_dyndns_recovery_password": "DynDNS recovery password", + "ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.", + "ask_dyndns_recovery_password_explain_unavailable": "This DynDNS domain is already registered. If you are the person who originally registered this domain, you may enter the recovery password to reclaim this domain.", "ask_fullname": "Full name", "ask_main_domain": "Main domain", "ask_new_admin_password": "New administration password", "ask_new_domain": "New domain", "ask_new_path": "New path", "ask_password": "Password", - "ask_user_domain": "Domain to use for the user's email address and XMPP account", + "ask_user_domain": "Domain to use for the user's email address", "backup_abstract_method": "This backup method has yet to be implemented", - "backup_actually_backuping": "Creating a backup archive from the collected files...", + "backup_actually_backuping": "Creating a backup archive from the collected files…", "backup_app_failed": "Could not back up {app}", - "backup_applying_method_copy": "Copying all files to backup...", - "backup_applying_method_custom": "Calling the custom backup method '{method}'...", - "backup_applying_method_tar": "Creating the backup TAR archive...", + "backup_applying_method_copy": "Copying all files to backup…", + "backup_applying_method_custom": "Calling the custom backup method '{method}'…", + "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 info for archive '{archive}'... The info.json file 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_exists": "A backup archive with the name '{name}' already exists.", "backup_archive_name_unknown": "Unknown local backup archive named '{name}'", "backup_archive_open_failed": "Could not open the backup archive", "backup_archive_system_part_not_available": "System part '{part}' unavailable in this backup", @@ -124,7 +125,7 @@ "backup_method_copy_finished": "Backup copy finalized", "backup_method_custom_finished": "Custom backup method '{method}' finished", "backup_method_tar_finished": "TAR backup archive created", - "backup_mount_archive_for_restore": "Preparing archive for restoration...", + "backup_mount_archive_for_restore": "Preparing archive for restoration…", "backup_no_uncompress_archive_dir": "There is no such uncompressed archive directory", "backup_nothings_done": "Nothing to save", "backup_output_directory_forbidden": "Pick a different output directory. Backups cannot be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", @@ -132,12 +133,12 @@ "backup_output_directory_required": "You must provide an output directory for the backup", "backup_output_symlink_dir_broken": "Your archive directory '{path}' is a broken symlink. Maybe you forgot to re/mount or plug in the storage medium it points to.", "backup_permission": "Backup permission for {app}", - "backup_running_hooks": "Running backup hooks...", + "backup_running_hooks": "Running backup hooks…", "backup_system_part_failed": "Could not backup the '{part}' system part", "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 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_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)", @@ -149,7 +150,7 @@ "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_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}' 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.)", @@ -158,7 +159,6 @@ "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}", @@ -167,13 +167,8 @@ "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", - "config_validate_date": "Should be a valid date like in the format YYYY-MM-DD", - "config_validate_email": "Should be a valid email", - "config_validate_time": "Should be a valid time like HH:MM", - "config_validate_url": "Should be a valid web URL", - "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_danger": "DANGER! This app is known to be still experimental (if not explicitly not working)! You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers}'", + "confirm_app_install_thirdparty": "DANGER! This app is not part of YunoHost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers}'", "confirm_app_install_warning": "Warning: This app may work, but is not well-integrated into YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers}] ", "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}]", @@ -191,7 +186,7 @@ "diagnosis_basesystem_hardware_model": "Server model is {model}", "diagnosis_basesystem_host": "Server is running Debian {debian_version}", "diagnosis_basesystem_kernel": "Server is running Linux kernel {kernel_version}", - "diagnosis_basesystem_ynh_inconsistent_versions": "You are running inconsistent versions of the YunoHost packages... most probably because of a failed or partial upgrade.", + "diagnosis_basesystem_ynh_inconsistent_versions": "You are running inconsistent versions of the YunoHost packages… most probably because of a failed or partial upgrade.", "diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})", "diagnosis_cache_still_valid": "(Cache still valid for {category} diagnosis. Won't re-diagnose it yet!)", @@ -244,8 +239,15 @@ "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 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_ignore_already_filtered": "(There is already a diagnosis {category} filter with these criterias)", + "diagnosis_ignore_criteria_error": "Criterias should be of the form key=value (e.g. domain=yolo.test)", + "diagnosis_ignore_filter_added": "Added a {category} diagnosis filter", + "diagnosis_ignore_filter_removed": "Removed a {category} diagnosis filter", + "diagnosis_ignore_missing_criteria": "You should provide at least one criteria being the diagnosis category to ignore", + "diagnosis_ignore_no_filter_found": "(There is no such diagnosis {category} filter with these criterias to remove)", + "diagnosis_ignore_no_issue_found": "No issues was found matching the given criteria.", "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!", @@ -254,8 +256,8 @@ "diagnosis_ip_local": "Local IP: {local}", "diagnosis_ip_no_ipv4": "The server does not have working IPv4.", "diagnosis_ip_no_ipv6": "The server does not have working IPv6.", - "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://yunohost.org/#/ipv6. If you cannot enable IPv6 or if it seems too technical for you, you can also safely ignore this warning.", - "diagnosis_ip_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_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.", @@ -275,13 +277,13 @@ "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 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 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_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 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 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_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)", @@ -304,9 +306,9 @@ "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})", "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_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_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 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} :(", @@ -321,16 +323,14 @@ "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_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_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 (bookworm).", + "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 bookworm. 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_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_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.", @@ -358,8 +358,19 @@ "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_config_portal_logo": "Custom logo", + "domain_config_portal_logo_help": "Accept .svg, .png and .jpeg. Prefer a monochrome .svg with fill: currentColor so that the logo adapts to the themes.", + "domain_config_portal_public_intro": "Custom public intro", + "domain_config_portal_public_intro_help": "You can use HTML, basic styles will be applied to generic elements.", + "domain_config_portal_theme": "Default theme", + "domain_config_portal_theme_help": "Users are allowed to choose another one in their settings.", + "domain_config_portal_title": "Custom title", + "domain_config_portal_user_intro": "Custom user intro", + "domain_config_portal_user_intro_help": "You can use HTML, basic styles will be applied to generic elements.", + "domain_config_search_engine": "Search engine URL", + "domain_config_search_engine_help": "An URL with an empty query string like `https://duckduckgo.com/?q=`, with `q=` as duckduckgo's empty query parameter", + "domain_config_search_engine_name": "Search engine name", + "domain_config_show_other_domains_apps": "Show other domain's apps", "domain_created": "Domain created", "domain_creation_failed": "Unable to create domain {domain}: {error}", "domain_deleted": "Domain deleted", @@ -375,14 +386,13 @@ "domain_dns_push_partial_failure": "DNS records partially updated: some warnings/errors were reported.", "domain_dns_push_record_failed": "Failed to {action} record {type}/{name} : {error}", "domain_dns_push_success": "DNS records updated!", - "domain_dns_pushing": "Pushing DNS records...", + "domain_dns_pushing": "Pushing DNS records…", "domain_dns_registrar_experimental": "So far, the interface with **{registrar}**'s API has not been properly tested and reviewed by the YunoHost community. Support is **very experimental** - be careful!", "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.", "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://yunohost.org/dns.", "domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )", "domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)", "domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain", - "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", @@ -391,21 +401,31 @@ "domain_unknown": "Domain '{domain}' unknown", "domains_available": "Available domains:", "done": "Done", - "downloading": "Downloading...", - "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a` and/or `sudo dpkg --audit`.", + "downloading": "Downloading…", + "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state… You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a` and/or `sudo dpkg --audit`.", "dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)", "dyndns_could_not_check_available": "Could not check if {domain} is available on {provider}.", "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", "dyndns_ip_updated": "Updated your IP on DynDNS", - "dyndns_key_generating": "Generating DNS key... It may take a while.", "dyndns_key_not_found": "DNS key not found for the domain", "dyndns_no_domain_registered": "No domain registered with DynDNS", + "dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!", "dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.", - "dyndns_registered": "DynDNS domain registered", - "dyndns_registration_failed": "Could not register DynDNS domain: {error}", + "dyndns_set_recovery_password_denied": "Failed to set recovery password: invalid key", + "dyndns_set_recovery_password_failed": "Failed to set recovery password: {error}", + "dyndns_set_recovery_password_invalid_password": "Failed to set recovery password: password is not strong enough", + "dyndns_set_recovery_password_success": "Recovery password set!", + "dyndns_set_recovery_password_unknown_domain": "Failed to set recovery password: domain not registered", + "dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}", + "dyndns_subscribed": "DynDNS domain subscribed", + "dyndns_too_many_requests": "YunoHost's dyndns service received too many requests from you, wait 1 hour or so before trying again.", "dyndns_unavailable": "The domain '{domain}' is unavailable.", - "extracting": "Extracting...", + "dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed", + "dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials", + "dyndns_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}", + "dyndns_unsubscribed": "DynDNS domain unsubscribed", + "extracting": "Extracting…", "field_invalid": "Invalid field '{}'", "file_does_not_exist": "The file {path} does not exist.", "firewall_reload_failed": "Could not reload the firewall", @@ -424,9 +444,7 @@ "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_pop3_enabled_help": "Enable the POP3 protocol for the mail server. POP3 is an older protocol to access mailboxes from email clients and is more lightweight, but has less features than IMAP (enabled by default)", "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.", @@ -447,18 +465,18 @@ "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_ssh_port_help": "A port lower than 1024 is preferred to prevent usurpation attempts by non-administrator services on the remote machine. You should also avoid using a port already in use, such as 80 or 443.", "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.", + "global_settings_setting_webadmin_allowlist_help": "IP adresses allowed to access the webadmin. CIDR notation is allowed.", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).", "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).", "group_already_exist": "Group {group} already exists", "group_already_exist_on_system": "Group {group} already exists in the system groups", - "group_already_exist_on_system_but_removing_it": "Group {group} already exists in the system groups, but YunoHost will remove it...", + "group_already_exist_on_system_but_removing_it": "Group {group} already exists in the system groups, but YunoHost will remove it…", "group_cannot_be_deleted": "The group {group} cannot be deleted manually.", "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost", "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.", @@ -467,13 +485,17 @@ "group_creation_failed": "Could not create the group '{group}': {error}", "group_deleted": "Group '{group}' deleted", "group_deletion_failed": "Could not delete the group '{group}': {error}", + "group_mailalias_add": "The email alias '{mail}' will be added to the group '{group}'", + "group_mailalias_remove": "The email alias '{mail}' will be removed from the group '{group}'", "group_no_change": "Nothing to change for group '{group}'", "group_unknown": "The group '{group}' is unknown", "group_update_aliases": "Updating aliases for group '{group}'", "group_update_failed": "Could not update the group '{group}': {error}", "group_updated": "Group '{group}' updated", + "group_user_add": "The user '{user}' will be added to the group '{group}'", "group_user_already_in_group": "User {user} is already in group {group}", "group_user_not_in_group": "User {user} is not in group {group}", + "group_user_remove": "The user '{user}' will be removed from the group '{group}'", "hook_exec_failed": "Could not run script: {path}", "hook_exec_not_terminated": "Script did not finish properly: {path}", "hook_json_return_error": "Could not read return from hook {path}. Error: {msg}. Raw content: {raw_content}", @@ -482,15 +504,14 @@ "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}'", "ldap_server_down": "Unable to reach LDAP server", - "ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it...", + "ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it…", "log_app_action_run": "Run action of the '{}' app", "log_app_change_url": "Change the URL of the '{}' app", "log_app_config_set": "Apply config to the '{}' app", @@ -510,6 +531,7 @@ "log_domain_main_domain": "Make '{}' the main domain", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", + "log_dyndns_unsubscribe": "Unsubscribe to a YunoHost subdomain '{}'", "log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'", "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}'", @@ -543,6 +565,8 @@ "log_user_permission_update": "Update accesses for permission '{}'", "log_user_update": "Update info for user '{}'", "mail_alias_remove_failed": "Could not remove e-mail alias '{mail}'", + "mail_alias_unauthorized": "You are not authorized to add aliases related to domain '{domain}'", + "mail_already_exists": "Mail adress '{mail}' already exists", "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 for the admins group", @@ -550,57 +574,43 @@ "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_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_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...", - "migration_0021_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}", - "migration_0021_start": "Starting migration to Bullseye", - "migration_0021_still_on_buster_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Buster", - "migration_0021_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Bullseye.", - "migration_0021_yunohost_upgrade": "Starting YunoHost core upgrade...", - "migration_0023_not_enough_space": "Make sufficient space available in {path} to run the migration.", - "migration_0023_postgresql_11_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", - "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 is installed, but not PostgreSQL 13!? Something weird might have happened on your system :(...", - "migration_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_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_0027_cleaning_up": "Cleaning up cache and packages not useful anymore...", - "migration_0027_hgjghjghjgeneral_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_0027_main_upgrade": "Starting main upgrade...", + "migration_0027_cleaning_up": "Cleaning up cache and packages not useful anymore…", + "migration_0027_delayed_api_restart": "The YunoHost API will automatically be restarted in 15 seconds. It may be unavailable for a few seconds, and then you will have to login again.", + "migration_0027_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 properly.", + "migration_0027_main_upgrade": "Starting main upgrade…", "migration_0027_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_0027_not_buster2": "The current Debian distribution is not Bullseye! If you already ran the Bullseye->Bookworm 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_0027_not_bullseye": "The current Debian distribution is not Bullseye! If you already ran the Bullseye -> Bookworm 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_0027_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.", - "migration_0027_patch_yunohost_conflicts": "Applying patch to workaround conflict issue...", - "migration_0027_patching_sources_list": "Patching the sources.lists...", + "migration_0027_patch_yunohost_conflicts": "Applying patch to workaround conflict issue…", + "migration_0027_patching_sources_list": "Patching the sources.lists file…", "migration_0027_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}", - "migration_0027_start": "Starting migration to Bookworm", - "migration_0027_still_on_buster_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Bullseye", + "migration_0027_start": "Starting migration to Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Bullseye.", "migration_0027_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Bookworm.", - "migration_0027_yunohost_upgrade": "Starting YunoHost core upgrade...", - "migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bookworm and YunoHost 12", - "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_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_0027_yunohost_upgrade": "Starting YunoHost core upgrade…", + "migration_0029_not_enough_space": "Make sufficient space available in {path} to run the migration.", + "migration_0029_postgresql_13_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", + "migration_0029_postgresql_15_not_installed": "PostgreSQL 13 is installed, but not PostgreSQL 15!? Something weird might have happened on your system :(…", + "migration_0030_rebuild_python_venv_in_bookworm_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_0030_rebuild_python_venv_in_bookworm_disclaimer_base": "Following the upgrade to Debian Bookworm, 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_0030_rebuild_python_venv_in_bookworm_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_0030_rebuild_python_venv_in_bookworm_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}", + "migration_0030_rebuild_python_venv_in_bookworm_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_0030_rebuild_python_venv_in_bookworm_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`", + "migration_description_0027_migrate_to_bookworm": "Upgrade the system to Debian Bookworm and YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Delete the old XMPP permissions, Metronome is now an app", + "migration_description_0029_postgresql_13_to_15": "Migrate databases from PostgreSQL 13 to 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Repair Python app after bookworm migration", "migration_ldap_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.", "migration_ldap_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error}", - "migration_ldap_migration_failed_trying_to_rollback": "Could not migrate... trying to roll back the system.", + "migration_ldap_migration_failed_trying_to_rollback": "Could not migrate… trying to roll back the system.", "migration_ldap_rollback_success": "System rolled back.", "migrations_already_ran": "Those migrations are already done: {ids}", "migrations_dependencies_not_satisfied": "Run these migrations: '{dependencies_id}', before migration {id}.", "migrations_exclusive_options": "'--auto', '--skip', and '--force-rerun' are mutually exclusive options.", "migrations_failed_to_load_migration": "Could not load migration {id}: {error}", "migrations_list_conflict_pending_done": "You cannot use both '--previous' and '--done' at the same time.", - "migrations_loading_migration": "Loading migration {id}...", + "migrations_loading_migration": "Loading migration {id}…", "migrations_migration_has_failed": "Migration {id} did not complete, aborting. Error: {exception}", "migrations_must_provide_explicit_targets": "You must provide explicit targets when using '--skip' or '--force-rerun'", "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'.", @@ -608,13 +618,13 @@ "migrations_no_such_migration": "There is no migration called '{id}'", "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_running_forward": "Running migration {id}…", + "migrations_skip_migration": "Skipping migration {id}…", "migrations_success_forward": "Migration {id} completed", "migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations run`.", "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", + "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", @@ -626,9 +636,7 @@ "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 (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}", @@ -653,6 +661,21 @@ "port_already_closed": "Port {port} is already closed for {ip_version} connections", "port_already_opened": "Port {port} is already opened for {ip_version} connections", "postinstall_low_rootfsspace": "The root filesystem has a total space less than 10 GB, which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16GB for the root filesystem. If you want to install YunoHost despite this warning, re-run the postinstall with --force-diskspace", + "pydantic_type_error": "Invalid type.", + "pydantic_type_error_none_not_allowed": "Value is required.", + "pydantic_type_error_str": "Invalid type, string expected.", + "pydantic_value_error_color": "Not a valid color, value must be a named or hex color.", + "pydantic_value_error_const": "Unexpected value; choose between {permitted}", + "pydantic_value_error_date": "Invalid date format", + "pydantic_value_error_email": "Value is not a valid email address", + "pydantic_value_error_number_not_ge": "Value must be greater than or equal to {limit_value}.", + "pydantic_value_error_number_not_le": "Value must be less than or equal to {limit_value}.", + "pydantic_value_error_str_regex": "Invalid string; value doesn't respects the pattern '{pattern}'", + "pydantic_value_error_time": "Invalid time format", + "pydantic_value_error_url_extra": "URL invalid, extra characters found after valid URL: '{extra}'", + "pydantic_value_error_url_host": "URL host invalid", + "pydantic_value_error_url_port": "URL port invalid, port cannot exceed 65535", + "pydantic_value_error_url_scheme": "Invalid or missing URL scheme", "regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'...", "regenconf_failed": "Could not regenerate the configuration for category(s): {categories}", "regenconf_file_backed_up": "Configuration file '{conf}' backed up to '{backup}'", @@ -665,7 +688,7 @@ "regenconf_file_updated": "Configuration file '{conf}' updated", "regenconf_need_to_explicitly_specify_ssh": "The ssh configuration has been manually modified, but you need to explicitly specify category 'ssh' with --force to actually apply the changes.", "regenconf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost (category {category}).", - "regenconf_pending_applying": "Applying pending configuration for category '{category}'...", + "regenconf_pending_applying": "Applying pending configuration for category '{category}'…", "regenconf_up_to_date": "The configuration is already up-to-date for category '{category}'", "regenconf_updated": "Configuration updated for '{category}'", "regenconf_would_be_updated": "The configuration would have been updated for category '{category}'", @@ -678,15 +701,15 @@ "restore_cleaning_failed": "Could not clean up the temporary restoration directory", "restore_complete": "Restoration completed", "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers}]", - "restore_extracting": "Extracting needed files from the archive...", + "restore_extracting": "Extracting needed files from the archive…", "restore_failed": "Could not restore system", "restore_hook_unavailable": "Restoration script for '{part}' not available on your system and not in the archive either", "restore_may_be_not_enough_disk_space": "Your system does not seem to have enough space (free: {free_space} B, needed space: {needed_space} B, security margin: {margin} B)", "restore_not_enough_disk_space": "Not enough space (space: {free_space} B, needed space: {needed_space} B, security margin: {margin} B)", "restore_nothings_done": "Nothing was restored", "restore_removing_tmp_dir_failed": "Could not remove an old temporary directory", - "restore_running_app_script": "Restoring the app '{app}'...", - "restore_running_hooks": "Running restoration hooks...", + "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!", @@ -702,17 +725,17 @@ "service_description_dnsmasq": "Handles domain name resolution (DNS)", "service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)", "service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet", - "service_description_metronome": "Manage XMPP instant messaging accounts", "service_description_mysql": "Stores app data (SQL database)", "service_description_nginx": "Serves or provides access to all the websites hosted on your server", + "service_description_opendkim": "Signs outgoing emails using DKIM such that they are less likely to be flagged as spam", "service_description_postfix": "Used to send and receive e-mails", "service_description_postgresql": "Stores app data (SQL database)", "service_description_redis-server": "A specialized database used for rapid data access, task queue, and communication between programs", - "service_description_rspamd": "Filters spam, and other e-mail related features", "service_description_slapd": "Stores users, domains and related info", "service_description_ssh": "Allows you to connect remotely to your server via a terminal (SSH protocol)", "service_description_yunohost-api": "Manages interactions between the YunoHost web interface and the system", "service_description_yunohost-firewall": "Manages open and close connection ports to services", + "service_description_yunohost-portal-api": "Manages interactions between the different user portal web interfaces and the system", "service_description_yunomdns": "Allows you to reach your server using 'yunohost.local' in your local network", "service_disable_failed": "Could not make the service '{service}' not start at boot.\n\nRecent service logs:{logs}", "service_disabled": "The service '{service}' will not be started anymore when system boots.", @@ -734,10 +757,10 @@ "service_unknown": "Unknown service '{service}'", "show_tile_cant_be_enabled_for_regex": "You cannot enable 'show_tile' right now, because the URL for the permission '{permission}' is a regex", "show_tile_cant_be_enabled_for_url_not_defined": "You cannot enable 'show_tile' right now, because you must first define an URL for the permission '{permission}'", - "ssowat_conf_generated": "SSOwat configuration regenerated", + "ssowat_conf_generated": "SSO and portal configurations regenerated", "system_upgraded": "System upgraded", "system_username_exists": "Username already exists in the list of system users", - "this_action_broke_dpkg": "This action broke dpkg/APT (the system package managers)... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", + "this_action_broke_dpkg": "This action broke dpkg/APT (the system package managers)… You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", "tools_upgrade": "Upgrading system packages", "tools_upgrade_failed": "Could not upgrade packages: {packages_list}", "unbackup_app": "{app} will not be saved", @@ -747,9 +770,9 @@ "unrestore_app": "{app} will not be restored", "update_apt_cache_failed": "Unable to update the cache of APT (Debian's package manager). Here is a dump of the sources.list lines, which might help identify problematic lines: \n{sourceslist}", "update_apt_cache_warning": "Something went wrong while updating the cache of APT (Debian's package manager). Here is a dump of the sources.list lines, which might help identify problematic lines: \n{sourceslist}", - "updating_apt_cache": "Fetching available upgrades for system packages...", + "updating_apt_cache": "Fetching available upgrades for system packages…", "upgrade_complete": "Upgrade complete", - "upgrading_packages": "Upgrading packages...", + "upgrading_packages": "Upgrading packages…", "upnp_dev_not_found": "No UPnP device found", "upnp_disabled": "UPnP turned off", "upnp_enabled": "UPnP turned on", @@ -773,7 +796,7 @@ "visitors": "Visitors", "yunohost_already_installed": "YunoHost is already installed", "yunohost_configured": "YunoHost is now configured", - "yunohost_installing": "Installing YunoHost...", + "yunohost_installing": "Installing YunoHost…", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} \ No newline at end of file +} diff --git a/locales/eo.json b/locales/eo.json index b0bdf280b..257dfb541 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -35,19 +35,19 @@ "apps_already_up_to_date": "Ĉiuj aplikoj estas jam ĝisdatigitaj", "app_location_unavailable": "Ĉi tiu URL aŭ ne haveblas, aŭ konfliktas kun la jam instalita (j) apliko (j):\n{apps}", "backup_archive_app_not_found": "Ne povis trovi {app} en la rezerva arkivo", - "backup_actually_backuping": "Krei rezervan arkivon de la kolektitaj dosieroj ...", + "backup_actually_backuping": "Krei rezervan arkivon de la kolektitaj dosieroj…", "app_change_url_no_script": "La app '{app_name}' ankoraŭ ne subtenas URL-modifon. Eble vi devus altgradigi ĝin.", - "app_start_install": "Instali {app}...", - "backup_created": "Sekurkopio kreita", + "app_start_install": "Instali {app}…", + "backup_created": "Sekurkopio kreita: {name}", "app_make_default_location_already_used": "Ne povis fari '{app}' la defaŭltan programon sur la domajno, '{domain}' estas jam uzata de '{other_app}'", "backup_method_copy_finished": "Rezerva kopio finis", "app_not_properly_removed": "{app} ne estis ĝuste forigita", "backup_archive_broken_link": "Ne povis aliri la rezervan ar archiveivon (rompita ligilo al {path})", - "app_requirements_checking": "Kontrolante bezonatajn pakaĵojn por {app} ...", + "app_requirements_checking": "Kontrolante bezonatajn pakaĵojn por {app}…", "app_not_installed": "Ne povis trovi {app} en la listo de instalitaj programoj: {all_apps}", "ask_new_path": "Nova vojo", "backup_custom_mount_error": "Propra rezerva metodo ne povis preterpasi la paŝon 'monto'", - "app_upgrade_app_name": "Nun ĝisdatigu {app}...", + "app_upgrade_app_name": "Nun ĝisdatigu {app}…", "backup_cleaning_failed": "Ne povis purigi la provizoran rezervan dosierujon", "backup_creation_failed": "Ne povis krei la rezervan ar archiveivon", "backup_hook_unknown": "La rezerva hoko '{hook}' estas nekonata", @@ -63,27 +63,27 @@ "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", - "app_start_backup": "Kolekti dosierojn por esti subtenata por {app}...", + "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...", + "backup_applying_method_tar": "Krei la rezervon TAR Arkivo…", "backup_method_custom_finished": "Propra rezerva metodo '{method}' finiĝis", "app_already_installed_cant_change_url": "Ĉi tiu app estas jam instalita. La URL ne povas esti ŝanĝita nur per ĉi tiu funkcio. Kontrolu en `app changeurl` se ĝi haveblas.", "app_not_correctly_installed": "{app} ŝajnas esti malĝuste instalita", "app_removed": "{app} forigita", "backup_delete_error": "Ne povis forigi '{path}'", "backup_nothings_done": "Nenio por ŝpari", - "backup_applying_method_custom": "Voki la laŭmendan rezervan metodon '{method}'...", + "backup_applying_method_custom": "Voki la laŭmendan rezervan metodon '{method}'…", "backup_app_failed": "Ne povis subteni {app}", "app_upgrade_some_app_failed": "Iuj aplikoj ne povis esti altgradigitaj", - "app_start_remove": "Forigado {app}...", + "app_start_remove": "Forigado {app}…", "backup_output_directory_not_empty": "Vi devas elekti malplenan eligitan dosierujon", "backup_archive_writing_error": "Ne povis aldoni la dosierojn '{source}' (nomitaj en la ar theivo '{dest}') por esti rezervitaj en la kunpremita arkivo '{archive}'", - "app_start_restore": "Restarigi {app}...", - "backup_applying_method_copy": "Kopii ĉiujn dosierojn por sekurigi...", + "app_start_restore": "Restarigi {app}…", + "backup_applying_method_copy": "Kopii ĉiujn dosierojn por sekurigi…", "backup_couldnt_bind": "Ne povis ligi {src} al {dest}.", "ask_password": "Pasvorto", "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_mount_archive_for_restore": "Preparante arkivon por restarigo…", "backup_csv_creation_failed": "Ne povis krei la CSV-dosieron bezonatan por restarigo", "backup_archive_name_unknown": "Nekonata loka rezerva ar archiveivo nomata '{name}'", "app_sources_fetch_failed": "Ne povis akiri fontajn dosierojn, ĉu la URL estas ĝusta?", @@ -92,10 +92,9 @@ "app_not_upgraded": "La '{failed_app}' de la programo ne sukcesis ĝisdatigi, kaj sekve la nuntempaj plibonigoj de la sekvaj programoj estis nuligitaj: {apps}", "aborting": "Aborti.", "app_upgraded": "{app} altgradigita", - "backup_deleted": "Rezerva forigita", + "backup_deleted": "Rezerva forigita: {name}", "backup_csv_addition_failed": "Ne povis aldoni dosierojn al sekurkopio en la CSV-dosiero", "dpkg_lock_not_available": "Ĉi tiu komando ne povas funkcii nun ĉar alia programo uzas la seruron de dpkg (la administrilo de paka sistemo)", - "domain_dyndns_root_unknown": "Nekonata radika domajno DynDNS", "field_invalid": "Nevalida kampo '{}'", "log_app_makedefault": "Faru '{}' la defaŭlta apliko", "backup_system_part_failed": "Ne eblis sekurkopi la sistemon de '{part}'", @@ -187,11 +186,11 @@ "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} '", - "backup_running_hooks": "Kurado de apogaj hokoj …", + "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", - "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`.", + "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", "log_available_on_yunopaste": "Ĉi tiu protokolo nun haveblas per {url}", @@ -205,7 +204,7 @@ "hook_exec_not_terminated": "Skripto ne finiĝis ĝuste: {path}", "service_stopped": "Servo '{service}' ĉesis", "restore_failed": "Ne povis restarigi sistemon", - "confirm_app_install_danger": "Danĝero! Ĉi tiu apliko estas konata ankoraŭ eksperimenta (se ne eksplicite ne funkcias)! 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}'", + "confirm_app_install_danger": "Danĝero! Ĉi tiu apliko estas konata ankoraŭ eksperimenta (se ne eksplicite ne funkcias)! 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}'", "log_operation_unit_unclosed_properly": "Operaciumo ne estis fermita ĝuste", "upgrade_complete": "Ĝisdatigo kompleta", "upnp_enabled": "UPnP ŝaltis", @@ -226,11 +225,10 @@ "dyndns_unavailable": "La domajno '{domain}' ne haveblas.", "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 …", + "downloading": "Elŝutante…", "user_deleted": "Uzanto forigita", "service_enable_failed": "Ne povis fari la servon '{service}' aŭtomate komenci ĉe la ekkuro.\n\nLastatempaj servaj protokoloj: {logs}", "domains_available": "Haveblaj domajnoj:", - "dyndns_registered": "Registrita domajno DynDNS", "service_description_fail2ban": "Protektas kontraŭ bruta forto kaj aliaj specoj de atakoj de la interreto", "file_does_not_exist": "La dosiero {path} ne ekzistas.", "yunohost_not_installed": "YunoHost ne estas ĝuste instalita. Bonvolu prilabori 'yunohost tools postinstall'", @@ -254,15 +252,13 @@ "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path}'", "dyndns_ip_update_failed": "Ne povis ĝisdatigi IP-adreson al DynDNS", "log_link_to_failed_log": "Ne povis plenumi la operacion '{desc}'. Bonvolu provizi la plenan protokolon de ĉi tiu operacio per alklakante ĉi tie por akiri helpon", - "user_home_creation_failed": "Ne povis krei dosierujon \"home\" por uzanto", + "user_home_creation_failed": "Ne povis krei dosierujon '{home}' por uzanto", "pattern_backup_archive_name": "Devas esti valida dosiernomo kun maksimume 30 signoj, alfanombraj kaj -_. signoj nur", "restore_cleaning_failed": "Ne eblis purigi la adresaron de provizora restarigo", - "dyndns_registration_failed": "Ne povis registri DynDNS-domajnon: {error}", "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}'", "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}'…", "migrations_skip_migration": "Salti migradon {id}…", "regenconf_file_removed": "Agordodosiero '{conf}' forigita", @@ -285,7 +281,7 @@ "log_does_exists": "Ne estas operacio kun la nomo '{log}', uzu 'yunohost log list' por vidi ĉiujn disponeblajn operaciojn", "service_add_failed": "Ne povis aldoni la servon '{service}'", "pattern_password_app": "Bedaŭrinde, pasvortoj ne povas enhavi jenajn signojn: {forbidden_chars}", - "this_action_broke_dpkg": "Ĉi tiu ago rompis dpkg / APT (la administrantoj pri la paka sistemo) ... Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.", + "this_action_broke_dpkg": "Ĉi tiu ago rompis dpkg / APT (la administrantoj pri la paka sistemo)… Vi povas provi solvi ĉi tiun problemon per konekto per SSH kaj funkcianta `sudo dpkg --configure -a`.", "log_regen_conf": "Regeneri sistemajn agordojn '{}'", "restore_hook_unavailable": "Restariga skripto por '{part}' ne haveblas en via sistemo kaj ankaŭ ne en la ar theivo", "log_dyndns_subscribe": "Aboni al YunoHost-subdominio '{}'", @@ -320,7 +316,7 @@ "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}", - "domain_uninstall_app_first": "Unu aŭ pluraj programoj estas instalitaj en ĉi tiu domajno. Bonvolu malinstali ilin antaŭ ol daŭrigi la domajnan forigon", + "domain_uninstall_app_first": "Unu aŭ pluraj programoj estas instalitaj en ĉi tiu domajno:\n{apps}\n\nBonvolu malinstali ilin antaŭ ol daŭrigi la domajnan forigon", "service_unknown": "Nekonata servo '{service}'", "domain_deletion_failed": "Ne eblas forigi domajnon {domain}: {error}", "log_user_update": "Ĝisdatigu uzantinformojn de '{}'", @@ -329,7 +325,7 @@ "done": "Farita", "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}'", + "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}'", "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", @@ -354,7 +350,7 @@ "log_app_remove": "Forigu la aplikon '{}'", "service_restart_failed": "Ne povis rekomenci la servon '{service}'\n\nLastatempaj servaj protokoloj: {logs}", "firewall_rules_cmd_failed": "Iuj komandoj pri fajroŝirmilo malsukcesis. Pliaj informoj en ensaluto.", - "certmanager_certificate_fetching_or_enabling_failed": "Provante uzi la novan atestilon por {domain} ne funkciis …", + "certmanager_certificate_fetching_or_enabling_failed": "Provante uzi la novan atestilon por {domain} ne funkciis…", "app_full_domain_unavailable": "Bedaŭrinde, ĉi tiu app devas esti instalita sur propra domajno, sed aliaj programoj jam estas instalitaj sur la domajno '{domain}'. Vi povus uzi subdominon dediĉitan al ĉi tiu app anstataŭe.", "group_cannot_edit_all_users": "La grupo 'all_users' ne povas esti redaktita permane. Ĝi estas speciala grupo celita enhavi ĉiujn uzantojn registritajn en YunoHost", "group_cannot_edit_visitors": "La grupo 'vizitantoj' ne povas esti redaktita permane. Ĝi estas speciala grupo reprezentanta anonimajn vizitantojn", @@ -364,17 +360,17 @@ "permission_currently_allowed_for_all_users": "Ĉi tiu permeso estas nuntempe donita al ĉiuj uzantoj aldone al aliaj grupoj. Vi probable volas aŭ forigi la permeson \"all_users\" aŭ forigi la aliajn grupojn, kiujn ĝi nuntempe donas.", "app_install_failed": "Ne povis instali {app}: {error}", "app_install_script_failed": "Eraro okazis en la skripto de instalado de la app", - "app_remove_after_failed_install": "Forigado de la programo post la instalado-fiasko ...", + "app_remove_after_failed_install": "Forigado de la programo post la instalado-fiasko…", "diagnosis_basesystem_host": "Servilo funkcias Debian {debian_version}", "apps_catalog_init_success": "Aplikoj katalogsistemo inicializita !", - "apps_catalog_updating": "Ĝisdatigante katalogo de aplikoj …", + "apps_catalog_updating": "Ĝisdatigante katalogo de aplikoj…", "apps_catalog_failed_to_download": "Ne eblas elŝuti la katalogon de {apps_catalog}: {error}", "apps_catalog_obsolete_cache": "La kaŝmemoro de la aplika katalogo estas malplena aŭ malaktuala.", "apps_catalog_update_success": "La aplika katalogo estis ĝisdatigita!", "diagnosis_basesystem_kernel": "Servilo funkcias Linuksan kernon {kernel_version}", "diagnosis_basesystem_ynh_single_version": "{package} versio: {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Servilo funkcias YunoHost {main_version} ({repo})", - "diagnosis_basesystem_ynh_inconsistent_versions": "Vi prizorgas malkonsekvencajn versiojn de la YunoHost-pakoj... plej probable pro malsukcesa aŭ parta ĝisdatigo.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Vi prizorgas malkonsekvencajn versiojn de la YunoHost-pakoj… plej probable pro malsukcesa aŭ parta ĝisdatigo.", "diagnosis_cache_still_valid": "(La kaŝmemoro ankoraŭ validas por {category} diagnozo. Vi ankoraŭ ne diagnozas ĝin!)", "diagnosis_cant_run_because_of_dep": "Ne eblas fari diagnozon por {category} dum estas gravaj problemoj rilataj al {dep}.", "diagnosis_failed_for_category": "Diagnozo malsukcesis por kategorio '{category}': {error}", @@ -384,7 +380,7 @@ "diagnosis_mail_outgoing_port_25_blocked": "Eliranta haveno 25 ŝajnas esti blokita. Vi devas provi malŝlosi ĝin en via agorda panelo de provizanto (aŭ gastiganto). Dume la servilo ne povos sendi retpoŝtojn al aliaj serviloj.", "diagnosis_http_bad_status_code": "Ĝi aspektas kiel alia maŝino (eble via interreta enkursigilo) respondita anstataŭ via servilo.
1. La plej ofta kaŭzo por ĉi tiu afero estas, ke la haveno 80 (kaj 443) ne estas ĝuste senditaj al via servilo .
2. Pri pli kompleksaj agordoj: certigu, ke neniu fajroŝirmilo aŭ reverso-prokuro ne interbatalas.", "main_domain_changed": "La ĉefa domajno estis ŝanĝita", - "yunohost_postinstall_end_tip": "La post-instalado finiĝis! Por fini vian agordon, bonvolu konsideri:\n - aldonado de unua uzanto tra la sekcio 'Uzantoj' de la retadreso (aŭ 'uzanto de yunohost kreu ' en komandlinio);\n - diagnozi eblajn problemojn per la sekcio 'Diagnozo' de la reteja administrado (aŭ 'diagnoza yunohost-ekzekuto' en komandlinio);\n - legante la partojn 'Finigi vian agordon' kaj 'Ekkoni Yunohost' en la administra dokumentado: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "La post-instalado finiĝis! Por fini vian agordon, bonvolu konsideri:\n - diagnozi eblajn problemojn per la sekcio 'Diagnozo' de la reteja administrado (aŭ 'diagnoza yunohost-ekzekuto' en komandlinio);\n - legante la partojn 'Finigi vian agordon' kaj 'Ekkoni Yunohost' en la administra dokumentado: https://yunohost.org/admindoc.", "diagnosis_ip_connected_ipv4": "La servilo estas konektita al la interreto per IPv4 !", "diagnosis_ip_no_ipv4": "La servilo ne havas funkciantan IPv4.", "diagnosis_ip_connected_ipv6": "La servilo estas konektita al la interreto per IPv6 !", @@ -398,13 +394,13 @@ "diagnosis_ram_ok": "La sistemo ankoraŭ havas {available} ({available_percent}%) RAM forlasita de {total}.", "diagnosis_swap_none": "La sistemo tute ne havas interŝanĝon. Vi devus pripensi aldoni almenaŭ {recommended} da interŝanĝo por eviti situaciojn en kiuj la sistemo restas sen memoro.", "diagnosis_swap_notsomuch": "La sistemo havas nur {total}-interŝanĝon. Vi konsideru havi almenaŭ {recommended} por eviti situaciojn en kiuj la sistemo restas sen memoro.", - "diagnosis_regenconf_manually_modified_details": "Ĉi tio probable estas bona, se vi scias, kion vi faras! YunoHost ĉesigos ĝisdatigi ĉi tiun dosieron aŭtomate ... Sed atentu, ke YunoHost-ĝisdatigoj povus enhavi gravajn rekomendajn ŝanĝojn. Se vi volas, vi povas inspekti la diferencojn per yyunohost tools regen-conf {category} --dry-run --with-diff kaj devigi la reset al la rekomendita agordo per yunohost tools regen-conf {category} --force", + "diagnosis_regenconf_manually_modified_details": "Ĉi tio probable estas bona, se vi scias, kion vi faras! YunoHost ĉesigos ĝisdatigi ĉi tiun dosieron aŭtomate… Sed atentu, ke YunoHost-ĝisdatigoj povus enhavi gravajn rekomendajn ŝanĝojn. Se vi volas, vi povas inspekti la diferencojn per yyunohost tools regen-conf {category} --dry-run --with-diff kaj devigi la reset al la rekomendita agordo per yunohost tools regen-conf {category} --force", "diagnosis_security_vulnerable_to_meltdown": "Vi ŝajnas vundebla al la kritiko-vundebleco de Meltdown", "diagnosis_no_cache": "Neniu diagnoza kaŝmemoro por kategorio '{category}'", - "diagnosis_ip_broken_dnsresolution": "Rezolucio pri domajna nomo rompiĝas pro iu kialo... Ĉu fajroŝirmilo blokas DNS-petojn ?", + "diagnosis_ip_broken_dnsresolution": "Rezolucio pri domajna nomo rompiĝas pro iu kialo… Ĉu fajroŝirmilo blokas DNS-petojn ?", "diagnosis_ip_broken_resolvconf": "Rezolucio pri domajna nomo estas rompita en via servilo, kiu ŝajnas rilata al /etc/resolv.conf ne montrante al 127.0.0.1 .", - "diagnosis_dns_missing_record": "Laŭ la rekomendita DNS-agordo, vi devas aldoni DNS-registron kun\ntipo: {type}\nnomo: {name}\nvaloro: {value}", - "diagnosis_dns_discrepancy": "La DNS-registro kun tipo {type} kaj nomo {name} ne kongruas kun la rekomendita agordo.\nNuna valoro: {current}\nEsceptita valoro: {value}", + "diagnosis_dns_missing_record": "Laŭ la rekomendita DNS-agordo, vi devas aldoni DNS-registron kun.
Tipo: {type}
Nomo: {name}
Valoro: {value}", + "diagnosis_dns_discrepancy": "La DNS-registro kun tipo {type} kaj nomo {name} ne kongruas kun la rekomendita agordo:
Nuna valoro: {current}
Esceptita valoro: {value}", "diagnosis_services_conf_broken": "Agordo estas rompita por servo {service} !", "diagnosis_services_bad_status": "Servo {service} estas {status} :(", "diagnosis_ram_low": "La sistemo havas {available} ({available_percent}%) RAM forlasita de {total}. Estu zorgema.", @@ -440,7 +436,7 @@ "diagnosis_http_could_not_diagnose_details": "Eraro: {error}", "diagnosis_http_ok": "Domajno {domain} atingebla per HTTP de ekster la loka reto.", "diagnosis_http_unreachable": "Domajno {domain} ŝajnas neatingebla per HTTP de ekster la loka reto.", - "domain_cannot_remove_main_add_new_one": "Vi ne povas forigi '{domain}' ĉar ĝi estas la ĉefa domajno kaj via sola domajno, vi devas unue aldoni alian domajnon uzante ''yunohost domain add ', tiam agordi kiel ĉefan domajnon uzante 'yunohost domain main-domain -n ' kaj tiam vi povas forigi la domajnon' {domain} 'uzante' yunohost domain remove {domain} '.'", + "domain_cannot_remove_main_add_new_one": "Vi ne povas forigi '{domain}' ĉar ĝi estas la ĉefa domajno kaj via sola domajno, vi devas unue aldoni alian domajnon uzante ''yunohost domain add ', tiam agordi kiel ĉefan domajnon uzante 'yunohost domain main-domain -n ' kaj tiam vi povas forigi la domajnon' {domain} 'uzante' yunohost domain remove {domain} '.", "permission_require_account": "Permesilo {permission} nur havas sencon por uzantoj, kiuj havas konton, kaj tial ne rajtas esti ebligitaj por vizitantoj.", "diagnosis_found_warnings": "Trovitaj {warnings} ero (j) kiuj povus esti plibonigitaj por {category}.", "diagnosis_everything_ok": "Ĉio aspektas bone por {category}!", @@ -453,10 +449,10 @@ "diagnosis_basesystem_hardware": "Arkitekturo de servila aparataro estas {virt} {arch}", "diagnosis_description_web": "Reta", "domain_cannot_add_xmpp_upload": "Vi ne povas aldoni domajnojn per 'xmpp-upload'. Ĉi tiu speco de nomo estas rezervita por la XMPP-alŝuta funkcio integrita en YunoHost.", - "group_already_exist_on_system_but_removing_it": "Grupo {group} jam ekzistas en la sistemaj grupoj, sed YunoHost forigos ĝin …", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Iuj provizantoj ne lasos vin malŝlosi elirantan havenon 25 ĉar ili ne zorgas pri Neta Neŭtraleco.
- Iuj el ili provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Amika privateco estas uzi VPN * kun dediĉita publika IP * por pretervidi ĉi tiun specon. de limoj. Vidu https://yunohost.org/#/vpn_avantage
- Vi ankaŭ povas konsideri ŝanĝi al pli neta neŭtraleco-amika provizanto", + "group_already_exist_on_system_but_removing_it": "Grupo {group} jam ekzistas en la sistemaj grupoj, sed YunoHost forigos ĝin…", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Iuj provizantoj ne lasos vin malŝlosi elirantan havenon 25 ĉar ili ne zorgas pri Neta Neŭtraleco.
- Iuj el ili provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Amika privateco estas uzi VPN * kun dediĉita publika IP * por pretervidi ĉi tiun specon. de limoj. Vidu https://yunohost.org/vpn_avantage
- Vi ankaŭ povas konsideri ŝanĝi al pli neta neŭtraleco-amika provizanto", "diagnosis_mail_fcrdns_nok_details": "Vi unue provu agordi la inversan DNS kun {ehlo_domain} en via interreta enkursigilo aŭ en via retprovizanta interfaco. (Iuj gastigantaj provizantoj eble postulas, ke vi sendu al ili subtenan bileton por ĉi tio).", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Iuj provizantoj ne lasos vin agordi vian inversan DNS (aŭ ilia funkcio povus esti rompita ...). Se vi spertas problemojn pro tio, konsideru jenajn solvojn:
- Iuj ISP provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Interreta privateco estas uzi VPN * kun dediĉita publika IP * por preterpasi ĉi tiajn limojn. Vidu https://yunohost.org/#/vpn_avantage
- Finfine eblas ankaŭ ŝanĝo de provizanto", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Iuj provizantoj ne lasos vin agordi vian inversan DNS (aŭ ilia funkcio povus esti rompita…). Se vi spertas problemojn pro tio, konsideru jenajn solvojn:
- Iuj ISP provizas la alternativon de uzante retpoŝtan servilon kvankam ĝi implicas, ke la relajso povos spioni vian retpoŝtan trafikon.
- Interreta privateco estas uzi VPN * kun dediĉita publika IP * por preterpasi ĉi tiajn limojn. Vidu https://yunohost.org/vpn_avantage
- Finfine eblas ankaŭ ŝanĝo de provizanto", "diagnosis_display_tip": "Por vidi la trovitajn problemojn, vi povas iri al la sekcio pri Diagnozo de la reteja administrado, aŭ funkcii \"yunohost diagnosis show --issues --human-readable\" el la komandlinio.", "diagnosis_ip_global": "Tutmonda IP: {global} ", "diagnosis_ip_local": "Loka IP: {local} ", @@ -474,7 +470,7 @@ "diagnosis_mail_ehlo_could_not_diagnose_details": "Eraro: {error}", "diagnosis_mail_fcrdns_ok": "Via inversa DNS estas ĝuste agordita!", "diagnosis_mail_fcrdns_dns_missing": "Neniu inversa DNS estas difinita en IPv {ipversion}. Iuj retpoŝtoj povas malsukcesi liveri aŭ povus esti markitaj kiel spamo.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Iuj provizantoj ne lasos vin agordi vian inversan DNS (aŭ ilia funkcio povus esti rompita ...). Se via inversa DNS estas ĝuste agordita por IPv4, vi povas provi malebligi la uzon de IPv6 kiam vi sendas retpoŝtojn per funkciado yunohost-agordoj set smtp.allow_ipv6 -v off . Noto: ĉi tiu lasta solvo signifas, ke vi ne povos sendi aŭ ricevi retpoŝtojn de la malmultaj IPv6-nur serviloj tie.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Iuj provizantoj ne lasos vin agordi vian inversan DNS (aŭ ilia funkcio povus esti rompita…). Se via inversa DNS estas ĝuste agordita por IPv4, vi povas provi malebligi la uzon de IPv6 kiam vi sendas retpoŝtojn per funkciado yunohost-agordoj set smtp.allow_ipv6 -v off . Noto: ĉi tiu lasta solvo signifas, ke vi ne povos sendi aŭ ricevi retpoŝtojn de la malmultaj IPv6-nur serviloj tie.", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "La inversa DNS ne ĝuste agordis en IPv {ipversion}. Iuj retpoŝtoj povas malsukcesi liveri aŭ povus esti markitaj kiel spamo.", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Aktuala reverso DNS: {rdns_domain}
Atendita valoro: {ehlo_domain}", "diagnosis_mail_blacklist_ok": "La IP kaj domajnoj uzataj de ĉi tiu servilo ne ŝajnas esti listigitaj nigre", @@ -492,7 +488,7 @@ "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.", "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).", + "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", "app_packaging_format_not_supported": "Ĉi tiu programo ne povas esti instalita ĉar ĝia pakita formato ne estas subtenata de via Yunohost-versio. Vi probable devas konsideri ĝisdatigi vian sistemon.", "app_restore_script_failed": "Eraro okazis ene de la App Restarigu Skripton", diff --git a/locales/es.json b/locales/es.json index 85d7b1f43..bd9a644c2 100644 --- a/locales/es.json +++ b/locales/es.json @@ -23,20 +23,20 @@ "ask_password": "Contraseña", "backup_app_failed": "No se pudo respaldar «{app}»", "backup_archive_app_not_found": "No se pudo encontrar «{app}» en el archivo de respaldo", - "backup_archive_name_exists": "Ya existe un archivo de respaldo con este nombre.", + "backup_archive_name_exists": "Ya existe un archivo de respaldo con el nombre '{name}'.", "backup_archive_name_unknown": "Copia de seguridad local desconocida '{name}'", "backup_archive_open_failed": "No se pudo abrir el archivo de respaldo", "backup_cleaning_failed": "No se pudo limpiar la carpeta de respaldo temporal", - "backup_created": "Se ha creado la copia de seguridad", + "backup_created": "Copia de seguridad creada: {name}", "backup_creation_failed": "No se pudo crear el archivo de respaldo", "backup_delete_error": "No se pudo eliminar «{path}»", - "backup_deleted": "Eliminada la copia de seguridad", + "backup_deleted": "Copia de seguridad eliminada: {name}", "backup_hook_unknown": "El gancho «{hook}» de la copia de seguridad es desconocido", "backup_nothings_done": "Nada que guardar", "backup_output_directory_forbidden": "Elija un directorio de salida diferente. Las copias de seguridad no se pueden crear en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives subcarpetas", "backup_output_directory_not_empty": "Debe elegir un directorio de salida vacío", "backup_output_directory_required": "Debe proporcionar un directorio de salida para la copia de seguridad", - "backup_running_hooks": "Ejecutando los hooks de copia de respaldo...", + "backup_running_hooks": "Ejecutando los hooks de copia de respaldo…", "custom_app_url_required": "Debe proporcionar una URL para actualizar su aplicación personalizada {app}", "domain_cert_gen_failed": "No se pudo generar el certificado", "domain_created": "Dominio creado", @@ -44,18 +44,14 @@ "domain_deleted": "Dominio eliminado", "domain_deletion_failed": "No se puede eliminar el dominio {domain}: {error}", "domain_dyndns_already_subscribed": "Ya se ha suscrito a un dominio de DynDNS", - "domain_dyndns_root_unknown": "Dominio raíz de DynDNS desconocido", "domain_exists": "El dominio ya existe", "domain_uninstall_app_first": "Estas aplicaciones siguen instaladas en tu dominio:\n{apps}\n\nPor favor desinstálalas con el comando 'yunohost app remove the_app_id' o cámbialas a otro dominio usando /etc/resolv.conf personalizada.", "diagnosis_ip_weird_resolvconf_details": "El fichero /etc/resolv.conf debería ser un enlace simbólico a /etc/resolvconf/run/resolv.conf a su vez debe apuntar a 127.0.0.1 (dnsmasq). Si lo que quieres es configurar la resolución DNS manualmente, porfavor modifica /etc/resolv.dnsmasq.conf.", "diagnosis_dns_good_conf": "La configuración de registros DNS es correcta para {domain} (categoría {category})", @@ -416,10 +412,10 @@ "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_mail_outgoing_port_25_blocked": "El servidor de correo SMTP no puede enviar correos electrónicos porque el puerto saliente 25 está bloqueado en IPv{ipversion}.", "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_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 al colapso de vulnerabilidad crítica de seguridad", "diagnosis_description_basesystem": "Sistema de base", "diagnosis_description_ip": "Conectividad a Internet", @@ -431,7 +427,7 @@ "diagnosis_ports_needed_by": "La apertura de este puerto es requerida para la funcionalidad {category} (service {service})", "diagnosis_ports_ok": "El puerto {port} es accesible desde internet.", "diagnosis_ports_unreachable": "El puerto {port} no es accesible desde internet.", - "diagnosis_ports_could_not_diagnose": "No se puede comprobar si los puertos están accesibles desde el exterior.", + "diagnosis_ports_could_not_diagnose": "No se puede comprobar si los puertos están accesibles desde el exterior en IPv{ipversion}.", "diagnosis_ports_could_not_diagnose_details": "Error: {error}", "diagnosis_description_regenconf": "Configuraciones de sistema", "diagnosis_description_mail": "Correo electrónico", @@ -439,8 +435,8 @@ "diagnosis_basesystem_hardware": "La arquitectura material del servidor es {virt} {arch}", "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á …", - "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}'.'", + "group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en los grupos del sistema, pero YunoHost lo suprimirá…", + "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_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.", @@ -448,7 +444,7 @@ "diagnosis_http_connection_error": "Error de conexión: Ne se pudo conectar al dominio solicitado.", "diagnosis_http_timeout": "Tiempo de espera agotado al intentar contactar tu servidor desde el exterior. Parece que no sea alcanzable.
1. La causa más común es que el puerto 80 (y el 443) no estén correctamente redirigidos a tu servidor.
2. Deberías asegurarte que el servicio nginx está en marcha.
3. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso estén interfiriendo.", "diagnosis_http_ok": "El Dominio {domain} es accesible desde internet a través de HTTP.", - "diagnosis_http_could_not_diagnose": "No se pudo verificar si el dominio es accesible desde internet.", + "diagnosis_http_could_not_diagnose": "No se pudo verificar si los dominios son accesibles desde el exterior en IPv{ipversion}.", "diagnosis_http_could_not_diagnose_details": "Error: {error}", "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.", @@ -468,13 +464,13 @@ "diagnosis_domain_expiration_not_found": "No se pudo revisar la fecha de expiración para algunos dominios", "diagnosis_dns_try_dyndns_update_force": "La configuración DNS de este dominio debería ser administrada automáticamente por YunoHost. Si no es el caso, puedes intentar forzar una actualización mediante yunohost dyndns update --force.", "diagnosis_ip_local": "IP Local: {local}", - "diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/#/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.", + "diagnosis_ip_no_ipv6_tip": "Tener IPv6 funcionando no es obligatorio para que su servidor funcione, pero es mejor para la salud del Internet en general. IPv6 debería ser configurado automáticamente por el sistema o su proveedor si está disponible. De otra manera, es posible que tenga que configurar varias cosas manualmente, tal y como se explica en esta documentación https://yunohost.org/ipv6. Si no puede habilitar IPv6 o si parece demasiado técnico, puede ignorar esta advertencia con toda seguridad.", "diagnosis_display_tip": "Para ver los problemas encontrados, puede ir a la sección de diagnóstico del webadmin, o ejecutar 'yunohost diagnosis show --issues --human-readable' en la línea de comandos.", "diagnosis_package_installed_from_sury_details": "Algunos paquetes fueron accidentalmente instalados de un repositorio de terceros llamado Sury. El equipo YunoHost ha mejorado la estrategia para manejar estos paquetes, pero es posible que algunas configuraciones que han instalado aplicaciones PHP7.3 al tiempo que presentes en Stretch tienen algunas inconsistencias. Para solucionar esta situación, deberías intentar ejecutar el siguiente comando: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Algunos paquetes del sistema deberían ser devueltos a una versión anterior", "certmanager_domain_not_diagnosed_yet": "Aún no hay resultado del diagnóstico para el dominio {domain}. Por favor ejecute el diagnóstico para las categorías 'Registros DNS' y 'Web' en la sección de diagnóstico para verificar si el dominio está listo para Let's Encrypt. (O si sabe lo que está haciendo, utilice '--no-checks' para deshabilitar esos chequeos.)", "backup_archive_corrupted": "Parece que el archivo de respaldo '{archive}' está corrupto : {error}", - "backup_archive_cant_retrieve_info_json": "No se pudieron cargar informaciones para el archivo '{archive}'... El archivo info.json no se puede cargar (o no es un json válido).", + "backup_archive_cant_retrieve_info_json": "No se pudieron cargar informaciones para el archivo '{archive}'… El archivo info.json no se pudo recuperar (o no es un json válido).", "ask_user_domain": "Dominio a usar para la dirección de correo del usuario y cuenta XMPP", "app_packaging_format_not_supported": "Esta aplicación no se puede instalar porque su formato de empaque no está soportado por su versión de YunoHost. Considere actualizar su sistema.", "app_manifest_install_ask_is_public": "¿Debería exponerse esta aplicación a visitantes anónimos?", @@ -505,8 +501,8 @@ "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 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_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.", "diagnosis_mail_fcrdns_ok": "¡Las DNS inversas están bien configuradas!", @@ -519,11 +515,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 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_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu tráfico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/vpn_advantage
- Otra alternativa es cambiar de proveedor de internet a uno más amable con la Neutralidad de la Red", "diagnosis_backports_in_sources_list": "Parece que apt (el gestor de paquetes) está configurado para usar el repositorio backports. A menos que realmente sepas lo que estás haciendo, desaconsejamos absolutamente instalar paquetes desde backports, ya que pueden provocar comportamientos intestables o conflictos en el sistema.", "diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}", - "additional_urls_already_removed": "La URL adicional '{url}' ya se ha eliminado para el permiso «{permission}»", - "additional_urls_already_added": "La URL adicional '{url}' ya se ha añadido para el permiso «{permission}»", + "additional_urls_already_removed": "URL adicional '{url}' ya eliminada en la URL adicional para permiso «{permission}»", + "additional_urls_already_added": "URL adicional '{url}' ya añadida en la URL adicional para permiso «{permission}»", "config_apply_failed": "Falló la aplicación de la nueva configuración: {error}", "app_restore_script_failed": "Ha ocurrido un error dentro del script de restauración de aplicaciones", "app_config_unable_to_apply": "No se pudieron aplicar los valores del panel configuración.", @@ -569,7 +565,7 @@ "domain_config_auth_consumer_key": "Llave de consumidor", "domain_config_default_app": "App predeterminada", "domain_dns_push_success": "¡Registros DNS actualizados!", - "domain_dns_push_failed_to_authenticate": "No se pudo autenticar en la API del registrador para el dominio '{domain}'. ¿Lo más probable es que las credenciales sean incorrectas? (Error: {error})", + "domain_dns_push_failed_to_authenticate": "No se pudo autenticar en la API del registrador para el dominio '{domain}'. ¿Es probable que las credenciales sean incorrectas? (Error: {error})", "domain_dns_registrar_experimental": "Hasta ahora, la comunidad de YunoHost no ha probado ni revisado correctamente la interfaz con la API de **{registrar}**. El soporte es **muy experimental**. ¡Ten cuidado!", "domain_dns_push_record_failed": "No se pudo {action} registrar {type}/{name}: {error}", "domain_config_mail_in": "Correos entrantes", @@ -578,7 +574,7 @@ "domain_config_auth_token": "Token de autenticación", "domain_dns_push_failed_to_list": "Error al enumerar los registros actuales mediante la API del registrador: {error}", "domain_dns_push_already_up_to_date": "Registros ya al día, nada que hacer.", - "domain_dns_pushing": "Empujando registros DNS...", + "domain_dns_pushing": "Empujando registros DNS…", "domain_config_auth_key": "Llave de autenticación", "domain_config_auth_secret": "Secreto de autenticación", "domain_config_api_protocol": "Protocolo de API", @@ -590,7 +586,7 @@ "domain_dns_registrar_not_supported": "YunoHost no pudo detectar automáticamente el registrador que maneja este dominio. Debe configurar manualmente sus registros DNS siguiendo la documentación en https://yunohost.org/dns.", "migration_ldap_backup_before_migration": "Creación de una copia de seguridad de la base de datos LDAP y la configuración de las aplicaciones antes de la migración real.", "invalid_number": "Debe ser un miembro", - "ldap_server_is_down_restart_it": "El servicio LDAP está inactivo, intente reiniciarlo...", + "ldap_server_is_down_restart_it": "El servicio LDAP está inactivo, intente reiniciarlo…", "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", @@ -599,10 +595,10 @@ "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 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_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_yunohost_upgrade": "Iniciando la actualización principal de YunoHost…", "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.", @@ -614,20 +610,20 @@ "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 '{}'", "log_domain_config_set": "Actualizar la configuración del dominio '{}'", - "migration_0021_cleaning_up": "Limpiar el caché y los paquetes que ya no son útiles...", - "migration_0021_patch_yunohost_conflicts": "Aplicando parche para resolver el problema de conflicto...", + "migration_0021_cleaning_up": "Limpiar el caché y los paquetes que ya no son útiles…", + "migration_0021_patch_yunohost_conflicts": "Aplicando parche para resolver el problema de conflicto…", "migration_description_0021_migrate_to_bullseye": "Actualice el sistema a Debian Bullseye y YunoHost 11.x", "regenconf_need_to_explicitly_specify_ssh": "La configuración de ssh se modificó manualmente, pero debe especificar explícitamente la categoría 'ssh' con --force para aplicar los cambios.", "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}", - "migration_ldap_migration_failed_trying_to_rollback": "No se pudo migrar... intentando revertir el sistema.", + "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.", - "migration_0023_postgresql_13_not_installed": "¿PostgreSQL 11 está instalado, pero no PostgreSQL 13? Algo extraño podría haber sucedido en su sistema :(...", + "migration_0023_postgresql_13_not_installed": "¿PostgreSQL 11 está instalado, pero no PostgreSQL 13? Algo extraño podría haber sucedido en su sistema :(…", "migration_description_0022_php73_to_php74_pools": "Migrar archivos conf 'pool' de php7.3-fpm a php7.4", "migration_description_0023_postgresql_11_to_13": "Migrar bases de datos de PostgreSQL 11 a 13", - "other_available_options": "... y {n} otras opciones disponibles no mostradas", + "other_available_options": "… y {n} otras opciones disponibles no mostradas", "regex_with_only_domain": "No puede usar una expresión regular para el dominio, solo para la ruta", "service_description_postgresql": "Almacena datos de aplicaciones (base de datos SQL)", "tools_upgrade_failed": "No se pudieron actualizar los paquetes: {packages_list}", @@ -654,10 +650,10 @@ "global_settings_setting_admin_strength": "Seguridad de la contraseña del administrador", "global_settings_setting_user_strength": "Seguridad de la contraseña de usuario", "global_settings_setting_postfix_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor Postfix. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", - "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad)", + "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidad y seguridad para el servidor SSH. Afecta al cifrado (y otros aspectos relacionados con la seguridad). Visite https://infosec.mozilla.org/guidelines/openssh (inglés) para más información.", "global_settings_setting_ssh_password_authentication_help": "Permitir autenticación de contraseña para SSH", "global_settings_setting_ssh_port": "Puerto SSH", - "global_settings_setting_webadmin_allowlist_help": "Direcciones IP permitidas para acceder al webadmin. Separado por comas.", + "global_settings_setting_webadmin_allowlist_help": "Direcciones IP permitidas para acceder al webadmin. Se permite notación CIDR.", "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.", @@ -681,15 +677,15 @@ "domain_cannot_add_muc_upload": "No puedes añadir dominios que empiecen por 'muc.'. Este tipo de nombre está reservado para la función de chat XMPP multi-usuarios integrada en YunoHost.", "domain_config_cert_renew_help": "El certificado se renovará automáticamente durante los últimos 15 días de validez. Si lo desea, puede renovarlo manualmente. (No recomendado).", "domain_config_cert_summary_expired": "CRÍTICO: ¡El certificado actual no es válido! ¡HTTPS no funcionará en absoluto!", - "domain_config_cert_summary_letsencrypt": "¡Muy bien! Estás utilizando un certificado Let's Encrypt válido.", + "domain_config_cert_summary_letsencrypt": "¡Muy bien! ¡Estás utilizando un certificado Let's Encrypt válido!", "global_settings_setting_postfix_compatibility": "Compatibilidad con Postfix", "global_settings_setting_root_password_confirm": "Nueva contraseña de root (confirmar)", "global_settings_setting_webadmin_allowlist_enabled": "Activar la lista de IPs permitidas para Webadmin", "migration_0024_rebuild_python_venv_broken_app": "Omitiendo {app} porque virtualenv no puede ser reconstruido fácilmente para esta app. En su lugar, deberías arreglar la situación forzando la actualización de esta app usando `yunohost app upgrade --force {app}`.", "migration_0024_rebuild_python_venv_in_progress": "Ahora intentando reconstruir el virtualenv de Python para `{app}`", - "confirm_app_insufficient_ram": "¡PELIGRO! Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente. Incluso si esta aplicación pudiera ejecutarse, su proceso de instalación/actualización requiere una gran cantidad de RAM, por lo que tu servidor puede congelarse y fallar miserablemente. Si estás dispuesto a asumir ese riesgo de todos modos, escribe '{answers}'", + "confirm_app_insufficient_ram": "¡PELIGRO! Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente. Incluso si esta aplicación pudiera ejecutarse, su proceso de instalación/actualización requiere una gran cantidad de RAM, por lo que tu servidor puede congelarse y fallar miserablemente. Si estás dispuesto a asumir ese riesgo de todos modos, teclea '{answers}'", "confirm_notifications_read": "ADVERTENCIA: Deberías revisar las notificaciones de la aplicación antes de continuar, puede haber información importante que debes conocer. [{answers}]", - "domain_config_cert_summary_selfsigned": "ADVERTENCIA: El certificado actual es autofirmado. ¡Los navegadores mostrarán una espeluznante advertencia a los nuevos visitantes!.", + "domain_config_cert_summary_selfsigned": "ADVERTENCIA: El certificado actual es autofirmado. ¡Los navegadores mostrarán una espeluznante advertencia a los nuevos visitantes!", "global_settings_setting_backup_compress_tar_archives": "Comprimir las copias de seguridad", "global_settings_setting_root_access_explain": "En sistemas Linux, 'root' es el administrador absoluto. En el contexto de YunoHost, el acceso directo 'root' SSH está deshabilitado por defecto - excepto desde la red local del servidor. Los miembros del grupo 'admins' pueden usar el comando sudo para actuar como root desde la linea de comandos. Sin embargo, puede ser útil tener una contraseña de root (robusta) para depurar el sistema si por alguna razón los administradores regulares ya no pueden iniciar sesión.", "migration_0021_not_buster2": "¡La distribución Debian actual no es Buster! Si ya ha ejecutado la migración Buster->Bullseye, entonces este error es sintomático del hecho de que el procedimiento de migración no fue 100% exitoso (de lo contrario YunoHost lo habría marcado como completado). Se recomienda investigar lo sucedido con el equipo de soporte, que necesitará el registro **completo** de la `migración, que se puede encontrar en Herramientas > Registros en el webadmin.", @@ -703,7 +699,7 @@ "global_settings_setting_security_experimental_enabled": "Funciones de seguridad experimentales", "migration_0024_rebuild_python_venv_disclaimer_ignored": "Virtualenvs no puede reconstruirse automáticamente para esas aplicaciones. Necesitas forzar una actualización para ellas, lo que puede hacerse desde la línea de comandos con: `yunohost app upgrade --force APP`: {ignored_apps}", "migration_0024_rebuild_python_venv_failed": "Error al reconstruir el virtualenv de Python para {app}. La aplicación puede no funcionar mientras esto no se resuelva. Deberías arreglar la situación forzando la actualización de esta app usando `yunohost app upgrade --force {app}`.", - "app_arch_not_supported": "Esta aplicación sólo puede instalarse en arquitecturas {required} pero la arquitectura de su servidor es {current}", + "app_arch_not_supported": "Esta aplicación solo se puede instalar en arquitecturas {required} pero la arquitectura de tu servidor es {current}", "app_resource_failed": "Falló la asignación, desasignación o actualización de recursos para {app}: {error}", "app_not_enough_disk": "Esta aplicación requiere {required} espacio libre.", "app_not_enough_ram": "Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente.", @@ -724,9 +720,9 @@ "pattern_fullname": "Debe ser un nombre completo válido (al menos 3 caracteres)", "password_confirmation_not_the_same": "La contraseña y su confirmación no coinciden", "password_too_long": "Elija una contraseña de menos de 127 caracteres", - "diagnosis_using_yunohost_testing": "apt (el gestor de paquetes del sistema) está actualmente configurado para instalar cualquier actualización de 'testing' para el núcleo de YunoHost.", + "diagnosis_using_yunohost_testing": "apt (el gestor de paquetes del sistema) está configurado actualmente para instalar cualquier actualización 'testing' para el núcleo de YunoHost.", "diagnosis_using_yunohost_testing_details": "Esto probablemente esté bien si sabes lo que estás haciendo, ¡pero presta atención a las notas de la versión antes de instalar actualizaciones de YunoHost! Si quieres deshabilitar las actualizaciones de prueba, debes eliminar la palabra clave testing de /etc/apt/sources.list.d/yunohost.list.", - "global_settings_setting_passwordless_sudo": "Permitir a los administradores utilizar 'sudo' sin tener que volver a escribir sus contraseñas.", + "global_settings_setting_passwordless_sudo": "Permitir a los administradores utilizar 'sudo' sin tener que volver a escribir sus contraseñas", "group_update_aliases": "Actualizando alias para el grupo '{group}'", "group_no_change": "Nada que cambiar para el grupo '{group}'", "global_settings_setting_portal_theme": "Tema del portal", @@ -737,8 +733,8 @@ "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_acme_eligible": "Requisitos ACME", + "global_settings_setting_ssh_password_authentication": "Autenticación de contraseña", "domain_config_cert_issuer": "Autoridad de certificación", "invalid_shell": "Shell inválido: {shell}", "log_settings_reset": "Restablecer ajuste", @@ -747,5 +743,43 @@ "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" + "registrar_infos": "Información sobre el registrador", + "app_failed_to_download_asset": "Error al descargar el recurso '{source_id}' ({url}) para {app}: {out}", + "app_corrupt_source": "YunoHost ha podido descargar el recurso '{source_id}' ({url}) para {app}, pero no coincide con la suma de comprobación esperada. Esto puede significar que ocurrió un fallo de red en tu servidor, o que el recurso ha sido modificado por el responsable de la aplicación (¿o un actor malicioso?) y los responsables de empaquetar esta aplicación para YunoHost necesitan investigar y actualizar el manifest.toml de la aplicación para reflejar estos cambios. \n Suma de control sha256 esperada: {expected_sha256}\n Suma de control sha256 descargada: {computed_sha256}\n Tamaño del archivo descargado: {size}", + "app_change_url_failed": "No es possible cambiar la URL para {app}: {error}", + "app_change_url_require_full_domain": "{app} no se puede mover a esta nueva URL porque requiere un dominio completo (es decir, con una ruta = /)", + "app_change_url_script_failed": "Se ha producido un error en el script de modificación de la url", + "group_mailalias_add": "El alias de correo electrónico '{mail}' será añadido al grupo '{group}'", + "group_user_add": "La persona usuaria '{user}' será añadida al grupo '{group}'", + "dyndns_no_recovery_password": "¡No especificó la password de recuperación! ¡En caso de perder control de este dominio, necesitará contactar con una persona administradora del equipo de YunoHost!", + "dyndns_too_many_requests": "El servicio DynDNS de YunoHost recibió demasiadas peticiones de su parte, por favor espere 1 hora antes de intentarlo de nuevo.", + "dyndns_set_recovery_password_unknown_domain": "Falló al establecer la password de recuperación: dominio no registrado", + "global_settings_setting_dns_exposure": "Versión IP del DNS establecida en la configuración y diagnósticos", + "group_mailalias_remove": "El alias de correo electrónico '{mail}' será eliminado del grupo '{group}'", + "group_user_remove": "La persona usuaria '{user}' será eliminada del grupo '{group}'", + "app_failed_to_upgrade_but_continue": "La aplicación {failed_app} no pudo actualizarse, continúe con las siguientes actualizaciones como solicitado. Ejecuta 'yunohost log show {operation_logger_name}' para visualizar el log de fallos", + "app_not_upgraded_broken_system": "La aplicacion '{failed_app}' falló en la actualización he hizo que el sistema pasase a un estado de fallo, como consecuencia las siguientes actualizaciones de aplicaciones han sido canceladas: {apps}", + "app_not_upgraded_broken_system_continue": "La aplicacion '{failed_app}' falló en la actualización he hizo que el sistema pasase a un estado de fallo (así que --continue-on-failure se ignoró), como consecuencia las siguientes actualizaciones de aplicaciones han sido canceladas: {apps}", + "apps_failed_to_upgrade": "Estas actualizaciones de aplicaciones fallaron: {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id} (para ver el correspondiente log utilice 'yunohost log show {operation_logger_name}')", + "diagnosis_ip_no_ipv6_tip_important": "La IPv6 normalmente debería ser automáticamente configurada por su proveedor de sistemas si estuviese disponible. Si no fuese saí, quizás deba configurar algunos parámetros manualmente tal y como lo explica la documentación: https://yunohost.org/ipv6.", + "ask_dyndns_recovery_password_explain": "Porfavor obtenga una password de recuperación para su dominio DynDNS, por si la necesita más adelante.", + "ask_dyndns_recovery_password": "Password de recuperación de DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Por favor introduzca la password de recuperación de este dominio DynDNS.", + "log_dyndns_unsubscribe": "Desuscribir del subdominio YunoHost '{}'", + "ask_dyndns_recovery_password_explain_unavailable": "Este domino DynDNS ya está registrado. Si usted es la persona que originalmente registro este dominio, puede introducir la password de recuperación de este dominio.", + "domain_config_default_app_help": "Las personas será automáticamente redirigidas a esta aplicación cuando visiten este dominio. Si no especifica una aplicación, estas serán redirigidas al formulario del portal de usuarias.", + "domain_config_xmpp_help": "NB: algunas opciones de XMPP necesitarán que actualice sus registros DNS y que regenere sus certificados Let's Encrypt para que sean habilitadas", + "dyndns_subscribed": "Dominio DynDNS suscrito", + "dyndns_subscribe_failed": "No pudo suscribirse al dominio DynDNS: {error}", + "dyndns_unsubscribe_failed": "No pudo desuscribirse del dominio DynDNS: {error}", + "dyndns_unsubscribed": "Dominio DynDNS desuscrito", + "dyndns_unsubscribe_denied": "Falló la desuscripción del dominio: credenciales incorrectas", + "dyndns_unsubscribe_already_unsubscribed": "El dominio está ya desuscrito", + "dyndns_set_recovery_password_denied": "Falló al establecer la password de recuperación: llave invalida", + "dyndns_set_recovery_password_invalid_password": "Falló al establecer la password de recuperación: la password no es suficientemente fuerte", + "dyndns_set_recovery_password_failed": "Falló al establecer la password de recuperación: {error}", + "dyndns_set_recovery_password_success": "¡Password de recuperación establecida!", + "global_settings_setting_dns_exposure_help": "NB: Esto afecta únicamente a la configuración recomentada de DNS y en las pruebas de diagnóstico. No afecta a la configuración del sistema.", + "global_settings_setting_ssh_port_help": "Un puerto menor a 1024 es preferible para evitar intentos de usurpación por servicios no administrativos en la máquina remota. También debe de evitar usar un puerto ya en uso, como el 80 o 443." } \ No newline at end of file diff --git a/locales/eu.json b/locales/eu.json index 0d424e6ca..b95fcd1a9 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -11,9 +11,9 @@ "backup_deleted": "Babeskopia ezabatu da: {name}", "app_argument_required": "'{name}' argumentua ezinbestekoa da", "certmanager_acme_not_configured_for_domain": "Une honetan ezinezkoa da ACME azterketa {domain} domeinurako burutzea nginx ezarpenek ez dutelako beharrezko kodea… Egiaztatu nginx ezarpenak egunean daudela 'yunohost tools regen-conf nginx --dry-run --with-diff' komandoa exekutatuz.", - "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Egiaztatu 'DNS balioak' (oinarrizkoa) kategoria diagnostikoen atalean. 'A' balioak duela gutxi aldatu badituzu, itxaron hedatu daitezen (badaude DNSen hedapena ikusteko erramintak interneten). (Zertan ari zeren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "'{domain}' domeinurako DNS balioak ez datoz bat zerbitzariaren IParekin. Egiaztatu 'DNS balioak' (oinarrizkoa) atala diagnostikoen gunean. 'A' balioak duela gutxi aldatu badituzu, itxaron hedatu daitezen (badaude DNSen hedapena ikusteko erramintak interneten). (Zertan ari zeren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", "confirm_app_install_thirdparty": "KONTUZ! Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Kanpoko aplikazioek sistemaren integritate eta segurtasuna arriskuan jar dezakete. Ziur asko EZ zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi duzu hala ere? Hautatu '{answers}'", - "app_start_remove": "{app} ezabatzen…", + "app_start_remove": "{app} kentzen…", "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, aldatu /etc/resolv.dnsmasq.conf fitxategia.", @@ -36,7 +36,7 @@ "diagnosis_http_bad_status_code": "Zerbitzari hau ez den beste gailu batek erantzun omen dio eskaerari (agian routerrak).
1. Honen arrazoi ohikoena 80 (eta 443) ataka zerbitzarira ondo birbidaltzen ez dela da.
2. Konfigurazio konplexua badarabilzu, egiaztatu suebakiak edo reverse-proxyk oztopatzen ez dutela.", "diagnosis_http_timeout": "Denbora agortu da sare lokaletik kanpo zure zerbitzarira konektatzeko ahaleginean. Eskuragarri ez dagoela dirudi.
1. 80 (eta 443) ataka zerbitzarira modu egokian birzuzentzen ez direla da ohiko zergatia.
2. Badaezpada egiaztatu nginx martxan dagoela.
3. Konfigurazio konplexuetan, egiaztatu suebakiak edo reverse-proxyk konexioa oztopatzen ez dutela.", "app_sources_fetch_failed": "Ezinezkoa izan da fitxategiak eskuratzea, zuzena al da URLa?", - "app_make_default_location_already_used": "Ezinezkoa izan da '{app}' '{domain}' domeinuan lehenestea, '{other_app}'(e)k dagoeneko '{domain}' erabiltzen duelako", + "app_make_default_location_already_used": "Ezinezkoa izan da '{app}' '{domain}' domeinuan lehenestea, '{other_app}'(e)k lehendik ere erabiltzen duelako", "app_already_installed_cant_change_url": "Aplikazio hau instalatuta dago dagoeneko. URLa ezin da aldatu aukera honekin. Markatu 'app changeurl' markatzeko moduan badago.", "diagnosis_ip_not_connected_at_all": "Badirudi zerbitzaria ez dagoela internetera konektatuta!?", "app_already_up_to_date": "{app} egunean da dagoeneko", @@ -55,9 +55,9 @@ "diagnosis_basesystem_kernel": "Zerbitzariak Linuxen {kernel_version} kernela darabil", "app_argument_invalid": "Aukeratu balio egoki bat '{name}' argumenturako: {error}", "app_already_installed": "{app} instalatuta dago dagoeneko", - "app_config_unable_to_apply": "Ezinezkoa izan da konfigurazio aukerak ezartzea.", - "app_config_unable_to_read": "Ezinezkoa izan da konfigurazio aukerak irakurtzea.", - "config_apply_failed": "Ezin izan da konfigurazio berria ezarri: {error}", + "app_config_unable_to_apply": "Konfigurazio-aukeren ezarpenak huts egin du.", + "app_config_unable_to_read": "Konfigurazio-aukeren irakurketak huts egin du.", + "config_apply_failed": "Konfigurazio berria ezartzeak huts egin du: {error}", "config_cant_set_value_on_section": "Ezinezkoa da balio bakar bat ezartzea konfigurazio atal oso batean.", "config_no_panel": "Ez da konfigurazio-panelik aurkitu.", "diagnosis_found_errors_and_warnings": "{category} atalari dago(z)kion {errors} arazo (eta {warnings} abisu) aurkitu d(ir)a!", @@ -83,7 +83,7 @@ "config_forbidden_keyword": "'{keyword}' etiketa sistemak bakarrik erabil dezake; ezin da ID hau daukan baliorik sortu edo erabili.", "config_unknown_filter_key": "'{filter_key}' filtroaren kakoa ez da zuzena.", "config_validate_color": "RGB hamaseitar kolore bat izan behar da", - "diagnosis_cant_run_because_of_dep": "Ezinezkoa da diagnosia abiaraztea {category} atalerako {dep}(r)i lotutako arazo garrantzitsuak / garrantzitsuek dirau(t)en artean.", + "diagnosis_cant_run_because_of_dep": "Ezinezkoa da diagnostikoa abiaraztea {category} atalerako {dep}(r)i lotutako arazo garrantzitsuak / garrantzitsuek dirau(t)en artean.", "diagnosis_dns_missing_record": "Proposatutako DNS konfigurazioaren arabera, ondorengo informazioa gehitu beharko zenuke DNS erregistroan:
Mota: {type}
Izena: {name}
Balioa: {value}", "diagnosis_http_nginx_conf_not_up_to_date": "Domeinu honen nginx ezarpenak eskuz moldatu direla dirudi eta YunoHostek ezin du egiaztatu HTTP bidez eskuragarri dagoenik.", "ask_new_admin_password": "Administrazio-pasahitz berria", @@ -113,10 +113,10 @@ "certmanager_cert_renew_success": "Let's Encrypt ziurtagiria berriztu da '{domain}' domeinurako", "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…", + "app_remove_after_failed_install": "Aplikazioa kentzen instalatzerakoan errorea dela-eta…", "diagnosis_basesystem_ynh_single_version": "{package} bertsioa: {version} ({repo})", - "diagnosis_failed_for_category": "'{category}' ataleko diagnostikoak kale egin du: {error}", - "diagnosis_cache_still_valid": "(Katxea oraindik baliogarria da {category} ataleko diagnosirako. Ez da berrabiaraziko!)", + "diagnosis_failed_for_category": "'{category}' ataleko diagnostikoak huts egin du: {error}", + "diagnosis_cache_still_valid": "(Katxea oraindik baliogarria da {category} ataleko diagnostikorako. Ez da berrabiaraziko!)", "diagnosis_found_errors": "{category} atalari dago(z)kion {errors} arazo aurkitu d(ir)a!", "diagnosis_found_warnings": "{category} atalari dagokion eta hobetu daite(z)keen {warnings} abisu aurkitu d(ir)a.", "diagnosis_ip_connected_ipv6": "Zerbitzaria IPv6 bidez dago internetera konektatuta!", @@ -138,7 +138,7 @@ "diagnosis_description_mail": "Posta elektronikoa", "diagnosis_http_connection_error": "Arazoa konexioan: ezin izan da domeinu horretara konektatu, litekeena da eskuragarri ez egotea.", "diagnosis_description_web": "Weba", - "diagnosis_display_tip": "Aurkitu diren arazoak ikusteko joan administrazio-atariko Diagnostikoak atalera, edo exekutatu 'yunohost diagnosis show --issues --human-readable' komandoak nahiago badituzu.", + "diagnosis_display_tip": "Aurkitu diren arazoak ikusteko joan administrazio-atariko Diagnostikoak gunera, edo exekutatu 'yunohost diagnosis show --issues --human-readable' komandoak nahiago badituzu.", "diagnosis_dns_point_to_doc": "Irakurri dokumentazioa DNS erregistroekin laguntza behar baduzu.", "diagnosis_mail_ehlo_unreachable": "SMTP posta zerbitzaria ez dago eskuragarri IPv{ipversion}ko sare lokaletik kanpo eta, beraz, ez da posta elektronikoa jasotzeko gai.", "diagnosis_mail_ehlo_bad_answer_details": "Litekeena da zure zerbitzaria ez den beste gailu batek erantzun izana.", @@ -147,9 +147,9 @@ "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 fitxategia 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 json-a).", "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.", + "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. 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.", @@ -165,7 +165,7 @@ "diagnosis_basesystem_host": "Zerbitzariak Debian {debian_version} darabil", "diagnosis_ignored_issues": "(kontuan hartu ez d(ir)en + {nb_ignored} arazo)", "diagnosis_ip_dnsresolution_working": "Domeinu izenaren ebazpena badabil!", - "diagnosis_failed": "Ezinezkoa izan da '{category}' ataleko diagnostikoa lortzea: {error}", + "diagnosis_failed": "'{category}' ataleko diagnostikoa lortzeak huts egin du: {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 oraindik erabilgarri ({total} orotara)!", @@ -176,13 +176,13 @@ "diagnosis_basesystem_ynh_main_version": "Zerbitzariak YunoHosten {main_version} ({repo}) darabil", "backup_custom_backup_error": "Neurrira egindako babeskopiak ezin izan du 'babeskopia egin' urratsetik haratago egin", "diagnosis_ip_broken_resolvconf": "Zure zerbitzarian domeinu izenaren ebazpena kaltetuta dagoela dirudi, antza denez /etc/resolv.conf fitxategia ez dago 127.0.0.1ra adi.", - "diagnosis_ip_no_ipv6_tip": "Dabilen IPv6 izatea ez da derrigorrezkoa zerbitzariaren funtzionamendurako, baina egokiena da interneten osasunerako. IPv6 automatikoki konfiguratu beharko luke sistemak edo operadoreak. Bestela, eskuz konfiguratu beharko zenituzke hainbat gauza dokumentazioan azaltzen den bezala. Ezin baduzu edo IPv6 gaitzea zuretzat kontu teknikoegia baldin bada, ez duzu abisu hau zertan kontuan hartu.", + "diagnosis_ip_no_ipv6_tip": "Dabilen IPv6 izatea ez da derrigorrezkoa zerbitzariaren funtzionamendurako, baina egokiena da interneten osasunerako. IPv6 automatikoki konfiguratu beharko luke sistemak edo operadoreak. Bestela, eskuz konfiguratu beharko zenituzke hainbat gauza dokumentazioan azaltzen den bezala. Ezin baduzu edo IPv6 gaitzea zuretzat kontu teknikoegia baldin bada, ez duzu abisu hau zertan kontuan hartu.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Egoera konpontzeko, ikuskatu desberdintasunak yunohost tools regen-conf nginx --dry-run --with-diff komandoren bidez eta, proposatutako aldaketak onartzen badituzu, ezarri itzazu yunohost tools regen-conf nginx --force erabiliz.", "diagnosis_domain_not_found_details": "{domain} domeinua ez da WHOISen datubasean existitzen edo iraungi da!", "app_start_backup": "{app}(r)en babeskopia egiteko fitxategiak eskuratzen…", "app_change_url_no_script": "'{app_name}' aplikazioak oraingoz ez du URLa moldatzerik onartzen. Agian eguneratu beharko zenuke.", "app_location_unavailable": "URL hau ez dago erabilgarri edota dagoeneko instalatutako aplikazioren batekin talka egiten du:\n{apps}", - "app_not_upgraded": "'{failed_app}' aplikazioa ezin izan da eguneratu, eta horregatik ondorengo aplikazioen eguneraketak bertan behera utzi dira: {apps}", + "app_not_upgraded": "'{failed_app}' aplikazioaren eguneratzeak huts egin du eta, hori dela-eta, ondorengo aplikazioen eguneraketak bertan behera utzi dira: {apps}", "app_not_correctly_installed": "Ez dirudi {app} ondo instalatuta dagoenik", "app_not_installed": "Ezinezkoa izan da {app} aurkitzea instalatutako aplikazioen zerrendan: {all_apps}", "app_not_properly_removed": "Ezinezkoa izan da {app} guztiz ezabatzea", @@ -192,7 +192,7 @@ "app_upgrade_several_apps": "Ondorengo aplikazioak eguneratuko dira: {apps}", "backup_app_failed": "Ezinezkoa izan da {app}(r)en babeskopia egitea", "backup_actually_backuping": "Bildutako fitxategiekin babeskopia sortzen…", - "backup_archive_name_exists": "Dagoeneko existitzen da izen bera duen babeskopia fitxategi bat.", + "backup_archive_name_exists": "Dagoeneko existitzen da '{name}' izena duen babeskopia-fitxategi bat.", "backup_archive_name_unknown": "Ez da '{name}' izeneko babeskopia ezagutzen", "backup_archive_open_failed": "Ezinezkoa izan da babeskopien fitxategia irekitzea", "backup_archive_system_part_not_available": "'{part}' sistemaren atala ez dago erabilgarri babeskopia honetan", @@ -216,7 +216,7 @@ "certmanager_cert_install_success_selfsigned": "Norberak sinatutako ziurtagiria instalatu da '{domain}' domeinurako", "certmanager_domain_cert_not_selfsigned": "{domain} domeinurako ziurtagiria ez da norberak sinatutakoa. Ziur al zaude ordezkatu nahi duzula? (Erabili '--force' hori egiteko.)", "certmanager_certificate_fetching_or_enabling_failed": "{domain} domeinurako ziurtagiri berriak kale egin du…", - "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Egiaztatu 'Weba' atala diagnosien gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", + "certmanager_domain_http_not_working": "Ez dirudi {domain} domeinua HTTP bidez ikusgai dagoenik. Egiaztatu 'Weba' atala diagnostikoen gunean informazio gehiagorako. (Zertan ari zaren baldin badakizu, erabili '--no-checks' egiaztapen horiek desgaitzeko.)", "certmanager_hit_rate_limit": "{domain} domeinu-multzorako ziurtagiri gehiegi jaulki dira dagoeneko. Saia saitez geroago. Ikus https://letsencrypt.org/docs/rate-limits/ xehetasun gehiagorako", "certmanager_no_cert_file": "Ezinezkoa izan da {domain} domeinurako ziurtagiri fitxategia irakurrtzea (fitxategia: {file})", "certmanager_self_ca_conf_file_not_found": "Ezinezkoa izan da konfigurazio-fitxategia aurkitzea norberak sinatutako ziurtagirirako (fitxategia: {file})", @@ -228,7 +228,7 @@ "app_removed": "{app} desinstalatu da", "backup_cleaning_failed": "Ezinezkoa izan da behin-behineko babeskopien karpeta hustea", "certmanager_attempt_to_replace_valid_cert": "{domain} domeinurako egokia eta baliogarria den ziurtagiri bat ordezkatzen saiatzen ari zara! (Erabili --force mezu hau deuseztatu eta ziurtagiria ordezkatzeko)", - "diagnosis_backports_in_sources_list": "Dirudienez apt (pakete kudeatzailea) backports biltegia erabiltzeko konfiguratuta dago. Zertan ari zaren ez badakizu, ez zenuke backports biltegietako aplikaziorik instalatu beharko, ezegonkortasun eta gatazkak eragin ditzaketelako sistemarekin.", + "diagnosis_backports_in_sources_list": "Dirudienez apt (pakete kudeatzailea) backports gordailua erabiltzeko konfiguratuta dago. Zertan ari zaren ez badakizu, ez zenuke backports gurdailuetako aplikaziorik instalatu beharko, ezegonkortasun eta gatazkak eragin ditzaketelako sistemarekin.", "app_restore_failed": "Ezinezkoa izan da {app} lehengoratzea: {error}", "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.", @@ -261,12 +261,9 @@ "log_does_exists": "Ez dago '{log}' izena duen eragiketa-erregistrorik; erabili 'yunohost log list' eragiketa-erregistro guztiak ikusteko", "log_user_group_delete": "Ezabatu '{}' taldea", "log_user_import": "Inportatu erabiltzaileak", - "dyndns_key_generating": "DNS gakoa sortzen… litekeena da honek denbora behar izatea.", "diagnosis_mail_fcrdns_ok": "Alderantzizko DNSa zuzen konfiguratuta dago!", "diagnosis_mail_queue_unavailable_details": "Errorea: {error}", "dyndns_provider_unreachable": "Ezinezkoa izan da DynDNS {provider} enpresarekin konektatzea: agian zure YunoHost zerbitzaria ez dago internetera konektatuta edo dynette zerbitzaria ez dago martxan.", - "dyndns_registered": "DynDNS domeinua erregistratu da", - "dyndns_registration_failed": "Ezinezkoa izan da DynDNS domeinua erregistratzea: {error}", "extracting": "Ateratzen…", "diagnosis_ports_unreachable": "{port}. ataka ez dago eskuragarri kanpotik.", "diagnosis_regenconf_manually_modified_details": "Ez dago arazorik zertan ari zaren baldin badakizu! YunoHostek fitxategi hau automatikoki eguneratzeari utziko dio… Baina kontuan izan YunoHosten eguneraketek aldaketa garrantzitsuak izan ditzaketela. Nahi izatekotan, desberdintasunak aztertu ditzakezu yunohost tools regen-conf {category} --dry-run --with-diff komandoa exekutatuz, eta gomendatutako konfiguraziora bueltatu yunohost tools regen-conf {category} --force erabiliz", @@ -290,15 +287,15 @@ "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_push_failed_to_authenticate": "'{domain}' domeinuko erregistro-enpresan APIa erabiliz saioa hasteak huts egin du. 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", "domain_cannot_add_xmpp_upload": "Ezin dira 'xmpp-upload.' hasiera duten domeinuak gehitu. Izen mota hau YunoHosten zati den XMPP igoeretarako erabiltzen da.", - "domain_cannot_remove_main_add_new_one": "Ezin duzu '{domain}' ezabatu domeinu nagusi eta bakarra delako. Beste domeinu bat gehitu 'yunohost domain add ' exekutatuz, gero erabili 'yunohost domain main-domain -n ' domeinu nagusi bilakatzeko, eta azkenik ezabatu {domain}' domeinua 'yunohost domain remove {domain}' komandoarekin.", - "domain_dns_push_record_failed": "Ezinezkoa izan da {type}/{name} ezarpenak {action}: {error}", + "domain_cannot_remove_main_add_new_one": "Ezin duzu '{domain}' ezabatu domeinu nagusi eta bakarra delako. Beste domeinu bat gehitu 'yunohost domain add ' exekutatuz; gero erabili 'yunohost domain main-domain -n ' domeinu nagusi bilakatzeko; eta azkenik ezabatu {domain}' domeinua 'yunohost domain remove {domain}' komandoarekin.", + "domain_dns_push_record_failed": "{type}/{name} ezarpenak {action} huts egin du: {error}", "domain_dns_push_success": "DNS ezarpenak eguneratu dira!", - "domain_dns_push_failed": "DNS ezarpenen eguneratzeak kale egin du.", + "domain_dns_push_failed": "DNS ezarpenen eguneratzeak huts egin du.", "domain_dns_push_partial_failure": "DNS ezarpenak erdipurdi eguneratu dira: jakinarazpen/errore batzuk egon dira.", "group_deletion_failed": "Ezinezkoa izan da '{group}' taldea ezabatzea: {error}", "invalid_number_min": "{min} baino handiagoa izan behar da", @@ -309,7 +306,6 @@ "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", "domain_exists": "Dagoeneko existitzen da domeinu hau", "domain_registrar_is_not_configured": "Oraindik ez da {domain} domeinurako erregistro-enpresa ezarri.", "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://yunohost.org/dns_config.", @@ -332,9 +328,9 @@ "log_domain_dns_push": "Bidali '{}' domeinuaren DNS ezarpenak", "log_tools_migrations_migrate_forward": "Exekutatu migrazioak", "log_tools_postinstall": "Abiarazi YunoHost zerbitzariaren instalazio ondorengo prozesua", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). Hau dela-eta arazoak badituzu, irtenbide batzuk eduki ditzakezu:
- Operadore batzuek relay posta zerbitzari bat eskaini dezakete, baina kasu horretan zure posta elektronikoa zelatatu dezakete.
- Pribatutasuna bermatzeko *IP publikoa* duen VPN bat erabiltzea izan daiteke irtenbidea. Ikus https://yunohost.org/#/vpn_advantage
- Edo operadore desberdin batera aldatu", - "domain_dns_registrar_supported": "YunoHostek automatikoki antzeman du domeinu hau **{registrar}** erregistro-enpresak kudeatzen duela. Nahi baduzu YunoHostek automatikoki konfiguratu ditzake DNS ezarpenak, API egiaztagiri zuzenak zehazten badituzu. API egiaztagiriak non lortzeko dokumentazioa orri honetan duzu: https://yunohost.org/registar_api_{registrar}. (Baduzu DNS erregistroak eskuz konfiguratzeko aukera ere, gidalerro hauetan ageri den bezala: https://yunohost.org/dns)", - "domain_dns_push_failed_to_list": "Ezinezkoa izan da APIa erabiliz oraingo erregistroak antzematea: {error}", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). Hau dela-eta arazoak badituzu, irtenbide batzuk eduki ditzakezu:
- Operadore batzuek relay posta zerbitzari bat eskaini dezakete, baina kasu horretan zure posta elektronikoa zelatatu dezakete.
- Pribatutasuna bermatzeko *IP publikoa* duen VPN bat erabiltzea izan daiteke irtenbidea. Ikus https://yunohost.org/vpn_advantage
- Edo operadore desberdin batera aldatu", + "domain_dns_registrar_supported": "YunoHostek automatikoki antzeman du domeinu hau **{registrar}** erregistro-enpresak kudeatzen duela. Nahi baduzu YunoHostek automatikoki konfiguratu ditzake DNS ezarpenak, API egiaztagiri zuzenak zehazten badituzu. API egiaztagiriak non lortzeko dokumentazioa orri honetan daukazu: https://yunohost.org/registar_api_{registrar}. (Baduzu DNS erregistroak eskuz konfiguratzeko aukera ere, gidalerro hauetan ageri den bezala: https://yunohost.org/dns)", + "domain_dns_push_failed_to_list": "APIa erabiliz uneko erregistroak antzemateak huts egin du: {error}", "domain_dns_push_already_up_to_date": "Ezarpenak egunean daude, ez dago zereginik.", "domain_config_mail_out": "Bidalitako mezuak", "domain_config_xmpp": "Bat-bateko mezularitza (XMPP)", @@ -389,7 +385,7 @@ "group_already_exist_on_system_but_removing_it": "{group} taldea existitzen da sistemaren taldeetan, baina YunoHostek ezabatuko du…", "diagnosis_mail_fcrdns_nok_details": "Lehenik eta behin zure routerraren konfigurazio gunean edo hostingaren enpresaren aukeretan alderantzizko DNSa konfiguratzen saiatu beharko zinateke {ehlo_domain} erabiliz. (Hosting enpresaren arabera, ezinbestekoa da beraiekin harremanetan jartzea).", "diagnosis_mail_outgoing_port_25_ok": "SMTP posta zerbitzaria posta elektronikoa bidaltzeko gai da (25. atakaren irteera ez dago blokeatuta).", - "diagnosis_ports_partially_unreachable": "{port}. ataka ez dago eskuragarri kanpotik Pv{failed} erabiliz.", + "diagnosis_ports_partially_unreachable": "{port}. ataka ez dago eskuragarri kanpotik IPv{failed} erabiliz.", "diagnosis_ports_forwarding_tip": "Arazoa konpontzeko, litekeena da operadorearen routerrean ataken birbideraketa konfiguratu behar izatea, https://yunohost.org/isp_box_config-n agertzen den bezala", "domain_creation_failed": "Ezinezkoa izan da {domain} domeinua sortzea: {error}", "domains_available": "Erabilgarri dauden domeinuak:", @@ -398,7 +394,7 @@ "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", - "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}", + "diagnosis_package_installed_from_sury_details": "Sury izena duen kanpoko gordailu batetik instalatu dira pakete batzuk, nahi gabe. YunoHosten taldeak hobekuntzak egin ditu pakete hauek kudeatzeko, baina litekeena da PHP7.3 aplikazioak Stretch sistema eragilean instalatu zituzten kasu batzuetan arazoak sortzea. Egoera hau konpontzeko, honako komando hau exekutatu beharko zenuke: {cmd_to_fix}", "log_help_to_get_log": "'{desc}' eragiketaren erregistroa ikusteko, exekutatu 'yunohost log show {name}'", "dpkg_is_broken": "Une honetan ezinezkoa da sistemaren dpkg/APT pakateen kudeatzaileek hondatutako itxura dutelako… Arazoa konpontzeko SSH bidez konektatzen saia zaitezke eta ondoren exekutatu 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a' edota 'sudo dpkg --audit'.", "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}", @@ -426,7 +422,7 @@ "global_settings_setting_smtp_relay_user": "SMTP relay erabiltzailea", "domain_cert_gen_failed": "Ezinezkoa izan da ziurtagiria sortzea", "field_invalid": "'{}' ez da baliogarria", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Operadore batzuei bost axola zaie internetaren neutraltasuna (Net Neutrality) eta ez dute 25. ataka desblokeatzen uzten.
- Operadore batzuek relay posta zerbitzari bat eskaini dezakete, baina kasu horretan zure posta elektronikoa zelatatu dezakete.
- Pribatutasuna bermatzeko *IP publikoa* duen VPN bat erabiltzea izan daiteke irtenbidea. Ikus https://yunohost.org/#/vpn_advantage
- Edo operadore desberdin batera aldatu", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Operadore batzuei bost axola zaie internetaren neutraltasuna (Net Neutrality) eta ez dute 25. ataka desblokeatzen uzten.
- Operadore batzuek relay posta zerbitzari bat eskaini dezakete, baina kasu horretan zure posta elektronikoa zelatatu dezakete.
- Pribatutasuna bermatzeko *IP publikoa* duen VPN bat erabiltzea izan daiteke irtenbidea. Ikus https://yunohost.org/vpn_advantage
- Edo operadore desberdin batera aldatu", "ldap_server_down": "Ezin izan da LDAP zerbitzarira konektatu", "ldap_server_is_down_restart_it": "LDAP zerbitzaria ez dago martxan, saia zaitez berrabiarazten…", "log_app_upgrade": "'{}' aplikazioa eguneratu", @@ -450,7 +446,7 @@ "unexpected_error": "Ezusteko zerbaitek huts egin du: {error}", "updating_apt_cache": "Sistemaren paketeen eguneraketak eskuratzen…", "mail_forward_remove_failed": "Ezinezkoa izan da '{mail}' posta elektronikoko birbidalketa ezabatzea", - "migration_ldap_migration_failed_trying_to_rollback": "Ezinezkoa izan da migratzea… sistema lehengoratzen saiatzen.", + "migration_ldap_migration_failed_trying_to_rollback": "Ezin izan da migratu… sistema lehengoratzen saiatzen.", "migrations_exclusive_options": "'--auto', '--skip', eta '--force-rerun' aukerek batak bestea baztertzen du.", "migrations_running_forward": "{id} migrazioa exekutatzen…", "regenconf_dry_pending_applying": "'{category}' atalari dagokion konfigurazioa egiaztatzen…", @@ -557,7 +553,7 @@ "unbackup_app": "{app} ez da gordeko", "unrestore_app": "{app} ez da lehengoratuko", "upgrade_complete": "Eguneraketa amaitu da", - "upgrading_packages": "Paketeak eguneratzen…", + "upgrading_packages": "Paketeak bertsio-berritzen…", "upnp_dev_not_found": "Ez da UPnP gailurik aurkitu", "user_update_failed": "Ezin izan da {user} erabiltzailea eguneratu: {error}", "user_updated": "Erabiltzailearen informazioa aldatu da", @@ -583,7 +579,7 @@ "port_already_opened": "{port}. ataka dagoeneko irekita dago {ip_version} konexioetarako", "user_home_creation_failed": "Ezin izan da erabiltzailearentzat '{home}' direktorioa sortu", "user_unknown": "Erabiltzaile ezezaguna: {user}", - "yunohost_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- erabili 'Diagnostikoak' atala ohiko arazoei aurre hartzeko. Administrazio-atarian abiarazi edo 'yunohost diagnosis run' exekutatu;\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "Instalazio ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- erabili 'Diagnostikoak' gunea ohiko arazoei aurre hartzeko. Administrazio-atarian abiarazi edo 'yunohost diagnosis run' exekutatu;\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://yunohost.org/admindoc.", "yunohost_not_installed": "YunoHost ez da zuzen instalatu. Exekutatu 'yunohost tools postinstall'", "unlimit": "Mugarik ez", "restore_already_installed_apps": "Ondorengo aplikazioak ezin dira lehengoratu dagoeneko instalatuta daudelako: {apps}", @@ -605,8 +601,8 @@ "restore_hook_unavailable": "'{part}'-(e)rako lehengoratze agindua ez dago erabilgarri ez sisteman ezta fitxategian ere", "restore_cleaning_failed": "Ezin izan dira lehengoratzeko behin-behineko fitxategiak ezabatu", "restore_confirm_yunohost_installed": "Ziur al zaude dagoeneko instalatuta dagoen sistema lehengoratu nahi duzula? [{answers}]", - "restore_may_be_not_enough_disk_space": "Badirudi zure sistemak ez duela nahikoa espazio (erabilgarri: {free_space} B, beharrezkoa {needed_space} B, segurtasuneko tartea: {margin} B)", - "restore_not_enough_disk_space": "Ez dago nahikoa espazio (erabilgarri: {free_space} B, beharrezkoa {needed_space} B, segurtasuneko tartea: {margin} B)", + "restore_may_be_not_enough_disk_space": "Badirudi zure sistemak ez duela nahikoa espazio (erabilgarri: {free_space} B, beharrezkoa {needed_space} B, segurtasun-tartea: {margin} B)", + "restore_not_enough_disk_space": "Ez dago nahikoa espazio (erabilgarri: {free_space} B, beharrezkoa {needed_space} B, segurtasun-tartea: {margin} B)", "restore_running_hooks": "Lehengoratzeko 'hook'ak exekutatzen…", "restore_system_part_failed": "Ezinezkoa izan da sistemaren '{part}' atala lehengoratzea", "server_reboot": "Zerbitzaria berrabiaraziko da", @@ -644,7 +640,7 @@ "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_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", "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.", @@ -654,10 +650,10 @@ "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_compatibility_help": "Bateragarritasunaren eta segurtasunaren arteko oreka SSH zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei). Ikus https://infosec.mozilla.org/guidelines/openssh informazio gehiagorako.", "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_help": "Administrazio-atarira sar daitezken IP helbideak. CIDR notazioa ahalbidetzen da.", "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.", @@ -665,23 +661,23 @@ "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_0024_rebuild_python_venv_failed": "{app} aplikazioaren Python virtualenv-aren birsorkuntza saiakerak huts egin du. 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": "Administratzaileak", + "admins": "Administratzaileek", "app_action_failed": "{app} aplikaziorako {action} eragiketak huts egin du", "config_action_disabled": "Ezin izan da '{action}' eragiketa exekutatu ezgaituta dagoelako, egiaztatu bere mugak betetzen dituzula. Laguntza: {help}", - "all_users": "YunoHosten erabiltzaile guztiak", + "all_users": "YunoHosten erabiltzaile guztiek", "app_manifest_install_ask_init_admin_permission": "Nork izan beharko luke aplikazio honetako administrazio aukeretara sarbidea? (Aldatzea dago)", - "app_manifest_install_ask_init_main_permission": "Nor izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", + "app_manifest_install_ask_init_main_permission": "Nork izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", "ask_admin_fullname": "Administratzailearen izen osoa", "ask_admin_username": "Administratzailearen erabiltzaile-izena", "ask_fullname": "Izen osoa", "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}", + "config_action_failed": "'{action}' eragiketa exekutatzeak huts egin du: {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", @@ -689,20 +685,20 @@ "log_settings_reset": "Berrezarri ezarpenak", "log_settings_reset_all": "Berrezarri ezarpen guztiak", "root_password_changed": "root pasahitza aldatu da", - "visitors": "Bisitariak", + "visitors": "Bisitariek", "global_settings_setting_security_experimental_enabled": "Segurtasun ezaugarri esperimentalak", "registrar_infos": "Erregistro-enpresaren informazioa", "global_settings_setting_pop3_enabled": "Gaitu POP3", - "global_settings_reset_success": "Berrezarri ezarpen globalak", + "global_settings_reset_success": "Berrezarri defektuzko ezarpenak", "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.", + "global_settings_setting_smtp_relay_host": "SMTP errele-ostatatzailea", + "domain_config_acme_eligible": "ACME hautagarritasuna", + "domain_config_acme_eligible_explain": "Ez dirudi domeinu hau Let's Encrypt ziurtagirirako prest dagoenik. Egiaztatu DNS ezarpenak eta zerbitzariaren HTTP irisgarritasuna. Diagnostikoen guneko 'DNS erregistroak' eta 'Web' atalek zer dagoen gaizki ulertzen lagun zaitzakete.", "domain_config_cert_install": "Instalatu Let's Encrypt ziurtagiria", "domain_config_cert_issuer": "Ziurtagiriaren jaulkitzailea", "domain_config_cert_no_checks": "Muzin egin diagnostikoaren egiaztapenei", @@ -719,28 +715,28 @@ "global_settings_setting_pop3_enabled_help": "Gaitu POP3 protokoloa eposta zerbitzarirako", "global_settings_setting_root_password": "root pasahitz berria", "global_settings_setting_root_password_confirm": "root pasahitz berria (egiaztatu)", - "global_settings_setting_smtp_relay_enabled": "Gaitu SMTP relay", + "global_settings_setting_smtp_relay_enabled": "Gaitu SMTP errelea", "global_settings_setting_ssh_compatibility": "SSH bateragarritasuna", "global_settings_setting_ssh_password_authentication": "Pasahitz bidezko autentifikazioa", "global_settings_setting_user_strength_help": "Betekizun hauek lehenbizikoz sortzerakoan edo pasahitza aldatzerakoan bete behar dira soilik", "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_resource_snippet": "Baliabide bat eguneratzen / eskuratzen / eskuragarritasuna uzten", "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.", + "diagnosis_using_stable_codename_details": "Ostatatzaileak zerbait oker ezarri duenean gertatu ohi da hau. Arriskutsua da, Debianen datorren bertsioa 'estable' (egonkorra) bilakatzen denean, apt-k sistemaren pakete guztiak bertsio-berritzen saiatuko da, beharrezko migrazio-prozedurarik burutu gabe. Debianen gordailuan 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}]", + "confirm_notifications_read": "ADI: Aztertu aplikazioaren jakinarazpenak jarraitu baino lehen, baliteke jakin beharreko zerbait esatea. [{answers}]", "app_arch_not_supported": "Aplikazio hau {required} arkitekturan instala daiteke bakarrik, baina zure zerbitzariaren arkitektura {current} da", - "app_resource_failed": "Huts egin du {app} aplikaziorako baliabideen eguneraketak / prestaketak / askapenak: {error}", + "app_resource_failed": "{app} aplikaziorako baliabideen eguneraketak / prestaketak / askapenak huts egin du: {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", @@ -751,16 +747,39 @@ "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.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 automatikoki ezarri ohi du sistemak edo hornitzaileak, erabilgarri baldin badago. Bestela eskuz ezarri beharko dituzu aukera batzuk ondorengo dokumentazioan azaldu bezala: https://yunohost.org/ipv6.", "pattern_fullname": "Baliozko izen oso bat izan behar da (gutxienez hiru karaktere)", "app_change_url_failed": "Ezin izan da {app} aplikazioaren URLa aldatu: {error}", - "app_change_url_require_full_domain": "Ezin da {app} aplikazioa URL berri honetara aldatu domeinu oso bat behar duelako (i.e. with path = /)", + "app_change_url_require_full_domain": "Ezin da {app} aplikazioa URL berri honetara aldatu domeinu oso bat behar duelako (hots, / bide-izena duena)", "app_change_url_script_failed": "Errorea gertatu da URLa aldatzeko aginduaren barnean", - "app_corrupt_source": "YunoHostek deskargatu du {app} aplikaziorako '{source_id}' ({url}) baliabidea baina ez dator bat espero zen 'checksum'arekin. Agian zerbitzariak interneteko konexioa galdu du tarte batez, EDO baliabidea nolabait moldatua izan da arduradunaren aldetik (edo partehartzaile maltzur batetik?) eta YunoHosten arduradunek egoera aztertu eta aplikazioaren manifestua eguneratu behar dute aldaketa hau kontuan hartzeko.\n Espero zen sha256 checksuma: {expected_sha256}\n Deskargatutakoaren sha256 checksuma: {computed_sha256}\n Deskargatutako fitxategiaren tamaina: {size}", - "app_failed_to_upgrade_but_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du, jarraitu hurrengo bertsio-berritzeetara eskatu bezala. Exekutatu 'yunohost log show {operation_logger_name}' errorearen erregistroa ikusteko", - "app_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_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 baten aldetik?) eta YunoHosten arduradunek egoera aztertu eta agian 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-berritzeekin, eskatu bezala. Exekutatu 'yunohost log show {operation_logger_name}' errorearen erregistroa ikusteko", + "app_not_upgraded_broken_system": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du eta, hori dela-eta, 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 (hori dela-eta, --continue-on-failure aukerari muzin egin zaio) eta ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}", "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", - "apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeak huts egin du: {apps}", - "apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')" + "apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeek huts egin dute: {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", + "ask_dyndns_recovery_password_explain": "Aukeratu DynDNS domeinurako berreskuratze-pasahitza, etorkizunean berrezarri beharko bazenu.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Sartu DynDNS domeinuaren berreskuratze-pasahitza.", + "dyndns_no_recovery_password": "Ez da berreskuratze-pasahitzik zehaztu! Domeinuaren gaineko kontrola galduz gero, YunoHost taldeko administrariarekin jarri beharko zara harremanetan!", + "ask_dyndns_recovery_password": "DynDNS berreskuratze-pasahitza", + "dyndns_subscribed": "DynDNS domeinua harpidetu da", + "dyndns_subscribe_failed": "Ezin izan da DynDNS domeinua harpidetu: {error}", + "dyndns_unsubscribe_failed": "Ezin izan da DynDNS domeinuaren harpidetza utzi: {error}", + "dyndns_unsubscribed": "DynDNS domeinuaren harpidetza utzi da", + "log_dyndns_unsubscribe": "Utzi '{}' YunoHost azpidomeinuaren harpidetza", + "ask_dyndns_recovery_password_explain_unavailable": "DynDNS domeinu hau erregistratuta dago lehendik ere. Domeinua zeuk erregistratu bazenuen, sartu berreskuratze-pasahitza domeinua berreskuratzeko.", + "dyndns_too_many_requests": "YunoHosten dyndns zerbitzuak zuk egindako eskaera gehiegi jaso ditu, itxaron ordubete inguru berriro saiatu baino lehen.", + "dyndns_unsubscribe_denied": "Domeinuaren harpidetza uzteak huts egin du: datu okerrak", + "dyndns_unsubscribe_already_unsubscribed": "Domeinuaren harpidetza utzita dago lehendik ere", + "dyndns_set_recovery_password_denied": "Berreskuratze-pasahitza ezartzeak huts egin du: gako okerra", + "dyndns_set_recovery_password_unknown_domain": "Berreskuratze-pasahitza ezartzeak huts egin du: domeinua ez dago erregistratuta", + "dyndns_set_recovery_password_invalid_password": "Berreskuratze-pasahitza ezartzeak huts egin du: pasahitza ez da nahikoa sendoa", + "dyndns_set_recovery_password_failed": "Berreskuratze-pasahitza ezartzeak huts egin du: {error}", + "dyndns_set_recovery_password_success": "Berreskuratze-pasahitza ezarri da!", + "global_settings_setting_ssh_port_help": "1024 baino ataka txikiago bat izan beharko litzateke, zerbitzu ez-administratzaileek urruneko makinan usurpazio-saiorik egin ez dezaten. Lehendik ere erabiltzen ari diren atakak ere ekidin beharko zenituzke, 80 edo 443 kasu." } \ No newline at end of file diff --git a/locales/fa.json b/locales/fa.json index fe6310c5d..8616aee2b 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -37,12 +37,12 @@ "diagnosis_ip_weird_resolvconf_details": "پرونده /etc/resolv.conf باید یک پیوند همراه برای /etc/resolvconf/run/resolv.conf خود اشاره می کند به 127.0.0.1 (dnsmasq). اگر می خواهید راه حل های DNS را به صورت دستی پیکربندی کنید ، لطفاً ویرایش کنید /etc/resolv.dnsmasq.conf.", "diagnosis_ip_weird_resolvconf": "اینطور که پیداست تفکیک پذیری DNS کار می کند ، اما به نظر می رسد از سفارشی استفاده می کنید /etc/resolv.conf.", "diagnosis_ip_broken_resolvconf": "به نظر می رسد تفکیک پذیری نام دامنه در سرور شما شکسته شده است ، که به نظر می رسد مربوط به /etc/resolv.conf و اشاره نکردن به 127.0.0.1 میباشد.", - "diagnosis_ip_broken_dnsresolution": "به نظر می رسد تفکیک پذیری نام دامنه به دلایلی خراب شده است... آیا فایروال درخواست های DNS را مسدود می کند؟", + "diagnosis_ip_broken_dnsresolution": "به نظر می رسد تفکیک پذیری نام دامنه به دلایلی خراب شده است… آیا فایروال درخواست های DNS را مسدود می کند؟", "diagnosis_ip_dnsresolution_working": "تفکیک پذیری نام دامنه کار می کند!", "diagnosis_ip_not_connected_at_all": "به نظر می رسد سرور اصلا به اینترنت متصل نیست !؟", "diagnosis_ip_local": "IP محلی: {local}", "diagnosis_ip_global": "IP جهانی: {global}", - "diagnosis_ip_no_ipv6_tip": "داشتن یک IPv6 فعال برای کار سرور شما اجباری نیست ، اما برای سلامت اینترنت به طور کلی بهتر است. IPv6 معمولاً باید در صورت موجود بودن توسط سیستم یا ارائه دهنده اینترنت شما به طور خودکار پیکربندی شود. در غیر این صورت ، ممکن است لازم باشد چند مورد را به صورت دستی پیکربندی کنید ، همانطور که در اسناد اینجا توضیح داده شده است: https://yunohost.org/#/ipv6.اگر نمی توانید IPv6 را فعال کنید یا اگر برای شما بسیار فنی به نظر می رسد ، می توانید با خیال راحت این هشدار را نادیده بگیرید.", + "diagnosis_ip_no_ipv6_tip": "داشتن یک IPv6 فعال برای کار سرور شما اجباری نیست ، اما برای سلامت اینترنت به طور کلی بهتر است. IPv6 معمولاً باید در صورت موجود بودن توسط سیستم یا ارائه دهنده اینترنت شما به طور خودکار پیکربندی شود. در غیر این صورت ، ممکن است لازم باشد چند مورد را به صورت دستی پیکربندی کنید ، همانطور که در اسناد اینجا توضیح داده شده است: https://yunohost.org/ipv6.اگر نمی توانید IPv6 را فعال کنید یا اگر برای شما بسیار فنی به نظر می رسد ، می توانید با خیال راحت این هشدار را نادیده بگیرید.", "diagnosis_ip_no_ipv6": "سرور IPv6 کار نمی کند.", "diagnosis_ip_connected_ipv6": "سرور از طریق IPv6 به اینترنت متصل است!", "diagnosis_ip_no_ipv4": "سرور IPv4 کار نمی کند.", @@ -61,7 +61,7 @@ "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 (مدیریت بسته) برای استفاده از مخزن پشتیبان پیکربندی شده است. مگر اینکه واقعاً بدانید چه کار می کنید ، ما به شدت از نصب بسته های پشتیبان خودداری می کنیم، زیرا به احتمال زیاد باعث ایجاد ناپایداری یا تداخل در سیستم شما می شود.", - "diagnosis_basesystem_ynh_inconsistent_versions": "شما نسخه های ناسازگار از بسته های YunoHost را اجرا می کنید... به احتمال زیاد به دلیل ارتقاء ناموفق یا جزئی است.", + "diagnosis_basesystem_ynh_inconsistent_versions": "شما نسخه های ناسازگار از بسته های YunoHost را اجرا می کنید… به احتمال زیاد به دلیل ارتقاء ناموفق یا جزئی است.", "diagnosis_basesystem_ynh_main_version": "سرور نسخه YunoHost {main_version} ({repo}) را اجرا می کند", "diagnosis_basesystem_ynh_single_version": "{package} نسخه: {version} ({repo})", "diagnosis_basesystem_kernel": "سرور نسخه {kernel_version} هسته لینوکس را اجرا می کند", @@ -69,8 +69,8 @@ "diagnosis_basesystem_hardware_model": "مدل سرور {model} میباشد", "diagnosis_basesystem_hardware": "معماری سخت افزاری سرور {virt} {arch} است", "custom_app_url_required": "برای ارتقاء سفارشی برنامه {app} خود باید نشانی اینترنتی ارائه دهید", - "confirm_app_install_thirdparty": "خطرناک! این برنامه بخشی از فهرست برنامه YunoHost نیست. نصب برنامه های شخص ثالث ممکن است یکپارچگی و امنیت سیستم شما را به خطر بیندازد. احتمالاً نباید آن را نصب کنید مگر اینکه بدانید در حال انجام چه کاری هستید. اگر این برنامه کار نکرد یا سیستم شما را خراب کرد ، هیچ پشتیبانی ارائه نخواهدشد... به هر حال اگر مایل به پذیرش این خطر هستید ، '{answers}' را تایپ کنید", - "confirm_app_install_danger": "خطرناک! این برنامه هنوز آزمایشی است (اگر صراحتاً کار نکند)! احتمالاً نباید آن را نصب کنید مگر اینکه بدانید در حال انجام چه کاری هستید. اگر این برنامه کار نکرد یا سیستم شما را خراب کرد، هیچ پشتیبانی ارائه نخواهد شد... اگر به هر حال مایل به پذیرش این خطر هستید ، '{answers}' را تایپ کنید", + "confirm_app_install_thirdparty": "خطرناک! این برنامه بخشی از فهرست برنامه YunoHost نیست. نصب برنامه های شخص ثالث ممکن است یکپارچگی و امنیت سیستم شما را به خطر بیندازد. احتمالاً نباید آن را نصب کنید مگر اینکه بدانید در حال انجام چه کاری هستید. اگر این برنامه کار نکرد یا سیستم شما را خراب کرد ، هیچ پشتیبانی ارائه نخواهدشد… به هر حال اگر مایل به پذیرش این خطر هستید ، '{answers}' را تایپ کنید", + "confirm_app_install_danger": "خطرناک! این برنامه هنوز آزمایشی است (اگر صراحتاً کار نکند)! احتمالاً نباید آن را نصب کنید مگر اینکه بدانید در حال انجام چه کاری هستید. اگر این برنامه کار نکرد یا سیستم شما را خراب کرد، هیچ پشتیبانی ارائه نخواهد شد… اگر به هر حال مایل به پذیرش این خطر هستید ، '{answers}' را تایپ کنید", "confirm_app_install_warning": "هشدار: این برنامه ممکن است کار کند ، اما در YunoHost یکپارچه نشده است. برخی از ویژگی ها مانند ورود به سیستم و پشتیبان گیری/بازیابی ممکن است در دسترس نباشد. به هر حال نصب شود؟ [{answers}] ", "certmanager_unable_to_parse_self_CA_name": "نتوانست نام مرجع خودامضائی را تجزیه و تحلیل کند (فایل: {file})", "certmanager_self_ca_conf_file_not_found": "فایل پیکربندی برای اجازه خود امضائی پیدا نشد (فایل: {file})", @@ -81,7 +81,7 @@ "certmanager_domain_dns_ip_differs_from_public_ip": "سوابق DNS برای دامنه '{domain}' با IP این سرور متفاوت است. لطفاً برای اطلاعات بیشتر ، دسته 'DNS records' (پایه) را در عیب یابی بررسی کنید. اگر اخیراً رکورد A خود را تغییر داده اید ، لطفاً منتظر انتشار آن باشید (برخی از چکرهای انتشار DNS بصورت آنلاین در دسترس هستند). (اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این چک ها استفاده کنید.)", "certmanager_domain_cert_not_selfsigned": "گواهی دامنه {domain} خود امضا نشده است. آیا مطمئن هستید که می خواهید آن را جایگزین کنید؟ (برای این کار از '--force' استفاده کنید.)", "certmanager_domain_not_diagnosed_yet": "هنوز هیچ نتیجه تشخیصی و عیب یابی دامنه {domain} وجود ندارد. لطفاً در بخش عیب یابی ، دسته های 'DNS records' و 'Web'مجدداً عیب یابی را اجرا کنید تا بررسی شود که آیا دامنه ای برای گواهی اجازه رمزنگاری آماده است. (یا اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این بررسی ها استفاده کنید.)", - "certmanager_certificate_fetching_or_enabling_failed": "تلاش برای استفاده از گواهینامه جدید برای {domain} جواب نداد...", + "certmanager_certificate_fetching_or_enabling_failed": "تلاش برای استفاده از گواهینامه جدید برای {domain} جواب نداد…", "certmanager_cert_signing_failed": "گواهی جدید امضا نشده است", "certmanager_cert_renew_success": "گواهی اجازه رمزنگاری برای دامنه '{domain}' تمدید شد", "certmanager_cert_install_success_selfsigned": "گواهی خود امضا شده اکنون برای دامنه '{domain}' نصب شده است", @@ -90,12 +90,12 @@ "certmanager_attempt_to_replace_valid_cert": "شما در حال تلاش برای بازنویسی یک گواهی خوب و معتبر برای دامنه {domain} هستید! (استفاده از --force برای bypass)", "certmanager_attempt_to_renew_valid_cert": "گواهی دامنه '{domain}' در حال انقضا نیست! (اگر می دانید چه کار می کنید می توانید از --force استفاده کنید)", "certmanager_attempt_to_renew_nonLE_cert": "گواهی دامنه '{domain}' توسط Let's Encrypt صادر نشده است. به طور خودکار تمدید نمی شود!", - "certmanager_acme_not_configured_for_domain": "در حال حاضر نمی توان چالش ACME را برای {domain} اجرا کرد زیرا nginx conf آن فاقد قطعه کد مربوطه است... لطفاً مطمئن شوید که پیکربندی nginx شما به روز است با استفاده از دستور `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "در حال حاضر نمی توان چالش ACME را برای {domain} اجرا کرد زیرا nginx conf آن فاقد قطعه کد مربوطه است… لطفاً مطمئن شوید که پیکربندی nginx شما به روز است با استفاده از دستور `yunohost tools regen-conf nginx --dry-run --with-diff`.", "backup_with_no_restore_script_for_app": "{app} فاقد اسکریپت بازگردانی است ، نمی توانید پشتیبان گیری این برنامه را به طور خودکار بازیابی کنید.", "backup_with_no_backup_script_for_app": "برنامه '{app}' فاقد اسکریپت پشتیبان است. نادیده گرفتن.", "backup_unable_to_organize_files": "نمی توان از روش سریع برای سازماندهی فایل ها در بایگانی استفاده کرد", "backup_system_part_failed": "از بخش سیستم '{part}' پشتیبان گیری نشد", - "backup_running_hooks": "درحال اجرای قلاب پشتیبان گیری...", + "backup_running_hooks": "درحال اجرای قلاب پشتیبان گیری…", "backup_permission": "مجوز پشتیبان گیری برای {app}", "backup_output_symlink_dir_broken": "فهرست بایگانی شما '{path}' یک پیوند symlink خراب است. شاید فراموش کرده اید که مجدداً محل ذخیره سازی که به آن اشاره می کند را دوباره نصب یا وصل کنید.", "backup_output_directory_required": "شما باید یک پوشه خروجی برای نسخه پشتیبان تهیه کنید", @@ -103,7 +103,7 @@ "backup_output_directory_forbidden": "دایرکتوری خروجی دیگری را انتخاب کنید. پشتیبان گیری نمی تواند در /bin، /boot، /dev ، /etc ، /lib ، /root ، /run ، /sbin ، /sys ، /usr ، /var یا /home/yunohost.backup/archives ایجاد شود", "backup_nothings_done": "چیزی برای ذخیره کردن وجود ندارد", "backup_no_uncompress_archive_dir": "چنین فهرست بایگانی فشرده نشده ایی وجود ندارد", - "backup_mount_archive_for_restore": "در حال آماده سازی بایگانی برای بازگردانی...", + "backup_mount_archive_for_restore": "در حال آماده سازی بایگانی برای بازگردانی…", "backup_method_tar_finished": "بایگانی پشتیبان TAR ایجاد شد", "backup_method_custom_finished": "روش پشتیبان گیری سفارشی '{method}' به پایان رسید", "backup_method_copy_finished": "نسخه پشتیبان نهایی شد", @@ -125,17 +125,17 @@ "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": "بایگانی پشتیبان با این نام در حال حاضر وجود دارد.", "backup_archive_broken_link": "دسترسی به بایگانی پشتیبان امکان پذیر نیست (پیوند خراب به {path})", "backup_archive_app_not_found": "در بایگانی پشتیبان {app} پیدا نشد", - "backup_applying_method_tar": "ایجاد آرشیو پشتیبان TAR...", - "backup_applying_method_custom": "فراخوانی روش پشتیبان گیری سفارشی '{method}'...", - "backup_applying_method_copy": "در حال کپی تمام فایل ها برای پشتیبان گیری...", + "backup_applying_method_tar": "ایجاد آرشیو پشتیبان TAR…", + "backup_applying_method_custom": "فراخوانی روش پشتیبان گیری سفارشی '{method}'…", + "backup_applying_method_copy": "در حال کپی تمام فایل ها برای پشتیبان گیری…", "backup_app_failed": "{app} پشتیبان گیری نشد", - "backup_actually_backuping": "ایجاد آرشیو پشتیبان از پرونده های جمع آوری شده...", + "backup_actually_backuping": "ایجاد آرشیو پشتیبان از پرونده های جمع آوری شده…", "backup_abstract_method": "این روش پشتیبان گیری هنوز اجرا نشده است", "ask_password": "رمز عبور", "ask_new_path": "مسیر جدید", @@ -146,7 +146,7 @@ "apps_catalog_update_success": "کاتالوگ برنامه به روز شد!", "apps_catalog_obsolete_cache": "حافظه پنهان کاتالوگ برنامه خالی یا منسوخ شده است.", "apps_catalog_failed_to_download": "بارگیری کاتالوگ برنامه {apps_catalog} امکان پذیر نیست: {error}", - "apps_catalog_updating": "در حال به روز رسانی کاتالوگ برنامه...", + "apps_catalog_updating": "در حال به روز رسانی کاتالوگ برنامه…", "apps_catalog_init_success": "سیستم کاتالوگ برنامه راه اندازی اولیه شد!", "apps_already_up_to_date": "همه برنامه ها در حال حاضر به روز هستند", "app_packaging_format_not_supported": "این برنامه قابل نصب نیست زیرا قالب بسته بندی آن توسط نسخه YunoHost شما پشتیبانی نمی شود. احتمالاً باید ارتقاء سیستم خود را در نظر بگیرید.", @@ -154,19 +154,19 @@ "app_upgrade_some_app_failed": "برخی از برنامه ها را نمی توان ارتقا داد", "app_upgrade_script_failed": "خطایی در داخل اسکریپت ارتقاء برنامه رخ داده است", "app_upgrade_failed": "{app} ارتقاء نیافت: {error}", - "app_upgrade_app_name": "در حال ارتقاء {app}...", + "app_upgrade_app_name": "در حال ارتقاء {app}…", "app_upgrade_several_apps": "برنامه های زیر ارتقا می یابند: {apps}", "app_unsupported_remote_type": "نوع راه دور پشتیبانی نشده برای برنامه استفاده می شود", "app_unknown": "برنامه ناشناخته", - "app_start_restore": "درحال بازیابی {app}...", - "app_start_backup": "در حال جمع آوری فایل ها برای پشتیبان گیری {app}...", - "app_start_remove": "در حال حذف {app}...", - "app_start_install": "در حال نصب {app}...", + "app_start_restore": "درحال بازیابی {app}…", + "app_start_backup": "در حال جمع آوری فایل ها برای پشتیبان گیری {app}…", + "app_start_remove": "در حال حذف {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_checking": "در حال بررسی بسته های مورد نیاز برای {app}...", + "app_remove_after_failed_install": "حذف برنامه در پی شکست نصب…", + "app_requirements_checking": "در حال بررسی بسته های مورد نیاز برای {app}…", "app_removed": "{app} حذف نصب شد", "app_not_properly_removed": "{app} به درستی حذف نشده است", "app_not_installed": "{app} در لیست برنامه های نصب شده یافت نشد: {all_apps}", @@ -215,8 +215,8 @@ "diagnosis_security_vulnerable_to_meltdown_details": "برای رفع این مشکل ، باید سیستم خود را ارتقا دهید و مجدداً راه اندازی کنید تا هسته لینوکس جدید بارگیری شود (یا در صورت عدم کارکرد با ارائه دهنده سرور خود تماس بگیرید). برای اطلاعات بیشتر به https://meltdownattack.com/ مراجعه کنید.", "diagnosis_security_vulnerable_to_meltdown": "به نظر می رسد شما در برابر آسیب پذیری امنیتی بحرانی Meltdown آسیب پذیر هستید", "diagnosis_rootfstotalspace_critical": "کل سیستم فایل فقط دارای {space} است که بسیار نگران کننده است! احتمالاً خیلی زود فضای دیسک شما تمام می شود! توصیه می شود حداقل 16 گیگابایت و بیشتر فضا برای سیستم فایل ریشه داشته باشید.", - "diagnosis_rootfstotalspace_warning": "سیستم فایل ریشه در مجموع فقط {space} دارد. ممکن است اشکالی نداشته باشد ، اما مراقب باشید زیرا در نهایت ممکن است فضای دیسک شما به سرعت تمام شود... توصیه می شود حداقل 16 گیگابایت و بیشتر فضا برای سیستم فایل ریشه داشته باشید.", - "diagnosis_regenconf_manually_modified_details": "اگر بدانید چه کار می کنید ، احتمالاً خوب است! YunoHost به روز رسانی خودکار این فایل را متوقف می کند... اما مراقب باشید که ارتقاء YunoHost می تواند شامل تغییرات مهم توصیه شده باشد. اگر می خواهید ، می توانید تفاوت ها را با yunohost tools regen-conf {category} --dry-run --with-diff و تنظیم مجدد پیکربندی توصیه شده به زور با فرمان yunohost tools regen-conf {category} --force", + "diagnosis_rootfstotalspace_warning": "سیستم فایل ریشه در مجموع فقط {space} دارد. ممکن است اشکالی نداشته باشد ، اما مراقب باشید زیرا در نهایت ممکن است فضای دیسک شما به سرعت تمام شود… توصیه می شود حداقل 16 گیگابایت و بیشتر فضا برای سیستم فایل ریشه داشته باشید.", + "diagnosis_regenconf_manually_modified_details": "اگر بدانید چه کار می کنید ، احتمالاً خوب است! YunoHost به روز رسانی خودکار این فایل را متوقف می کند… اما مراقب باشید که ارتقاء YunoHost می تواند شامل تغییرات مهم توصیه شده باشد. اگر می خواهید ، می توانید تفاوت ها را با yunohost tools regen-conf {category} --dry-run --with-diff و تنظیم مجدد پیکربندی توصیه شده به زور با فرمان yunohost tools regen-conf {category} --force", "diagnosis_regenconf_manually_modified": "به نظر می رسد فایل پیکربندی {file} به صورت دستی اصلاح شده است.", "diagnosis_regenconf_allgood": "همه فایلهای پیکربندی مطابق با تنظیمات توصیه شده است!", "diagnosis_mail_queue_too_big": "تعداد زیادی ایمیل معلق در صف پست ({nb_pending} ایمیل)", @@ -229,8 +229,8 @@ "diagnosis_mail_blacklist_ok": "به نظر می رسد IP ها و دامنه های مورد استفاده این سرور در لیست سیاه قرار ندارند", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS معکوس فعلی: {rdns_domain}
مقدار مورد انتظار: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "DNS معکوس به درستی در IPv{ipversion} پیکربندی نشده است. ممکن است برخی از ایمیل ها تحویل داده نشوند یا به عنوان هرزنامه پرچم گذاری شوند.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "برخی از ارائه دهندگان به شما اجازه نمی دهند DNS معکوس خود را پیکربندی کنید (یا ممکن است ویژگی آنها شکسته شود...). اگر DNS معکوس شما به درستی برای IPv4 پیکربندی شده است، با استفاده از آن می توانید هنگام ارسال ایمیل، استفاده از IPv6 را غیرفعال کنید. yunohost settings set smtp.allow_ipv6 -v off. توجه: این راه حل آخری به این معنی است که شما نمی توانید از چند سرور IPv6 موجود ایمیل ارسال یا دریافت کنید.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "برخی از ارائه دهندگان به شما اجازه نمی دهند DNS معکوس خود را پیکربندی کنید (یا ممکن است ویژگی آنها شکسته شود...). اگر به همین دلیل مشکلاتی را تجربه می کنید ، راه حل های زیر را در نظر بگیرید: - برخی از ISP ها جایگزین ارائه می دهند با استفاده از رله سرور ایمیل اگرچه به این معنی است که رله می تواند از ترافیک ایمیل شما جاسوسی کند.
- یک جایگزین دوستدار حریم خصوصی استفاده از VPN * با IP عمومی اختصاصی * برای دور زدن این نوع محدودیت ها است. ببینید https://yunohost.org/#/vpn_advantage
- یا ممکن است به ارائه دهنده دیگری بروید", + "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_fcrdns_nok_details": "ابتدا باید DNS معکوس را پیکربندی کنید با {ehlo_domain} در رابط روتر اینترنت یا رابط ارائه دهنده میزبانی تان. (ممکن است برخی از ارائه دهندگان میزبانی از شما بخواهند که برای این کار تیکت پشتیبانی ارسال کنید).", "diagnosis_mail_fcrdns_dns_missing": "در IPv{ipversion} هیچ DNS معکوسی تعریف نشده است. ممکن است برخی از ایمیل ها تحویل داده نشوند یا به عنوان هرزنامه پرچم گذاری شوند.", "diagnosis_mail_fcrdns_ok": "DNS معکوس شما به درستی پیکربندی شده است!", @@ -243,7 +243,7 @@ "diagnosis_mail_ehlo_unreachable_details": "اتصال روی پورت 25 سرور شما در IPv{ipversion} باز نشد. به نظر می رسد غیرقابل دسترس است.
1. شایع ترین علت این مشکل ، پورت 25 است به درستی به سرور شما ارسال نشده است.
2. همچنین باید مطمئن شوید که سرویس postfix در حال اجرا است.
3. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.", "diagnosis_mail_ehlo_unreachable": "سرور ایمیل SMTP از خارج در IPv {ipversion} غیرقابل دسترسی است. قادر به دریافت ایمیل نخواهد بود.", "diagnosis_mail_ehlo_ok": "سرور ایمیل SMTP از خارج قابل دسترسی است و بنابراین می تواند ایمیل دریافت کند!", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "برخی از ارائه دهندگان به شما اجازه نمی دهند پورت خروجی 25 را رفع انسداد کنید زیرا به بی طرفی شبکه اهمیتی نمی دهند.
- برخی از آنها جایگزین را ارائه می دهند با استفاده از رله سرور ایمیل اگرچه به این معنی است که رله می تواند از ترافیک ایمیل شما جاسوسی کند.
- یک جایگزین دوستدار حریم خصوصی استفاده از VPN * با IP عمومی اختصاصی * برای دور زدن این نوع محدودیت ها است. ببینید https://yunohost.org/#/vpn_advantage
- همچنین می توانید تغییر را در نظر بگیرید به یک ارائه دهنده بی طرف خالص تر", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "برخی از ارائه دهندگان به شما اجازه نمی دهند پورت خروجی 25 را رفع انسداد کنید زیرا به بی طرفی شبکه اهمیتی نمی دهند.
- برخی از آنها جایگزین را ارائه می دهند با استفاده از رله سرور ایمیل اگرچه به این معنی است که رله می تواند از ترافیک ایمیل شما جاسوسی کند.
- یک جایگزین دوستدار حریم خصوصی استفاده از VPN * با IP عمومی اختصاصی * برای دور زدن این نوع محدودیت ها است. ببینید https://yunohost.org/vpn_advantage
- همچنین می توانید تغییر را در نظر بگیرید به یک ارائه دهنده بی طرف خالص تر", "diagnosis_mail_outgoing_port_25_blocked_details": "ابتدا باید سعی کنید پورت خروجی 25 را در رابط اینترنت روتر یا رابط ارائه دهنده میزبانی خود باز کنید. (ممکن است برخی از ارائه دهندگان میزبانی از شما بخواهند که برای این کار تیکت پشتیبانی ارسال کنید).", "diagnosis_mail_outgoing_port_25_blocked": "سرور ایمیل SMTP نمی تواند به سرورهای دیگر ایمیل ارسال کند زیرا درگاه خروجی 25 در IPv {ipversion} مسدود شده است.", "diagnosis_mail_outgoing_port_25_ok": "سرور ایمیل SMTP قادر به ارسال ایمیل است (پورت خروجی 25 مسدود نشده است).", @@ -268,7 +268,7 @@ "diagnosis_never_ran_yet": "به نظر می رسد این سرور به تازگی راه اندازی شده است و هنوز هیچ گزارش تشخیصی برای نمایش وجود ندارد. شما باید با اجرای یک عیب یابی و تشخیص کامل، از طریق رابط مدیریت تحت وب webadmin یا با استفاده از '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.", - "domain_cannot_remove_main_add_new_one": "شما نمی توانید '{domain}' را حذف کنید زیرا دامنه اصلی و تنها دامنه شما است، ابتدا باید دامنه دیگری را با 'yunohost domain add ' اضافه کنید، سپس با استفاده از 'yunohost domain main-domain -n ' به عنوان دامنه اصلی تنظیم شده. و بعد از آن می توانید'{domain}' را حذف کنید با استفاده از'yunohost domain remove {domain}'.'", + "domain_cannot_remove_main_add_new_one": "شما نمی توانید '{domain}' را حذف کنید زیرا دامنه اصلی و تنها دامنه شما است، ابتدا باید دامنه دیگری را با 'yunohost domain add ' اضافه کنید، سپس با استفاده از 'yunohost domain main-domain -n ' به عنوان دامنه اصلی تنظیم شده. و بعد از آن می توانید'{domain}' را حذف کنید با استفاده از'yunohost domain remove {domain}'.", "domain_cannot_add_xmpp_upload": "شما نمی توانید دامنه هایی را که با \"xmpp-upload\" شروع می شوند اضافه کنید. این نوع نام مختص ویژگی بارگذاری XMPP است که در YunoHost یکپارچه شده است.", "domain_cannot_remove_main": "شما نمی توانید '{domain}' را حذف کنید زیرا دامنه اصلی است ، ابتدا باید با استفاده از 'yunohost domain main-domain -n ' دامنه دیگری را به عنوان دامنه اصلی تعیین کنید. در اینجا لیست دامنه های کاندید وجود دارد: {other_domains}", "installation_complete": "عملیّات نصب کامل شد", @@ -290,7 +290,7 @@ "group_cannot_edit_all_users": "گروه 'all_users' را نمی توان به صورت دستی ویرایش کرد. این یک گروه ویژه است که شامل همه کاربران ثبت شده در YunoHost میباشد", "group_creation_failed": "گروه '{group}' ایجاد نشد: {error}", "group_created": "گروه '{group}' ایجاد شد", - "group_already_exist_on_system_but_removing_it": "گروه {group} از قبل در گروه های سیستم وجود دارد ، اما YunoHost آن را حذف می کند...", + "group_already_exist_on_system_but_removing_it": "گروه {group} از قبل در گروه های سیستم وجود دارد ، اما YunoHost آن را حذف می کند…", "group_already_exist_on_system": "گروه {group} از قبل در گروه های سیستم وجود دارد", "group_already_exist": "گروه {group} از قبل وجود دارد", "good_practices_about_user_password": "گذرواژه باید حداقل 8 کاراکتر باشد - اگرچه استفاده از گذرواژه طولانی تر تمرین خوبی است (به عنوان مثال عبارت عبور) و/یا استفاده از تنوع کاراکترها (بزرگ ، کوچک ، رقم و کاراکتر های خاص).", @@ -304,28 +304,24 @@ "firewall_reload_failed": "بارگیری مجدد فایروال امکان پذیر نیست", "file_does_not_exist": "فایل {path} وجود ندارد.", "field_invalid": "فیلد نامعتبر '{}'", - "extracting": "استخراج...", + "extracting": "استخراج…", "dyndns_unavailable": "دامنه '{domain}' در دسترس نیست.", "dyndns_domain_not_provided": "ارائه دهنده DynDNS {provider} نمی تواند دامنه {domain} را ارائه دهد.", - "dyndns_registration_failed": "دامنه DynDNS ثبت نشد: {error}", - "dyndns_registered": "دامنه DynDNS ثبت شد", "dyndns_provider_unreachable": "دسترسی به ارائه دهنده DynDNS {provider} امکان پذیر نیست: یا YunoHost شما به درستی به اینترنت متصل نیست یا سرور dynette خراب است.", "dyndns_no_domain_registered": "هیچ دامنه ای با DynDNS ثبت نشده است", "dyndns_key_not_found": "کلید DNS برای دامنه یافت نشد", - "dyndns_key_generating": "ایجاد کلید DNS... ممکن است مدتی طول بکشد.", "dyndns_ip_updated": "IP خود را در DynDNS به روز کرد", "dyndns_ip_update_failed": "آدرس IP را به DynDNS به روز نکرد", "dyndns_could_not_check_available": "بررسی نشد که آیا {domain} در {provider} در دسترس است یا خیر.", "dpkg_lock_not_available": "این دستور در حال حاضر قابل اجرا نیست زیرا به نظر می رسد برنامه دیگری از قفل dpkg (مدیر بسته سیستم) استفاده می کند", "dpkg_is_broken": "شما نمی توانید این کار را در حال حاضر انجام دهید زیرا dpkg/APT (اداره کنندگان سیستم بسته ها) به نظر می رسد در وضعیت خرابی است… می توانید با اتصال از طریق SSH و اجرا این فرمان `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a` مشکل را حل کنید.", - "downloading": "در حال بارگیری...", + "downloading": "در حال بارگیری…", "done": "انجام شد", "domains_available": "دامنه های موجود:", "domain_uninstall_app_first": "این برنامه ها هنوز روی دامنه شما نصب هستند:\n{apps}\n\nلطفاً قبل از اقدام به حذف دامنه ، آنها را با استفاده از 'برنامه yunohost remove the_app_id' حذف کرده یا با استفاده از 'yunohost app change-url the_app_id' به دامنه دیگری منتقل کنید", "domain_remove_confirm_apps_removal": "حذف این دامنه برنامه های زیر را حذف می کند:\n{apps}\n\nآیا طمئن هستید که میخواهید انجام دهید؟ [{answers}]", "domain_hostname_failed": "نام میزبان جدید قابل تنظیم نیست. این ممکن است بعداً مشکلی ایجاد کند (ممکن هم هست خوب باشد).", "domain_exists": "دامنه از قبل وجود دارد", - "domain_dyndns_root_unknown": "دامنه ریشه DynDNS ناشناخته", "domain_dyndns_already_subscribed": "شما قبلاً در یک دامنه DynDNS مشترک شده اید", "domain_dns_conf_is_just_a_recommendation": "این دستور پیکربندی * توصیه شده * را به شما نشان می دهد. این وظیفه شماست که مطابق این توصیه ، منطقه DNS خود را در ثبت کننده خود پیکربندی کنید. در واقع پیکربندی DNS را برای شما تنظیم نمی کند.", "domain_deletion_failed": "حذف دامنه {domain} امکان پذیر نیست: {error}", @@ -361,8 +357,8 @@ "not_enough_disk_space": "فضای آزاد کافی در '{path}' وجود ندارد", "migrations_to_be_ran_manually": "مهاجرت {id} باید به صورت دستی اجرا شود. لطفاً به صفحه Tools → Migrations در صفحه webadmin بروید، یا `yunohost tools migrations run` را اجرا کنید.", "migrations_success_forward": "مهاجرت {id} تکمیل شد", - "migrations_skip_migration": "رد کردن مهاجرت {id}...", - "migrations_running_forward": "مهاجرت در حال اجرا {id}»...", + "migrations_skip_migration": "رد کردن مهاجرت {id}…", + "migrations_running_forward": "مهاجرت در حال اجرا {id}»…", "migrations_pending_cant_rerun": "این مهاجرت ها هنوز در انتظار هستند ، بنابراین نمی توان آنها را دوباره اجرا کرد: {ids}", "migrations_not_pending_cant_skip": "این مهاجرت ها معلق نیستند ، بنابراین نمی توان آنها را رد کرد: {ids}", "migrations_no_such_migration": "مهاجرتی به نام '{id}' وجود ندارد", @@ -370,14 +366,14 @@ "migrations_need_to_accept_disclaimer": "برای اجرای مهاجرت {id} ، باید سلب مسئولیت زیر را بپذیرید:\n---\n{disclaimer}\n---\nاگر می خواهید مهاجرت را اجرا کنید ، لطفاً فرمان را با گزینه '--accept-disclaimer' دوباره اجرا کنید.", "migrations_must_provide_explicit_targets": "هنگام استفاده '--skip' یا '--force-rerun' باید اهداف مشخصی را ارائه دهید", "migrations_migration_has_failed": "مهاجرت {id} کامل نشد ، لغو شد. خطا: {exception}", - "migrations_loading_migration": "بارگیری مهاجرت {id}...", + "migrations_loading_migration": "بارگیری مهاجرت {id}…", "migrations_list_conflict_pending_done": "شما نمیتوانید از هر دو انتخاب '--previous' و '--done' به طور همزمان استفاده کنید.", "migrations_exclusive_options": "'--auto', '--skip'، و '--force-rerun' گزینه های متقابل هستند.", "migrations_failed_to_load_migration": "مهاجرت بار نشد {id}: {error}", "migrations_dependencies_not_satisfied": "این مهاجرت ها را اجرا کنید: '{dependencies_id}' ، قبل از مهاجرت {id}.", "migrations_already_ran": "این مهاجرت ها قبلاً انجام شده است: {ids}", "migration_ldap_rollback_success": "سیستم برگردانده شد.", - "migration_ldap_migration_failed_trying_to_rollback": "نمی توان مهاجرت کرد... تلاش برای بازگرداندن سیستم.", + "migration_ldap_migration_failed_trying_to_rollback": "نمی توان مهاجرت کرد… تلاش برای بازگرداندن سیستم.", "migration_ldap_can_not_backup_before_migration": "نمی توان پشتیبان گیری سیستم را قبل از شکست مهاجرت تکمیل کرد. خطا: {error}", "migration_ldap_backup_before_migration": "ایجاد پشتیبان از پایگاه داده LDAP و تنظیمات برنامه ها قبل از مهاجرت واقعی.", "main_domain_changed": "دامنه اصلی تغییر کرده است", @@ -431,14 +427,14 @@ "log_help_to_get_log": "برای مشاهده گزارش عملیات '{desc}'، از دستور 'yunohost log show {name}' استفاده کنید", "log_link_to_log": "گزارش کامل این عملیات: {desc}'", "log_corrupted_md_file": "فایل فوق داده YAML مربوط به گزارش ها آسیب دیده است: '{md_file}\nخطا: {error} '", - "ldap_server_is_down_restart_it": "سرویس LDAP خاموش است ، سعی کنید آن را دوباره راه اندازی کنید...", + "ldap_server_is_down_restart_it": "سرویس LDAP خاموش است ، سعی کنید آن را دوباره راه اندازی کنید…", "ldap_server_down": "دسترسی به سرور LDAP امکان پذیر نیست", "iptables_unavailable": "در اینجا نمی توانید با iptables بازی کنید. شما یا در ظرفی هستید یا هسته شما آن را پشتیبانی نمی کند", "ip6tables_unavailable": "در اینجا نمی توانید با جدول های ipv6 کار کنید. شما یا در کانتینتر هستید یا هسته شما آن را پشتیبانی نمی کند", "invalid_regex": "عبارت منظم نامعتبر: '{regex}'", - "yunohost_postinstall_end_tip": "پس از نصب کامل شد! برای نهایی کردن تنظیمات خود ، لطفاً موارد زیر را در نظر بگیرید:\n - افزودن اولین کاربر از طریق بخش \"کاربران\" webadmin (یا 'yunohost user create ' در خط فرمان) ؛\n - تشخیص مشکلات احتمالی از طریق بخش \"عیب یابی\" webadmin (یا 'yunohost diagnosis run' در خط فرمان) ؛\n - خواندن قسمت های \"نهایی کردن راه اندازی خود\" و \"آشنایی با YunoHost\" در اسناد مدیریت: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "پس از نصب کامل شد! برای نهایی کردن تنظیمات خود ، لطفاً موارد زیر را در نظر بگیرید:\n - تشخیص مشکلات احتمالی از طریق بخش \"عیب یابی\" webadmin (یا 'yunohost diagnosis run' در خط فرمان) ؛\n - خواندن قسمت های \"نهایی کردن راه اندازی خود\" و \"آشنایی با YunoHost\" در اسناد مدیریت: https://yunohost.org/admindoc.", "yunohost_not_installed": "YunoHost به درستی نصب نشده است. لطفا 'yunohost tools postinstall' را اجرا کنید", - "yunohost_installing": "در حال نصب YunoHost...", + "yunohost_installing": "در حال نصب YunoHost…", "yunohost_configured": "YunoHost اکنون پیکربندی شده است", "yunohost_already_installed": "YunoHost قبلاً نصب شده است", "user_updated": "اطلاعات کاربر تغییر کرد", @@ -454,9 +450,9 @@ "upnp_enabled": "UPnP روشن شد", "upnp_disabled": "UPnP خاموش شد", "upnp_dev_not_found": "هیچ دستگاه UPnP یافت نشد", - "upgrading_packages": "در حال ارتقاء بسته ها...", + "upgrading_packages": "در حال ارتقاء بسته ها…", "upgrade_complete": "ارتقا کامل شد", - "updating_apt_cache": "در حال واکشی و دریافت ارتقاء موجود برای بسته های سیستم...", + "updating_apt_cache": "در حال واکشی و دریافت ارتقاء موجود برای بسته های سیستم…", "update_apt_cache_warning": "هنگام به روز رسانی حافظه پنهان APT (مدیر بسته دبیان) مشکلی پیش آمده. در اینجا مجموعه ای از خطوط source.list موجود میباشد که ممکن است به شناسایی خطوط مشکل ساز کمک کند:\n{sourceslist}", "update_apt_cache_failed": "امکان بروزرسانی حافظه پنهان APT (مدیر بسته دبیان) وجود ندارد. در اینجا مجموعه ای از خطوط source.list هست که ممکن است به شناسایی خطوط مشکل ساز کمک کند:\n{sourceslist}", "unrestore_app": "{app} بازیابی نمی شود", @@ -464,7 +460,7 @@ "unknown_main_domain_path": "دامنه یا مسیر ناشناخته برای '{app}'. شما باید یک دامنه و یک مسیر را مشخص کنید تا بتوانید یک آدرس اینترنتی برای مجوز تعیین کنید.", "unexpected_error": "مشکل غیر منتظره ای پیش آمده: {error}", "unbackup_app": "{app} ذخیره نمی شود", - "this_action_broke_dpkg": "این اقدام dpkg/APT (مدیران بسته های سیستم) را خراب کرد... می توانید با اتصال از طریق SSH و اجرای فرمان `sudo apt install --fix -break` و/یا` sudo dpkg --configure -a` این مشکل را حل کنید.", + "this_action_broke_dpkg": "این اقدام dpkg/APT (مدیران بسته های سیستم) را خراب کرد… می توانید با اتصال از طریق SSH و اجرای فرمان `sudo apt install --fix -break` و/یا` sudo dpkg --configure -a` این مشکل را حل کنید.", "system_username_exists": "نام کاربری قبلاً در لیست کاربران سیستم وجود دارد", "system_upgraded": "سیستم ارتقا یافت", "ssowat_conf_generated": "پیکربندی SSOwat بازسازی شد", @@ -512,15 +508,15 @@ "server_shutdown": "سرور خاموش می شود", "root_password_desynchronized": "گذرواژه مدیریت تغییر کرد ، اما YunoHost نتوانست این را به رمز عبور ریشه منتقل کند!", "restore_system_part_failed": "بخش سیستم '{part}' بازیابی و ترمیم نشد", - "restore_running_hooks": "در حال اجرای قلاب های ترمیم و بازیابی...", - "restore_running_app_script": "ترمیم و بازیابی برنامه '{app}'...", + "restore_running_hooks": "در حال اجرای قلاب های ترمیم و بازیابی…", + "restore_running_app_script": "ترمیم و بازیابی برنامه '{app}'…", "restore_removing_tmp_dir_failed": "پوشه موقت قدیمی حذف نشد", "restore_nothings_done": "هیچ چیز ترمیم و بازسازی نشد", "restore_not_enough_disk_space": "فضای کافی موجود نیست (فضا: {free_space} B ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)", "restore_may_be_not_enough_disk_space": "به نظر می رسد سیستم شما فضای کافی ندارد (فضای آزاد: {free_space} B ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)", "restore_hook_unavailable": "اسکریپت ترمیم و بازسازی برای '{part}' در سیستم شما در دسترس نیست و همچنین در بایگانی نیز وجود ندارد", "restore_failed": "سیستم بازیابی نشد", - "restore_extracting": "استخراج فایل های مورد نیاز از بایگانی...", + "restore_extracting": "استخراج فایل های مورد نیاز از بایگانی…", "restore_confirm_yunohost_installed": "آیا واقعاً می خواهید سیستمی که هم اکنون نصب شده را بازیابی کنید؟ [{answers}]", "restore_complete": "مرمت به پایان رسید", "restore_cleaning_failed": "فهرست بازسازی موقت پاک نشد", @@ -530,9 +526,9 @@ "regex_with_only_domain": "شما نمی توانید از عبارات منظم برای دامنه استفاده کنید، فقط برای مسیر قابل استفاده است", "regex_incompatible_with_tile": "/!\\ بسته بندی کنندگان! مجوز '{permission}' show_tile را روی 'true' تنظیم کرده اند و بنابراین نمی توانید عبارت منظم آدرس اینترنتی را به عنوان URL اصلی تعریف کنید", "regenconf_need_to_explicitly_specify_ssh": "پیکربندی ssh به صورت دستی تغییر یافته است ، اما شما باید صراحتاً دسته \"ssh\" را با --force برای اعمال تغییرات در واقع مشخص کنید.", - "regenconf_pending_applying": "در حال اعمال پیکربندی معلق برای دسته '{category}'...", + "regenconf_pending_applying": "در حال اعمال پیکربندی معلق برای دسته '{category}'…", "regenconf_failed": "پیکربندی برای دسته (ها) بازسازی نشد: {categories}", - "regenconf_dry_pending_applying": "در حال بررسی پیکربندی معلق که برای دسته '{category}' اعمال می شد...", + "regenconf_dry_pending_applying": "در حال بررسی پیکربندی معلق که برای دسته '{category}' اعمال می شد…", "regenconf_would_be_updated": "پیکربندی برای دسته '{category}' به روز می شد", "regenconf_updated": "پیکربندی برای دسته '{category}' به روز شد", "regenconf_up_to_date": "پیکربندی در حال حاضر برای دسته '{category}' به روز است", diff --git a/locales/fr.json b/locales/fr.json index 1ba11b723..64b682998 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -2,69 +2,65 @@ "action_invalid": "Action '{action}' incorrecte", "admin_password": "Mot de passe d'administration", "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}", + "app_argument_choice_invalid": "Choisissez une valeur valide pour l'argument '{name}' : '{value}' ne fait pas partie des choix disponibles ({choices})", + "app_argument_invalid": "Valeur invalide pour le paramètre '{name}' : {error}", "app_argument_required": "Le paramètre '{name}' est requis", "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", "app_id_invalid": "Identifiant d'application invalide", "app_install_files_invalid": "Fichiers d'installation incorrects", "app_not_correctly_installed": "{app} semble être mal installé", - "app_not_installed": "Nous n'avons pas trouvé {app} dans la liste des applications installées : {all_apps}", + "app_not_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 prérequis pour {app} ...", - "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l'URL est-elle correcte ?", + "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_upgrade_failed": "Impossible de mettre à jour {app} : {error}", "app_upgraded": "{app} mis à jour", "ask_main_domain": "Domaine principal", "ask_new_admin_password": "Nouveau mot de passe d'administration", "ask_password": "Mot de passe", "backup_app_failed": "Impossible de sauvegarder {app}", "backup_archive_app_not_found": "{app} n'a pas été trouvée dans l'archive de la sauvegarde", - "backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà.", + "backup_archive_name_exists": "Une archive de sauvegarde avec le nom '{name}' existe déjà.", "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 créée : {name}", + "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": "Sauvegarde supprimée : {name}", + "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", "backup_output_directory_not_empty": "Le répertoire de destination n'est pas vide", "backup_output_directory_required": "Vous devez spécifier un dossier de destination pour la sauvegarde", - "backup_running_hooks": "Exécution des scripts de sauvegarde ...", + "backup_running_hooks": "Exécution des scripts de sauvegarde…", "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application personnalisée {app}", "disk_space_not_sufficient_install": "Il ne reste pas assez d'espace disque pour installer cette application", "disk_space_not_sufficient_update": "Il ne reste pas assez d'espace disque pour mettre à jour cette application", "domain_cert_gen_failed": "Impossible de générer le certificat", "domain_created": "Le domaine a été créé", - "domain_creation_failed": "Impossible de créer le domaine {domain} : {error}", + "domain_creation_failed": "Impossible de créer le domaine {domain} : {error}", "domain_deleted": "Le domaine a été supprimé", - "domain_deletion_failed": "Impossible de supprimer le domaine {domain} : {error}", + "domain_deletion_failed": "Impossible de supprimer le domaine {domain} : {error}", "domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS", - "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nVeuillez les désinstaller avec la commande 'yunohost app remove nom-de-l-application' ou les déplacer vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application' avant de procéder à la suppression du domaine", + "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nVeuillez les désinstaller avec la commande 'yunohost app remove nom-de-l-application' ou les déplacer vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application' avant de procéder à la suppression du domaine", "done": "Terminé", - "downloading": "Téléchargement en cours...", + "downloading": "Téléchargement en cours…", "dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS", "dyndns_ip_updated": "Mise à jour de votre IP pour le domaine DynDNS", - "dyndns_key_generating": "Génération de la clé DNS..., cela peut prendre un certain temps.", "dyndns_key_not_found": "Clé DNS introuvable pour le domaine", "dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS", - "dyndns_registered": "Domaine DynDNS enregistré", - "dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error}", "dyndns_unavailable": "Le domaine {domain} est indisponible.", - "extracting": "Extraction en cours...", - "field_invalid": "Champ incorrect : '{}'", + "extracting": "Extraction en cours…", + "field_invalid": "Champ incorrect : '{}'", "firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reloaded": "Pare-feu rechargé", "firewall_rules_cmd_failed": "Certaines commandes de règles de pare-feu ont échoué. Plus d'informations dans le journal.", - "hook_exec_failed": "Échec de l'exécution du script : {path}", + "hook_exec_failed": "Échec de l'exécution du script : {path}", "hook_exec_not_terminated": "L'exécution du script {path} ne s'est pas terminée correctement", "hook_list_by_invalid": "Propriété invalide pour lister les actions par celle-ci", "hook_name_unknown": "Nom de l'action '{name}' inconnu", @@ -78,103 +74,103 @@ "main_domain_changed": "Le domaine principal a été modifié", "not_enough_disk_space": "L'espace disque est insuffisant sur '{path}'", "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_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 (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)", + "pattern_port_or_range": "Doit être un numéro de port valide compris entre 0 et 65535, ou une gamme de ports (exemple : 100 :200)", "pattern_username": "Doit être composé uniquement de caractères alphanumériques minuscules et de tirets bas (aussi appelé tiret du 8 ou underscore)", "port_already_closed": "Le port {port} est déjà fermé pour les connexions {ip_version}", "port_already_opened": "Le port {port} est déjà ouvert pour les connexions {ip_version}", "restore_already_installed_app": "Une application est déjà installée avec l'identifiant '{app}'", - "app_restore_failed": "Impossible de restaurer {app} : {error}", + "app_restore_failed": "Impossible de restaurer {app} : {error}", "restore_cleaning_failed": "Impossible de nettoyer le dossier temporaire de restauration", "restore_complete": "Restauration terminée", - "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers}]", + "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers}]", "restore_failed": "Impossible de restaurer le système", "restore_hook_unavailable": "Le script de restauration '{part}' n'est pas disponible sur votre système, et ne l'est pas non plus dans l'archive", "restore_nothings_done": "Rien n'a été restauré", - "restore_running_app_script": "Exécution du script de restauration de l'application '{app}'...", - "restore_running_hooks": "Exécution des scripts de restauration...", + "restore_running_app_script": "Exécution du script de restauration de l'application '{app}'…", + "restore_running_hooks": "Exécution des scripts de restauration…", "service_add_failed": "Impossible d'ajouter le service '{service}'", "service_added": "Le service '{service}' a été ajouté", "service_already_started": "Le service '{service}' est déjà en cours d'exécution", "service_already_stopped": "Le service '{service}' est déjà arrêté", "service_cmd_exec_failed": "Impossible d'exécuter la commande '{command}'", - "service_disable_failed": "Impossible de ne pas lancer le service '{service}' au démarrage.\n\nJournaux récents du service : {logs}", + "service_disable_failed": "Impossible de ne pas lancer le service '{service}' au démarrage.\n\nJournaux récents du service : {logs}", "service_disabled": "Le service '{service}' ne sera plus lancé au démarrage du système.", - "service_enable_failed": "Impossible de lancer automatiquement le service '{service}' au démarrage.\n\nJournaux récents du service : {logs}", + "service_enable_failed": "Impossible de lancer automatiquement le service '{service}' au démarrage.\n\nJournaux récents du service : {logs}", "service_enabled": "Le service '{service}' sera désormais lancé automatiquement au démarrage du système.", "service_remove_failed": "Impossible de supprimer le service '{service}'", "service_removed": "Le service '{service}' a été supprimé", - "service_start_failed": "Impossible de démarrer le service '{service}'\n\nJournaux historisés récents : {logs}", + "service_start_failed": "Impossible de démarrer le service '{service}'\n\nJournaux historisés récents : {logs}", "service_started": "Le service '{service}' a été démarré", - "service_stop_failed": "Impossible d'arrêter le service '{service}'\n\nJournaux récents de service : {logs}", + "service_stop_failed": "Impossible d'arrêter le service '{service}'\n\nJournaux récents de service : {logs}", "service_stopped": "Le service '{service}' a été arrêté", "service_unknown": "Le service '{service}' est inconnu", "ssowat_conf_generated": "La configuration de SSOwat a été regénérée", "system_upgraded": "Système mis à jour", - "system_username_exists": "Ce nom d'utilisateur existe déjà dans les utilisateurs système", + "system_username_exists": "Ce nom de compte existe déjà dans les comptes système", "unbackup_app": "'{app}' ne sera pas sauvegardée", - "unexpected_error": "Une erreur inattendue est survenue : {error}", + "unexpected_error": "Une erreur inattendue est survenue : {error}", "unlimit": "Pas de quota", "unrestore_app": "'{app}' ne sera pas restaurée", - "updating_apt_cache": "Récupération des mises à jour disponibles pour les paquets du système...", + "updating_apt_cache": "Récupération des mises à jour disponibles pour les paquets du système…", "upgrade_complete": "Mise à jour terminée", - "upgrading_packages": "Mise à jour des paquets en cours...", + "upgrading_packages": "Mise à jour des paquets en cours…", "upnp_dev_not_found": "Aucun périphérique compatible UPnP n'a été trouvé", "upnp_disabled": "L'UPnP est désactivé", "upnp_enabled": "L'UPnP est activé", "upnp_port_open_failed": "Impossible d'ouvrir les ports UPnP", - "user_created": "L'utilisateur a été créé", - "user_creation_failed": "Impossible de créer l'utilisateur {user} : {error}", - "user_deleted": "L'utilisateur a été supprimé", - "user_deletion_failed": "Impossible de supprimer l'utilisateur {user} : {error}", - "user_home_creation_failed": "Impossible de créer le dossier personnel '{home}' de l'utilisateur", - "user_unknown": "L'utilisateur {user} est inconnu", - "user_update_failed": "Impossible de mettre à jour l'utilisateur {user} : {error}", - "user_updated": "L'utilisateur a été modifié", + "user_created": "Le compte a été créé", + "user_creation_failed": "Impossible de créer le compte {user} : {error}", + "user_deleted": "Le compte a été supprimé", + "user_deletion_failed": "Impossible de supprimer le compte {user} : {error}", + "user_home_creation_failed": "Impossible de créer le dossier personnel '{home}' pour ce compte", + "user_unknown": "Le compte {user} est inconnu", + "user_update_failed": "Impossible de mettre à jour le compte {user} : {error}", + "user_updated": "Le compte a été modifié", "yunohost_already_installed": "YunoHost est déjà installé", "yunohost_configured": "YunoHost est maintenant configuré", - "yunohost_installing": "L'installation de YunoHost est en cours...", + "yunohost_installing": "L'installation de YunoHost est en cours…", "yunohost_not_installed": "YunoHost n'est pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'", - "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de vouloir remplacer un certificat correct et valide pour le domaine {domain} ! (Utilisez --force pour contourner cela)", - "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain} n'est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force pour cela)", - "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain} a échoué...", - "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_attempt_to_replace_valid_cert": "Vous êtes en train de vouloir remplacer un certificat correct et valide pour le domaine {domain} ! (Utilisez --force pour contourner cela)", + "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain} n'est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force pour cela)", + "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain} a échoué…", + "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_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_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}'", "certmanager_cert_renew_success": "Certificat Let's Encrypt renouvelé pour le domaine '{domain}'", "certmanager_cert_signing_failed": "Impossible de signer le nouveau certificat", - "certmanager_no_cert_file": "Impossible de lire le fichier du certificat pour le domaine {domain} (fichier : {file})", + "certmanager_no_cert_file": "Impossible de lire le fichier du certificat pour le domaine {domain} (fichier : {file})", "certmanager_hit_rate_limit": "Trop de certificats ont déjà été émis récemment pour ce même ensemble de domaines {domain}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails sur les ratios et limitations", - "domain_cannot_remove_main": "Vous ne pouvez pas supprimer '{domain}' car il s'agit du domaine principal. Vous devez d'abord définir un autre domaine comme domaine principal à l'aide de 'yunohost domain main-domain -n ', voici la liste des domaines candidats : {other_domains}", - "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l'autorité du certificat auto-signé est introuvable (fichier : {file})", - "certmanager_unable_to_parse_self_CA_name": "Impossible d'analyser le nom de l'autorité du certificat auto-signé (fichier : {file})", + "domain_cannot_remove_main": "Vous ne pouvez pas supprimer '{domain}' car il s'agit du domaine principal. Vous devez d'abord définir un autre domaine comme domaine principal à l'aide de 'yunohost domain main-domain -n ', voici la liste des domaines candidats : {other_domains}", + "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l'autorité du certificat auto-signé est introuvable (fichier : {file})", + "certmanager_unable_to_parse_self_CA_name": "Impossible d'analyser le nom de l'autorité du certificat auto-signé (fichier : {file})", "mailbox_used_space_dovecot_down": "Le service Dovecot doit être démarré si vous souhaitez voir l'espace disque occupé par la messagerie", - "domains_available": "Domaines disponibles :", + "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": "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`.", + "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.", "app_change_url_no_script": "L'application '{app_name}' ne prend pas encore en charge le changement d'URL. Vous devriez peut-être la mettre à jour.", "app_change_url_success": "L'URL de l'application {app} a été changée en {domain}{path}", - "app_location_unavailable": "Cette URL n'est pas disponible ou est en conflit avec une application existante :\n{apps}", + "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", "backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter", - "backup_applying_method_tar": "Création de l'archive TAR de la sauvegarde ...", - "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder ...", - "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method}' ...", + "backup_applying_method_tar": "Création de l'archive TAR de la sauvegarde…", + "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder…", + "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method}'…", "backup_archive_system_part_not_available": "La partie '{part}' du système n'est pas disponible dans cette sauvegarde", - "backup_archive_writing_error": "Impossible d'ajouter des fichiers '{source}' (nommés dans l'archive : '{dest}') à sauvegarder dans l'archive compressée '{archive}'", - "backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size}Mo temporairement ? (Cette méthode est utilisée car certains fichiers n'ont pas pu être préparés avec une méthode plus efficace.)", + "backup_archive_writing_error": "Impossible d'ajouter des fichiers '{source}' (nommés dans l'archive : '{dest}') à sauvegarder dans l'archive compressée '{archive}'", + "backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size}Mo temporairement ? (Cette méthode est utilisée car certains fichiers n'ont pas pu être préparés avec une méthode plus efficace.)", "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l'archive décompressée", "backup_copying_to_organize_the_archive": "Copie de {size} Mo pour organiser l'archive", "backup_csv_creation_failed": "Impossible de créer le fichier CSV nécessaire à la restauration", @@ -190,28 +186,28 @@ "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.", "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)", + "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)", "restore_not_enough_disk_space": "Espace disponible insuffisant (L'espace libre est de {free_space} octets. Le besoin d'espace nécessaire est de {needed_space} octets. En appliquant une marge de sécurité, la quantité d'espace nécessaire est de {margin} octets)", "restore_system_part_failed": "Impossible de restaurer la partie '{part}' du système", "backup_couldnt_bind": "Impossible de lier {src} avec {dest}.", "domain_dns_conf_is_just_a_recommendation": "Cette commande vous montre la configuration *recommandée*. Elle ne configure pas le DNS pour vous. Il est de votre ressort de configurer votre zone DNS chez votre registrar/fournisseur conformément à cette recommandation.", - "migrations_loading_migration": "Chargement de la migration {id} ...", - "migrations_migration_has_failed": "La migration {id} a échoué avec l'exception {exception} : annulation", + "migrations_loading_migration": "Chargement de la migration {id}…", + "migrations_migration_has_failed": "La migration {id} a échoué, abandon. Erreur : {exception}", "migrations_no_migrations_to_run": "Aucune migration à lancer", - "migrations_skip_migration": "Ignorer et passer la migration {id}...", + "migrations_skip_migration": "Ignorer et passer la migration {id}…", "server_shutdown": "Le serveur va s'éteindre", - "server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers}]", + "server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers}]", "server_reboot": "Le serveur va redémarrer", - "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers}]", + "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers}]", "app_upgrade_some_app_failed": "Certaines applications n'ont pas été mises à jour", "dyndns_domain_not_provided": "Le fournisseur DynDNS {provider} ne peut pas fournir le domaine {domain}.", "app_make_default_location_already_used": "Impossible de configurer l'application '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l'application '{other_app}'", - "app_upgrade_app_name": "Mise à jour de {app}...", + "app_upgrade_app_name": "Mise à jour de {app}…", "backup_output_symlink_dir_broken": "Votre répertoire d'archivage '{path}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.", "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans la webadmin, ou lancer `yunohost tools migrations run`.", - "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l'option --accept-disclaimer.", + "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l'option --accept-disclaimer.", "service_description_yunomdns": "Vous permet d'atteindre votre serveur en utilisant 'yunohost.local' sur votre réseau local", "service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)", "service_description_dovecot": "Permet aux clients de messagerie d'accéder/récupérer les emails (via IMAP et POP3)", @@ -222,15 +218,15 @@ "service_description_postfix": "Utilisé pour envoyer et recevoir des emails", "service_description_redis-server": "Une base de données spécialisée utilisée pour l'accès rapide aux données, les files d'attentes et la communication entre les programmes", "service_description_rspamd": "Filtre le pourriel, et d'autres fonctionnalités liées aux emails", - "service_description_slapd": "Stocke les utilisateurs, domaines et leurs informations liées", + "service_description_slapd": "Stocke les comptes, domaines et leurs informations liées", "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", - "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_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}'", - "log_link_to_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en cliquant ici", - "log_help_to_get_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en utilisant la commande 'yunohost log share {name}'", + "log_link_to_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en cliquant ici", + "log_help_to_get_failed_log": "L'opération '{desc}' a échoué ! Pour obtenir de l'aide, merci de partager le journal de l'opération en utilisant la commande 'yunohost log share {name}'", "log_does_exists": "Il n'y a pas de journal des opérations avec le nom '{log}', utilisez 'yunohost log list' pour voir tous les journaux d'opérations disponibles", "log_operation_unit_unclosed_properly": "L'opération ne s'est pas terminée correctement", "log_app_change_url": "Changer l'URL de l'application '{}'", @@ -249,51 +245,51 @@ "log_letsencrypt_cert_install": "Installer le certificat Let's Encrypt sur le domaine '{}'", "log_selfsigned_cert_install": "Installer un certificat auto-signé sur le domaine '{}'", "log_letsencrypt_cert_renew": "Renouveler le certificat Let's Encrypt de '{}'", - "log_user_create": "Ajouter l'utilisateur '{}'", - "log_user_delete": "Supprimer l'utilisateur '{}'", - "log_user_update": "Mettre à jour les informations de l'utilisateur '{}'", + "log_user_create": "Ajouter le compte '{}'", + "log_user_delete": "Supprimer le compte '{}'", + "log_user_update": "Mettre à jour les informations du compte '{}'", "log_domain_main_domain": "Faire de '{}' le domaine principal", "log_tools_migrations_migrate_forward": "Exécuter les migrations", "log_tools_postinstall": "Faire la post-installation de votre serveur YunoHost", "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 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).", + "mail_unavailable": "Cette adresse email est réservée au groupe des comptes d'administration", + "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe de compte d'administration. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", + "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus robuste.", "password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères", "password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules et des minuscules", "password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux", "password_too_simple_4": "Le mot de passe doit comporter au moins 12 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux", - "root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost n'a pas pu le propager au mot de passe root !", + "root_password_desynchronized": "Le mot de passe du compte administrateur a été changé, mais YunoHost n'a pas pu le propager au mot de passe root !", "aborting": "Annulation en cours.", - "app_not_upgraded": "L'application {failed_app} n'a pas été mise à jour et par conséquence les applications suivantes n'ont pas été mises à jour : {apps}", - "app_start_install": "Installation de {app}...", - "app_start_remove": "Suppression de {app}...", - "app_start_backup": "Collecte des fichiers devant être sauvegardés pour {app}...", - "app_start_restore": "Restauration de {app}...", - "app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}", + "app_not_upgraded": "L'application {failed_app} n'a pas été mise à jour et par conséquence les applications suivantes n'ont pas été mises à jour : {apps}", + "app_start_install": "Installation de {app}…", + "app_start_remove": "Suppression de {app}…", + "app_start_backup": "Collecte des fichiers devant être sauvegardés pour {app}…", + "app_start_restore": "Restauration de {app}…", + "app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}", "ask_new_domain": "Nouveau domaine", "ask_new_path": "Nouveau chemin", - "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés ...", - "backup_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 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` et/ou `sudo dpkg --audit`.", + "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 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 (et peut-être dysfonctionnelle) ! Vous ne devriez certainement 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 voulez prendre ce risque malgré tout, 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 certainement 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 voulez prendre ce risque malgré tout, 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` 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.", - "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}", - "service_reload_failed": "Impossible de recharger le service '{service}'.\n\nJournaux historisés récents de ce service : {logs}", + "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}", + "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}", + "service_restart_failed": "Impossible de redémarrer le service '{service}'\n\nJournaux historisés récents de ce service : {logs}", "service_restarted": "Le service '{service}' a été redémarré", - "service_reload_or_restart_failed": "Impossible de recharger ou de redémarrer le service '{service}'\n\nJournaux historisés récents de ce service : {logs}", + "service_reload_or_restart_failed": "Impossible de recharger ou de redémarrer le service '{service}'\n\nJournaux historisés récents de ce service : {logs}", "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).", + "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).", "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}'", @@ -308,50 +304,50 @@ "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}'", - "regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}'...", - "regenconf_failed": "Impossible de régénérer la configuration pour la ou les catégorie(s) : '{categories}'", - "regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}'...", + "regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}'…", + "regenconf_failed": "Impossible de régénérer la configuration pour la ou les catégorie(s) : '{categories}'", + "regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}'…", "dpkg_lock_not_available": "Cette commande ne peut pas être exécutée pour le moment car un autre programme semble utiliser le verrou de dpkg (le gestionnaire de package système)", - "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", - "update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", + "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", + "update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "backup_permission": "Permission de sauvegarde pour {app}", "group_created": "Le groupe '{group}' a été créé", "group_deleted": "Suppression du groupe '{group}'", "group_unknown": "Le groupe {group} est inconnu", "group_updated": "Le groupe '{group}' a été mis à jour", - "group_update_failed": "La mise à jour du groupe '{group}' a échoué : {error}", - "group_creation_failed": "Échec de la création du groupe '{group}' : {error}", - "group_deletion_failed": "Échec de la suppression du groupe '{group}' : {error}", + "group_update_failed": "La mise à jour du groupe '{group}' a échoué : {error}", + "group_creation_failed": "Échec de la création du groupe '{group}' : {error}", + "group_deletion_failed": "Échec de la suppression du groupe '{group}' : {error}", "log_user_group_delete": "Supprimer le groupe '{}'", "log_user_group_update": "Mettre à jour '{}' pour le groupe", - "mailbox_disabled": "La boîte aux lettres est désactivée pour l'utilisateur {user}", - "app_action_broke_system": "Cette action semble avoir cassé des services importants : {services}", + "mailbox_disabled": "La boîte aux lettres est désactivée pour le compte {user}", + "app_action_broke_system": "Cette action semble avoir cassé des services importants : {services}", "apps_already_up_to_date": "Toutes les applications sont déjà à jour", "migrations_must_provide_explicit_targets": "Vous devez fournir des cibles explicites lorsque vous utilisez '--skip' ou '--force-rerun'", "migrations_no_such_migration": "Il n'y a pas de migration appelée '{id}'", - "migrations_pending_cant_rerun": "Ces migrations étant toujours en attente, vous ne pouvez pas les exécuter à nouveau : {ids}", + "migrations_pending_cant_rerun": "Ces migrations étant toujours en attente, vous ne pouvez pas les exécuter à nouveau : {ids}", "migrations_exclusive_options": "'auto', '--skip' et '--force-rerun' sont des options mutuellement exclusives.", - "migrations_not_pending_cant_skip": "Ces migrations ne sont pas en attente et ne peuvent donc pas être ignorées : {ids}", + "migrations_not_pending_cant_skip": "Ces migrations ne sont pas en attente et ne peuvent donc pas être ignorées : {ids}", "permission_not_found": "Permission '{permission}' introuvable", - "permission_update_failed": "Impossible de mettre à jour la permission '{permission}' : {error}", + "permission_update_failed": "Impossible de mettre à jour la permission '{permission}' : {error}", "permission_updated": "Permission '{permission}' mise à jour", - "dyndns_provider_unreachable": "Impossible d'atteindre le fournisseur DynDNS {provider} : votre YunoHost n'est pas correctement connecté à Internet ou le serveur Dynette est en panne.", - "migrations_already_ran": "Ces migrations sont déjà effectuées : {ids}", - "migrations_dependencies_not_satisfied": "Exécutez ces migrations : '{dependencies_id}', avant migration {id}.", - "migrations_failed_to_load_migration": "Impossible de charger la migration {id} : {error}", - "migrations_running_forward": "Exécution de la migration {id}...", + "dyndns_provider_unreachable": "Impossible d'atteindre le fournisseur DynDNS {provider} : votre YunoHost n'est pas correctement connecté à Internet ou le serveur Dynette est en panne.", + "migrations_already_ran": "Ces migrations sont déjà effectuées : {ids}", + "migrations_dependencies_not_satisfied": "Exécutez ces migrations : '{dependencies_id}', avant migration {id}.", + "migrations_failed_to_load_migration": "Impossible de charger la migration {id} : {error}", + "migrations_running_forward": "Exécution de la migration {id}…", "migrations_success_forward": "Migration {id} terminée", - "operation_interrupted": "L'opération a-t-elle été interrompue manuellement ?", + "operation_interrupted": "L'opération a-t-elle été interrompue manuellement ?", "permission_already_exist": "L'autorisation '{permission}' existe déjà", "permission_created": "Permission '{permission}' créée", - "permission_creation_failed": "Impossible de créer l'autorisation '{permission}' : {error}", + "permission_creation_failed": "Impossible de créer l'autorisation '{permission}' : {error}", "permission_deleted": "Permission '{permission}' supprimée", - "permission_deletion_failed": "Impossible de supprimer la permission '{permission}' : {error}", + "permission_deletion_failed": "Impossible de supprimer la permission '{permission}' : {error}", "group_already_exist": "Le groupe {group} existe déjà", "group_already_exist_on_system": "Le groupe {group} existe déjà dans les groupes système", "group_cannot_be_deleted": "Le groupe {group} ne peut pas être supprimé manuellement.", - "group_user_already_in_group": "L'utilisateur {user} est déjà dans le groupe {group}", - "group_user_not_in_group": "L'utilisateur {user} n'est pas dans le groupe {group}", + "group_user_already_in_group": "Le compte {user} est déjà dans le groupe {group}", + "group_user_not_in_group": "Le compte {user} n'est pas dans le groupe {group}", "log_permission_create": "Créer permission '{}'", "log_permission_delete": "Supprimer permission '{}'", "log_user_group_create": "Créer le groupe '{}'", @@ -360,64 +356,64 @@ "permission_already_allowed": "Le groupe '{group}' a déjà l'autorisation '{permission}' activée", "permission_already_disallowed": "Le groupe '{group}' a déjà l'autorisation '{permission}' désactivé", "permission_cannot_remove_main": "Supprimer une autorisation principale n'est pas autorisé", - "user_already_exists": "L'utilisateur '{user}' existe déjà", + "user_already_exists": "Le compte '{user}' existe déjà", "app_full_domain_unavailable": "Désolé, cette application doit être installée sur un domaine qui lui est propre, mais d'autres applications sont déjà installées sur le domaine '{domain}'. Vous pouvez utiliser un sous-domaine dédié à cette application à la place.", - "group_cannot_edit_all_users": "Le groupe 'all_users' ne peut pas être édité manuellement. C'est un groupe spécial destiné à contenir tous les utilisateurs enregistrés dans YunoHost", + "group_cannot_edit_all_users": "Le groupe 'all_users' ne peut pas être édité manuellement. C'est un groupe spécial destiné à contenir tous les comptes enregistrés dans YunoHost", "group_cannot_edit_visitors": "Le groupe 'visiteurs' ne peut pas être édité manuellement. C'est un groupe spécial représentant les visiteurs anonymes", - "group_cannot_edit_primary_group": "Le groupe '{group}' ne peut pas être édité manuellement. C'est le groupe principal destiné à ne contenir qu'un utilisateur spécifique.", + "group_cannot_edit_primary_group": "Le groupe '{group}' ne peut pas être édité manuellement. C'est le groupe principal destiné à ne contenir qu'un compte spécifique.", "log_permission_url": "Mise à jour de l'URL associée à l'autorisation '{}'", "permission_already_up_to_date": "L'autorisation n'a pas été mise à jour car les demandes d'ajout/suppression correspondent déjà à l'état actuel.", - "permission_currently_allowed_for_all_users": "Cette autorisation est actuellement accordée à tous les utilisateurs en plus des autres groupes. Vous voudrez probablement soit supprimer l'autorisation 'all_users', soit supprimer les autres groupes auxquels il est actuellement autorisé.", - "app_install_failed": "Impossible d'installer {app} : {error}", + "permission_currently_allowed_for_all_users": "Cette autorisation est actuellement accordée à tous les comptes en plus des autres groupes. Vous voudrez probablement soit supprimer l'autorisation 'all_users', soit supprimer les autres groupes auxquels il est actuellement autorisé.", + "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": "Suppression de l'application après l'échec de l'installation ...", + "permission_require_account": "Permission {permission} n'a de sens que pour les personnes ayant un compte et ne peut donc pas être activé pour les visiteurs.", + "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} !", - "diagnosis_ip_not_connected_at_all": "Le serveur ne semble pas du tout connecté à Internet !?", + "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} !", + "diagnosis_ip_not_connected_at_all": "Le serveur ne semble pas du tout connecté à Internet ! ?", "diagnosis_ip_weird_resolvconf": "La résolution DNS semble fonctionner, mais il semble que vous utilisez un /etc/resolv.conf personnalisé.", "diagnosis_ip_weird_resolvconf_details": "Le fichier /etc/resolv.conf doit être un lien symbolique vers /etc/resolvconf/run/resolv.conf lui-même pointant vers 127.0.0.1 (dnsmasq). Si vous souhaitez configurer manuellement les résolveurs DNS, veuillez modifier /etc/resolv.dnsmasq.conf.", - "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS
Type : {type}
Nom : {name}
Valeur : {value}", - "diagnosis_diskusage_ok": "L'espace de stockage {mountpoint} (sur le périphérique {device}) a encore {free} ({free_percent}%) d'espace restant (sur {total}) !", + "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 aux préconisations !", + "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_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_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)", + "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_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)", "diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))", "diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.", - "diagnosis_everything_ok": "Tout semble OK pour {category} !", - "diagnosis_failed": "Échec de la récupération du résultat du diagnostic pour la catégorie '{category}' : {error}", - "diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !", + "diagnosis_everything_ok": "Tout semble OK pour {category} !", + "diagnosis_failed": "Échec de la récupération du résultat du diagnostic pour la catégorie '{category}' : {error}", + "diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !", "diagnosis_ip_no_ipv4": "Le serveur ne dispose pas d'une adresse IPv4.", - "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6 !", + "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 bloquée ou interrompue pour une raison quelconque ... Un pare-feu bloque-t-il les requêtes DNS ?", + "diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !", + "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble cassée, 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})", - "diagnosis_dns_discrepancy": "Cet enregistrement DNS ne semble pas correspondre à la configuration recommandée :
Type : {type}
Nom : {name}
La valeur actuelle est : {current}
La valeur attendue est : {value}", - "diagnosis_services_bad_status": "Le service {service} est {status} :-(", - "diagnosis_diskusage_verylow": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) d'espace restant (sur {total}). Vous devriez vraiment envisager de nettoyer de l'espace !", + "diagnosis_dns_discrepancy": "Cet enregistrement DNS ne semble pas correspondre à la configuration recommandée :
Type : {type}
Nom : {name}
La valeur actuelle est : {current}
La valeur attendue est : {value}", + "diagnosis_services_bad_status": "Le service {service} est {status} :-(", + "diagnosis_diskusage_verylow": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) d'espace restant (sur {total}). Vous devriez vraiment envisager de nettoyer de l'espace !", "diagnosis_diskusage_low": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) d'espace restant (sur {total}). Faites attention.", - "diagnosis_ram_verylow": "Le système ne dispose plus que de {available} ({available_percent}%) de RAM ! (sur {total})", + "diagnosis_ram_verylow": "Le système ne dispose plus que de {available} ({available_percent}%) de RAM ! (sur {total})", "diagnosis_ram_low": "Le système n'a plus de {available} ({available_percent}%) RAM sur {total}. Faites attention.", "diagnosis_swap_none": "Le système n'a aucun espace de swap. Vous devriez envisager d'ajouter au moins {recommended} de swap pour éviter les situations où le système manque de mémoire.", "diagnosis_swap_notsomuch": "Le système ne dispose que de {total} de swap. Vous devez envisager d'avoir au moins {recommended} pour éviter les situations où le système manque de mémoire.", - "diagnosis_swap_ok": "Le système dispose de {total} de swap !", + "diagnosis_swap_ok": "Le système dispose de {total} de swap !", "diagnosis_regenconf_manually_modified": "Le fichier de configuration {file} semble avoir été modifié manuellement.", - "diagnosis_regenconf_manually_modified_details": "C'est probablement OK si vous savez ce que vous faites ! YunoHost cessera de mettre à jour ce fichier automatiquement ... Mais attention, les mises à jour de YunoHost pourraient contenir d'importantes modifications recommandées. Si vous le souhaitez, vous pouvez inspecter les différences avec yunohost tools regen-conf {category} --dry-run --with-diff et forcer la réinitialisation à la configuration recommandée avec yunohost tools regen-conf {category} --force", - "apps_catalog_init_success": "Système de catalogue d'applications initialisé !", - "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_regenconf_manually_modified_details": "C'est probablement OK si vous savez ce que vous faites ! YunoHost cessera de mettre à jour ce fichier automatiquement… Mais attention, les mises à jour de YunoHost pourraient contenir d'importantes modifications recommandées. Si vous le souhaitez, vous pouvez inspecter les différences avec yunohost tools regen-conf {category} --dry-run --with-diff et forcer la réinitialisation à la configuration recommandée avec yunohost tools regen-conf {category} --force", + "apps_catalog_init_success": "Système de catalogue d'applications initialisé !", + "apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog} : {error}", + "diagnosis_mail_outgoing_port_25_blocked": "Le serveur SMTP n'est pas capable d'envoyer de email à d'autres serveurs car le port sortant 25 semble être bloqué en IPv{ipversion}.", + "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 à 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", @@ -426,27 +422,27 @@ "diagnosis_description_systemresources": "Ressources système", "diagnosis_description_ports": "Exposition des ports", "diagnosis_description_regenconf": "Configurations système", - "diagnosis_ports_could_not_diagnose": "Impossible de diagnostiquer si les ports sont accessibles de l'extérieur.", - "diagnosis_ports_could_not_diagnose_details": "Erreur : {error}", - "apps_catalog_updating": "Mise à jour du catalogue d'applications...", + "diagnosis_ports_could_not_diagnose": "Impossible de diagnostiquer si les ports sont accessibles de l'extérieur dans IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Erreur : {error}", + "apps_catalog_updating": "Mise à jour du catalogue des applications…", "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 !", + "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 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_could_not_diagnose": "Impossible de diagnostiquer si les domaines sont accessibles de l'extérieur dans IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Erreur : {error}", "diagnosis_http_ok": "Le domaine {domain} est accessible en HTTP depuis l'extérieur.", "diagnosis_http_unreachable": "Le domaine {domain} est inaccessible en HTTP depuis l'extérieur.", - "diagnosis_unknown_categories": "Les catégories suivantes sont inconnues : {categories}", + "diagnosis_unknown_categories": "Les catégories suivantes sont inconnues : {categories}", "app_upgrade_script_failed": "Une erreur s'est produite durant l'exécution du script de mise à niveau de l'application", - "diagnosis_services_running": "Le service {service} est en cours de fonctionnement !", - "diagnosis_services_conf_broken": "La configuration est cassée pour le service {service} !", + "diagnosis_services_running": "Le service {service} est en cours de fonctionnement !", + "diagnosis_services_conf_broken": "La configuration est cassée pour le service {service} !", "diagnosis_ports_needed_by": "Rendre ce port accessible est nécessaire pour les fonctionnalités de type {category} (service {service})", "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_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 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.", + "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 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.", @@ -455,7 +451,7 @@ "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...", + "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é 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é).", @@ -464,101 +460,101 @@ "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_ehlo_could_not_diagnose_details": "Erreur : {error}", "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_ok": "Votre DNS inverse est correctement configuré !", "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_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é 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_unavailable_details": "Erreur : {error}", "diagnosis_mail_queue_too_big": "Trop d'emails en attente dans la file d'attente ({nb_pending} emails)", "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_ip_global": "IP globale : {global}", + "diagnosis_ip_local": "IP locale : {local}", "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_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_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu : {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante à ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur. Vous pouvez également vous assurer qu'aucun pare-feu ou reverse-proxy n'interfère.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains opérateurs ne vous laisseront pas configurer votre reverse-DNS (ou leur fonctionnalité pourrait être cassée ...). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI offre cette possibilité à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir https://yunohost.org/#/vpn_advantage
- Enfin, il est également possible de changer d'opérateur", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée...). Si votre DNS inversé est correctement configuré en IPv4, vous pouvez essayer de désactiver l'utilisation d'IPv6 lors de l'envoi d'emails en exécutant yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Remarque : cette dernière solution signifie que vous ne pourrez pas envoyer ou recevoir d'emails avec les quelques serveurs qui ont uniquement de l'IPv6.", - "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS inverse actuel : {rdns_domain}
Valeur attendue : {ehlo_domain}", + "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible.
1. La cause la plus courante de ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur.
2. Vous devez également vous assurer que le service postfix est en cours d'exécution.
3. Sur les configurations plus complexes : assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", + "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur.
EHLO reçu : {wrong_ehlo}
Attendu : {right_ehlo}
La cause la plus courante à ce problème est que le port 25 n'est pas correctement redirigé vers votre serveur. Vous pouvez également vous assurer qu'aucun pare-feu ou reverse-proxy n'interfère.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains opérateurs ne vous laisseront pas configurer votre reverse-DNS (ou leur fonctionnalité pourrait être cassée…). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes :
- Certains FAI offre cette possibilité à l'aide d'un relais de serveur de messagerie bien que cela implique que le relais pourra espionner votre trafic de messagerie.
- Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de mesures. 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 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_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, 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.", + "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.", "diagnosis_domain_expiration_not_found": "Impossible de vérifier la date d'expiration de certains domaines", - "diagnosis_domain_expiration_not_found_details": "Les informations WHOIS pour le domaine {domain} ne semblent pas contenir les informations concernant la date d'expiration ?", - "diagnosis_domain_not_found_details": "Le domaine {domain} n'existe pas dans la base de donnée WHOIS ou est expiré !", + "diagnosis_domain_expiration_not_found_details": "Les informations WHOIS pour le domaine {domain} ne semblent pas contenir les informations concernant la date d'expiration ?", + "diagnosis_domain_not_found_details": "Le domaine {domain} n'existe pas dans la base de donnée WHOIS ou est expiré !", "diagnosis_domain_expiration_success": "Vos domaines sont enregistrés et ne vont pas expirer prochainement.", - "diagnosis_domain_expiration_warning": "Certains domaines vont expirer prochainement !", - "diagnosis_domain_expiration_error": "Certains domaines vont expirer TRÈS PROCHAINEMENT !", + "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 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}", + "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": "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", + "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 et XMPP du compte", + "app_manifest_install_ask_is_public": "Cette application devrait-elle être visible par les visiteurs anonymes ?", + "app_manifest_install_ask_admin": "Choisissez un compte administrateur pour cette application", + "app_manifest_install_ask_password": "Choisissez un mot de passe d'administration 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_host": "Adresse du relais SMTP", - "global_settings_setting_smtp_relay_user": "Utilisateur du relais SMTP", + "global_settings_setting_smtp_relay_user": "Compte du relais SMTP", "global_settings_setting_smtp_relay_port": "Port du relais SMTP", - "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 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)", + "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 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 SMTP", "diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version", - "additional_urls_already_added": "URL supplémentaire '{url}' déjà ajoutée pour la permission '{permission}'", + "additional_urls_already_added": "L'URL supplémentaire '{url}' a déjà été ajoutée pour la permission '{permission}'", "unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.", "show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, cela car l'URL de l'autorisation '{permission}' est une expression régulière", "show_tile_cant_be_enabled_for_url_not_defined": "Vous ne pouvez pas activer 'show_tile' pour le moment, car vous devez d'abord définir une URL pour l'autorisation '{permission}'", "regex_with_only_domain": "Vous ne pouvez pas utiliser une expression régulière pour le domaine, uniquement pour le chemin", - "regex_incompatible_with_tile": "/!\\ Packagers ! La permission '{permission}' a 'show_tile' définie sur 'true' et vous ne pouvez donc pas définir une URL regex comme URL principale", + "regex_incompatible_with_tile": "/ !\\ Packagers ! La permission '{permission}' a 'show_tile' définie sur 'true' et vous ne pouvez donc pas définir une URL regex comme URL principale", "permission_protected": "L'autorisation {permission} est protégée. Vous ne pouvez pas ajouter ou supprimer le groupe visiteurs à/de cette autorisation.", - "invalid_regex": "Regex non valide : '{regex}'", - "app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.", - "additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'", + "invalid_regex": "Regex non valide : '{regex}'", + "app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.", + "additional_urls_already_removed": "L'URL supplémentaire '{url}' a déjà été supprimée pour la permission '{permission}'", "invalid_number": "Doit être un nombre", "diagnosis_basesystem_hardware_model": "Le modèle/architecture du serveur est {model}", "diagnosis_backports_in_sources_list": "Il semble que le gestionnaire de paquet APT soit configuré pour utiliser le dépôt des rétro-portages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant du dépôt 'backports', car cela risque de créer des instabilités ou des conflits sur votre système.", - "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.", - "diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Cela peut suffire, mais faites attention car vous risquez de les remplir rapidement... Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", + "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 une racine de système de fichier d'au moins 16 Go. 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\nVoulez-vous vraiment faire 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 une racine de système de fichier d'au moins 16 Go.", + "diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Cela peut suffire, mais faites attention car vous risquez de les remplir rapidement… Il est recommandé d'avoir une racine de système de fichier d'au moins 16 Go.", "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 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 }", + "permission_cant_add_to_all_users": "L'autorisation {permission} ne peut pas être ajoutée à tous les comptes.", + "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.", "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.", + "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 comptes autorisés.", "backup_create_size_estimation": "L'archive contiendra environ {size} de données.", "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.", - "ldap_server_is_down_restart_it": "Le service LDAP est en panne, essayez de le redémarrer...", + "ldap_server_is_down_restart_it": "Le service LDAP est arrêté, tentative de redémarrage…", "ldap_server_down": "Impossible d'atteindre le serveur LDAP", "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.", @@ -568,19 +564,19 @@ "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", - "user_import_success": "Utilisateurs importés avec succès", - "user_import_nothing_to_do": "Aucun utilisateur n'a besoin d'être importé", - "user_import_failed": "L'opération d'importation des utilisateurs a totalement échoué", - "user_import_partial_failed": "L'opération d'importation des utilisateurs a partiellement échoué", - "user_import_missing_columns": "Les colonnes suivantes sont manquantes : {columns}", + "user_import_success": "Comptes importés avec succès", + "user_import_nothing_to_do": "Aucun compte n'a besoin d'être importé", + "user_import_failed": "L'opération d'importation des comptes a totalement échoué", + "user_import_partial_failed": "L'opération d'importation des comptes a partiellement échoué", + "user_import_missing_columns": "Les colonnes suivantes sont manquantes : {columns}", "user_import_bad_file": "Votre fichier CSV n'est pas correctement formaté, il sera ignoré afin d'éviter une potentielle perte de données", - "user_import_bad_line": "Ligne incorrecte {line} : {details}", - "log_user_import": "Importer des utilisateurs", + "user_import_bad_line": "Ligne incorrecte {line} : {details}", + "log_user_import": "Importer des comptes", "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.", "config_validate_color": "Doit être une couleur hexadécimale RVB valide", "app_config_unable_to_apply": "Échec de l'application des valeurs du panneau de configuration.", "app_config_unable_to_read": "Échec de la lecture des valeurs du panneau de configuration.", - "config_apply_failed": "Échec de l'application de la nouvelle configuration : {error}", + "config_apply_failed": "Échec de l'application de la nouvelle configuration : {error}", "config_cant_set_value_on_section": "Vous ne pouvez pas définir une seule valeur sur une section de configuration entière.", "config_forbidden_keyword": "Le mot-clé '{keyword}' est réservé, vous ne pouvez pas créer ou utiliser un panneau de configuration avec une question avec cet identifiant.", "config_no_panel": "Aucun panneau de configuration trouvé.", @@ -589,27 +585,27 @@ "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", - "danger": "Danger :", + "danger": "Danger :", "invalid_number_min": "Doit être supérieur à {min}", "invalid_number_max": "Doit être inférieur à {max}", "log_app_config_set": "Appliquer la configuration à l'application '{}'", - "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", + "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", "domain_dns_push_not_applicable": "La fonction de configuration DNS automatique n'est pas applicable au domaine {domain}. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns_config.", "domain_dns_registrar_yunohost": "Ce domaine est de type nohost.me / nohost.st / ynh.fr et sa configuration DNS est donc automatiquement gérée par YunoHost sans qu'il n'y ait d'autre configuration à faire. (voir la commande 'yunohost dyndns update')", - "domain_dns_registrar_supported": "YunoHost a détecté automatiquement que ce domaine est géré par le registrar **{registrar}**. Si vous le souhaitez, YunoHost configurera automatiquement cette zone DNS, si vous lui fournissez les identifiants API appropriés. Vous pouvez trouver de la documentation sur la façon d'obtenir vos identifiants API sur cette page : https://yunohost.org/registar_api_{registrar}. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns )", + "domain_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_dns_push_managed_in_parent_domain": "La fonctionnalité de configuration DNS automatique est gérée dans le domaine parent {parent_domain}.", "domain_dns_registrar_managed_in_parent_domain": "Ce domaine est un sous-domaine de {parent_domain_link}. La configuration du registrar DNS doit être gérée dans le panneau de configuration de {parent_domain}.", "domain_dns_registrar_not_supported": "YunoHost n'a pas pu détecter automatiquement le bureau d'enregistrement gérant ce domaine. Vous devez configurer manuellement vos enregistrements DNS en suivant la documentation sur https://yunohost.org/dns.", - "domain_dns_registrar_experimental": "Jusqu'à présent, l'interface avec l'API de **{registrar}** n'a pas été correctement testée et revue par la communauté YunoHost. L'assistance est **très expérimentale** - soyez prudent !", - "domain_dns_push_failed_to_authenticate": "Échec de l'authentification sur l'API du registrar qui gère votre nom de domaine internet pour '{domain}'. Il est très probable que les informations d'identification soient incorrectes ? (Erreur : {error})", - "domain_dns_push_failed_to_list": "Échec de la liste des enregistrements actuels à l'aide de l'API du registraire : {error}", + "domain_dns_registrar_experimental": "Pour l'instant, l'interface avec l'API de **{registrar}** n'a pas été correctement testée et revue par la communauté YunoHost. Son support est **très expérimentale** - faites preuve de prudence !", + "domain_dns_push_failed_to_authenticate": "Échec de l'authentification sur l'API du registrar pour le domaine '{domain}'. Les informations d'identification sont elles justes ? (Erreur : {error})", + "domain_dns_push_failed_to_list": "Échec de la liste des enregistrements actuels à l'aide de l'API du registraire : {error}", "domain_dns_push_already_up_to_date": "Dossiers déjà à jour.", - "domain_dns_pushing": "Transmission des enregistrements DNS...", - "domain_dns_push_record_failed": "Échec de l'enregistrement {action} {type}/{name} : {error}", - "domain_dns_push_success": "Enregistrements DNS mis à jour !", + "domain_dns_pushing": "Transmission des enregistrements DNS…", + "domain_dns_push_record_failed": "Échec de l'enregistrement {action} {type}/{name} : {error}", + "domain_dns_push_success": "Enregistrements DNS mis à jour !", "domain_dns_push_failed": "La mise à jour des enregistrements DNS a échoué.", - "domain_dns_push_partial_failure": "Enregistrements DNS partiellement mis à jour : certains avertissements/erreurs ont été signalés.", + "domain_dns_push_partial_failure": "Enregistrements DNS partiellement mis à jour : certains avertissements/erreurs ont été signalés.", "domain_config_mail_in": "Emails entrants", "domain_config_mail_out": "Emails sortants", "domain_config_xmpp": "Messagerie instantanée (XMPP)", @@ -625,88 +621,88 @@ "log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'", "diagnosis_http_special_use_tld": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et n'est donc pas censé être exposé en dehors du réseau local.", "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", - "other_available_options": "... et {n} autres options disponibles non affichées", - "domain_config_auth_consumer_key": "Clé utilisateur", + "other_available_options": "… et {n} autres options disponibles non affichées", + "domain_config_auth_consumer_key": "Clé d'identification", "domain_unknown": "Domaine '{domain}' inconnu", "migration_0021_start": "Démarrage de la migration vers Bullseye", - "migration_0021_patching_sources_list": "Mise à jour du fichier sources.lists...", - "migration_0021_main_upgrade": "Démarrage de la mise à niveau générale...", + "migration_0021_patching_sources_list": "Mise à jour du fichier sources.lists…", + "migration_0021_main_upgrade": "Démarrage de la mise à niveau générale…", "migration_0021_still_on_buster_after_main_upgrade": "Quelque chose s'est mal passé lors de la mise à niveau, le système semble toujours être sous Debian Buster", - "migration_0021_yunohost_upgrade": "Démarrage de la mise à jour du noyau YunoHost...", - "migration_0021_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devriez avoir au moins 1 Go de libre pour effectuer cette migration.", + "migration_0021_yunohost_upgrade": "Démarrage de la mise à jour du noyau YunoHost…", + "migration_0021_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devriez avoir au moins 1 Go de libre pour effectuer cette migration.", "migration_0021_system_not_fully_up_to_date": "Votre système n'est pas entièrement à jour. Veuillez effectuer une mise à jour normale avant de lancer la migration vers Bullseye.", - "migration_0021_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n - de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https://yunohost.org/backup ;\n - d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.", - "migration_0021_problematic_apps_warning": "Veuillez noter que des applications qui peuvent poser problèmes ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost, ou bien qu'elles ne soient pas signalées comme \\\"fonctionnelles\\\". Par conséquent, il n'est pas possible de garantir que les applications suivantes fonctionneront encore après la mise à niveau : {problematic_apps}", - "migration_0021_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}", - "migration_0021_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires...", - "migration_0021_patch_yunohost_conflicts": "Application du correctif pour contourner le problème de conflit...", - "migration_0021_not_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_0021_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n - de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https ://yunohost.org/backup ;\n - d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.", + "migration_0021_problematic_apps_warning": "Veuillez noter que des applications qui peuvent poser problèmes ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost, ou bien qu'elles ne soient pas signalées comme \\\"fonctionnelles\\\". Par conséquent, il n'est pas possible de garantir que les applications suivantes fonctionneront encore après la mise à niveau : {problematic_apps}", + "migration_0021_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}", + "migration_0021_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires…", + "migration_0021_patch_yunohost_conflicts": "Application du correctif pour contourner le problème de conflit…", + "migration_0021_not_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", "domain_config_default_app": "Application par défaut", "migration_description_0022_php73_to_php74_pools": "Migration des fichiers de configuration php7.3-fpm 'pool' vers php7.4", "migration_description_0023_postgresql_11_to_13": "Migration des bases de données de PostgreSQL 11 vers 13", "service_description_postgresql": "Stocke les données d'application (base de données SQL)", "tools_upgrade": "Mise à niveau des packages système", - "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 est installé, mais pas PostgreSQL 13 ! ? Quelque chose d'anormal s'est peut-être produit sur votre système :(...", - "tools_upgrade_failed": "Impossible de mettre à jour les paquets : {packages_list}", + "migration_0023_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.", - "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_backup_compress_tar_archives_help": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", "global_settings_setting_security_experimental_enabled": "Fonctionnalités de sécurité expérimentales", - "global_settings_setting_security_experimental_enabled_help": "Activer les fonctionnalités de sécurité expérimentales (ne l'activez pas si vous ne savez pas ce que vous faites !)", + "global_settings_setting_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_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 de comptes administrateurs", + "global_settings_setting_user_strength": "Critères pour les mots de passe des comptes", "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_help": "Adresses IP autorisées à accéder à la webadmin. La notation CIDR est autorisée.", "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}", + "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", + "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": "Comptes administrateurs", + "all_users": "Tous les comptes 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", + "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 du compte administrateur", + "ask_admin_username": "Nom du compte d'administration", "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.", + "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_access_explain": "Sur les systèmes Linux, 'root' est le compte administrateur absolu du système : il possède 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 comptes administrateurs habituels 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)", + "migration_description_0026_new_admins_group": "Migrer vers le nouveau système de gestion 'multi-administrateurs' (plusieurs comptes 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}').", + "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": "É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", @@ -715,10 +711,10 @@ "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_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 alarmiste aux nouveaux visiteurs !", "domain_config_cert_validity": "Validité", "global_settings_setting_admin_strength_help": "Ces paramètres ne seront appliqués que lors de l'initialisation ou de la modification du mot de passe", "global_settings_setting_nginx_compatibility": "Compatibilité NGINX", @@ -726,7 +722,7 @@ "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", + "invalid_credentials": "Mot de passe ou nom de compte 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", @@ -734,33 +730,78 @@ "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.", + "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-comptes 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", + "global_settings_setting_passwordless_sudo": "Permettre aux comptes 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_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 voulez 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}", + "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}", + "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 au portail YunoHost.", + "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 alternatif car quelque chose est au moins momentanément \"cassé\". En conséquence, les mises à jour des applications suivantes ont été annulées : {apps}", - "apps_failed_to_upgrade": "Ces applications n'ont pas pu être mises à jour : {apps}", - "apps_failed_to_upgrade_line": "\n * {app_id} (pour voir le journal/log correspondant, faites un 'yunohost log show {operation_logger_name}')", - "app_failed_to_download_asset": "Échec du téléchargement de la ressource '{source_id}' ({url}) pour {app} : {out}", - "app_corrupt_source": "YunoHost a pu télécharger la ressource '{source_id}' ({url}) pour {app}, malheureusement celle-ci ne correspond pas à la somme de contrôle attendue. Cela peut signifier qu'une défaillance temporaire du réseau s'est produite sur votre serveur, OU que la ressource a été modifiée par le mainteneur de l'application en amont (ou un acteur malveillant ?) et que les responsables du paquet de cette application pour YunoHost doivent investiguer et mettre à jour le manifeste de l'application pour refléter ce changement.\n Somme de contrôle sha256 attendue : {expected_sha256}\n Somme de contrôle sha256 téléchargée : {computed_sha256}\n Taille du fichier téléchargé : {size}" + "app_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 des contrôles 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 indiquer 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": "Le compte '{user}' sera ajouté au groupe '{group}'", + "group_user_remove": "Le compte '{user}' sera retiré du groupe '{group}'", + "group_mailalias_remove": "L'alias de courrier électronique '{mail}' sera supprimé du groupe '{group}'", + "ask_dyndns_recovery_password_explain": "Veuillez choisir un mot de passe de récupération pour votre domaine DynDNS, au cas où vous devriez le réinitialiser plus tard.", + "ask_dyndns_recovery_password": "Mot de passe de récupération pour DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Veuillez saisir le mot de passe de récupération pour ce domaine DynDNS.", + "dyndns_no_recovery_password": "Aucun mot de passe de récupération n'a été spécifié ! Si vous perdez le contrôle de ce domaine, vous devrez contacter une des personnes gérant l'administration dans l'équipe YunoHost !", + "dyndns_subscribed": "Domaine DynDNS enregistré", + "dyndns_subscribe_failed": "Le nom de domaine DynDNS n'a pas pu être enregistré : {error}", + "dyndns_unsubscribe_failed": "Le nom de domaine DynDNS n'a pas pu être résilié : {error}", + "dyndns_unsubscribed": "Domaine DynDNS résilié", + "dyndns_unsubscribe_denied": "Échec de la résiliation du domaine : informations d'identification non valides", + "dyndns_unsubscribe_already_unsubscribed": "Le domaine a déjà été résilié", + "dyndns_set_recovery_password_denied": "Échec de la mise en place du mot de passe de récupération : mot de passe non valide", + "dyndns_set_recovery_password_unknown_domain": "Échec de la définition du mot de passe de récupération : le domaine n'est pas enregistré", + "dyndns_set_recovery_password_invalid_password": "Échec de la mise en place du mot de passe de récupération : le mot de passe n'est pas assez fort/solide", + "dyndns_set_recovery_password_failed": "Échec de la mise en place du mot de passe de récupération : {error}", + "dyndns_set_recovery_password_success": "Mot de passe de récupération changé !", + "log_dyndns_unsubscribe": "Se désabonner d'un sous-domaine YunoHost '{}'", + "dyndns_too_many_requests": "Le service dyndns de YunoHost a reçu trop de requêtes/demandes de votre part, attendez environ 1 heure avant de réessayer.", + "ask_dyndns_recovery_password_explain_unavailable": "Ce domaine DynDNS est déjà enregistré. Si vous êtes la personne qui a enregistré ce domaine lors de sa création, vous pouvez entrer le mot de passe de récupération pour récupérer ce domaine.", + "global_settings_setting_ssh_port_help": "Il est préférable d'utiliser un port inférieur à 1024 pour éviter les tentatives d'usurpation par des services non administrateurs sur la machine distante. Vous devez également éviter d'utiliser un port déjà utilisé tel que le 80 ou le 443.", + "diagnosis_ignore_already_filtered": "(Il y a déjà un filtre de diagnostic {category} qui correspond à ces critères)", + "diagnosis_ignore_no_filter_found": "(Il n'y pas de filtre de diagnostic pour la catégorie {category} qui correspond à ces critères)", + "diagnosis_ignore_filter_added": "Filtre de diagnostic pour {category} ajouté", + "diagnosis_ignore_filter_removed": "Filtre de diagnostic pour {category} supprimé", + "diagnosis_ignore_missing_criteria": "Vous devez fournir au moins un critère qui est une catégorie de diagnostic à ignorer", + "diagnosis_ignore_criteria_error": "Les critères doivent être sous la forme de clé=valeur (ex. domain=yolo.test)", + "diagnosis_ignore_no_issue_found": "Aucun problème correspondant au critère donné n'a été trouvé.", + "migration_description_0027_migrate_to_bookworm": "Mettre à jour le système vers Debian Bookworm et YunoHost 12", + "migration_0027_delayed_api_restart": "L'API de YunoHost sera automatiquement redémarrée dans 15 secondes. Il se peut qu'elle soit indisponible pendant quelques secondes, après quoi vous devrez vous connecter à nouveau.", + "migration_0027_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe de YunoHost a fait de son mieux pour l'examiner et la tester, mais la migration peut encore casser des parties du système ou de ses applications.\n\nPar conséquent, il est recommandé :\n - d'effectuer une sauvegarde de toutes les données ou applications critiques. Plus d'informations sur https://yunohost.org/backup ;\n - de faire preuve de patience après avoir lancé la migration : en fonction de votre connexion Internet et de votre matériel, la mise à niveau peut prendre quelques heures pour s'effectuer correctement.", + "migration_0027_not_bullseye": "La distribution Debian actuelle n'est pas Bullseye ! Si vous avez déjà effectué la migration Bullseye -> Bookworm, cette erreur est symptomatique du fait que la procédure de migration n'a pas réussi à 100 % (sinon YunoHost l'aurait marquée comme terminée). Il est recommandé de chercher ce qui s'est passé avec l'équipe de support, qui aura besoin du journal **complet** de la migration, qui peut être trouvé dans Outils > Journaux dans la webadmin.", + "migration_0027_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus utiles…", + "migration_0027_main_upgrade": "Démarrage de la mise à niveau du système…", + "migration_0027_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés après la mise à niveau : {manually_modified_files}", + "migration_0027_not_enough_free_space": "L'espace libre est plutôt faible dans /var/ ! Vous devez disposer d'au moins 1 Go d'espace libre pour effectuer cette migration.", + "migration_0027_patch_yunohost_conflicts": "Application d'un correctif pour résoudre le problème de conflit…", + "migration_0027_patching_sources_list": "Correction du fichier sources.lists…", + "migration_0027_problematic_apps_warning": "Veuillez noter que des applications installées susceptibles de poser problème ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications de YunoHost, ou bien qu'elles ne soient pas marquées comme 'fonctionnelles'. Par conséquent, il ne peut pas être garanti qu'elles continueront à fonctionner après la mise à jour : {problematic_apps}", + "migration_0027_start": "Démarrage de la migration vers Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Quelque chose s'est mal passé lors de la mise à jour du système, il semble que celui-ci soit toujours sous Debian Bullseye.", + "migration_0027_system_not_fully_up_to_date": "Votre système n'est pas complètement à jour. Veuillez effectuer une mise à jour classique avant de procéder à la migration vers Bullseye.", + "migration_0027_yunohost_upgrade": "Démarrage de la mise à jour du cœur de YunoHost…" } \ No newline at end of file diff --git a/locales/gl.json b/locales/gl.json index b8b6e5cd0..7140a8620 100644 --- a/locales/gl.json +++ b/locales/gl.json @@ -18,17 +18,17 @@ "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 do arquivo '{archive}'... Non se obtivo o ficheiro info.json (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.", + "backup_archive_name_exists": "Xa existe un ficheiro de copia con nome '{name}'.", "backup_archive_broken_link": "Non se puido acceder ao arquivo da copia (ligazón rota a {path})", "backup_archive_app_not_found": "Non se atopa {app} no arquivo da copia", - "backup_applying_method_tar": "Creando o arquivo TAR da copia...", - "backup_applying_method_custom": "Chamando polo método de copia de apoio personalizado '{method}'...", - "backup_applying_method_copy": "Copiando tódolos ficheiros necesarios...", + "backup_applying_method_tar": "Creando o arquivo TAR da copia…", + "backup_applying_method_custom": "A requerir o método de copia de apoio personalizado '{method}'…", + "backup_applying_method_copy": "Gardando os ficheiros na copia…", "backup_app_failed": "Non se fixo copia de {app}", - "backup_actually_backuping": "Creando o arquivo de copia cos ficheiros recollidos...", + "backup_actually_backuping": "Creando o arquivo de copia cos ficheiros recollidos…", "backup_abstract_method": "Este método de copia de apoio aínda non foi implementado", "ask_password": "Contrasinal", "ask_new_path": "Nova ruta", @@ -39,27 +39,27 @@ "apps_catalog_update_success": "O catálogo de aplicacións foi actualizado!", "apps_catalog_obsolete_cache": "A caché do catálogo de apps está baleiro ou obsoleto.", "apps_catalog_failed_to_download": "Non se puido descargar o catálogo de apps {apps_catalog}: {error}", - "apps_catalog_updating": "Actualizando o catálogo de aplicacións...", + "apps_catalog_updating": "Actualizando o catálogo de aplicacións…", "apps_catalog_init_success": "Sistema do catálogo de apps iniciado!", - "apps_already_up_to_date": "Xa tes tódalas apps ao día", + "apps_already_up_to_date": "Xa tes todas as apps ao día", "app_packaging_format_not_supported": "Esta app non se pode instalar porque o formato de empaquetado non está soportado pola túa versión de YunoHost. Deberías considerar actualizar o teu sistema.", "app_upgraded": "{app} actualizadas", "app_upgrade_some_app_failed": "Algunhas apps non se puideron actualizar", "app_upgrade_script_failed": "Houbo un fallo interno no script de actualización da app", "app_upgrade_failed": "Non se actualizou {app}: {error}", - "app_upgrade_app_name": "Actualizando {app}...", + "app_upgrade_app_name": "A actualizar {app}…", "app_upgrade_several_apps": "Vanse actualizar as seguintes apps: {apps}", "app_unsupported_remote_type": "Tipo remoto non soportado para a app", "app_unknown": "App descoñecida", - "app_start_restore": "Restaurando {app}...", - "app_start_backup": "Xuntando os ficheiros para a copia de apoio de {app}...", - "app_start_remove": "Eliminando {app}...", - "app_start_install": "Instalando {app}...", + "app_start_restore": "A restablecer {app}…", + "app_start_backup": "Xuntando os ficheiros para a copia de apoio de {app}…", + "app_start_remove": "A eliminar {app}…", + "app_start_install": "A instalar {app}…", "app_sources_fetch_failed": "Non se puideron obter os ficheiros fonte, é o URL correcto?", "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_checking": "Comprobando os requisitos de {app}...", + "app_remove_after_failed_install": "Eliminando a app debido ao fallo na instalación…", + "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}", @@ -96,7 +96,7 @@ "backup_cleaning_failed": "Non se puido baleirar o cartafol temporal para a copia", "backup_cant_mount_uncompress_archive": "Non se puido montar o arquivo sen comprimir porque está protexido contra escritura", "backup_ask_for_copying_if_needed": "Queres realizar a copia de apoio utilizando temporalmente {size}MB? (Faise deste xeito porque algúns ficheiros non hai xeito de preparalos usando unha forma máis eficiente.)", - "backup_running_hooks": "Executando os ganchos da copia...", + "backup_running_hooks": "Executando os ganchos da copia…", "backup_permission": "Permiso de copia para {app}", "backup_output_symlink_dir_broken": "O directorio de arquivo '{path}' é unha ligazón simbólica rota. Pode ser que esqueceses re/montar ou conectar o medio de almacenaxe ao que apunta.", "backup_output_directory_required": "Debes proporcionar un directorio de saída para a copia", @@ -104,14 +104,14 @@ "backup_output_directory_forbidden": "Elixe un directorio de saída diferente. As copias non poden crearse en /bin, /boot, /dev, /etc, /lib, /root, /sbin, /sys, /usr, /var ou subcartafoles de /home/yunohost.backup/archives", "backup_nothings_done": "Nada que gardar", "backup_no_uncompress_archive_dir": "Non hai tal directorio do arquivo descomprimido", - "backup_mount_archive_for_restore": "Preparando o arquivo para restauración...", + "backup_mount_archive_for_restore": "Preparando o arquivo a restablecer…", "backup_method_tar_finished": "Creouse o arquivo de copia TAR", "backup_method_custom_finished": "O método de copia personalizado '{method}' rematou", "backup_method_copy_finished": "Rematou o copiado dos ficheiros", "backup_hook_unknown": "O gancho da copia '{hook}' é descoñecido", "certmanager_domain_cert_not_selfsigned": "O certificado para o dominio {domain} non está auto-asinado. Tes a certeza de querer substituílo? (Usa '--force' para facelo.)", "certmanager_domain_not_diagnosed_yet": "Por agora non hai resultado de diagnóstico para o dominio {domain}. Volve facer o diagnóstico para a categoría 'Rexistros DNS' e 'Web' na sección de diagnóstico para comprobar se o dominio é compatible con Let's Encrypt. (Ou se sabes o que estás a facer, usa '--no-checks' para desactivar esas comprobacións.)", - "certmanager_certificate_fetching_or_enabling_failed": "Fallou o intento de usar o novo certificado para '{domain}'...", + "certmanager_certificate_fetching_or_enabling_failed": "Fallou o intento de usar o novo certificado para '{domain}'…", "certmanager_cert_signing_failed": "Non se puido asinar o novo certificado", "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o dominio '{domain}'", "certmanager_cert_install_success_selfsigned": "O certificado auto-asinado está instalado para o dominio '{domain}'", @@ -120,14 +120,14 @@ "certmanager_attempt_to_replace_valid_cert": "Estás intentando sobrescribir un certificado correcto e en bo estado para o dominio {domain}! (Usa --force para obviar)", "certmanager_attempt_to_renew_valid_cert": "O certificado para o dominio '{domain}' non caduca pronto! (Podes usar --force se sabes o que estás a facer)", "certmanager_attempt_to_renew_nonLE_cert": "O certificado para o dominio '{domain}' non está proporcionado por Let's Encrypt. Non se pode renovar automáticamente!", - "certmanager_acme_not_configured_for_domain": "Non se realizou o desafío ACME para {domain} porque a súa configuración nginx non ten a parte do código correspondente... Comproba que a túa configuración nginx está ao día utilizando `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "Non se realizou o desafío ACME para {domain} porque a súa configuración nginx non ten a parte do código correspondente… Comproba que a túa configuración nginx está ao día utilizando `yunohost tools regen-conf nginx --dry-run --with-diff`.", "backup_with_no_restore_script_for_app": "'{app}' non ten script de restablecemento, non poderás restablecer automáticamente a copia de apoio desta app.", "backup_with_no_backup_script_for_app": "A app '{app}' non ten script para a copia. Ignorada.", "backup_unable_to_organize_files": "Non se puido usar o método rápido para organizar ficheiros no arquivo", "backup_system_part_failed": "Non se puido facer copia da parte do sistema '{part}'", "certmanager_domain_http_not_working": "O dominio {domain} semella non ser accesible a través de HTTP. Comproba a categoría 'Web' no diagnóstico para máis info. (Se sabes o que estás a facer, utiliza '--no-checks' para obviar estas comprobacións.)", "certmanager_domain_dns_ip_differs_from_public_ip": "Os rexistros DNS para o dominio '{domain}' son diferentes aos da IP deste servidor. Comproba a categoría 'Rexistros DNS' (básico) no diagnóstico para ter máis info. Se cambiaches recentemente o rexistro A, agarda a que se propague o cambio (están dispoñibles ferramentas en liña para comprobar estos cambios). (Se sabes o que estás a facer, utiliza '--no-checks' para obviar estas comprobacións.)", - "confirm_app_install_danger": "PERIGO! Esta app aínda é experimental (pode que nin funcione)! Probablemente NON deberías instalala a non ser que sepas o que estás a facer. NON TERÁS SOPORTE nin axuda se esta app estraga o teu sistema... Se queres asumir o risco, escribe '{answers}'", + "confirm_app_install_danger": "PERIGO! Esta app aínda é experimental (pode que nin funcione)! Probablemente NON deberías instalala a non ser que saibas o que estás a facer. NON TERÁS SOPORTE nin axuda se esta app estraga o teu sistema… Se queres asumir o risco, escribe '{answers}'", "confirm_app_install_warning": "Aviso: Esta app podería funcionar, pero non está ben integrada en YunoHost. Algunhas funcións como a identificación centralizada e as copias de apoio poderían non estar dispoñibles. Desexas instalala igualmente? [{answers}] ", "certmanager_unable_to_parse_self_CA_name": "Non se puido obter o nome da autoridade do auto-asinado (ficheiro: {file})", "certmanager_self_ca_conf_file_not_found": "Non se atopa o ficheiro de configuración para a autoridade de auto-asinado (ficheiro: {file})", @@ -144,7 +144,7 @@ "diagnosis_package_installed_from_sury_details": "Algúns paquetes foron instalados se darse conta desde un repositorio de terceiros chamado Sury. O equipo de YunoHost mellorou a estratexia para xestionar estos paquetes, pero é de agardar que algunhas instalacións que instalaron aplicacións PHP7.3 estando aínda en Stretch teñan inconsistencias co sistema. Para arranxar esta situación, deberías intentar executar o comando: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Algúns paquetes do sistema deberían ser baixados de versión", "diagnosis_backports_in_sources_list": "Semella que apt (o xestor de paquetes) está configurado para usar o repositorio backports. A non ser que saibas o que fas NON che recomendamos instalar paquetes desde backports, porque é probable que produzas inestabilidades e conflitos no teu sistema.", - "diagnosis_basesystem_ynh_inconsistent_versions": "Estás executando versións inconsistentes de paquetes YunoHost... probablemente debido a actualizacións parciais ou fallidas.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Estás executando versións inconsistentes de paquetes YunoHost… probablemente debido a actualizacións parciais ou falladas.", "diagnosis_basesystem_ynh_main_version": "O servidor está a executar Yunohost {main_version} ({repo})", "diagnosis_basesystem_ynh_single_version": "{package} versión: {version} ({repo})", "diagnosis_basesystem_kernel": "O servidor está a executar o kernel Linux {kernel_version}", @@ -152,7 +152,7 @@ "diagnosis_basesystem_hardware_model": "O modelo de servidor é {model}", "diagnosis_basesystem_hardware": "A arquitectura do hardware do servidor é {virt} {arch}", "custom_app_url_required": "Tes que proporcionar o URL para actualizar a app personalizada {app}", - "confirm_app_install_thirdparty": "PERIGO! Esta app non forma parte do catálogo de YunoHost. Ao instalar apps de terceiros poderías comprometer a integridade e seguridade do sistema. Probablemente NON deberías instalala a menos que saibas o que fas. NON SE PROPORCIONARÁ SOPORTE se esta app non funciona ou estraga o sistema... Se aínda así asumes o risco, escribe '{answers}'", + "confirm_app_install_thirdparty": "PERIGO! Esta app non forma parte do catálogo de YunoHost. Ao instalar apps de terceiras partes poderías comprometer a integridade e seguridade do sistema. Probablemente NON deberías instalala a menos que saibas o que fas. NON SE PROPORCIONARÁ SOPORTE se esta app non funciona ou estraga o sistema… Se aínda así asumes o risco, escribe '{answers}'", "diagnosis_dns_point_to_doc": "Revisa a documentación en https://yunohost.org/dns_config se precisas axuda para configurar os rexistros DNS.", "diagnosis_dns_discrepancy": "O seguinte rexistro DNS non segue a configuración recomendada:
Tipo: {type}
Nome: {name}
Valor actual: {current}
Valor agardado: {value}", "diagnosis_dns_missing_record": "Facendo caso á configuración DNS recomendada, deberías engadir un rexistro DNS coa seguinte info.
Tipo: {type}
Nome: {name}
Valor: {value}", @@ -161,12 +161,12 @@ "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 non funciona... Está 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}", "diagnosis_ip_global": "IP global: {global}", - "diagnosis_ip_no_ipv6_tip": "Que o servidor teña conexión IPv6 non é obrigatorio para que funcione, pero é mellor para o funcionamento de Internet en conxunto. IPv6 debería estar configurado automáticamente no teu sistema ou provedor se está dispoñible. Doutro xeito, poderías ter que configurar manualmente algúns parámetros tal como se explica na documentación: https://yunohost.org/#/ipv6. Se non podes activar IPv6 ou é moi complicado para ti, podes ignorar tranquilamente esta mensaxe.", + "diagnosis_ip_no_ipv6_tip": "Que o servidor teña conexión IPv6 non é obrigatorio para que funcione, pero é mellor para o funcionamento de Internet en conxunto. IPv6 debería estar configurado automáticamente no teu sistema ou provedor se está dispoñible. Doutro xeito, poderías ter que configurar manualmente algúns parámetros tal como se explica na documentación: https://yunohost.org/ipv6. Se non podes activar IPv6 ou é moi complicado para ti, podes ignorar tranquilamente esta mensaxe.", "diagnosis_ip_no_ipv6": "O servidor non ten conexión IPv6.", "diagnosis_ip_connected_ipv6": "O servidor está conectado a internet a través de IPv6!", "diagnosis_ip_no_ipv4": "O servidor non ten conexión IPv4.", @@ -201,7 +201,7 @@ "diagnosis_mail_outgoing_port_25_blocked": "O servidor SMTP de email non pode enviar emails a outros servidores porque o porto saínte 25 está bloqueado en IPv{ipversion}.", "diagnosis_mail_ehlo_unreachable": "O servidor de email SMTP non é accesible desde o exterior en IPv{ipversion}. Non poderá recibir emails.", "diagnosis_mail_ehlo_ok": "O servidor de email SMTP é accesible desde o exterior e por tanto pode recibir emails!", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algúns provedores non che van permitir desbloquear o porto 25 saínte porque non se preocupan pola Neutralidade da Rede.
- Algúns deles dan unha alternativa usando un repetidor de servidor de email mais isto implica que o repetidor poderá espiar todo o teu tráfico de email.
- Unha alternativa é utilizar unha VPN *cun IP público dedicado* para evitar este tipo de limitación. Le https://yunohost.org/#/vpn_advantage
- Tamén podes considerar cambiar a un provedor máis amigable coa neutralidade da rede", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algúns provedores non che van permitir desbloquear o porto 25 saínte porque non se preocupan pola Neutralidade da Rede.
- Algúns deles dan unha alternativa usando un repetidor de servidor de email mais isto implica que o repetidor poderá espiar todo o teu tráfico de email.
- Unha alternativa é utilizar unha VPN *cun IP público dedicado* para evitar este tipo de limitación. Le https://yunohost.org/vpn_advantage
- Tamén podes considerar cambiar a un provedor máis amigable coa neutralidade da rede", "diagnosis_mail_outgoing_port_25_blocked_details": "Antes deberías intentar desbloquear o porto 25 saínte no teu rúter de internet ou na web do provedor de hospedaxe. (Algúns provedores poderían pedirche que fagas unha solicitude para isto).", "diagnosis_mail_ehlo_wrong": "Un servidor de email SMPT diferente responde en IPv{ipversion}. O teu servidor probablemente non poida recibir emails.", "diagnosis_mail_ehlo_bad_answer_details": "Podería deberse a que outro servidor está a responder no lugar do teu.", @@ -213,9 +213,9 @@ "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}.", "diagnosis_mail_ehlo_wrong_details": "O EHLO recibido polo diagnosticador remoto en IPv{ipversion} é diferente ao dominio do teu servidor.
EHLO recibido: {wrong_ehlo}
Agardado: {right_ehlo}
A razón máis habitual para este problema é que o porto 25 non está correctamente redirixido ao teu servidor. Alternativamente, asegúrate de non ter un cortalumes ou reverse-proxy interferindo.", - "diagnosis_regenconf_manually_modified_details": "Probablemente todo sexa correcto se sabes o que estás a facer! YunoHost non vai actualizar este ficheiro automáticamente... Pero ten en conta que as actualizacións de YunoHost poderían incluír cambios importantes recomendados. Se queres podes ver as diferenzas con yunohost tools regen-conf {category} --dry-run --with-diff e forzar o restablecemento da configuración recomendada con yunohost tools regen-conf {category} --force", + "diagnosis_regenconf_manually_modified_details": "Probablemente todo sexa correcto se sabes o que estás a facer! YunoHost non vai actualizar este ficheiro automáticamente… Pero ten en conta que as actualizacións de YunoHost poderían incluír cambios importantes recomendados. Se queres podes ver as diferenzas con yunohost tools regen-conf {category} --dry-run --with-diff e forzar o restablecemento da configuración recomendada con yunohost tools regen-conf {category} --force", "diagnosis_regenconf_manually_modified": "O ficheiro de configuración {file} semella que foi modificado manualmente.", - "diagnosis_regenconf_allgood": "Tódolos ficheiros de configuración seguen a configuración recomendada!", + "diagnosis_regenconf_allgood": "Todos os ficheiros de configuración seguen a configuración recomendada!", "diagnosis_mail_queue_too_big": "Hai demasiados emails pendentes na cola de correo ({nb_pending} emails)", "diagnosis_mail_queue_unavailable_details": "Erro: {error}", "diagnosis_mail_queue_unavailable": "Non se pode consultar o número de emails pendentes na cola", @@ -226,8 +226,8 @@ "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 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_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}", "diagnosis_http_could_not_diagnose": "Non se puido comprobar se os dominios son accesibles desde o exterior en IPv{ipversion}.", @@ -252,7 +252,7 @@ "diagnosis_security_vulnerable_to_meltdown_details": "Para arranxar isto, deberías actualizar o sistema e reiniciar para cargar o novo kernel linux (ou contactar co provedor do servizo se isto non o soluciona). Le https://meltdownattack.com/ para máis info.", "diagnosis_security_vulnerable_to_meltdown": "Semella que es vulnerable á vulnerabilidade crítica de seguridade Meltdown", "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.", + "diagnosis_rootfstotalspace_warning": "O sistema de ficheiros root só ten un total de {space}. Podería ser suficiente, mais ten coidado 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.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.", @@ -268,28 +268,24 @@ "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 '{}'", - "extracting": "Extraendo...", + "extracting": "A extraer…", "dyndns_unavailable": "O dominio '{domain}' non está dispoñible.", "dyndns_domain_not_provided": "O provedor DynDNS {provider} non pode proporcionar o dominio {domain}.", - "dyndns_registration_failed": "Non se rexistrou o dominio DynDNS: {error}", - "dyndns_registered": "Dominio DynDNS rexistrado", "dyndns_provider_unreachable": "Non se puido acadar o provedor DynDNS {provider}: pode que o teu YunoHost non teña conexión a internet ou que o servidor dynette non funcione.", "dyndns_no_domain_registered": "Non hai dominio rexistrado con DynDNS", "dyndns_key_not_found": "Non se atopou a chave DNS para o dominio", - "dyndns_key_generating": "Creando chave DNS... podería demorarse.", "dyndns_ip_updated": "Actualizouse o IP en DynDNS", "dyndns_ip_update_failed": "Non se actualizou o enderezo IP en DynDNS", "dyndns_could_not_check_available": "Non se comprobou se {domain} está dispoñible en {provider}.", "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` e/ou `sudo dpkg --audit`.", - "downloading": "Descargando...", + "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:", "domain_uninstall_app_first": "Aínda están instaladas estas aplicacións no teu dominio:\n{apps}\n\nPrimeiro desinstalaas utilizando 'yunohost app remove id_da_app' ou móveas a outro dominio con 'yunohost app change-url id_da_app' antes de eliminar o dominio", "domain_remove_confirm_apps_removal": "Ao eliminar o dominio tamén vas eliminar estas aplicacións:\n{apps}\n\nTes a certeza de querer facelo? [{answers}]", "domain_hostname_failed": "Non se puido establecer o novo nome de servidor. Esto pode causar problemas máis tarde (tamén podería ser correcto).", "domain_exists": "Xa existe o dominio", - "domain_dyndns_root_unknown": "Dominio raiz DynDNS descoñecido", "domain_dyndns_already_subscribed": "Xa tes unha subscrición a un dominio DynDNS", "domain_dns_conf_is_just_a_recommendation": "Este comando móstrache a configuración *recomendada*. Non realiza a configuración DNS no teu nome. É responsabilidade túa configurar as zonas DNS no servizo da empresa que xestiona o rexistro do dominio seguindo esta recomendación.", "domain_deletion_failed": "Non se puido eliminar o dominio {domain}: {error}", @@ -297,7 +293,7 @@ "domain_creation_failed": "Non se puido crear o dominio {domain}: {error}", "domain_created": "Dominio creado", "domain_cert_gen_failed": "Non se puido crear o certificado", - "domain_cannot_remove_main_add_new_one": "Non podes eliminar '{domain}' porque é o dominio principal e único dominio, primeiro tes que engadir outro dominio usando 'yunohost domain add ', e despois establecelo como o dominio principal utilizando 'yunohost domain main-domain -n ' e entón poderás eliminar '{domain}' con 'yunohost domain remove {domain}'.'", + "domain_cannot_remove_main_add_new_one": "Non podes eliminar '{domain}' porque é o dominio principal e único dominio, primeiro tes que engadir outro dominio usando 'yunohost domain add ', e despois establecelo como o dominio principal utilizando 'yunohost domain main-domain -n ' e entón poderás eliminar '{domain}' con 'yunohost domain remove {domain}'.", "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", @@ -306,7 +302,7 @@ "firewall_reloaded": "Recargouse o cortalumes", "group_creation_failed": "Non se puido crear o grupo '{group}': {error}", "group_created": "Creouse o grupo '{group}'", - "group_already_exist_on_system_but_removing_it": "O grupo {group} xa é un dos grupos do sistema, pero YunoHost vaino eliminar...", + "group_already_exist_on_system_but_removing_it": "O grupo {group} xa é un dos grupos do sistema, pero YunoHost vaino eliminar…", "group_already_exist_on_system": "O grupo {group} xa é un dos grupos do sistema", "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).", @@ -321,7 +317,7 @@ "group_cannot_be_deleted": "O grupo {group} non se pode eliminar manualmente.", "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", + "group_cannot_edit_all_users": "O grupo 'all_users' non se pode editar manualmente. É un grupo especial que contén todas as usuarias rexistradas en YunoHost", "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}'", @@ -358,11 +354,11 @@ "log_app_install": "Instalar a app '{}'", "log_app_change_url": "Cambiar o URL da app '{}'", "log_operation_unit_unclosed_properly": "Non se pechou correctamente a unidade da operación", - "log_does_exists": "Non hai rexistro de operación co nome '{log}', usa 'yunohost log list' para ver tódolos rexistros de operacións dispoñibles", + "log_does_exists": "Non hai rexistro de operación co nome '{log}', usa 'yunohost log list' para ver todos os rexistros de operacións dispoñibles", "log_help_to_get_failed_log": "A operación '{desc}' non se completou. Comparte o rexistro completo da operación utilizando o comando 'yunohost log share {name}' para obter axuda", "log_link_to_failed_log": "Non se completou a operación '{desc}'. Por favor envía o rexistro completo desta operación premendo aquí para obter axuda", "migration_ldap_rollback_success": "Sistema restablecido.", - "migration_ldap_migration_failed_trying_to_rollback": "Non se puido migrar... intentando volver á versión anterior do sistema.", + "migration_ldap_migration_failed_trying_to_rollback": "Non se puido migrar… intentando volver á versión anterior do sistema.", "migration_ldap_can_not_backup_before_migration": "O sistema de copia de apoio do sistema non se completou antes de que fallase a migración. Erro: {error}", "migration_ldap_backup_before_migration": "Crear copia de apoio da base de datos LDAP e axustes de apps antes de realizar a migración.", "main_domain_changed": "Foi cambiado o dominio principal", @@ -422,8 +418,8 @@ "not_enough_disk_space": "Non hai espazo libre abondo en '{path}'", "migrations_to_be_ran_manually": "A migración {id} ten que ser executada manualmente. Vaite a Ferramentas → Migracións na páxina webadmin, ou executa `yunohost tools migrations run`.", "migrations_success_forward": "Migración {id} completada", - "migrations_skip_migration": "Omitindo migración {id}...", - "migrations_running_forward": "Realizando migración {id}...", + "migrations_skip_migration": "Omitindo migración {id}…", + "migrations_running_forward": "Realizando a migración {id}…", "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}'", @@ -431,7 +427,7 @@ "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'.", "migrations_must_provide_explicit_targets": "Debes proporcionar obxectivos explícitos ao utilizar '--skip' ou '--force-rerun'", "migrations_migration_has_failed": "A migración {id} non se completou, abortando. Erro: {exception}", - "migrations_loading_migration": "Cargando a migración {id}...", + "migrations_loading_migration": "A cargar a migración {id}…", "migrations_list_conflict_pending_done": "Non podes usar ao mesmo tempo '--previous' e '--done'.", "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' son opcións que se exclúen unhas a outras.", "migrations_failed_to_load_migration": "Non se cargou a migración {id}: {error}", @@ -451,10 +447,10 @@ "permission_not_found": "Non se atopa o permiso '{permission}'", "permission_deletion_failed": "Non se puido eliminar o permiso '{permission}': {error}", "permission_deleted": "O permiso '{permission}' foi eliminado", - "permission_cant_add_to_all_users": "O permiso {permission} non pode ser concecido a tódalas usuarias.", - "permission_currently_allowed_for_all_users": "Este permiso está concedido actualmente a tódalas usuarias ademáis de a outros grupos. Probablemente queiras ben eliminar o permiso 'all_users' ou ben eliminar os outros grupos que teñen permiso.", + "permission_cant_add_to_all_users": "O permiso {permission} non se pode conceder a todas as usuarias.", + "permission_currently_allowed_for_all_users": "Este permiso está concedido actualmente para todas as usuarias ademáis de a outros grupos. Probablemente queiras ben eliminar o permiso 'all_users' ou ben eliminar os outros grupos que teñen permiso.", "restore_failed": "Non se puido restablecer o sistema", - "restore_extracting": "Extraendo os ficheiros necesarios desde o arquivo...", + "restore_extracting": "A extraer os ficheiros necesarios desde o arquivo…", "restore_confirm_yunohost_installed": "Tes a certeza de querer restablecer un sistema xa instalado? [{answers}]", "restore_complete": "Restablecemento completado", "restore_cleaning_failed": "Non se puido despexar o directorio temporal de restablecemento", @@ -464,9 +460,9 @@ "regex_with_only_domain": "Agora xa non podes usar un regex para o dominio, só para ruta", "regex_incompatible_with_tile": "/!\\ Empacadoras! O permiso '{permission}' agora ten show_tile establecido como 'true' polo que non podes definir o regex URL como URL principal", "regenconf_need_to_explicitly_specify_ssh": "A configuración ssh foi modificada manualmente, pero tes que indicar explícitamente a categoría 'ssh' con --force para realmente aplicar os cambios.", - "regenconf_pending_applying": "Aplicando a configuración pendente para categoría '{category}'...", + "regenconf_pending_applying": "Aplicando a configuración pendente para categoría '{category}'…", "regenconf_failed": "Non se rexenerou a configuración para a categoría(s): {categories}", - "regenconf_dry_pending_applying": "Comprobando as configuracións pendentes que deberían aplicarse á categoría '{category}'...", + "regenconf_dry_pending_applying": "Comprobando as configuracións pendentes que deberían aplicarse á categoría '{category}'…", "regenconf_would_be_updated": "A configuración debería ser actualizada para a categoría '{category}'", "regenconf_updated": "Configuración actualizada para '{category}'", "regenconf_up_to_date": "A configuración xa está ao día para a categoría '{category}'", @@ -484,7 +480,7 @@ "service_description_rspamd": "Filtra spam e outras características relacionadas co email", "service_description_redis-server": "Unha base de datos especial utilizada para o acceso rápido a datos, cola de tarefas e comunicación entre programas", "service_description_postfix": "Utilizado para enviar e recibir emails", - "service_description_nginx": "Serve ou proporciona acceso a tódolos sitios web hospedados no teu servidor", + "service_description_nginx": "Serve ou proporciona acceso a todos os sitios web hospedados no teu servidor", "service_description_mysql": "Almacena datos da app (base de datos SQL)", "service_description_metronome": "Xestiona as contas de mensaxería instantánea XMPP", "service_description_fail2ban": "Protexe contra ataques de forza bruta e outro tipo de ataques desde internet", @@ -502,18 +498,18 @@ "server_shutdown": "Vaise apagar o servidor", "root_password_desynchronized": "Mudou o contrasinal de administración, pero YunoHost non puido transferir este cambio ao contrasinal root!", "restore_system_part_failed": "Non se restableceu a parte do sistema '{part}'", - "restore_running_hooks": "Executando os ganchos do restablecemento...", - "restore_running_app_script": "Restablecendo a app '{app}'...", + "restore_running_hooks": "Executando os ganchos do restablecemento…", + "restore_running_app_script": "A restablecer a app '{app}'…", "restore_removing_tmp_dir_failed": "Non se puido eliminar o directorio temporal antigo", "restore_nothings_done": "Nada foi restablecido", - "restore_not_enough_disk_space": "Non hai espazo abondo (espazo: {free_space.d} B, espazo necesario: {needed_space} B, marxe de seguridade: {margin} B)", + "restore_not_enough_disk_space": "Non hai espazo abondo (espazo: {free_space} 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", - "ldap_server_is_down_restart_it": "O servidor LDAP está caído, intenta reinicialo...", + "ldap_server_is_down_restart_it": "O servidor LDAP está caído, intenta reinicialo…", "ldap_server_down": "Non se chegou ao servidor LDAP", "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_installing": "A instalar YunoHost…", "yunohost_configured": "YunoHost está configurado", "yunohost_already_installed": "YunoHost xa está instalado", "user_updated": "Cambiada a info da usuaria", @@ -527,9 +523,9 @@ "user_already_exists": "A usuaria '{user}' xa existe", "upnp_port_open_failed": "Non se puido abrir porto a través de UPnP", "upnp_dev_not_found": "Non se atopa dispositivo UPnP", - "upgrading_packages": "Actualizando paquetes...", + "upgrading_packages": "Actualizando paquetes…", "upgrade_complete": "Actualización completa", - "updating_apt_cache": "Obtendo actualizacións dispoñibles para os paquetes do sistema...", + "updating_apt_cache": "A obter as actualizacións dispoñibles para os paquetes do sistema…", "update_apt_cache_warning": "Algo fallou ao actualizar a caché de APT (xestor de paquetes Debian). Aquí tes un volcado de sources.list, que podería axudar a identificar liñas problemáticas:\n{sourceslist}", "update_apt_cache_failed": "Non se puido actualizar a caché de APT (xestor de paquetes de Debian). Aquí tes un volcado do sources.list, que podería axudarche a identificar liñas incorrectas:\n{sourceslist}", "unrestore_app": "{app} non vai ser restablecida", @@ -537,7 +533,7 @@ "unknown_main_domain_path": "Dominio ou ruta descoñecida '{app}'. Tes que indicar un dominio e ruta para poder especificar un URL para o permiso.", "unexpected_error": "Aconteceu un fallo non agardado: {error}", "unbackup_app": "{app} non vai ser gardada", - "this_action_broke_dpkg": "Esta acción rachou dpkg/APT (xestores de paquetes do sistema)... Podes intentar resolver o problema conectando a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.", + "this_action_broke_dpkg": "Esta acción rachou dpkg/APT (xestores de paquetes do sistema)… Podes intentar resolver o problema conectando a través de SSH e executando `sudo apt install --fix-broken`e/ou `sudo dpkg --configure -a`.", "system_username_exists": "Xa existe este nome de usuaria na lista de usuarias do sistema", "system_upgraded": "Sistema actualizado", "ssowat_conf_generated": "Rexenerada a configuración para SSOwat", @@ -557,7 +553,7 @@ "service_removed": "Eliminado o servizo '{service}'", "service_remove_failed": "Non se eliminou o servizo '{service}'", "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_allgood": "Todas as 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.", "log_user_import": "Importar usuarias", "user_import_failed": "A operación de importación de usuarias fracasou", @@ -598,7 +594,7 @@ "domain_dns_registrar_experimental": "Ata o momento, a interface coa API de **{registrar}** aínda non foi comprobada e revisada pola comunidade YunoHost. O soporte é **moi experimental** - ten coidado!", "domain_dns_push_failed_to_list": "Non se pode mostrar a lista actual de rexistros na API da rexistradora: {error}", "domain_dns_push_already_up_to_date": "Rexistros ao día, nada que facer.", - "domain_dns_pushing": "Enviando rexistros DNS...", + "domain_dns_pushing": "A enviar os rexistros DNS…", "domain_dns_push_record_failed": "Fallou {action} do rexistro {type}/{name}: {error}", "domain_dns_push_success": "Rexistros DNS actualizados!", "domain_dns_push_failed": "Fallou completamente a actualización dos rexistros DNS.", @@ -611,8 +607,8 @@ "domain_config_auth_application_secret": "Chave segreda da aplicación", "domain_config_auth_consumer_key": "Chave consumidora", "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", - "other_available_options": "... e outras {n} opcións dispoñibles non mostradas", - "domain_dns_registrar_yunohost": "Este dominio un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost se máis requisitos. (mira o comando 'yunohost dyndns update')", + "other_available_options": "… e outras {n} opcións dispoñibles non mostradas", + "domain_dns_registrar_yunohost": "Este dominio é un dos de nohost.me / nohost.st / ynh.fr e a configuración DNS xestionaa directamente YunoHost sen máis requisitos. (mira o comando 'yunohost dyndns update')", "domain_dns_registrar_supported": "YunoHost detectou automáticamente que este dominio está xestionado pola rexistradora **{registrar}**. Se queres, YunoHost pode configurar automáticamente as súas zonas DNS, se proporcionas as credenciais de acceso á API. Podes ver a documentación sobre como obter as credenciais da API nesta páxina: https://yunohost.org/registrar_api_{registrar}. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://yunohost.org/dns )", "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", "domain_config_auth_token": "Token de autenticación", @@ -626,22 +622,22 @@ "log_domain_config_set": "Actualizar configuración para o dominio '{}'", "domain_unknown": "Dominio '{domain}' descoñecido", "migration_0021_start": "Comezando a migración a Bullseye", - "migration_0021_patching_sources_list": "Actualizando sources.list...", - "migration_0021_main_upgrade": "Iniciando a actualización principal...", + "migration_0021_patching_sources_list": "Actualizando sources.list…", + "migration_0021_main_upgrade": "A comezar 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_yunohost_upgrade": "Iniciando actualización compoñente core de YunoHost…", "migration_0021_not_enough_free_space": "Queda pouco espazo en /var/! Deberías ter polo menos 1GB libre para facer a migración.", "migration_0021_problematic_apps_warning": "Detectamos que están instaladas estas app que poderían ser problemáticas. Semella que non foron instaladas desde o catálogo YunoHost, ou non están marcadas como que 'funcionan'. Así, non podemos garantir que seguiran funcionando ben tras a migración: {problematic_apps}", "migration_0021_modified_files": "Ten en conta que os seguintes ficheiros semella que foron editados manualmente e poderían ser sobrescritos durante a migración: {manually_modified_files}", - "migration_0021_cleaning_up": "Limpando a caché e os paquetes que xa non son precisos...", - "migration_0021_patch_yunohost_conflicts": "Solucionando os problemas e conflitos...", + "migration_0021_cleaning_up": "A limpar a caché e os paquetes que xa non son precisos…", + "migration_0021_patch_yunohost_conflicts": "Aplicando o parche para resolver o problema de conflitos…", "migration_description_0021_migrate_to_bullseye": "Actualizar o sistema a Debian Bullseye e YunoHost 11.x", "migration_0021_system_not_fully_up_to_date": "O teu sistema non está completamente actualizado. Fai unha actualización normal antes de executar a migración a Bullseye.", "migration_0021_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo de YunoHost fixo todo o que puido para revisalo e probalo, pero aínda así poderían acontecer fallos no sistema ou apps.\n\nAsí as cousas, é recomendable:\n - Facer unha copia de apoio dos datos e apps importantes. Máis info en https://yunohost.org/backup;\n - Ter paciencia unha vez inicias a migración: dependendo da túa conexión a internet e hardware, podería levarlle varias horas completar o proceso.", "tools_upgrade_failed": "Non se actualizaron os paquetes: {packages_list}", "migration_0023_not_enough_space": "Crear espazo suficiente en {path} para realizar a migración.", "migration_0023_postgresql_11_not_installed": "PostgreSQL non estaba instalado no sistema. Nada que facer.", - "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 está instalado, pero PostgreSQL 13 non!? Algo raro debeu pasarlle ao teu sistema :(...", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 está instalado, pero PostgreSQL 13 non!? Algo raro debeu pasarlle ao teu sistema :(…", "migration_description_0022_php73_to_php74_pools": "Migrar ficheiros de configuración de php7.3-fpm 'pool' a php7.4", "migration_description_0023_postgresql_11_to_13": "Migrar bases de datos de PostgreSQL 11 a 13", "service_description_postgresql": "Almacena datos da app (Base datos SQL)", @@ -654,10 +650,10 @@ "global_settings_setting_admin_strength": "Fortaleza do contrasinal de Admin", "global_settings_setting_user_strength": "Fortaleza do contrasinal da usuaria", "global_settings_setting_postfix_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor Postfix. Aféctalle ao cifrado (e outros aspectos da seguridade)", - "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade)", + "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle ao cifrado (e outros aspectos da seguridade). Le https://infosec.mozilla.org/guidelines/openssh for more info.", "global_settings_setting_ssh_password_authentication_help": "Permitir autenticación con contrasinal para SSH", "global_settings_setting_ssh_port": "Porto SSH", - "global_settings_setting_webadmin_allowlist_help": "Enderezos IP con permiso para acceder á webadmin. Separados por vírgulas.", + "global_settings_setting_webadmin_allowlist_help": "Enderezos IP con permiso para acceder á webadmin. Permítese a notación CIDR.", "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.", @@ -668,7 +664,7 @@ "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.", + "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", @@ -693,10 +689,10 @@ "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_reset_all": "Restablecer todos os axustes", "log_settings_set": "Aplicar axustes", "admins": "Admins", - "all_users": "Tódalas usuarias de YunoHost", + "all_users": "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)", @@ -714,7 +710,7 @@ "visitors": "Visitantes", "global_settings_setting_security_experimental_enabled": "Ferramentas experimentais de seguridade", "diagnosis_using_stable_codename": "apt (o xestor de paquetes do sistema) está configurado para instalar paquetes co nome de código 'stable', no lugar do nome de código da versión actual de Debian (bullseye).", - "diagnosis_using_stable_codename_details": "Normalmente esto é debido a unha configuración incorrecta do teu provedor de hospedaxe. Esto é perigoso, porque tan pronto como a nova versión de Debian se convirta en 'stable', apt vai querer actualizar tódolos paquetes do sistema se realizar o procedemento de migración requerido. É recomendable arranxar isto editando a fonte de apt ao repositorio base de Debian, e substituir a palabra stable por bullseye. O ficheiro de configuración correspondente debería ser /etc/sources.list, ou ficheiro dentro de /etc/apt/sources.list.d/.", + "diagnosis_using_stable_codename_details": "Normalmente isto débese a unha configuración incorrecta do teu provedor de hospedaxe. Isoto é perigoso, porque tan pronto como a nova versión de Debian se convirta en 'stable', apt vai querer actualizar todos os paquetes do sistema sen realizar o procedemento de migración axeitado. É recomendable arranxar isto editando a fonte de apt ao repositorio base de Debian, e substituir a palabra stable por bullseye. O ficheiro de configuración correspondente debería ser /etc/sources.list, ou ficheiro dentro de /etc/apt/sources.list.d/.", "diagnosis_using_yunohost_testing": "apt (o xestor de paquetes do sistema) está configurado actualmente para instalar calquera actualización 'testing' para o núcleo YunoHost.", "diagnosis_using_yunohost_testing_details": "Isto probablemente sexa correcto se sabes o que estás a facer, pero pon coidado e le as notas de publicación antes de realizar actualizacións de YunoHost! Se queres desactivar as actualizacións 'testing', deberías eliminar a palabra testing de /etc/apt/sources.list.d/yunohost.list.", "global_settings_setting_backup_compress_tar_archives": "Comprimir copias de apoio", @@ -747,7 +743,7 @@ "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible.", "global_settings_setting_dns_exposure": "Versións de IP a ter en conta para a configuración DNS e diagnóstico", "global_settings_setting_dns_exposure_help": "Nota: Esto só lle afecta á configuración DNS recomendada e diagnóstico do sistema. Non lle afecta aos axustes do sistema.", - "diagnosis_ip_no_ipv6_tip_important": "Se está dispoñible, IPv6 debería estar automáticamente configurado polo sistema ou o teu provedor. Se non, pode que teñas que facer algúns axustes manualmente tal como se explica na documentación: https://yunohost.org/#/ipv6.", + "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}", @@ -762,5 +758,50 @@ "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}", "app_failed_to_download_asset": "Fallou a descarga do recurso '{source_id}' ({url}) para {app}: {out}", - "app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e actualizar o manifesto da app para mostrar este cambio.\n Suma sha256 agardada: {expected_sha256} \n Suma sha256 do descargado: {computed_sha256}\n Tamaño do ficheiro: {size}" -} \ No newline at end of file + "app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e podería ter que actualizar o manifesto da app para ter este cambio en conta.\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 '{group}'", + "group_user_remove": "Vaise quitar a '{user}' do grupo '{group}'", + "ask_dyndns_recovery_password_explain": "Elixe un contrasinal de recuperación para o teu dominio DynDNS, por se precisas restablecelo no futuro.", + "ask_dyndns_recovery_password": "Contrasinal de recuperación DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Escribe o contrasinal de recuperación para este dominio DynDNS.", + "dyndns_no_recovery_password": "Non se estableceu un contrasinal de recuperación! Se perdes o control sobre dominio precisarás contactar coa administración do equipo YunoHost!", + "dyndns_subscribed": "Tes unha subscrición a un dominio DynDNS", + "dyndns_subscribe_failed": "Non te subscribiches ao dominio DynDNS: {error}", + "dyndns_unsubscribe_failed": "Non se retirou a subscrición ao dominio DynDNS: {error}", + "dyndns_unsubscribed": "Retirada a subscrición ao dominio DynDNS", + "dyndns_unsubscribe_denied": "Fallo ao intentar retirar subscrición: credenciais incorrectas", + "dyndns_unsubscribe_already_unsubscribed": "Non tes unha subscrición ao dominio", + "dyndns_set_recovery_password_denied": "Fallou o establecemento do contrasinal de recuperación: chave non válida", + "dyndns_set_recovery_password_unknown_domain": "Fallo ao establecer o contrasinal de recuperación: dominio non rexistrado", + "dyndns_set_recovery_password_invalid_password": "Fallo ao establecer contrasinal de recuperación: o contrasinal non é suficientemente forte", + "dyndns_set_recovery_password_failed": "Fallo ao establecer o contrasinal de recuperación: {error}", + "dyndns_set_recovery_password_success": "Estableceuse o contrasinal de recuperación!", + "log_dyndns_unsubscribe": "Retirar subscrición para o subdominio YunoHost '{}'", + "ask_dyndns_recovery_password_explain_unavailable": "Este dominio DynDNS xa está rexistrado. Se es a persoa que o rexistrou orixinalmente, podes escribir o código de recuperación para reclamar o dominio.", + "dyndns_too_many_requests": "O servicio dyndns de YunoHost recibeu demasiadas peticións do teu sistema, agarda 1 hora e volve intentalo.", + "global_settings_setting_ssh_port_help": "É recomendable un porto inferior a 1024 para evitar os intentos de apropiación por parte de servizos de non-administración na máquina remota. Tamén deberías evitar elexir un porto que xa está sendo utilizado, como 80 ou 443.", + "diagnosis_ignore_criteria_error": "Os criterios deben ter o formato key=value (ex. domain=yolo.test)", + "diagnosis_ignore_already_filtered": "(Xa existe un filtro de diagnóstico de {category} con estes criterios)", + "diagnosis_ignore_filter_removed": "Eliminouse o filtro do diagnóstico para {category}", + "diagnosis_ignore_no_filter_found": "(Non hai tal filtro do diagnóstico de {category} con este criterio a eliminar)", + "diagnosis_ignore_no_issue_found": "Non se atoparon incidencias para o criterio establecido.", + "diagnosis_ignore_filter_added": "Engadiuse o filtro do diagnóstico para {category}", + "diagnosis_ignore_missing_criteria": "Deberías proporcionar cando menos un criterio que a categoría de diagnóstico omitirá", + "migration_0027_start": "A iniciar a migración a Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Algo fallou durante a actualización principal, o sistema parece que aínda está en Debian Bullseye.", + "migration_0027_patching_sources_list": "A configurar o ficheiro sources.list…", + "migration_0027_general_warning": "Ten en conta que a migración é unha operación comprometida. O equipo de YunoHost intentou deseñala o mellor posible e probala, aínda así a migración podería estragar partes do sistema ou as aplicacións.\n\nAsí, é recomendable:\n - Facer unha copia de apoio dos datos críticos ou aplicacións. Máis info en https://yunohost.org/backup;\n - Ter pacencia unha vez iniciada a migración: en función da túa conexión a Internet e hardware podería levarlle varias horas completar todo o procedemento.", + "migration_0027_yunohost_upgrade": "A iniciar a actualización do núcleo de YunoHost…", + "migration_0027_not_bullseye": "A distribución Debian actual non é Bullseye! Se xa realizaches a migración Bullseye -> Bookworm este erro é síntoma de que o procedemento de migración non foi exitoso ao 100% (doutro xeito YunoHost teríao marcado como completado). É recomendable que investigues o que aconteceu, informando ao equipo de axuda que precisará o rexistro **completo** da migración, pódelo atopar na web de administración en Ferramentas -> Rexistros.", + "migration_0027_problematic_apps_warning": "Ten en conta que se atoparon as seguintes apps que poderían ser problemáticas. Semella que non foron instaladas desde o catálogo de aplicacións de YunoHost, ou non están marcadas como que 'funcionan'. Como consecuencia non podemos garantir que seguirán funcionando ben unha vez conclúa a migración: {problematic_apps}", + "migration_description_0027_migrate_to_bookworm": "Actualiza o sistema a Debian Bookworm e YunoHost 12", + "migration_0027_cleaning_up": "Limpando a caché e os paquetes que xa non son necesarios…", + "migration_0027_delayed_api_restart": "A API de YunoHost vaise reiniciar automaticamente en 15 segundos. Durante uns segundos non poderás usala, e despois terás que iniciar sesión outra vez.", + "migration_0027_main_upgrade": "A iniciar a actualización principal…", + "migration_0027_modified_files": "Detectamos que os seguintes ficheiros semella foron modificados manualmente e poderían ser sobreescritos durante a actualización: {manually_modified_files}", + "migration_0027_not_enough_free_space": "Hai moi pouco espazo en /var/! Deberías ter polo menos 1GB libre para realizar a migración.", + "migration_0027_patch_yunohost_conflicts": "Aplicando a solución para resolver o problema conflictivo…", + "migration_0027_system_not_fully_up_to_date": "O teu sistema non está totalmente actualizado. Fai unha actualización corrente antes de iniciar a migración a Bullseye." +} diff --git a/locales/hi.json b/locales/hi.json index 6f40ad1ae..31eb7831e 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -2,18 +2,18 @@ "action_invalid": "अवैध कार्रवाई '{action}'", "admin_password": "व्यवस्थापक पासवर्ड", "app_already_installed": "'{app}' पहले से ही इंस्टाल्ड है", - "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name}' , तर्क इन विकल्पों में से होने चाहिए {choices}", - "app_argument_invalid": "तर्क के लिए अमान्य मान '{name}': {error}", + "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name}' , तर्क इन विकल्पों में से होने चाहिए {choices}", + "app_argument_invalid": "तर्क के लिए अमान्य मान '{name}': {error}", "app_argument_required": "तर्क '{name}' की आवश्यकता है", "app_extraction_failed": "इन्सटाल्ड फ़ाइलों को निकालने में असमर्थ", "app_id_invalid": "अवैध एप्लिकेशन id", "app_install_files_invalid": "फाइलों की अमान्य स्थापना", "app_not_correctly_installed": "{app} ठीक ढंग से इनस्टॉल नहीं हुई", "app_not_installed": "{app} इनस्टॉल नहीं हुई", - "app_not_properly_removed": "{app} ठीक ढंग से नहीं अनइन्सटॉल की गई", + "app_not_properly_removed": "{app} ठीक ढंग से नहीं अनइन्सटॉल की गई", "app_removed": "{app} को अनइन्सटॉल कर दिया गया", - "app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है ....", - "app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ", + "app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है…", + "app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ?", "app_unknown": "अनजान एप्लीकेशन", "app_unsupported_remote_type": "एप्लीकेशन के लिए उन्सुपपोर्टेड रिमोट टाइप इस्तेमाल किया गया", "app_upgrade_failed": "{app} अपडेट करने में असमर्थ", @@ -33,10 +33,10 @@ "backup_deleted": "इस बैकअप को डिलीट दिया गया है", "backup_hook_unknown": "'{hook}' यह बैकअप हुक नहीं मिला", "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_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_running_hooks": "बैकअप हुक्स चल रहे है...", + "backup_running_hooks": "बैकअप हुक्स चल रहे है…", "custom_app_url_required": "आप को अपनी कस्टम एप्लिकेशन '{app}' को अपग्रेड करने के लिए यूआरएल(URL) देने की आवश्यकता है", "domain_cert_gen_failed": "सर्टिफिकेट उत्पन करने में असमर्थ", "domain_created": "डोमेन बनाया गया", diff --git a/locales/id.json b/locales/id.json index c6b023102..9036b12f7 100644 --- a/locales/id.json +++ b/locales/id.json @@ -16,40 +16,40 @@ "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 dilepas dengan benar", - "app_remove_after_failed_install": "Melepas aplikasi setelah kegagalan pemasangan...", + "app_remove_after_failed_install": "Menyingkirkan 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...", + "apps_catalog_updating": "Memperbarui katalog aplikasi…", "ask_main_domain": "Domain utama", "ask_new_domain": "Domain baru", "ask_user_domain": "Domain yang digunakan untuk alamat surel dan akun XMPP pengguna", "app_not_correctly_installed": "{app} kelihatannya terpasang dengan salah", - "app_start_restore": "Memulihkan {app}...", + "app_start_restore": "Memulihkan {app}…", "app_unknown": "Aplikasi tak dikenal", "ask_new_admin_password": "Kata sandi administrasi baru", "ask_password": "Kata sandi", - "app_upgrade_app_name": "Memperbarui {app}...", + "app_upgrade_app_name": "Sedang meningkatkan {app}…", "app_upgrade_failed": "Tidak dapat memperbarui {app}: {error}", - "app_start_install": "Memasang {app}...", - "app_start_remove": "Melepas {app}...", + "app_start_install": "Memasang {app}…", + "app_start_remove": "Menyingkirkan {app}…", "app_manifest_install_ask_password": "Pilih kata sandi administrasi untuk aplikasi ini", "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_archive_name_exists": "Arsip cadangan dengan nama '{name}' ini sudah ada.", "backup_created": "Cadangan dibuat: {name}", "backup_creation_failed": "Tidak dapat membuat arsip cadangan", "backup_delete_error": "Tidak dapat menghapus '{path}'", "backup_deleted": "Cadangan dihapus: {name}", "diagnosis_apps_issue": "Sebuah masalah ditemukan pada aplikasi {app}", - "backup_applying_method_tar": "Membuat arsip TAR cadangan...", + "backup_applying_method_tar": "Membuat arsip TAR cadangan…", "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...", + "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).", @@ -70,24 +70,24 @@ "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_broken_link": "Tidak dapat mengakses arsip cadangan (tautan rusak pada {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_apps_allgood": "Semua aplikasi yang dipasang mengikuti panduan pemaketan 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_error": "Beberapa domain akan SEGERA kedaluwarsa!", "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_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)", @@ -113,16 +113,16 @@ "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_main_upgrade": "Memulai pembaruan utama…", "migration_0021_start": "Memulai migrasi ke Bullseye", - "migration_0021_yunohost_upgrade": "Memulai pembaruan YunoHost Core...", + "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}...", + "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}]", @@ -145,12 +145,12 @@ "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}...", + "app_requirements_checking": "Memeriksa persyaratan pada {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_certificate_fetching_or_enabling_failed": "Mencoba sertifikat baru pada {domain} tidak dapat digunakan…", "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_inconsistent_versions": "Anda menjalankan versi paket YunoHost yang tidak konsisten… sepertinya karena kegagalan atau sebagian pembaruan.", "diagnosis_basesystem_ynh_single_version": "versi {package}: {version} ({repo})", "diagnosis_description_services": "Status layanan", "diagnosis_description_systemresources": "Sumber daya sistem", @@ -165,7 +165,7 @@ "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`.", + "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", @@ -186,7 +186,7 @@ "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", + "app_manifest_install_ask_path": "Pilih jalur URL (setelah domain) dimana aplikasi ini harus dipasang", "certmanager_cert_signing_failed": "Tidak dapat memverifikasi sertifikat baru", "config_validate_url": "Harus URL web yang valid", "diagnosis_description_ports": "Penyingkapan porta", @@ -211,16 +211,16 @@ "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_start_backup": "Mengumpulkan berkas untuk dicadangkan pada {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...", + "extracting": "Mengekstrak…", "system_username_exists": "Nama pengguna telah ada di daftar pengguna sistem", "upgrade_complete": "Pembaruan selesai", - "upgrading_packages": "Memperbarui paket...", + "upgrading_packages": "Memperbarui paket…", "diagnosis_description_apps": "Aplikasi", - "diagnosis_description_basesystem": "Basis sistem", + "diagnosis_description_basesystem": "Sistem basis", "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", @@ -228,13 +228,13 @@ "user_updated": "Informasi pengguna diubah", "visitors": "Pengunjung", "yunohost_already_installed": "YunoHost sudah terpasang", - "yunohost_installing": "Memasang YunoHost...", + "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}", + "diagnosis_basesystem_hardware_model": "Model server 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", @@ -252,11 +252,11 @@ "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_cant_run_because_of_dep": "Tidak dapat menjalankan diagnosis pada {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...", + "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)", @@ -269,8 +269,8 @@ "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_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", @@ -296,7 +296,7 @@ "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!", + "diagnosis_diskusage_verylow": "Penyimpanan {mountpoint} (di perangkat {device}) hanya memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total}). Sebaiknya Anda mempertimbangkan 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", @@ -307,7 +307,7 @@ "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}...", + "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", @@ -329,7 +329,7 @@ "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}'", + "backup_archive_name_unknown": "Arsip cadangan lokal dengan nama '{name}' tidak diketahui", "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", @@ -348,10 +348,10 @@ "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_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).", + "backup_archive_cant_retrieve_info_json": "Tidak dapat memuat info pada arsip '{archive}'… Berkas info.json tidak dapat dipulihkan (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})", @@ -366,7 +366,7 @@ "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", + "backup_system_part_failed": "Tidak dapat mencadangkan bagian sistem '{part}'", "log_user_create": "Menambahkan pengguna '{}'", "log_user_delete": "Menghapus pengguna '{}'", "log_user_group_create": "Membuat kelompok '{}'", @@ -377,7 +377,7 @@ "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.", + "config_no_panel": "Panel konfigurasi tidak ditemukan.", "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}.", @@ -391,5 +391,65 @@ "log_letsencrypt_cert_renew": "Memperbarui sertifikat Let's Encrypt '{}'", "log_selfsigned_cert_install": "Memasang sertifikat ditandai sendiri pada domain '{}'", "log_user_permission_reset": "Mengatur ulang izin '{}'", - "domain_config_xmpp": "Pesan Langsung (XMPP)" + "domain_config_xmpp": "Pesan Langsung (XMPP)", + "diagnosis_http_connection_error": "Masalah jaringan: tidak dapat terhubung dengan domain yang diminta, sangat mungkin terputus.", + "dyndns_ip_updated": "IP Anda diperbarui di DynDNS", + "ask_dyndns_recovery_password_explain": "Pilih kata sandi pemulihan untuk domain DynDNS Anda.", + "ask_dyndns_recovery_password": "Kata sandi pemulihan DynDNS", + "backup_output_directory_not_empty": "Anda harus memilih direktori yang kosong", + "service_reload_or_restart_failed": "Tidak dapat memuat atau memulai ulang layanan '{service}'\n\nLog layanan baru-baru ini:{logs}", + "service_reload_failed": "Tidak dapat memuat ulang layanan '{service}'\n\nLog layanan baru-baru ini:{logs}", + "service_start_failed": "Tidak dapat memulai layanan '{service}'\n\nLog layanan baru-baru ini: {logs}", + "diagnosis_apps_deprecated_practices": "Versi aplikasi yang dipasang ini masih menggunakan praktik pengemasan yang lama. Anda lebih baik untuk memperbarui aplikasi tersebut.", + "diagnosis_dns_bad_conf": "Beberapa rekaman DNS untuk domain {domain} ada yang tidak ada atau salah (kategori {category})", + "diagnosis_dns_good_conf": "Rekaman DNS untuk domain {domain} sudah diatur dengan benar (kategori {category})", + "dyndns_unavailable": "Domain '{domain}' tidak tersedia.", + "dyndns_set_recovery_password_denied": "Tidak dapat menyetel kata sandi pemulihan: tidak valid", + "dyndns_set_recovery_password_unknown_domain": "Tidak dapat menyetel kata sandi pemulihan: domain belum terdaftar", + "dyndns_set_recovery_password_invalid_password": "Tidak dapat menyetel kata sandi pemulihan: kata sandi tidak cukup kuat", + "dyndns_set_recovery_password_failed": "Tidak dapat menyetel kata sandi pemulihan: {error}", + "dyndns_set_recovery_password_success": "Kata sandi pemulihan berhasil disetel!", + "file_does_not_exist": "Berkas {path} tidak ada.", + "firewall_reload_failed": "Tidak dapat memuat ulang tembok api", + "firewall_reloaded": "Tembok api dimuat ulang", + "migration_description_0023_postgresql_11_to_13": "Migrasi basis data dari PostgreSQL 11 ke 13", + "service_enabled": "Layanan '{service}' akan secara mandiri dimulai saat pemulaian.", + "service_reloaded_or_restarted": "Layanan {service} dimuat atau dimulai ulang", + "service_stopped": "Layanan '{service}' diberhentikan", + "service_unknown": "Layanan yang tidak diketahui: '{service}'", + "updating_apt_cache": "Mengambil pembaruan yang tersedia untuk paket sistem…", + "group_mailalias_remove": "Alias surel '{mail}' akan dihapus dari kelompok '{group}'", + "migration_description_0021_migrate_to_bullseye": "Peningkatan sistem ke Debian Bullseye dan YunoHost 11.x", + "migration_description_0024_rebuild_python_venv": "Memperbaiki aplikasi Python setelah migrasi Bullseye", + "service_disable_failed": "Tidak dapat membuat layanan '{service}' dimulai saat pemulaian.\n\nLog layanan baru-baru ini:{logs}", + "service_disabled": "Layanan '{service}' tidak akan dimulai kembali saat pemulaian.", + "tools_upgrade_failed": "Tidak dapat memperbarui paket: {packages_list}", + "global_settings_setting_nginx_redirect_to_https": "Paksa HTTPS", + "backup_archive_system_part_not_available": "Segmen '{part}' tidak tersedia di cadangan ini", + "backup_output_directory_forbidden": "Pilih direktori yang berbeda. Cadangan tidak dapat dibuat di /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var, atau subfolder dari /home/yunohost.backup/archives", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Masukkan kata sandi pemulihan untuk domain DynDNS ini.", + "backup_output_symlink_dir_broken": "Direktori arsip Anda '{path}' rusak penautannya. Mungkin Anda lupa untuk menambatkan ulang atau memasukkan kembali penyimpanan tujuan penautan direktori arsip tersebut.", + "diagnosis_apps_not_in_app_catalog": "Aplikasi ini tidak ada di katalog aplikasi YunoHost. Jika aplikasi ini ada di sana sebelumnya dan dihapus, Anda disarankan untuk melepas aplikasi ini dikarenakan ini tidak akan menerima pembaruan dan mungkin bisa menghancurkan integritas dan keamanan sistem Anda.", + "dyndns_ip_update_failed": "Tidak dapat memperbarui IP Anda di DynDNS", + "service_restarted": "Layanan {service} dimulai ulang", + "service_started": "Layanan '{service}' dimulai", + "service_stop_failed": "Tidak dapat menghentikan layanan '{service}'\n\nLog layanan baru-baru ini: {logs}", + "apps_catalog_failed_to_download": "Tidak dapat mengunduh katalog aplikasi {apps_catalog}: {error}", + "backup_archive_corrupted": "Sepertinya arsip cadangan '{archive}' rusak: {error}", + "diagnosis_found_errors": "{errors} masalah signifikan ditemukan terkait dengan {category}!", + "restore_system_part_failed": "Tidak dapat memulihkan segmen '{part}'", + "service_enable_failed": "Tidak dapat membuat layanan '{service}' dimulai mandiri saat pemulaian.\n\nLog layanan baru-baru ini:{logs}", + "service_not_reloading_because_conf_broken": "Tidak memuat atau memulai ulang layanan '{name}' karena konfigurasinya rusak: {errors}", + "service_reloaded": "Layanan {service} dimuat ulang", + "additional_urls_already_removed": "URL tambahan '{url}' sudah disingkirkan pada URL tambahan untuk perizinan '{permission}'", + "additional_urls_already_added": "URL tambahan '{url}' sudah ditambahkan pada URL tambahan untuk perizinan '{permission}'", + "app_argument_password_no_default": "Galat ketika mengurai argumen sandi '{name}': argumen sandi tidak diperbolehkan mempunyai suatu nilai baku demi alasan keamanan", + "app_corrupt_source": "YunoHost telah berhasil mengunduh aset tersebut '{source_id}' ({url}) untuk {app}, tetapi aset tidak sesuai dengan checksum. Hal ini bisa jadi karena beberapa jaringan temporer mengalami kegagalan pada peladen Anda, ATAU entah bagaimana aset mengalami perubahan oleh penyelenggara hulu (atau pelakon jahat?) dan pemaket YunoHost perlu untuk menyelidiki dan mungkin pembaruan manifes applikasi tersebut untuk mempertimbangkan perubahan ini.\n\tEkspektasi checksum sha256: {expected_sha256}\n\tUnduhan checksum sha256: {computed_sha256}\n\tUnduhan ukuran berkas: {size}", + "app_not_upgraded_broken_system": "Aplikasi '{failed_app}' telah gagal meningkatkan dan menyebabkan sistem dalam status rusak, dan sebagai konsekuensi terhadap peningkatan aplikasi tersebut telah dibatalkan: {apps}", + "app_resource_failed": "Menyediakan, membatalkan penyediaan, atau memperbarui sumber daya pada {app} telah gagal: {error}", + "app_failed_to_download_asset": "Gagal mengunduh aset '{source_id}' ({url}) untuk {app}: {out}", + "apps_catalog_init_success": "Inisialisasi sistem katalog aplikasi!", + "app_unsupported_remote_type": "Tidak mendukung type remot yang digunakan pada aplikasi", + "app_not_upgraded_broken_system_continue": "Aplikasi '{failed_app}' telah gagal meningkatkan dan menyebabkan sistem dalam status rusak (jadi --continue-on-failure diabaikan), dan sebagai konsekuensi terhadap peningkatan aplikasi tersebut telah dibatalkan: {apps}", + "apps_failed_to_upgrade_line": "\n * {app_id}(untuk melihat log yang berkaitan lakukan 'yunohost log show {operation_logger_name}')" } diff --git a/locales/it.json b/locales/it.json index 21fb52367..24714500a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -4,8 +4,8 @@ "app_not_installed": "Impossibile trovare l'applicazione {app} nell'elenco delle applicazioni installate: {all_apps}", "app_unknown": "Applicazione sconosciuta", "ask_password": "Password", - "backup_archive_name_exists": "Il nome dell'archivio del backup è già esistente.", - "backup_created": "Backup completo", + "backup_archive_name_exists": "Esiste già un archivio di backup con il nome ‘{name}’.", + "backup_created": "Backup creato: {name}", "backup_output_directory_not_empty": "Dovresti scegliere una cartella di output vuota", "domain_created": "Dominio creato", "domain_exists": "Il dominio esiste già", @@ -20,7 +20,7 @@ "service_stop_failed": "Impossibile fermare il servizio '{service}'\n\nRegistri di servizio recenti:{logs}", "system_username_exists": "Il nome utente esiste già negli utenti del sistema", "unrestore_app": "{app} non verrà ripristinata", - "upgrading_packages": "Aggiornamento dei pacchetti...", + "upgrading_packages": "Aggiornamento dei pacchetti…", "user_deleted": "Utente cancellato", "admin_password": "Password dell'amministrazione", "app_install_files_invalid": "Questi file non possono essere installati", @@ -28,10 +28,10 @@ "app_not_properly_removed": "{app} non è stata correttamente rimossa", "action_invalid": "L'azione '{action}' non è valida", "app_removed": "{app} disinstallata", - "app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l'URL è corretto?", + "app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l’URL è corretto?", "app_upgrade_failed": "Impossibile aggiornare {app}: {error}", "app_upgraded": "{app} aggiornata", - "app_requirements_checking": "Controllo i pacchetti richiesti per {app}...", + "app_requirements_checking": "Controllo dei requisiti per {app}…", "ask_main_domain": "Dominio principale", "ask_new_admin_password": "Nuova password dell'amministrazione", "backup_app_failed": "Non è possibile fare il backup {app}", @@ -47,7 +47,7 @@ "backup_cleaning_failed": "Non è possibile pulire la directory temporanea di backup", "backup_creation_failed": "Impossibile creare l'archivio di backup", "backup_delete_error": "Impossibile cancellare '{path}'", - "backup_deleted": "Backup cancellato", + "backup_deleted": "Backup eliminato: {name}", "backup_hook_unknown": "Hook di backup '{hook}' sconosciuto", "backup_nothings_done": "Niente da salvare", "backup_output_directory_forbidden": "Scegli una diversa directory di output. I backup non possono esser creati nelle sotto-cartelle /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives", @@ -58,7 +58,6 @@ "domain_deleted": "Dominio cancellato", "domain_deletion_failed": "Impossibile cancellare il dominio {domain}: {error}", "domain_dyndns_already_subscribed": "Hai già sottoscritto un dominio DynDNS", - "domain_dyndns_root_unknown": "Dominio radice DynDNS sconosciuto", "domain_hostname_failed": "Impossibile impostare il nuovo hostname. Potrebbe causare problemi in futuro (o anche no).", "domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio:\n{apps}\n\nDisinstallale eseguendo 'yunohost app remove app_id' o spostale in un altro dominio eseguendo 'yunohost app change-url app_id' prima di procedere alla cancellazione del dominio", "done": "Terminato", @@ -66,13 +65,10 @@ "downloading": "Scaricamento…", "dyndns_ip_update_failed": "Impossibile aggiornare l'indirizzo IP in DynDNS", "dyndns_ip_updated": "Il tuo indirizzo IP è stato aggiornato su DynDNS", - "dyndns_key_generating": "Generando la chiave DNS... Potrebbe richiedere del tempo.", "dyndns_key_not_found": "La chiave DNS non è stata trovata per il dominio", "dyndns_no_domain_registered": "Nessuno dominio registrato con DynDNS", - "dyndns_registered": "Dominio DynDNS registrato", - "dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error}", "dyndns_unavailable": "Il dominio {domain} non disponibile.", - "extracting": "Estrazione...", + "extracting": "Estrazione…", "field_invalid": "Campo '{}' non valido", "firewall_reload_failed": "Impossibile ricaricare il firewall", "firewall_reloaded": "Firewall ricaricato", @@ -107,8 +103,8 @@ "user_update_failed": "Impossibile aggiornare l'utente {user}: {error}", "restore_hook_unavailable": "Lo script di ripristino per '{part}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio", "restore_nothings_done": "Nulla è stato ripristinato", - "restore_running_app_script": "Ripristino dell'app '{app}'...", - "restore_running_hooks": "Esecuzione degli hook di ripristino...", + "restore_running_app_script": "Ripristino dell'app '{app}'…", + "restore_running_hooks": "Esecuzione degli hook di ripristino…", "service_added": "Il servizio '{service}' è stato aggiunto", "service_already_started": "Il servizio '{service}' è già avviato", "service_already_stopped": "Il servizio '{service}' è già stato fermato", @@ -124,7 +120,7 @@ "unbackup_app": "{app} non verrà salvata", "unexpected_error": "È successo qualcosa di inatteso: {error}", "unlimit": "Nessuna quota", - "updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema...", + "updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema…", "upgrade_complete": "Aggiornamento completo", "upnp_dev_not_found": "Nessuno supporto UPnP trovato", "upnp_disabled": "UPnP è disattivato", @@ -138,15 +134,15 @@ "user_updated": "Info dell'utente cambiate", "yunohost_already_installed": "YunoHost è già installato", "yunohost_configured": "YunoHost ora è configurato", - "yunohost_installing": "Installazione di YunoHost...", + "yunohost_installing": "Installazione di YunoHost…", "yunohost_not_installed": "YunoHost non è correttamente installato. Esegui 'yunohost tools postinstall'", "domain_cert_gen_failed": "Impossibile generare il certificato", "certmanager_attempt_to_replace_valid_cert": "Stai provando a sovrascrivere un certificato buono e valido per il dominio {domain}! (Usa --force per ignorare)", "certmanager_domain_cert_not_selfsigned": "Il certificato per il dominio {domain} non è auto-firmato. Sei sicuro di volere sostituirlo? (Usa '--force')", - "certmanager_certificate_fetching_or_enabling_failed": "Il tentativo di usare il nuovo certificato per {domain} non funziona...", + "certmanager_certificate_fetching_or_enabling_failed": "Il tentativo di usare il nuovo certificato per {domain} non funziona…", "certmanager_attempt_to_renew_nonLE_cert": "Il certificato per il dominio {domain} non è emesso da Let's Encrypt. Impossibile rinnovarlo automaticamente!", "certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain} non è in scadenza! (Puoi usare --force per forzare se sai quel che stai facendo)", - "certmanager_domain_http_not_working": "Il dominio {domain} non sembra accessibile attraverso HTTP. Verifica nella sezione 'Web' nella diagnosi per maggiori informazioni. (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)", + "certmanager_domain_http_not_working": "Il dominio {domain} non sembra accessibile tramite HTTP. Controlla la sezione ‘Web’ della diagnosi per maggiori informazioni. (Se sai cosa stai facendo, usa ‘--no-checks’ per disattivare i controlli.)", "app_already_installed_cant_change_url": "Questa applicazione è già installata. L'URL non può essere cambiato solo da questa funzione. Controlla se `app changeurl` è disponibile.", "app_already_up_to_date": "{app} è già aggiornata", "app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain}{path}'), nessuna operazione necessaria.", @@ -154,12 +150,12 @@ "app_change_url_success": "L'URL dell'applicazione {app} è stato cambiato in {domain}{path}", "app_make_default_location_already_used": "Impostazione dell'applicazione '{app}' come predefinita del dominio non riuscita perché il dominio '{domain}' è in uso per dall'applicazione '{other_app}'", "app_location_unavailable": "Questo URL non è più disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps}", - "app_upgrade_app_name": "Aggiornamento di {app}...", + "app_upgrade_app_name": "Aggiornamento di {app}…", "app_upgrade_some_app_failed": "Alcune applicazioni non possono essere aggiornate", "backup_abstract_method": "Questo metodo di backup deve essere ancora implementato", - "backup_applying_method_copy": "Copiando tutti i files nel backup...", - "backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method}'...", - "backup_applying_method_tar": "Creando l'archivio TAR del backup...", + "backup_applying_method_copy": "Copiando tutti i files nel backup…", + "backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method}'…", + "backup_applying_method_tar": "Creando l'archivio TAR del backup…", "backup_archive_system_part_not_available": "La parte di sistema '{part}' non è disponibile in questo backup", "backup_archive_writing_error": "Impossibile aggiungere i file '{source}' (indicati nell'archivio '{dest}') al backup nell'archivio compresso '{archive}'", "backup_ask_for_copying_if_needed": "Vuoi effettuare il backup usando {size}MB temporaneamente? (È necessario usare questo sistema poiché alcuni file non possono essere preparati in un modo più efficiente)", @@ -178,19 +174,19 @@ "backup_unable_to_organize_files": "Impossibile organizzare i file nell'archivio con il metodo veloce", "backup_with_no_backup_script_for_app": "L'app {app} non ha script di backup. Ignorata.", "backup_with_no_restore_script_for_app": "L'app {app} non ha script di ripristino, non sarai in grado di ripristinarla automaticamente dal backup di questa app.", - "certmanager_acme_not_configured_for_domain": "La challenge ACME non può validare il {domain} perché la relativa configurazione di nginx è mancante... Assicurati che la tua configurazione di nginx sia aggiornata con il comando `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "La prova ACME non può validare {domain} ora, perché manca la relativa configurazione di nginx… Assicurati che la tua configurazione di nginx sia aggiornata con il comando `yunohost tools regen-conf nginx --dry-run --with-diff`.", "certmanager_cannot_read_cert": "Qualcosa è andato storto nel tentativo di aprire il certificato attuale per il dominio {domain} (file: {file}), motivo: {reason}", "certmanager_cert_install_success": "Certificato Let's Encrypt per il dominio {domain} installato", "aborting": "Annullamento.", - "app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}", - "app_start_install": "Installando '{app}'...", - "app_start_remove": "Rimozione di {app}...", - "app_start_backup": "Raccogliendo file da salvare nel backup per '{app}'...", - "app_start_restore": "Ripristino di '{app}'...", + "app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}", + "app_start_install": "Installando '{app}'…", + "app_start_remove": "Rimozione di {app}…", + "app_start_backup": "Raccogliendo file da salvare nel backup per '{app}'…", + "app_start_restore": "Ripristino di '{app}'…", "app_upgrade_several_apps": "Le seguenti applicazioni saranno aggiornate : {apps}", "ask_new_domain": "Nuovo dominio", "ask_new_path": "Nuovo percorso", - "backup_actually_backuping": "Creazione di un archivio di backup con i file raccolti...", + "backup_actually_backuping": "Creazione di un archivio di backup con i file raccolti…", "backup_mount_archive_for_restore": "Preparazione dell'archivio per il ripristino…", "certmanager_cert_install_success_selfsigned": "Certificato autofirmato installato con successo per il dominio {domain}", "certmanager_cert_renew_success": "Certificato di Let's Encrypt rinnovato con successo per il dominio {domain}", @@ -203,15 +199,15 @@ "password_too_simple_4": "La password deve essere lunga almeno 12 caratteri e contenere numeri, maiuscole e minuscole", "app_action_cannot_be_ran_because_required_services_down": "I seguenti servizi dovrebbero essere in funzione per completare questa azione: {services}. Prova a riavviarli per proseguire (e possibilmente cercare di capire come ma non funzionano più).", "backup_output_symlink_dir_broken": "La tua cartella d'archivio '{path}' è un link simbolico interrotto. Probabilmente hai dimenticato di montare o montare nuovamente il supporto al quale punta il link.", - "certmanager_domain_dns_ip_differs_from_public_ip": "I record DNS per il dominio '{domain}' è diverso dall'IP di questo server. Controlla la sezione (basic) 'Record DNS' nella diagnosi per maggiori informazioni. Se hai modificato recentemente il tuo valore A, attendi che si propaghi (esistono online alcuni siti per il controllo della propagazione DNS). (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "I record DNS per il dominio ‘{domain}’ sono diversi dall’indirizzo IP di questo server. Controlla la sezione ‘Record DNS’ (base) nella diagnosi per maggiori informazioni. Se hai modificato il tuo record A recentemente, attendi che si propaghi (esistono alcuni siti web per il controllo della propagazione DNS). (Se sai cosa stai facendo, usa ‘--no-checks’ per disattivare i controlli.)", "certmanager_hit_rate_limit": "Troppi certificati già rilasciati per questa esatta serie di domini {domain} recentemente. Per favore riprova più tardi. Guarda https://letsencrypt.org/docs/rate-limits/ per maggiori dettagli", "certmanager_no_cert_file": "Impossibile leggere il file di certificato per il dominio {domain} (file: {file})", "certmanager_self_ca_conf_file_not_found": "File di configurazione non trovato per l'autorità di auto-firma (file: {file})", "certmanager_unable_to_parse_self_CA_name": "Impossibile analizzare il nome dell'autorità di auto-firma (file: {file})", "confirm_app_install_warning": "Attenzione: Questa applicazione potrebbe funzionare, ma non è ben integrata in YunoHost. Alcune funzionalità come il single sign-on e il backup/ripristino potrebbero non essere disponibili. Installare comunque? [{answers}] ", - "confirm_app_install_danger": "ATTENZIONE! Questa applicazione è ancora sperimentale (se non esplicitamente dichiarata non funzionante)! Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio,digita '{answers}'", - "confirm_app_install_thirdparty": "PERICOLO! Quest'applicazione non fa parte del catalogo YunoHost. Installando app di terze parti potresti compromettere l'integrita e la sicurezza del tuo sistema. Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio, digita '{answers}'", - "dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/APT (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.", + "confirm_app_install_danger": "ATTENZIONE! Questa applicazione è ancora sperimentale (se non esplicitamente dichiarata non funzionante)! Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema… Se comunque accetti di prenderti questo rischio,digita '{answers}'", + "confirm_app_install_thirdparty": "PERICOLO! Quest'applicazione non fa parte del catalogo YunoHost. Installando app di terze parti potresti compromettere l'integrita e la sicurezza del tuo sistema. Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema… Se comunque accetti di prenderti questo rischio, digita '{answers}'", + "dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/APT (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato… Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.", "domain_cannot_remove_main": "Non puoi rimuovere '{domain}' essendo il dominio principale, prima devi impostare un nuovo dominio principale con il comando 'yunohost domain main-domain -n '; ecco la lista dei domini candidati: {other_domains}", "domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.", "dyndns_could_not_check_available": "Impossibile controllare se {domain} è disponibile su {provider}.", @@ -252,9 +248,9 @@ "log_tools_shutdown": "Spegni il tuo server", "log_tools_reboot": "Riavvia il tuo server", "mail_unavailable": "Questo indirizzo email è riservato e dovrebbe essere automaticamente assegnato al primo utente", - "this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/APT (i gestori di pacchetti del sistema)... Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.", + "this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/APT (i gestori di pacchetti del sistema)… Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.", "app_action_broke_system": "Questa azione sembra avere rotto questi servizi importanti: {services}", - "app_remove_after_failed_install": "Rimozione dell'applicazione a causa del fallimento dell'installazione...", + "app_remove_after_failed_install": "Rimozione dell’applicazione dopo del fallimento della sua installazione…", "app_install_script_failed": "Si è verificato un errore nello script di installazione dell'applicazione", "app_install_failed": "Impossibile installare {app}:{error}", "app_full_domain_unavailable": "Spiacente, questa app deve essere installata su un proprio dominio, ma altre applicazioni sono già installate sul dominio '{domain}'. Potresti usare invece un sotto-dominio dedicato per questa app.", @@ -266,9 +262,9 @@ "apps_catalog_obsolete_cache": "La cache del catalogo della applicazioni è vuoto o obsoleto.", "apps_catalog_update_success": "Il catalogo delle applicazioni è stato aggiornato!", "backup_archive_corrupted": "Sembra che l'archivio di backup '{archive}' sia corrotto: {error}", - "backup_archive_cant_retrieve_info_json": "Impossibile caricare informazione per l'archivio '{archive}'... Impossibile scaricare info.json (oppure non è un json valido).", + "backup_archive_cant_retrieve_info_json": "Impossibile caricare informazioni per l’archivio ‘{archive}’… Il file info.json non può essere recuperato (oppure non è in formato JSON valido).", "app_packaging_format_not_supported": "Quest'applicazione non può essere installata perché il formato non è supportato dalla vostra versione di YunoHost. Dovreste considerare di aggiornare il vostro sistema.", - "certmanager_domain_not_diagnosed_yet": "Non c'è ancora alcun risultato di diagnosi per il dominio {domain}. Riavvia una diagnosi per la categoria 'DNS records' e 'Web' nella sezione di diagnosi per verificare se il dominio è pronto per Let's Encrypt. (Se sai cosa stai facendo, usa '--no-checks' per disattivare i controlli.)", + "certmanager_domain_not_diagnosed_yet": "Non c’è ancora alcun risultato di diagnosi per il dominio {domain}. Riavvia una diagnosi per la categoria ‘DNS records’ e ‘Web’ nella sezione di diagnosi per verificare se il dominio è pronto per Let’s Encrypt. (Se sai cosa stai facendo, usa ‘--no-checks’ per disattivare i controlli.)", "backup_permission": "Backup dei permessi per {app}", "ask_user_domain": "Dominio da usare per l'indirizzo email e l'account XMPP dell'utente", "app_manifest_install_ask_is_public": "Quest'applicazione dovrà essere visibile ai visitatori anonimi?", @@ -276,9 +272,9 @@ "app_manifest_install_ask_password": "Scegli una password di amministrazione per quest'applicazione", "app_manifest_install_ask_path": "Scegli il percorso URL (dopo il dominio) dove installare quest'applicazione", "app_manifest_install_ask_domain": "Scegli il dominio dove installare quest'app", - "app_argument_password_no_default": "Errore durante il parsing dell'argomento '{name}': l'argomento password non può avere un valore di default per ragioni di sicurezza", - "additional_urls_already_added": "L'URL aggiuntivo '{url}' è già utilizzato come URL aggiuntivo per il permesso '{permission}'", - "diagnosis_basesystem_ynh_inconsistent_versions": "Stai eseguendo versioni incompatibili dei pacchetti YunoHost... probabilmente a causa di aggiornamenti falliti o parziali.", + "app_argument_password_no_default": "Errore registrato processando l’argomento ‘{name}’: l’argomento password non può avere un valore di default per ragioni di sicurezza", + "additional_urls_already_added": "L’URL aggiuntivo ‘{url}’ è già utilizzato come URL aggiuntivo per il permesso ‘{permission}’", + "diagnosis_basesystem_ynh_inconsistent_versions": "Stai eseguendo versioni incompatibili dei pacchetti YunoHost… probabilmente a causa di aggiornamenti falliti o parziali.", "diagnosis_basesystem_ynh_main_version": "Il server sta eseguendo YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_single_version": "Versione {package}: {version} ({repo})", "diagnosis_basesystem_kernel": "Il server sta eseguendo Linux kernel {kernel_version}", @@ -286,7 +282,7 @@ "diagnosis_basesystem_hardware": "L'architettura hardware del server è {virt} {arch}", "certmanager_warning_subdomain_dns_record": "Il sottodominio '{subdomain}' non si risolve nello stesso indirizzo IP di '{domain}'. Alcune funzioni non saranno disponibili finchè questa cosa non verrà sistemata e rigenerato il certificato.", "app_label_deprecated": "Questo comando è deprecato! Utilizza il nuovo comando 'yunohost user permission update' per gestire la label dell'app.", - "additional_urls_already_removed": "L'URL aggiuntivo '{url}' è già stato rimosso come URL aggiuntivo per il permesso '{permission}'", + "additional_urls_already_removed": "L’URL aggiuntivo ‘{url}’ è già stato rimosso come URL aggiuntivo per il permesso ‘{permission}’", "diagnosis_services_bad_status_tip": "Puoi provare a riavviare il servizio, e se non funziona, controlla ai log del servizio in amministrazione (dalla linea di comando, puoi farlo con yunohost service restart {service} e yunohost service log {service}).", "diagnosis_services_bad_status": "Il servizio {service} è {status} :(", "diagnosis_services_conf_broken": "Il servizio {service} è mal-configurato!", @@ -300,19 +296,19 @@ "diagnosis_domain_expiration_not_found": "Non riesco a controllare la data di scadenza di alcuni domini", "diagnosis_dns_try_dyndns_update_force": "La configurazione DNS di questo dominio dovrebbe essere gestita automaticamente da YunoHost. Se non avviene, puoi provare a forzare un aggiornamento usando il comando yunohost dyndns update --force.", "diagnosis_dns_point_to_doc": "Controlla la documentazione a https://yunohost.org/dns_config se hai bisogno di aiuto nel configurare i record DNS.", - "diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", - "diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {value}", + "diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {value}", + "diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {value}", "diagnosis_dns_bad_conf": "Alcuni record DNS sono mancanti o incorretti per il dominio {domain} (categoria {category})", "diagnosis_dns_good_conf": "I recordDNS sono configurati correttamente per il dominio {domain} (categoria {category})", "diagnosis_ip_weird_resolvconf_details": "Il file /etc/resolv.conf dovrebbe essere un symlink a /etc/resolvconf/run/resolv.conf che punta a 127.0.0.1 (dnsmasq). Se vuoi configurare manualmente i DNS, modifica /etc/resolv.dnsmasq.conf.", "diagnosis_ip_weird_resolvconf": "La risoluzione dei nomi di rete sembra funzionare, ma mi pare che tu stia usando un /etc/resolv.conf personalizzato.", "diagnosis_ip_broken_resolvconf": "La risoluzione dei nomi di rete sembra non funzionare sul tuo server, e sembra collegato a /etc/resolv.conf che non punta a 127.0.0.1.", - "diagnosis_ip_broken_dnsresolution": "La risoluzione dei nomi di rete sembra non funzionare per qualche ragione... È presente un firewall che blocca le richieste DNS?", + "diagnosis_ip_broken_dnsresolution": "La risoluzione dei nomi di rete sembra non funzionare per qualche ragione… È presente un firewall che blocca le richieste DNS?", "diagnosis_ip_dnsresolution_working": "Risoluzione dei nomi di rete funzionante!", "diagnosis_ip_not_connected_at_all": "Sei sicuro che il server sia collegato ad Internet!?", "diagnosis_ip_local": "IP locale: {local}", "diagnosis_ip_global": "IP globale: {global}", - "diagnosis_ip_no_ipv6_tip": "Avere IPv6 funzionante non è obbligatorio per far funzionare il server, ma è un bene per Internet stesso. IPv6 dovrebbe essere configurato automaticamente dal sistema o dal tuo provider se è disponibile. Altrimenti, potresti aver bisogno di configurare alcune cose manualmente come è spiegato nella documentazione: https://yunohost.org/#/ipv6. Se non puoi abilitare IPv6 o se ti sembra troppo complicato per te, puoi tranquillamente ignorare questo avvertimento.", + "diagnosis_ip_no_ipv6_tip": "Avere IPv6 funzionante non è obbligatorio per far funzionare il server, ma è un bene per Internet stesso. IPv6 dovrebbe essere configurato automaticamente dal sistema o dal tuo provider se è disponibile. Altrimenti, potresti aver bisogno di configurare alcune cose manualmente come è spiegato nella documentazione: https://yunohost.org/ipv6. Se non puoi abilitare IPv6 o se ti sembra troppo complicato per te, puoi tranquillamente ignorare questo avvertimento.", "diagnosis_ip_no_ipv6": "Il server non ha IPv6 funzionante.", "diagnosis_ip_connected_ipv6": "Il server è connesso ad Internet tramite IPv6!", "diagnosis_ip_no_ipv4": "Il server non ha IPv4 funzionante.", @@ -334,7 +330,7 @@ "diagnosis_mail_ehlo_unreachable_details": "Impossibile aprire una connessione sulla porta 25 sul tuo server su IPv{ipversion}. Sembra irraggiungibile.
1. La causa più probabile di questo problema è la porta 25 non correttamente inoltrata al tuo server.
2. Dovresti esser sicuro che il servizio postfix sia attivo.
3. Su setup complessi: assicuratu che nessun firewall o reverse-proxy stia interferendo.", "diagnosis_mail_ehlo_unreachable": "Il server SMTP non è raggiungibile dall'esterno su IPv{ipversion}. Non potrà ricevere email.", "diagnosis_mail_ehlo_ok": "Il server SMTP è raggiungibile dall'esterno e quindi può ricevere email!", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alcuni provider non ti permettono di aprire la porta 25 in uscita perché non gli importa della Net Neutrality.
- Alcuni mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare per un provider pro Net Neutrality", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alcuni provider non ti permettono di aprire la porta 25 in uscita perché non gli importa della Net Neutrality.
- Alcuni mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/vpn_advantage
- Puoi anche prendere in considerazione di cambiare per un provider pro Net Neutrality", "diagnosis_mail_outgoing_port_25_blocked_details": "Come prima cosa dovresti sbloccare la porta 25 in uscita dall'interfaccia del tuo router internet o del tuo hosting provider. (Alcuni hosting provider potrebbero richiedere l'invio di un ticket di supporto per la richiesta).", "diagnosis_mail_outgoing_port_25_blocked": "Il server SMTP non può inviare email ad altri server perché la porta 25 è bloccata in uscita su IPv{ipversion}.", "diagnosis_mail_outgoing_port_25_ok": "Il server SMTP è abile all'invio delle email (porta 25 in uscita non bloccata).", @@ -355,13 +351,13 @@ "diagnosis_mail_ehlo_could_not_diagnose": "Non è possibile verificare se il server mail postfix è raggiungibile dall'esterno su IPv{ipversion}.", "diagnosis_mail_ehlo_wrong": "Un server mail SMTP diverso sta rispondendo su IPv{ipversion}. Probabilmente il tuo server non può ricevere email.", "diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server.", - "diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona...). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:
- Alcuni ISP mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona…). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:
- Alcuni ISP mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider", "diagnosis_mail_ehlo_wrong_details": "L'EHLO ricevuto dalla diagnostica remota su IPv{ipversion} è differente dal dominio del tuo server.
EHLO ricevuto: {wrong_ehlo}
EHLO atteso: {right_ehlo}
La causa più comune di questo problema è la porta 25 non correttamente inoltrata al tuo server. Oppure assicurati che nessun firewall o reverse-proxy stia interferendo.", "diagnosis_mail_blacklist_ok": "Gli IP e i domini utilizzati da questo server non sembrano essere nelle blacklist", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS invero corrente: {rdns_domain}
Valore atteso: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Il DNS inverso non è correttamente configurato su IPv{ipversion}. Alcune email potrebbero non essere spedite o segnalate come SPAM.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene...). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo yunohost settings set smtp.allow_ipv6 -v off. NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti.", - "yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- aggiungere il primo utente nella sezione 'Utenti' del webadmin (o eseguendo da terminale 'yunohost user create ');\n\t- eseguire una diagnosi alla ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know YunoHost' nella documentazione admin: https://yunohost.org/admindoc.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene…). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo yunohost settings set smtp.allow_ipv6 -v off. NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti.", + "yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- eseguire una diagnosi per la ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know YunoHost' nella documentazione admin: https://yunohost.org/admindoc.", "user_already_exists": "L'utente '{user}' esiste già", "update_apt_cache_warning": "Qualcosa è andato storto mentre eseguivo l'aggiornamento della cache APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", "update_apt_cache_failed": "Impossibile aggiornare la cache di APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", @@ -396,14 +392,14 @@ "restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea", "restore_not_enough_disk_space": "Spazio libero insufficiente (spazio: {free_space}B, necessario: {needed_space}B, margine di sicurezza: {margin}B)", "restore_may_be_not_enough_disk_space": "Il tuo sistema non sembra avere abbastanza spazio (libero: {free_space}B, necessario: {needed_space}B, margine di sicurezza: {margin}B)", - "restore_extracting": "Sto estraendo i file necessari dall'archivio...", + "restore_extracting": "Sto estraendo i file necessari dall'archivio…", "restore_already_installed_apps": "Le seguenti app non possono essere ripristinate perché sono già installate: {apps}", "regex_with_only_domain": "Non puoi usare una regex per il dominio, solo per i percorsi", "regex_incompatible_with_tile": "/!\\ Packagers! Il permesso '{permission}' ha show_tile impostato su 'true' e perciò non è possibile definire un URL regex per l'URL principale", "regenconf_need_to_explicitly_specify_ssh": "La configurazione ssh è stata modificata manualmente, ma devi specificare la categoria 'ssh' con --force per applicare le modifiche.", - "regenconf_pending_applying": "Applico le configurazioni in attesa per la categoria '{category}'...", + "regenconf_pending_applying": "Applico le configurazioni in attesa per la categoria '{category}'…", "regenconf_failed": "Impossibile rigenerare la configurazione per le categorie: {categories}", - "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'...", + "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'…", "regenconf_would_be_updated": "La configurazione sarebbe stata aggiornata per la categoria '{category}'", "regenconf_updated": "Configurazione aggiornata per '{category}'", "regenconf_up_to_date": "Il file di configurazione è già aggiornato per la categoria '{category}'", @@ -437,8 +433,8 @@ "invalid_number": "Dev'essere un numero", "migrations_to_be_ran_manually": "Migrazione {id} dev'essere eseguita manualmente. Vai in Strumenti → Migrazioni nella pagina webadmin, o esegui `yunohost tools migrations run`.", "migrations_success_forward": "Migrazione {id} completata", - "migrations_skip_migration": "Salto migrazione {id}...", - "migrations_running_forward": "Eseguo migrazione {id}...", + "migrations_skip_migration": "Salto migrazione {id}…", + "migrations_running_forward": "Eseguo migrazione {id}…", "migrations_pending_cant_rerun": "Queste migrazioni sono ancora in attesa, quindi non possono essere eseguite nuovamente: {ids}", "migrations_not_pending_cant_skip": "Queste migrazioni non sono in attesa, quindi non possono essere saltate: {ids}", "migrations_no_such_migration": "Non esiste una migrazione chiamata '{id}'", @@ -446,7 +442,7 @@ "migrations_need_to_accept_disclaimer": "Per eseguire la migrazione {id}, devi accettare il disclaimer seguente:\n---\n{disclaimer}\n---\nSe accetti di eseguire la migrazione, per favore reinserisci il comando con l'opzione '--accept-disclaimer'.", "migrations_must_provide_explicit_targets": "Devi specificare i target quando utilizzi '--skip' o '--force-rerun'", "migrations_migration_has_failed": "Migrazione {id} non completata, annullamento. Errore: {exception}", - "migrations_loading_migration": "Caricamento migrazione {id}...", + "migrations_loading_migration": "Caricamento migrazione {id}…", "migrations_list_conflict_pending_done": "Non puoi usare sia '--previous' e '--done' allo stesso tempo.", "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' sono opzioni che si escludono a vicenda.", "migrations_failed_to_load_migration": "Impossibile caricare la migrazione {id}: {error}", @@ -479,7 +475,7 @@ "group_cannot_edit_all_users": "Il gruppo 'all_users' non può essere modificato manualmente. È un gruppo speciale che contiene tutti gli utenti registrati in YunoHost", "group_creation_failed": "Impossibile creare il gruppo '{group}': {error}", "group_created": "Gruppo '{group}' creato", - "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_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_smtp_relay_password": "Password del relay SMTP", @@ -487,7 +483,7 @@ "global_settings_setting_smtp_relay_port": "Porta del relay SMTP", "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}'.'", + "domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add ', impostarlo come dominio principale con 'yunohost domain main-domain n ', e solo allora potrai rimuovere il dominio '{domain}' eseguendo 'yunohost domain remove {domain}'.", "domain_cannot_add_xmpp_upload": "Non puoi aggiungere domini che iniziano per 'xmpp-upload.'. Questo tipo di nome è riservato per la funzionalità di upload XMPP integrata in YunoHost.", "diagnosis_processes_killed_by_oom_reaper": "Alcuni processi sono stati terminati dal sistema che era a corto di memoria. Questo è un sintomo di insufficienza di memoria nel sistema o di un processo che richiede troppa memoria. Lista dei processi terminati:\n{kills_summary}", "diagnosis_never_ran_yet": "Sembra che questo server sia stato impostato recentemente e non è presente nessun report di diagnostica. Dovresti partire eseguendo una diagnostica completa, da webadmin o da terminale con il comando 'yunohost diagnosis run'.", @@ -522,7 +518,7 @@ "diagnosis_description_basesystem": "Sistema base", "diagnosis_security_vulnerable_to_meltdown_details": "Per sistemare, dovresti aggiornare il tuo sistema e fare il reboot per caricare il nuovo kernel linux (o contatta il tuo server provider se non funziona). Visita https://meltdownattack.com/ per maggiori info.", "diagnosis_security_vulnerable_to_meltdown": "Sembra che tu sia vulnerabile alla vulnerabilità di sicurezza critica \"Meltdown\"", - "diagnosis_regenconf_manually_modified_details": "Questo è probabilmente OK se sai cosa stai facendo! YunoHost smetterà di aggiornare automaticamente questo file... Ma sappi che gli aggiornamenti di YunoHost potrebbero contenere importanti cambiamenti. Se vuoi, puoi controllare le differente con yunohost tools regen-conf {category} --dry-run --with-diff e forzare il reset della configurazione raccomandata con yunohost tools regen-conf {category} --force", + "diagnosis_regenconf_manually_modified_details": "Questo è probabilmente OK se sai cosa stai facendo! YunoHost smetterà di aggiornare automaticamente questo file… Ma sappi che gli aggiornamenti di YunoHost potrebbero contenere importanti cambiamenti. Se vuoi, puoi controllare le differente con yunohost tools regen-conf {category} --dry-run --with-diff e forzare il reset della configurazione raccomandata con yunohost tools regen-conf {category} --force", "diagnosis_regenconf_manually_modified": "Il file di configurazione {file} sembra esser stato modificato manualmente.", "diagnosis_regenconf_allgood": "Tutti i file di configurazione sono allineati con le configurazioni raccomandate!", "diagnosis_mail_queue_too_big": "Troppe email in attesa nella coda ({nb_pending} emails)", @@ -537,11 +533,11 @@ "postinstall_low_rootfsspace": "La radice del filesystem ha uno spazio totale inferiore ai 10 GB, ed è piuttosto preoccupante! Consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem. Se vuoi installare YunoHost ignorando questo avviso, esegui nuovamente il postinstall con l'argomento --force-diskspace", "domain_remove_confirm_apps_removal": "Rimuovere questo dominio rimuoverà anche le seguenti applicazioni:\n{apps}\n\nSei sicuro di voler continuare? [{answers}]", "diagnosis_rootfstotalspace_critical": "La radice del filesystem ha un totale di solo {space}, ed è piuttosto preoccupante! Probabilmente consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem.", - "diagnosis_rootfstotalspace_warning": "La radice del filesystem ha un totale di solo {space}. Potrebbe non essere un problema, ma stai attento perché potresti consumare tutta la memoria velocemente... Raccomandiamo di avere almeno 16 GB per la radice del filesystem.", + "diagnosis_rootfstotalspace_warning": "La radice del filesystem ha un totale di solo {space}. Potrebbe non essere un problema, ma stai attento perché potresti consumare tutta la memoria velocemente… Raccomandiamo di avere almeno 16 GB per la radice del filesystem.", "restore_backup_too_old": "Questo archivio backup non può essere ripristinato perché è stato generato da una versione troppo vecchia di YunoHost.", "permission_cant_add_to_all_users": "Il permesso {permission} non può essere aggiunto a tutto gli utenti.", "migration_ldap_rollback_success": "Sistema ripristinato allo stato precedente.", - "migration_ldap_migration_failed_trying_to_rollback": "Impossibile migrare... provo a ripristinare il sistema.", + "migration_ldap_migration_failed_trying_to_rollback": "Impossibile migrare… provo a ripristinare il sistema.", "migration_ldap_can_not_backup_before_migration": "Il backup del sistema non è stato completato prima che la migrazione fallisse. Errore: {error}", "migration_ldap_backup_before_migration": "Sto generando il backup del database LDAP e delle impostazioni delle app prima di effettuare la migrazione.", "log_backup_create": "Crea un archivio backup", @@ -561,7 +557,7 @@ "domain_dns_push_not_applicable": "La configurazione automatica del DNS non è applicabile al dominio {domain}. Dovresti configurare i tuoi record DNS manualmente, seguendo la documentazione su https://yunohost.org/dns_config.", "domain_dns_registrar_not_supported": "YunoHost non è riuscito a riconoscere quale registrar sta gestendo questo dominio. Dovresti configurare i tuoi record DNS manualmente, seguendo la documentazione.", "domain_dns_registrar_experimental": "Per ora, il collegamento con le API di **{registrar}** non è stata opportunamente testata e revisionata dalla comunità di YunoHost. Questa funzionalità è **altamente sperimentale**, fai attenzione!", - "domain_dns_push_failed_to_authenticate": "L’autenticazione sulle API del registrar per il dominio '{domain}' è fallita. Probabilmente le credenziali non sono corrette. (Error: {error})", + "domain_dns_push_failed_to_authenticate": "L’autenticazione sulle API del registrar per il dominio '{domain}' è fallita. Probabilmente le credenziali non sono giuste. (Error: {error})", "domain_dns_push_failed_to_list": "Il reperimento dei record attuali usando le API del registrar è fallito: {error}", "domain_dns_push_already_up_to_date": "I record sono aggiornati, nulla da fare.", "domain_dns_pushing": "Sincronizzando i record DNS…", @@ -631,12 +627,45 @@ "global_settings_setting_admin_strength": "Complessità della password di amministratore", "global_settings_setting_user_strength": "Complessità della password utente", "global_settings_setting_postfix_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", - "global_settings_setting_ssh_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_ssh_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza). Segue https://infosec.mozilla.org/guidelines/openssh (inglesse) per averne piú informazione.", "global_settings_setting_ssh_port": "Porta SSH", "global_settings_setting_webadmin_allowlist_help": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.", "global_settings_setting_webadmin_allowlist_enabled_help": "Permetti solo ad alcuni IP di accedere al webadmin.", "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}" + "app_change_url_failed": "Non è possibile cambiare l'URL per {app}:{error}", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Digita la password di recupero per questo dominio DynDNS.", + "ask_fullname": "Nome completo", + "app_not_enough_disk": "Quest’app richiede {required} di spazio libero.", + "app_resource_failed": "Fallimento della fornitura, della rimozione o dell’aggiornamento di risorse per {app}: {error}", + "apps_failed_to_upgrade_line": "\n * {app_id} (per vedere il log corrispondente, esegui ‘yunohost log show {operation_logger_name}’)", + "app_change_url_script_failed": "È stato registrato un errore eseguendo lo script per la modifica dell’URL", + "certmanager_cert_install_failed": "L’installazione del certificato Let’s Encrypt è fallita per {domains}", + "app_change_url_require_full_domain": "{app} non può essere spostato su questo nuovo URL, poiché richiede un dominio intero (ovvero con percorso /)", + "app_not_upgraded_broken_system_continue": "L’aggiornamento dell’app ‘{failed_app}’ è fallito e ha messo il sistema in uno stato di rottura (dunque, --continue-on-failure è stato ignorato) e di conseguenza gli aggiornamenti delle seguenti app sono stati annullati: {apps}", + "admins": "Amministratori", + "all_users": "Tutti gli utenti di YunoHost", + "app_arch_not_supported": "Quest’app può essere installata su architetture {required} ma l’architettura del tuo server è {current}", + "app_manifest_install_ask_init_main_permission": "Chi dovrebbe aver accesso a quest’app? (Questa scelta potrà esser cambiata in seguito)", + "app_action_failed": "L’esecuzione dell’azione {action} per l’app {app} è fallita", + "app_failed_to_download_asset": "Lo scaricamento della risorsa ‘{source_id}’ ({url}) per l’app {app} è fallito: {out}", + "ask_dyndns_recovery_password_explain_unavailable": "Questo dominio DynDNS è già registrato. Se sei la persona che l’ha originariamente registrato, puoi inserire la password di recupero per ripristinare questo dominio.", + "config_action_disabled": "Impossibile eseguire l’azione ‘{action}’ poiché è disattivata, assicurati di rispettare i suoi vincoli. Aiuto: {help}", + "config_action_failed": "L’esecuzione dell’azione ‘{action}’ è fallita: {error}", + "confirm_notifications_read": "ATTENZIONE: Dovresti controllare le notifiche dell’app qui sopra prima di continuare, potrebbero esserci cose importanti da sapere. [{answers}]", + "app_corrupt_source": "YunoHost è riuscito a scaricare la risorsa ‘{source_id}’ ({url}) per {app}, ma la risorsa non corrisponde al checksum previsto. Questo potrebbe significare che potrebbe essere avvenuto un errore di rete nel tuo server, OPPURE che la risorsa è stata cambiata in qualche modo da chi la mantiene o da una terza parte malevola. Le persone che si occupano del pacchetto YunoHost devono investigare e aggiornare il manifesto dell’app per riflettere questo cambiamento.\n Checksum sha256 previsto: {expected_sha256}\n Checksum sha256 scaricato: {computed_sha256}\n Dimensioni del file scaricato: {size}", + "app_failed_to_upgrade_but_continue": "L’aggiornamento dell’app {failed_app} è fallito. Continuando ora con gli aggiornamenti successivi, come richiesto. Esegui ‘yunohost log show {operation_logger_name}’ per visualizzare il log del fallimento", + "app_manifest_install_ask_init_admin_permission": "Chi dovrebbe aver accesso alle funzionalità di amministrazione per quest’app? (Questa scelta potrà esser cambiata in seguito)", + "app_not_enough_ram": "Quest’app richiede {required} di RAM per essere installata/aggiornata, ma solo {current} sono disponibili al momento.", + "app_not_upgraded_broken_system": "L’aggiornamento dell’app ‘{failed_app}’ è fallito e ha messo il sistema in uno stato di rottura, di conseguenza i gli aggiornamenti delle seguenti app sono stati annullati: {apps}", + "app_yunohost_version_not_supported": "Quest’app richiede YunoHost ≥ {required}, ma la versione installata ora è {current}", + "apps_failed_to_upgrade": "È fallito l’aggiornamento di queste applicazioni: {apps}", + "ask_admin_fullname": "Nome completo dell’utente amministratore", + "ask_dyndns_recovery_password": "Password di recupero per DynDNS", + "certmanager_cert_install_failed_selfsigned": "L’installazione di un certificato auto-firmato è fallita per {domains}", + "ask_admin_username": "Username dell’utente amministratore", + "certmanager_cert_renew_failed": "Il rinnovo del certificato Let’s Encrypt è fallito per {domains}", + "ask_dyndns_recovery_password_explain": "Scegli una password di recupero per il tuo dominio DynDNS, in caso dovessi ripristinarlo successivamente.", + "confirm_app_insufficient_ram": "PERICOLO! Quest’app richiede {required} di RAM per essere installata/aggiornata, ma solo {current} sono disponibili ora. Nonostante l’app possa funzionare, la sua installazione o aggiornamento richiedono una grande quantità di RAM, perciò il tuo server potrebbe bloccarsi o fallire miseramente. Se sei dispostə a prenderti questo rischio comunque, digita ‘{answers}’" } \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 000000000..b8a781f04 --- /dev/null +++ b/locales/ja.json @@ -0,0 +1,763 @@ +{ + "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": "アプリ{app}のアクション{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 log show {operation_logger_name}’ を実行して失敗ログを表示します", + "app_full_domain_unavailable": "申し訳ありませんが、このアプリは独自のドメインにインストールする必要がありますが、他のアプリは既にドメイン '{domain}' にインストールされています。代わりに、このアプリ専用のサブドメインを使用できます。", + "app_id_invalid": "不正なアプリID", + "app_install_failed": "インストールできません {app}:{error}", + "app_manifest_install_ask_password": "このアプリの管理パスワードを選択してください", + "app_manifest_install_ask_path": "このアプリをインストールするURLパス(ドメインの後)を選択します", + "app_not_properly_removed": "{app}が正しく削除されていません", + "app_not_upgraded": "アプリ'{failed_app}'のアップグレードに失敗したため、次のアプリのアップグレードがキャンセルされました: {apps}", + "app_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 {app} をアップグレードするには URL を指定する必要があります", + "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_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_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": "アクセス許可 '{}' のアクセスを更新する", + "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": "システムを復元できませんでした", + "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": "{domain}に対するACMEチャレンジは、nginx confに対応するコードスニペットがないため現在実行できません… 'yunohost tools regen-conf nginx --dry-run --with-diff' を使用して、nginx の設定が最新であることを確認してください。", + "certmanager_attempt_to_renew_nonLE_cert": "ドメイン '{domain}' の証明書は、Let's Encryptによって発行されていません。自動的に更新できません!", + "certmanager_attempt_to_renew_valid_cert": "ドメイン '{domain}' の証明書の有効期限が近づいていません。(あなたが何をしているのかわかっている場合は、--forceを使用できます)", + "certmanager_cert_renew_failed": "{domains}のLet’s Encrypt 証明書更新に失敗しました", + "certmanager_cert_renew_success": "{domain}のLet’s Encrypt 証明書が更新されました", + "certmanager_cert_signing_failed": "新しい証明書に署名できませんでした", + "certmanager_certificate_fetching_or_enabling_failed": "{domain}に新しい証明書を使用しようとしましたが、機能しませんでした…", + "certmanager_domain_cert_not_selfsigned": "ドメイン {domain} の証明書は自己署名されていません。置き換えてよろしいですか(これを行うには '--force' を使用してください)?", + "certmanager_hit_rate_limit": "直近でドメイン {domain} に対して発行されている証明書が多すぎます。しばらくしてからもう一度お試しください。詳細については、https://letsencrypt.org/docs/rate-limits/ を参照してください。", + "certmanager_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": "ストレージ{mountpoint}(デバイス{device}上)には、( )残りの領域({free_percent} )しかありません{free}。{total}注意してください。", + "diagnosis_diskusage_ok": "ストレージ{mountpoint}(デバイス{device}上)にはまだ({free_percent}%)スペースが{free}残っています(から{total})!", + "diagnosis_diskusage_verylow": "ストレージ{mountpoint}(デバイス{device}上)には、( )残りの領域({free_percent} )しかありません{free}。{total}あなたは本当にいくつかのスペースをきれいにすることを検討する必要があります!", + "diagnosis_display_tip": "見つかった問題を確認するには、ウェブ管理者の診断セクションに移動するか、コマンドラインから'yunohost診断ショー--問題--人間が読める'を実行します。", + "diagnosis_dns_bad_conf": "一部の DNS レコードが見つからないか、ドメイン {domain} (カテゴリ {category}) が正しくない", + "diagnosis_dns_discrepancy": "次の DNS レコードは、推奨される構成に従っていないようです。
種類: {type}
名前: {name}
現在の値: {current}
期待値: {value}", + "diagnosis_dns_good_conf": "DNS レコードがドメイン {domain} (カテゴリ {category}) 用に正しく構成されている", + "diagnosis_dns_missing_record": "推奨される DNS 構成に従って、次の情報を含む DNS レコードを追加する必要があります。
種類: {type}
名前: {name}
価値: {value}", + "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: {wrong_ehlo}
期待: {right_ehlo}
この問題の最も一般的な原因は、ポート 25 が サーバーに正しく転送されていないことです。または、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "逆引き DNS が IPv{ipversion} 用に正しく構成されていません。一部のメールは配信されないか、スパムとしてフラグが立てられる場合があります。", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "現在の逆引きDNS: {rdns_domain}
期待値: {ehlo_domain}", + "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": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスで {ehlo_domain} 逆引き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チームはこれらのパッケージを処理する戦略を改善しましたが、Debian Stretchを使用してPHP7.3アプリをインストールした一部のセットアップには、いくつかの点で一貫性のない状態であることが予想されます。この状況を修正するには、次のコマンドを実行してみてください: {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "IPv{ipversion} で外部からポートに到達できるかどうかを診断できませんでした。", + "diagnosis_ports_could_not_diagnose_details": "エラー: {error}", + "diagnosis_ports_unreachable": "ポート {port} は外部から到達できません。", + "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のサービスログを確認してください(コマンドラインから、yunohostyunohost service restart {service}yunohost service log {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_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 app remove ’ を実行してアンインストールするか、’yunohost app change-url ’ を実行してアプリケーションを別のドメインに移動してください", + "domain_unknown": "ドメイン '{domain}' は不明です", + "domains_available": "利用可能なドメイン:", + "done": "完了", + "downloading": "ダウンロード中…", + "dpkg_is_broken": "dpkg / APT(システムパッケージマネージャー)が壊れた状態にあるように見えるため、現在はこれを行うことができません… SSH経由で接続し、 'sudo apt install --fix-broken' および/または 'sudo dpkg --configure -a' および/または 'sudo dpkg --audit' を実行することで、この問題を解決できるかもしれません。", + "dpkg_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 に登録されているドメインがありません", + "dyndns_provider_unreachable": "DynDNSプロバイダー {provider} に到達できません: YunoHostがインターネットに正しく接続されていないか、dynetteサーバーがダウンしています。", + "dyndns_unavailable": "ドメイン '{domain}' は使用できません。", + "dyndns_domain_not_provided": "DynDNS プロバイダー{provider} はドメイン{domain}を提供できません。", + "extracting": "抽出中…", + "field_invalid": "無効なフィールド '{}'", + "file_does_not_exist": "ファイル {path}が存在しません。", + "firewall_reloaded": "ファイアウォールがリロードされました", + "firewall_rules_cmd_failed": "一部のファイアウォール ルール コマンドが失敗しました。詳細情報はログに残されています。", + "global_settings_reset_success": "グローバル設定をリセットする", + "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": "Postfixの互換性", + "global_settings_setting_pop3_enabled": "POP3 を有効にする", + "global_settings_setting_pop3_enabled_help": "メール サーバーの POP3 プロトコルを有効にする", + "global_settings_setting_portal_theme": "ポータルのテーマ", + "global_settings_setting_root_access_explain": "Linux システムでは'root'が絶対に管理者です。YunoHost のコンテキストでは、'root'ユーザーでのSSH ログインは(サーバーのローカルネットワークからのSSHである場合を除き)デフォルトで無効になっています。'admins' グループのメンバーは、sudo コマンドを使用することで、root としてコマンドを実行できます。ただし、何らかの理由で通常の管理者がログインできなくなった場合には、システムをデバッグするための(堅牢な)rootパスワードがあると便利です。", + "global_settings_setting_security_experimental_enabled": "実験的なセキュリティ機能", + "global_settings_setting_security_experimental_enabled_help": "実験的なセキュリティ機能を有効にします(何をしているのかわからない場合は有効にしないでください)", + "global_settings_setting_smtp_allow_ipv6_help": "IPv6 を使用したメールの送受信を許可する", + "global_settings_setting_smtp_relay_enabled": "SMTP リレーを有効にする", + "global_settings_setting_smtp_relay_enabled_help": "SMTP リレーを有効にすることで、この yunohost サーバー以外の。サーバーが(代わりに)メールを送信するようになります。この設定は次の状態にある場合に便利です: 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_available_on_yunopaste": "このログは、{url}", + "log_backup_create": "バックアップ作成できませんでした", + "log_backup_restore_app": "バックアップアーカイブから'{}'を復元する", + "log_backup_restore_system": "バックアップアーカイブからシステムを復元する", + "log_corrupted_md_file": "ログに関連付けられている YAML メタデータ ファイルが破損しています: '{md_file}\nエラー: {error}'", + "log_does_exists": "'{log}'という名前の操作ログはありません。'yunohostログリスト'を使用して、利用可能なすべての操作ログを表示します", + "log_domain_add": "'{}'ドメインをシステム構成に追加する", + "log_domain_config_set": "ドメイン '{}' の構成を更新する", + "log_domain_dns_push": "ドメイン '{}' の DNS レコードをプッシュする", + "log_domain_main_domain": "'{}'をメインドメインにする", + "log_domain_remove": "システム構成から'{}'ドメインを削除する", + "log_dyndns_subscribe": "YunoHostサブドメイン'{}'を購読する", + "log_dyndns_update": "YunoHostサブドメイン'{}'に関連付けられているIPを更新します", + "log_help_to_get_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、'yunohostログ共有{name}'コマンドを使用してこの操作の完全なログを共有してください", + "log_help_to_get_log": "操作'{desc}'のログを表示するには、'yunohostログショー{name}'コマンドを使用します。", + "log_letsencrypt_cert_install": "'{}'ドメインにLet's Encrypt証明書をインストールする", + "log_letsencrypt_cert_renew": "Let’s Encrypt証明書を更新する", + "log_link_to_failed_log": "操作 '{desc}' を完了できませんでした。ヘルプを取得するには、 ここをクリックして この操作の完全なログを提供してください", + "log_link_to_log": "この操作の完全なログ: ''{desc}", + "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% 完璧に成功しなかったという事実を意味します(そうでなければ、YunoHostは完了のフラグを立てます)。Web管理画面のツール>ログにある移行の**完全な**ログを取得し、サポートチームと共に何が起こったのか調査することをお勧めします。", + "migration_0021_patch_yunohost_conflicts": "競合の問題を回避するためにパッチを適用しています…", + "migration_0021_patching_sources_list": "sources.listsにパッチを適用しています…", + "migration_0021_problematic_apps_warning": "以下の問題のあるインストール済みアプリが検出されました。これらはYunoHostアプリカタログからインストールされていないか、'working'としてフラグが立てられていないようです。したがって、アップグレード後も動作することを保証することはできません: {problematic_apps}", + "migration_0021_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 Bullseyeと YunoHost 11.x にアップグレードする", + "migration_description_0022_php73_to_php74_pools": "php7.3-fpm 'pool' conf ファイルを php7.4 に移行します", + "migration_description_0023_postgresql_11_to_13": "PostgreSQL 11 から 13 へのデータベースの移行", + "migration_description_0024_rebuild_python_venv": "Bullseye移行後に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}は手動で実行する必要があります。Web管理ページの移行→ツールに移動するか、'yunohost tools migrations run'を実行してください。", + "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": "'{category}' の構成が更新されました", + "regenconf_would_be_updated": "カテゴリ '{category}' の構成が更新されているはずです。", + "regex_incompatible_with_tile": "パッケージャー!アクセス許可 '{permission}' show_tile が 'true' に設定されているため、正規表現 URL をメイン URL として定義できません", + "regex_with_only_domain": "ドメインに正規表現を使用することはできませんが、パスにのみ使用できます", + "registrar_infos": "レジストラ情報", + "restore_already_installed_app": "ID が'{app}'のアプリが既にインストールされている", + "restore_already_installed_apps": "次のアプリは既にインストールされているため復元できません。 {apps}", + "restore_backup_too_old": "このバックアップアーカイブは、古すぎるYunoHostバージョンからのものであるため、復元できません。", + "restore_cleaning_failed": "一時復元ディレクトリをクリーンアップできませんでした", + "restore_complete": "復元が完了しました", + "restore_may_be_not_enough_disk_space": "システムに十分なスペースがないようです(空き:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "root_password_desynchronized": "管理者パスワードが変更されましたが、YunoHostはこれをrootパスワードに反映できませんでした!", + "server_reboot_confirm": "サーバーはすぐに再起動しますが、よろしいですか? [{answers}]", + "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}’ は既に削除されています" +} \ No newline at end of file diff --git a/locales/ko.json b/locales/ko.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/locales/ko.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/locales/nb_NO.json b/locales/nb_NO.json index 8cacaff6d..c2219e719 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -1,5 +1,5 @@ { - "aborting": "Avbryter…", + "aborting": "Avbryter.", "admin_password": "Administrasjonspassord", "app_already_installed": "{app} er allerede installert", "app_already_up_to_date": "{app} er allerede oppdatert", @@ -15,20 +15,20 @@ "app_start_install": "Installerer programmet '{app}'…", "action_invalid": "Ugyldig handling '{action}'", "app_start_restore": "Gjenoppretter programmet '{app}'…", - "backup_created": "Sikkerhetskopi opprettet", - "backup_archive_name_exists": "En sikkerhetskopi med dette navnet finnes allerede.", + "backup_created": "Sikkerhetskopi opprettet: {name}", + "backup_archive_name_exists": "En sikkerhetskopi med dette navnet '{name}' finnes allerede.", "backup_archive_name_unknown": "Ukjent lokalt sikkerhetskopiarkiv ved navn '{name}'", "already_up_to_date": "Ingenting å gjøre. Alt er oppdatert.", "backup_method_copy_finished": "Sikkerhetskopi fullført", "backup_method_tar_finished": "TAR-sikkerhetskopiarkiv opprettet", - "app_action_cannot_be_ran_because_required_services_down": "Dette programmet krever noen tjenester som ikke kjører. Før du fortsetter, du bør prøve å starte følgende tjenester på ny (og antagelig undersøke hvorfor de er nede): {services}", + "app_action_cannot_be_ran_because_required_services_down": "Dette programmet krever noen tjenester som ikke kjører. Før du fortsetter, du bør prøve å starte følgende tjenester på ny (og antagelig undersøke hvorfor de er nede): {services}.", "app_already_installed_cant_change_url": "Dette programmet er allerede installert. Nettadressen kan ikke endres kun med denne funksjonen. Ta en titt på `app changeurl` hvis den er tilgjengelig.", "domain_exists": "Domenet finnes allerede", "domains_available": "Tilgjengelige domener:", "done": "Ferdig", "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}'", + "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain}'.", "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 '{}'", @@ -49,7 +49,7 @@ "backup_creation_failed": "Kunne ikke opprette sikkerhetskopiarkiv", "backup_couldnt_bind": "Kunne ikke binde {src} til {dest}.", "backup_csv_addition_failed": "Kunne ikke legge til filer for sikkerhetskopi inn i CSV-filen", - "backup_deleted": "Sikkerhetskopi slettet", + "backup_deleted": "Sikkerhetskopi slettet: {name}", "backup_no_uncompress_archive_dir": "Det finnes ingen slik utpakket arkivmappe", "backup_delete_error": "Kunne ikke slette '{path}'", "certmanager_cert_signing_failed": "Kunne ikke signere det nye sertifikatet", @@ -75,14 +75,10 @@ "domain_cannot_remove_main": "Kan ikke fjerne hoveddomene. Sett et først", "domain_cert_gen_failed": "Kunne ikke opprette sertifikat", "domain_created": "Domene opprettet", - "domain_creation_failed": "Kunne ikke opprette domene", - "domain_dyndns_root_unknown": "Ukjent DynDNS-rotdomene", + "domain_creation_failed": "Kunne ikke opprette domene {domain}: {error}", "dyndns_ip_update_failed": "Kunne ikke oppdatere IP-adresse til DynDNS", "dyndns_ip_updated": "Oppdaterte din IP på DynDNS", - "dyndns_key_generating": "Oppretter DNS-nøkkel… Dette kan ta en stund.", "dyndns_no_domain_registered": "Inget domene registrert med DynDNS", - "dyndns_registered": "DynDNS-domene registrert", - "dyndns_registration_failed": "Kunne ikke registrere DynDNS-domene: {error}", "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", @@ -91,7 +87,7 @@ "log_user_group_update": "Oppdater '{}' gruppe", "app_unknown": "Ukjent program", "app_upgrade_app_name": "Oppgraderer {app}…", - "app_upgrade_failed": "Kunne ikke oppgradere {app}", + "app_upgrade_failed": "Kunne ikke oppgradere {app}: {error}", "app_upgrade_some_app_failed": "Noen programmer kunne ikke oppgraderes", "app_upgraded": "{app} oppgradert", "ask_main_domain": "Hoveddomene", @@ -101,7 +97,7 @@ "ask_new_path": "Ny sti", "ask_password": "Passord", "domain_deleted": "Domene slettet", - "domain_deletion_failed": "Kunne ikke slette domene", + "domain_deletion_failed": "Kunne ikke slette domene {domain}: {error}", "domain_dyndns_already_subscribed": "Du har allerede abonnement på et DynDNS-domene", "log_link_to_log": "Full logg for denne operasjonen: '{desc}'", "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}'", diff --git a/locales/nl.json b/locales/nl.json index bcfb76acd..0620ffc4e 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -21,20 +21,18 @@ "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app} bij te werken", "domain_cert_gen_failed": "Kan certificaat niet genereren", "domain_created": "Domein succesvol aangemaakt", - "domain_creation_failed": "Kan domein niet aanmaken", + "domain_creation_failed": "Kan domein niet aanmaken {domain}: {error}", "domain_deleted": "Domein succesvol verwijderd", - "domain_deletion_failed": "Kan domein niet verwijderen", + "domain_deletion_failed": "Kan domein niet verwijderen {domain}: {error}", "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": "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...", + "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": "Domein '{domain}' is niet beschikbaar.", - "extracting": "Uitpakken...", + "extracting": "Uitpakken…", "installation_complete": "Installatie voltooid", "mail_alias_remove_failed": "Kan mail-alias '{mail}' niet verwijderen", "pattern_email": "Moet een geldig e-mailadres bevatten, zonder '+' symbool er in (bv. abc@example.org)", @@ -49,14 +47,14 @@ "service_cmd_exec_failed": "Kan '{command}' niet uitvoeren", "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_removed": "Service '{service}' werd verwijderd", "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", + "unexpected_error": "Er is een onbekende fout opgetreden: {error}", "unrestore_app": "App '{app}' wordt niet teruggezet", - "updating_apt_cache": "Lijst van beschikbare pakketten wordt bijgewerkt...", + "updating_apt_cache": "Lijst van beschikbare pakketten wordt bijgewerkt…", "upgrade_complete": "Upgrade voltooid", - "upgrading_packages": "Pakketten worden geüpdate...", + "upgrading_packages": "Pakketten worden geüpdate…", "upnp_dev_not_found": "Geen UPnP apparaten gevonden", "upnp_disabled": "UPnP succesvol uitgeschakeld", "upnp_enabled": "UPnP succesvol ingeschakeld", @@ -64,12 +62,12 @@ "user_deleted": "Gebruiker werd verwijderd", "user_home_creation_failed": "Kan de map voor deze gebruiker niet aanmaken", "user_unknown": "Gebruikersnaam {user} is onbekend", - "user_update_failed": "Kan gebruiker niet bijwerken", + "user_update_failed": "Kan gebruiker niet bijwerken {user}: {error}", "yunohost_configured": "YunoHost configuratie is OK", "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_checking": "Noodzakelijke pakketten voor {app} aan het controleren…", "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", @@ -77,20 +75,20 @@ "backup_archive_broken_link": "Het backup archief kon niet geopend worden (Ongeldig verwijs naar {path})", "backup_archive_name_unknown": "Onbekend lokaal backup archief namens '{name}' gevonden", "backup_archive_open_failed": "Kan het backup archief niet openen", - "backup_created": "Backup aangemaakt", + "backup_created": "Backup aangemaakt: {name}", "backup_creation_failed": "Aanmaken van backup mislukt", "backup_delete_error": "Kon pad '{path}' niet verwijderen", - "backup_deleted": "Backup werd verwijderd", + "backup_deleted": "Backup werd verwijderd: {name}", "backup_hook_unknown": "backup hook '{hook}' onbekend", "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.", "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...", + "app_upgrade_app_name": "Bezig {app} te upgraden…", "app_make_default_location_already_used": "Kan '{app}' niet de standaardapp maken op het domein, '{domain}' wordt al gebruikt door '{other_app}'", "app_install_failed": "Kan {app} niet installeren: {error}", - "app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie...", + "app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie…", "app_manifest_install_ask_domain": "Kies het domein waar deze app op geïnstalleerd moet worden", "app_manifest_install_ask_path": "Kies het URL-pad (achter het domein) waar deze app geïnstalleerd moet worden", "app_manifest_install_ask_admin": "Kies een administrator voor deze app", @@ -101,10 +99,10 @@ "app_manifest_install_ask_password": "Kies een administratiewachtwoord voor deze app", "app_manifest_install_ask_is_public": "Moet deze app zichtbaar zijn voor anomieme bezoekers?", "app_not_upgraded": "De app '{failed_app}' kon niet upgraden en daardoor zijn de upgrades van de volgende apps geannuleerd: {apps}", - "app_start_install": "Bezig met installeren van {app}...", - "app_start_remove": "Bezig met verwijderen van {app}...", - "app_start_backup": "Bestanden aan het verzamelen voor de backup van {app}...", - "app_start_restore": "{app} herstellen...", + "app_start_install": "Bezig met installeren van {app}…", + "app_start_remove": "Bezig met verwijderen van {app}…", + "app_start_backup": "Bestanden aan het verzamelen voor de backup van {app}…", + "app_start_restore": "{app} herstellen…", "app_upgrade_several_apps": "De volgende apps zullen worden geüpgraded: {apps}", "app_upgrade_script_failed": "Er is een fout opgetreden in het upgradescript van de app", "apps_already_up_to_date": "Alle apps zijn al bijgewerkt met de nieuwste versie", @@ -119,12 +117,12 @@ "apps_catalog_init_success": "De app-catalogus is succesvol geinitieerd!", "apps_catalog_failed_to_download": "Het is niet gelukt de {apps_catalog} app-catalogus te downloaden: {error}", "app_packaging_format_not_supported": "Deze app kon niet geinstalleerd worden, omdat het pakketformaat niet ondersteund wordt door je Yunohost. Probeer of je Yunohost bijgewerkt kan worden.", - "additional_urls_already_added": "Extra URL '{url:s}' is al toegevoegd in de extra URL voor privilege '{permission:s}'", + "additional_urls_already_added": "Extra URL '{url}' is al toegevoegd in de extra URL voor privilege '{permission}'", "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": "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", + "other_available_options": "… en {n} andere beschikbare opties die niet getoond worden", "password_listed": "Dit wachtwoord is een van de meest gebruikte wachtwoorden ter wereld. Kies alstublieft iets wat minder voor de hand ligt.", "password_too_simple_4": "Het wachtwoord moet minimaal 12 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten", "pattern_email_forward": "Het moet een geldig e-mailadres zijn, '+' symbool is toegestaan (ikzelf@mijndomein.nl bijvoorbeeld, of ikzelf+yunohost@mijndomein.nl)", diff --git a/locales/oc.json b/locales/oc.json index 1c13fc6b5..24ed6503f 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -10,7 +10,7 @@ "app_not_properly_removed": "{app} es pas estat corrèctament suprimit", "app_removed": "{app} es estada suprimida", "app_unknown": "Aplicacion desconeguda", - "app_upgrade_app_name": "Actualizacion de l’aplicacion {app}...", + "app_upgrade_app_name": "Actualizacion de l’aplicacion {app}…", "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", @@ -18,18 +18,18 @@ "ask_new_admin_password": "Nòu senhal administrator", "ask_password": "Senhal", "backup_app_failed": "Impossible de salvagardar l’aplicacion « {app} »", - "backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda...", - "backup_applying_method_tar": "Creacion de l’archiu TAR de la salvagarda...", - "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja.", - "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name} » es desconegut", + "backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda…", + "backup_applying_method_tar": "Creacion de l’archiu TAR de la salvagarda…", + "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom '{name}' existís ja.", + "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name} » es desconegut", "action_invalid": "Accion « {action} » incorrècta", "app_argument_choice_invalid": "Utilizatz una de las opcions « {choices} » per l’argument « {name} »", - "app_argument_invalid": "Causissètz una valor invalida pel paramètre « {name} » : {error}", + "app_argument_invalid": "Causissètz una valor invalida pel paramètre « {name} » : {error}", "app_argument_required": "Lo paramètre « {name} » es requesit", "app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain}{path}, pas res a far.", "app_change_url_success": "L’URL de l’aplicacion {app} es ara {domain}{path}", "app_extraction_failed": "Extraccion dels fichièrs d’installacion impossibla", - "app_requirements_checking": "Verificacion dels paquets requesits per {app}...", + "app_requirements_checking": "Verificacion dels paquets requesits per {app}…", "app_sources_fetch_failed": "Recuperacion dels fichièrs fonts impossibla, l’URL es corrècta ?", "app_unsupported_remote_type": "Lo tipe alonhat utilizat per l’aplicacion es pas suportat", "backup_archive_app_not_found": "L’aplicacion « {app} » es pas estada trobada dins l’archiu de la salvagarda", @@ -38,23 +38,23 @@ "backup_archive_system_part_not_available": "La part « {part} » del sistèma es pas disponibla dins aquesta salvagarda", "backup_cleaning_failed": "Impossible de netejar lo repertòri temporari de salvagarda", "backup_copying_to_organize_the_archive": "Còpia de {size} Mio per organizar l’archiu", - "backup_created": "Salvagarda acabada", + "backup_created": "Salvagarda acabada: {name}", "backup_creation_failed": "Creacion impossibla de l’archiu de salvagarda", "app_already_installed_cant_change_url": "Aquesta aplicacion es ja installada. Aquesta foncion pòt pas simplament cambiar l’URL. Agachatz « app changeurl » s’es disponible.", "app_change_url_no_script": "L’aplicacion {app_name} pren pas en compte lo cambiament d’URL, benlèu que vos cal l’actualizar.", "app_make_default_location_already_used": "Impossible de configurar l’aplicacion « {app} » per defaut pel domeni {domain} perque es ja utilizat per l’aplicacion {other_app}", "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta :\n{apps}", "backup_delete_error": "Supression impossibla de « {path} »", - "backup_deleted": "La salvagarda es estada suprimida", + "backup_deleted": "La salvagarda es estada suprimida: {name}", "backup_hook_unknown": "Script de salvagarda « {hook} » desconegut", "backup_method_copy_finished": "La còpia de salvagarda es acabada", "backup_method_tar_finished": "L’archiu TAR de la salvagarda es estat creat", "backup_output_directory_not_empty": "Devètz causir un dorsièr de sortida void", "backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda", - "backup_running_hooks": "Execucion dels scripts de salvagarda...", + "backup_running_hooks": "Execucion dels scripts de salvagarda…", "backup_system_part_failed": "Impossible de salvagardar la part « {part} » del sistèma", "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_applying_method_custom": "Crida del metòde de salvagarda personalizat « {method} »…", "backup_couldnt_bind": "Impossible de ligar {src} amb {dest}.", "backup_csv_addition_failed": "Impossible d’ajustar de fichièrs a la salvagarda dins lo fichièr CSV", "backup_custom_backup_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « backup »", @@ -73,9 +73,9 @@ "upnp_port_open_failed": "Impossible de dobrir los pòrts amb UPnP", "yunohost_already_installed": "YunoHost es ja installat", "yunohost_configured": "YunoHost es ara configurat", - "yunohost_installing": "Installacion de YunoHost...", + "yunohost_installing": "Installacion de YunoHost…", "backup_csv_creation_failed": "Creacion impossibla del fichièr CSV necessari a las operacions futuras de restauracion", - "backup_output_symlink_dir_broken": "Vòstre repertòri d’archiu « {path} » es un ligam simbolic copat. Saique oblidèretz de re/montar o de connectar supòrt.", + "backup_output_symlink_dir_broken": "Vòstre repertòri d’archiu « {path} » es un ligam simbolic copat. Saique oblidèretz de re/montar o de connectar supòrt.", "backup_with_no_backup_script_for_app": "L’aplicacion {app} a pas cap de script de salvagarda. I fasèm pas cas.", "backup_with_no_restore_script_for_app": "{app} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.", "certmanager_acme_not_configured_for_domain": "Lo certificat pel domeni {domain} sembla pas corrèctament installat. Mercés de lançar d’en primièr « cert-install » per aqueste domeni.", @@ -86,7 +86,7 @@ "certmanager_cert_install_success_selfsigned": "Lo certificat auto-signat es ara installat pel domeni « {domain} »", "certmanager_cert_signing_failed": "Signatura impossibla del nòu certificat", "certmanager_domain_cert_not_selfsigned": "Lo certificat pel domeni {domain} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utilizatz « --force » per o far)", - "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » pel domeni {domain} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz çò que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)", + "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » pel domeni {domain} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz çò que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)", "certmanager_domain_http_not_working": "Sembla que lo domeni {domain} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e NGINK son corrèctas", "certmanager_no_cert_file": "Lectura impossibla del fichièr del certificat pel domeni {domain} (fichièr : {file})", "certmanager_self_ca_conf_file_not_found": "Impossible de trobar lo fichièr de configuracion per l’autoritat del certificat auto-signat (fichièr : {file})", @@ -95,10 +95,9 @@ "domain_cannot_remove_main": "Impossible de levar lo domeni màger. Definissètz un novèl domeni màger d’en primièr", "domain_cert_gen_failed": "Generacion del certificat impossibla", "domain_created": "Domeni creat", - "domain_creation_failed": "Creacion del domeni {domain}: impossibla", + "domain_creation_failed": "Creacion del domeni {domain} impossibla: {error}", "domain_deleted": "Domeni suprimit", "domain_deletion_failed": "Supression impossibla del domeni {domain}: {error}", - "domain_dyndns_root_unknown": "Domeni DynDNS màger desconegut", "domain_exists": "Lo domeni existís ja", "domain_hostname_failed": "Fracàs de la creacion d’un nòu nom d’òst. Aquò poirà provocar de problèmas mai tard (mas es pas segur… benlèu que coparà pas res).", "domains_available": "Domenis disponibles :", @@ -106,11 +105,8 @@ "downloading": "Telecargament…", "dyndns_ip_update_failed": "Impossible d’actualizar l’adreça IP sul domeni DynDNS", "dyndns_ip_updated": "Vòstra adreça IP actualizada pel domeni DynDNS", - "dyndns_key_generating": "La clau DNS es a se generar… pòt trigar una estona.", "dyndns_key_not_found": "Clau DNS introbabla pel domeni", "dyndns_no_domain_registered": "Cap de domeni pas enregistrat amb DynDNS", - "dyndns_registered": "Domeni DynDNS enregistrat", - "dyndns_registration_failed": "Enregistrament del domeni DynDNS impossible : {error}", "dyndns_domain_not_provided": "Lo provesidor DynDNS {provider} pòt pas fornir lo domeni {domain}.", "dyndns_unavailable": "Lo domeni {domain} es pas disponible.", "extracting": "Extraccion…", @@ -135,11 +131,11 @@ "backup_output_directory_forbidden": "Causissètz un repertòri de destinacion deferent. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain} ! (Utilizatz --force per cortcircuitar)", "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni « {domain} »", - "certmanager_certificate_fetching_or_enabling_failed": "Sembla qu’utilizar lo nòu certificat per {domain} fonciona pas...", + "certmanager_certificate_fetching_or_enabling_failed": "Sembla qu’utilizar lo nòu certificat per {domain} fonciona pas…", "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs", "domain_dns_conf_is_just_a_recommendation": "Aqueste pagina mòstra la configuracion *recomandada*. Non configura *pas* lo DNS per vos. Sètz responsable de la configuracion de vòstra zòna DNS en çò de vòstre registrar DNS amb aquesta recomandacion.", "domain_dyndns_already_subscribed": "Avètz ja soscrich a un domeni DynDNS", - "domain_uninstall_app_first": "Una o mantuna aplicacions son installadas sus aqueste domeni. Mercés de las desinstallar d’en primièr abans de suprimir aqueste domeni", + "domain_uninstall_app_first": "Una o mantuna aplicacions son installadas sus aqueste domeni.\n{apps}\n\nMercés de las desinstallar d’en primièr abans de suprimir aqueste domeni", "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.", @@ -147,12 +143,12 @@ "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", "hook_name_unknown": "Nom de script « {name} » desconegut", - "mail_domain_unknown": "Lo domeni de corrièl « {domain} » es desconegut", + "mail_domain_unknown": "Lo domeni de corrièl « {domain} » es desconegut.", "mailbox_used_space_dovecot_down": "Lo servici corrièl Dovecot deu èsser aviat, se volètz conéisser l’espaci ocupat per la messatjariá", "service_disable_failed": "Impossible de desactivar lo servici « {service} »↵\n↵\nJornals recents : {logs}", - "service_disabled": "Lo servici « {service} » es desactivat", + "service_disabled": "Lo servici « {service} » es desactivat.", "service_enable_failed": "Impossible d’activar lo servici « {service} »↵\n↵\nJornals recents : {logs}", - "service_enabled": "Lo servici « {service} » es activat", + "service_enabled": "Lo servici « {service} » es activat.", "service_remove_failed": "Impossible de levar lo servici « {service} »", "service_removed": "Lo servici « {service} » es estat levat", "service_start_failed": "Impossible d’aviar lo servici « {service} »↵\n↵\nJornals recents : {logs}", @@ -161,16 +157,16 @@ "ssowat_conf_generated": "La configuracion SSowat es generada", "system_upgraded": "Lo sistèma es estat actualizat", "system_username_exists": "Lo nom d’utilizaire existís ja dins los utilizaires sistèma", - "unexpected_error": "Una error inesperada s’es producha", + "unexpected_error": "Una error inesperada s’es producha: {error}", "upgrade_complete": "Actualizacion acabada", "upgrading_packages": "Actualizacion dels paquets…", "user_created": "L’utilizaire es creat", - "user_creation_failed": "Creacion de l’utilizaire impossibla", + "user_creation_failed": "Creacion de l’utilizaire {user} impossibla: {error}", "user_deleted": "L’utilizaire es suprimit", - "user_deletion_failed": "Supression impossibla de l’utilizaire", - "user_home_creation_failed": "Creacion impossibla del repertòri personal a l’utilizaire", + "user_deletion_failed": "Supression impossibla de l’utilizaire {user}: {error}", + "user_home_creation_failed": "Creacion impossibla del repertòri personal '{home}' a l’utilizaire", "user_unknown": "Utilizaire « {user} » desconegut", - "user_update_failed": "Modificacion impossibla de l’utilizaire", + "user_update_failed": "Modificacion impossibla de l’utilizaire {user}: {error}", "user_updated": "L’utilizaire es estat modificat", "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles…", @@ -189,7 +185,7 @@ "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space} octets, necessari : {needed_space} octets, marge de seguretat : {margin} octets)", "restore_nothings_done": "Res es pas estat restaurat", "restore_removing_tmp_dir_failed": "Impossible de levar u ancian repertòri temporari", - "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app} »…", + "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app} »…", "restore_running_hooks": "Execucion dels scripts de restauracion…", "restore_system_part_failed": "Restauracion impossibla de la part « {part} » del sistèma", "server_shutdown": "Lo servidor serà atudat", @@ -211,10 +207,10 @@ "migrations_skip_migration": "Passatge de la migracion {id}…", "migrations_to_be_ran_manually": "La migracion {id} deu èsser lançada manualament. Mercés d’anar a Aisinas > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations run ».", "migrations_need_to_accept_disclaimer": "Per lançar la migracion {id} , avètz d’acceptar aquesta clausa de non-responsabilitat :\n---\n{disclaimer}\n---\nS’acceptatz de lançar la migracion, mercés de tornar executar la comanda amb l’opcion accept-disclaimer.", - "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", + "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", "service_description_dovecot": "permet als clients de messatjariá d’accedir/recuperar los corrièls (via IMAP e POP3)", "service_description_fail2ban": "protegís contra los atacs brute-force e d’autres atacs venents d’Internet", - "service_description_metronome": "gerís los comptes de messatjariás instantanèas XMPP", + "service_description_metronome": "gerís los comptes de messatjariás instantanèas XMPP", "service_description_nginx": "fornís o permet l’accès a totes los sites web albergats sus vòstre servidor", "service_description_redis-server": "una basa de donadas especializada per un accès rapid a las donadas, las filas d’espèra e la comunicacion entre programas", "service_description_rspamd": "filtra lo corrièl pas desirat e mai foncionalitats ligadas al corrièl", @@ -266,24 +262,24 @@ "root_password_desynchronized": "Lo senhal de l’administrator es estat cambiat, mas YunoHost a pas pogut l’espandir al senhal root !", "aborting": "Interrupcion.", "app_not_upgraded": "L’aplicacion « {failed_app} » a pas reüssit a s’actualizar e coma consequéncia las mesas a jorn de las aplicacions seguentas son estadas anulladas : {apps}", - "app_start_install": "Installacion de l’aplicacion {app}...", - "app_start_remove": "Supression de l’aplicacion {app}...", - "app_start_backup": "Recuperacion dels fichièrs de salvagardar per {app}...", - "app_start_restore": "Restauracion de l’aplicacion {app}...", + "app_start_install": "Installacion de l’aplicacion {app}…", + "app_start_remove": "Supression de l’aplicacion {app}…", + "app_start_backup": "Recuperacion dels fichièrs de salvagardar per {app}…", + "app_start_restore": "Restauracion de l’aplicacion {app}…", "app_upgrade_several_apps": "Las aplicacions seguentas seràn actualizadas : {apps}", "ask_new_domain": "Nòu domeni", "ask_new_path": "Nòu camin", - "backup_actually_backuping": "Creacion d’un archiu de seguretat a partir dels fichièrs recuperats...", - "backup_mount_archive_for_restore": "Preparacion de l’archiu per restauracion...", + "backup_actually_backuping": "Creacion d’un archiu de seguretat a partir dels fichièrs recuperats…", + "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.", "service_restarted": "Lo servici '{service}' es estat reaviat", "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}", + "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}.", "confirm_app_install_warning": "Atencion : aquesta aplicacion fonciona mas non es pas ben integrada amb YunoHost. Unas foncionalitats coma l’autentificacion unica e la còpia de seguretat/restauracion pòdon èsser indisponiblas. volètz l’installar de totas manièras ? [{answers}] ", "confirm_app_install_danger": "PERILH ! Aquesta aplicacion es encara experimentala (autrament dich, fonciona pas) e es possible que còpe lo sistèma ! Deuriatz PAS l’installar se non sabètz çò que fasètz. Volètz vertadièrament córrer aqueste risc ? [{answers}]", - "confirm_app_install_thirdparty": "ATENCION ! L’installacion d’aplicacions tèrças pòt comprometre l’integralitat e la seguretat del sistèma. Deuriatz PAS l’installar se non sabètz pas çò que fasètz. Volètz vertadièrament córrer aqueste risc ? [{answers}] ", + "confirm_app_install_thirdparty": "ATENCION ! L’installacion d’aplicacions tèrças pòt comprometre l’integralitat e la seguretat del sistèma. Deuriatz PAS l’installar se non sabètz pas çò que fasètz. Volètz vertadièrament córrer aqueste risc ? [{answers}]", "dpkg_lock_not_available": "Aquesta comanda pòt pas s’executar pel moment perque un autre programa sembla utilizar lo varrolh de dpkg (lo gestionari de paquets del sistèma)", "log_regen_conf": "Regenerar las configuracions del sistèma « {} »", "service_reloaded_or_restarted": "Lo servici « {service} » es estat recargat o reaviat", @@ -323,16 +319,16 @@ "log_user_group_update": "Actualizar lo grop « {} »", "permission_already_exist": "La permission « {permission} » existís ja", "permission_created": "Permission « {permission} » creada", - "permission_creation_failed": "Creacion impossibla de la permission", + "permission_creation_failed": "Creacion impossibla de la permission '{permission}': {error}", "permission_deleted": "Permission « {permission} » suprimida", - "permission_deletion_failed": "Fracàs de la supression de la permission « {permission} »", + "permission_deletion_failed": "Fracàs de la supression de la permission « {permission} »: {error}", "permission_not_found": "Permission « {permission} » pas trobada", - "permission_update_failed": "Fracàs de l’actualizacion de la permission", - "permission_updated": "La permission « {permission} » es estada actualizada", + "permission_update_failed": "Fracàs de l’actualizacion de la permission '{permission}': {error}", + "permission_updated": "La permission « {permission} » es estada actualizada", "mailbox_disabled": "La bóstia de las letras es desactivada per l’utilizaire {user}", - "migrations_success_forward": "Migracion {id} corrèctament realizada !", + "migrations_success_forward": "Migracion {id} corrèctament realizada", "migrations_running_forward": "Execucion de la migracion {id}…", - "migrations_must_provide_explicit_targets": "Devètz fornir una cibla explicita quand utilizatz using --skip o --force-rerun", + "migrations_must_provide_explicit_targets": "Devètz fornir una cibla explicita quand utilizatz using --skip o --force-rerun", "migrations_exclusive_options": "--auto, --skip, e --force-rerun son las opcions exclusivas.", "migrations_failed_to_load_migration": "Cargament impossible de la migracion {id} : {error}", "migrations_already_ran": "Aquelas migracions s’executèron ja : {ids}", @@ -342,7 +338,7 @@ "migrations_not_pending_cant_skip": "Aquestas migracions son pas en espèra, las podètz pas doncas ignorar : {ids}", "app_action_broke_system": "Aquesta accion sembla aver copat de servicis importants : {services}", "diagnosis_ip_no_ipv6": "Lo servidor a pas d’adreça IPv6 activa.", - "diagnosis_ip_not_connected_at_all": "Lo servidor sembla pas connectat a Internet ?!", + "diagnosis_ip_not_connected_at_all": "Lo servidor sembla pas connectat a Internet !?", "diagnosis_description_regenconf": "Configuracion sistèma", "diagnosis_http_ok": "Lo domeni {domain} accessible de l’exterior.", "app_full_domain_unavailable": "Aquesta aplicacion a d’èsser installada sul seu pròpri domeni, mas i a d’autras aplicacions installadas sus aqueste domeni « {domain} ». Podètz utilizar allòc un josdomeni dedicat a aquesta aplicacion.", @@ -356,7 +352,7 @@ "app_install_failed": "Installacion impossibla de {app} : {error}", "app_install_script_failed": "Una error s’es producha en installar lo script de l’aplicacion", "apps_already_up_to_date": "Totas las aplicacions son ja al jorn", - "app_remove_after_failed_install": "Supression de l’aplicacion aprèp fracàs de l’installacion...", + "app_remove_after_failed_install": "Supression de l’aplicacion aprèp fracàs de l’installacion…", "group_already_exist": "Lo grop {group} existís ja", "group_already_exist_on_system": "Lo grop {group} existís ja dins lo sistèma de grops", "group_user_not_in_group": "L’utilizaire {user} es pas dins lo grop {group}", @@ -380,7 +376,6 @@ "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_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", "diagnosis_description_ip": "Connectivitat Internet", @@ -400,20 +395,20 @@ "operation_interrupted": "L’operacion es estada interrompuda manualament ?", "group_cannot_be_deleted": "Lo grop « {group} » pòt pas èsser suprimit manualament.", "diagnosis_found_warnings": "Trobat {warnings} element(s) que se poirián melhorar per {category}.", - "diagnosis_dns_missing_record": "Segon la configuracion DNS recomandada, vos calriá ajustar un enregistrament DNS\ntipe: {type}\nnom: {name}\nvalor: {value}", - "diagnosis_dns_discrepancy": "La configuracion DNS seguenta sembla pas la configuracion recomandada :
Tipe : {type}
Nom : {name}
Valors actualas : {current]
Valor esperada : {value}", + "diagnosis_dns_missing_record": "Segon la configuracion DNS recomandada, vos calriá ajustar un enregistrament DNS.
Tipe: {type}
Nom: {name}
Valor: {value}", + "diagnosis_dns_discrepancy": "La configuracion DNS seguenta sembla pas la configuracion recomandada :
Tipe : {type}
Nom : {name}
Valors actualas : {current}
Valor esperada : {value}", "diagnosis_ports_could_not_diagnose": "Impossible de diagnosticar se los pòrts son accessibles de l’exterior.", "diagnosis_ports_could_not_diagnose_details": "Error : {error}", "diagnosis_http_could_not_diagnose": "Impossible de diagnosticar se lo domeni es accessible de l’exterior.", "diagnosis_http_could_not_diagnose_details": "Error : {error}", - "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion...", + "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion…", "apps_catalog_failed_to_download": "Telecargament impossible del catalòg d’aplicacions {apps_catalog} : {error}", "apps_catalog_obsolete_cache": "La memòria cache del catalòg d’aplicacion es voida o obsolèta.", "apps_catalog_update_success": "Lo catalòg d’aplicacions es a jorn !", "diagnosis_description_mail": "Corrièl", "app_upgrade_script_failed": "Una error s’es producha pendent l’execucion de l’script de mesa a nivèl de l’aplicacion", "diagnosis_cant_run_because_of_dep": "Execucion impossibla del diagnostic per {category} mentre que i a de problèmas importants ligats amb {dep}.", - "diagnosis_found_errors_and_warnings": "Avèm trobat {errors} problèma(s) important(s) (e {warnings} avis(es)) ligats a {category} !", + "diagnosis_found_errors_and_warnings": "Avèm trobat {errors} problèma(s) important(s) (e {warnings} avis(es)) ligats a {category} !", "diagnosis_failed": "Recuperacion impossibla dels resultats del diagnostic per la categoria « {category} » : {error}", "diagnosis_ip_broken_dnsresolution": "La resolucion del nom de domeni es copada per una rason… Lo parafuòc bloca las requèstas DNS ?", "diagnosis_no_cache": "I a pas encara de diagnostic de cache per la categoria « {category} »", @@ -422,13 +417,13 @@ "diagnosis_services_conf_broken": "La configuracion es copada pel servici {service} !", "diagnosis_ports_needed_by": "Es necessari qu’aqueste pòrt siá accessible pel servici {service}", "diagnosis_diskusage_low": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a solament {free} ({free_percent}%). Siatz prudent.", - "dyndns_provider_unreachable": "Impossible d’atenher lo provesidor Dyndns : siá vòstre YunoHost es pas corrèctament connectat a Internet siá lo servidor dynette es copat.", + "dyndns_provider_unreachable": "Impossible d’atenher lo provesidor Dyndns {provider} : siá vòstre YunoHost es pas corrèctament connectat a Internet siá lo servidor dynette es copat.", "diagnosis_services_bad_status_tip": "Podètz ensajar de reaviar lo servici, e se non fonciona pas, podètz agachar los jornals de servici a la pagina web d’administracion(en linha de comanda podètz utilizar yunohost service restart {service} e yunohost service log {service}).", "diagnosis_http_connection_error": "Error de connexion : connexion impossibla al domeni demandat, benlèu qu’es pas accessible.", "group_user_already_in_group": "L’utilizaire {user} es ja dins lo grop « {group} »", "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.", + "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!", "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.", @@ -449,12 +444,12 @@ "app_manifest_install_ask_path": "Causissètz lo camin ont volètz installar aquesta aplicacion", "app_manifest_install_ask_domain": "Causissètz lo domeni ont volètz installar aquesta aplicacion", "app_argument_password_no_default": "Error pendent l’analisi de l’argument del senhal « {name} » : l’argument de senhal pòt pas aver de valor per defaut per de rason de seguretat", - "app_label_deprecated": "Aquesta comanda es estada renduda obsolèta. Mercés d'utilizar lo nòva \"yunohost user permission update\" per gerir letiquetada de l'aplication", - "additional_urls_already_removed": "URL addicionala {url} es ja estada elimida per la permission «#permission:s»", + "app_label_deprecated": "Aquesta comanda es estada renduda obsolèta. Mercés d'utilizar lo nòva \"yunohost user permission update\" per gerir letiquetada de l'aplication.", + "additional_urls_already_removed": "URL addicionala {url} es ja estada elimida per la permission «{permission}»", "additional_urls_already_added": "URL addicionadal «{url}'» es ja estada aponduda per la permission «{permission}»", "log_app_action_run": "Executar l’accion de l’aplicacion « {} »", "diagnosis_basesystem_hardware_model": "Lo modèl del servidor es {model}", - "backup_archive_cant_retrieve_info_json": "Obtencion impossibla de las informacions de l’archiu « {archive} »... Se pòt pas recuperar lo fichièr info.json (o es pas un fichièr json valid).", + "backup_archive_cant_retrieve_info_json": "Obtencion impossibla de las informacions de l’archiu « {archive} »… Se pòt pas recuperar lo fichièr info.json (o es pas un fichièr json valid).", "app_packaging_format_not_supported": "Se pòt pas installar aquesta aplicacion pr’amor que son format es pas pres en carga per vòstra version de YunoHost. Deuriatz considerar actualizar lo sistèma.", "diagnosis_mail_fcrdns_ok": "Vòstre DNS inverse es corrèctament configurat !", "diagnosis_mail_outgoing_port_25_ok": "Lo servidor de messatge SMTP pòt enviar de corrièls (lo pòrt 25 es pas blocat).", @@ -468,5 +463,5 @@ "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)" + "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 c58f7223e..8434eb0f1 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -4,18 +4,18 @@ "app_already_installed": "{app} jest już zainstalowana", "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", "admin_password": "Hasło administratora", - "action_invalid": "Nieprawidłowe działanie '{action:s}'", + "action_invalid": "Nieprawidłowe działanie '{action}'", "aborting": "Przerywanie.", "domain_config_auth_consumer_key": "Klucz konsumenta", "domain_config_cert_validity": "Ważność", "visitors": "Odwiedzający", - "app_start_install": "Instalowanie {app}...", + "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...", + "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", @@ -29,16 +29,16 @@ "system_upgraded": "Zaktualizowano system", "diagnosis_description_regenconf": "Konfiguracja systemu", "diagnosis_description_apps": "Aplikacje", - "diagnosis_description_basesystem": "Podstawowy system", + "diagnosis_description_basesystem": "Baza systemu", "unlimit": "Brak limitu", "global_settings_setting_pop3_enabled": "Włącz POP3", "domain_created": "Utworzono domenę", "ask_new_admin_password": "Nowe hasło administracyjne", "ask_new_domain": "Nowa domena", "ask_new_path": "Nowa ścieżka", - "downloading": "Pobieranie...", + "downloading": "Pobieranie…", "ask_password": "Hasło", - "backup_deleted": "Usunięto kopię zapasową: {name}.", + "backup_deleted": "Usunięto kopię zapasową: {name}", "done": "Gotowe", "diagnosis_description_dnsrecords": "Rekordy DNS", "diagnosis_description_ip": "Połączenie z internetem", @@ -47,10 +47,10 @@ "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_start_remove": "Usuwanie {app}…", + "app_start_restore": "Przywracanie {app}…", "app_upgraded": "Zaktualizowano {app}", - "extracting": "Rozpakowywanie...", + "extracting": "Rozpakowywanie…", "app_removed": "Odinstalowano {app}", "upgrade_complete": "Aktualizacja zakończona", "global_settings_setting_backup_compress_tar_archives": "Kompresuj kopie zapasowe", @@ -58,7 +58,7 @@ "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...", + "upgrading_packages": "Aktualizowanie paczek…", "admins": "Administratorzy", "diagnosis_ports_could_not_diagnose_details": "Błąd: {error}", "log_settings_set": "Zastosuj ustawienia", @@ -68,7 +68,7 @@ "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_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}'", @@ -77,10 +77,10 @@ "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_already_installed_cant_change_url": "Ta aplikacja jest już zainstalowana. URL nie może zostać zmieniony przy użyciu tej funkcji. Sprawdź czy można zmienić w `app changeurl`.", "app_id_invalid": "Nieprawidłowy identyfikator aplikacji(ID)", "app_change_url_require_full_domain": "Nie można przenieść aplikacji {app} na nowy adres URL, ponieważ wymaga ona pełnej domeny (tj. ze ścieżką = /)", - "app_install_files_invalid": "Tych plików nie można zainstalować", + "app_install_files_invalid": "Te pliki nie mogą zostać zainstalowane", "app_make_default_location_already_used": "Nie można ustawić '{app}' jako domyślnej aplikacji w domenie '{domain}' ponieważ jest już używana przez '{other_app}'", "app_change_url_identical_domains": "Stara i nowa domena/ścieżka_url są identyczne („{domain}{path}”), nic nie trzeba robić.", "app_config_unable_to_read": "Nie udało się odczytać wartości panelu konfiguracji.", @@ -91,19 +91,19 @@ "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_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_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_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...", + "backup_applying_method_custom": "Wywołuję niestandardową metodę tworzenia kopii zapasowych '{method}'…", + "app_remove_after_failed_install": "Usuwanie aplikacji po niepowodzeniu instalacji…", "app_upgrade_script_failed": "Wystąpił błąd w skrypcie aktualizacji aplikacji", - "apps_catalog_init_success": "Zainicjowano system katalogu aplikacji!", + "apps_catalog_init_success": "System katalogu aplikacji został zainicjowany!", "apps_catalog_obsolete_cache": "Pamięć podręczna katalogu aplikacji jest pusta lub przestarzała.", "app_extraction_failed": "Nie można wyodrębnić plików instalacyjnych", "app_packaging_format_not_supported": "Ta aplikacja nie może zostać zainstalowana, ponieważ jej format opakowania nie jest obsługiwany przez twoją wersję YunoHost. Prawdopodobnie powinieneś rozważyć aktualizację swojego systemu.", @@ -112,10 +112,10 @@ "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_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...", + "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}", @@ -123,10 +123,10 @@ "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_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_name_exists": "Archiwum kopii zapasowych o nazwie '{name}' 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.)", @@ -136,7 +136,7 @@ "backup_archive_corrupted": "Wygląda na to, że archiwum kopii zapasowej '{archive}' jest uszkodzone: {error}", "backup_cleaning_failed": "Nie udało się wyczyścić folderu tymczasowej kopii zapasowej", "backup_create_size_estimation": "Archiwum będzie zawierać około {size} danych.", - "app_location_unavailable": "Ten adres URL jest niedostępny lub powoduje konflikt z już zainstalowanymi aplikacja(mi):\n{apps}", + "app_location_unavailable": "Ten adres URL jest niedostępny lub koliduje z już zainstalowanymi aplikacjami:\n{apps}", "app_restore_failed": "Nie można przywrócić {app}: {error}", "app_restore_script_failed": "Wystąpił błąd w skrypcie przywracania aplikacji", "app_full_domain_unavailable": "Przepraszamy, ta aplikacja musi być zainstalowana we własnej domenie, ale inna aplikacja jest już zainstalowana w tej domenie „{domain}”. Zamiast tego możesz użyć subdomeny dedykowanej tej aplikacji.", @@ -144,11 +144,11 @@ "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_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'.", + "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ć)", @@ -161,7 +161,7 @@ "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_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", @@ -171,13 +171,159 @@ "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...", + "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...", + "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" + "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 być teraz uruchomione dla {domain}, ponieważ jego konfiguracja nginx nie zawiera odpowiedniego fragmentu kodu… Upewnij się, że twoja konfiguracja nginx jest aktualna, używając `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_domain_dns_ip_differs_from_public_ip": "Rekordy DNS dla domeny '{domain}' różnią się od adresu IP tego serwera. Sprawdź kategorię 'Rekordy DNS' (podstawowe) w diagnozie, aby uzyskać więcej informacji. Jeśli niedawno dokonałeś zmiany rekordu A, poczekaj, aż zostanie on zaktualizowany (można skorzystać z narzędzi online do sprawdzania propagacji DNS). (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)", + "confirm_app_install_danger": "UWAGA! Ta aplikacja jest wciąż w fazie eksperymentalnej (jeśli nie działa jawnie)! Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu… Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}", + "confirm_app_install_thirdparty": "UWAGA! Ta aplikacja nie jest częścią katalogu aplikacji YunoHost. Instalowanie aplikacji innych firm może naruszyć integralność i bezpieczeństwo systemu. Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu… Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'", + "config_apply_failed": "Nie udało się zastosować nowej konfiguracji: {error}", + "config_cant_set_value_on_section": "Nie możesz ustawić pojedyńczej 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": "Zapasowy hook '{hook}' jest nieznany", + "backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum", + "backup_output_symlink_dir_broken": "Twój katalog archiwum ‘{path}’ to uszkodzony symlink. Być może zapomniałeś o ponownym zamontowaniu lub podłączeniu nośnika przechowującego, do którego on wskazuje.", + "backup_system_part_failed": "Nie udało się wykonać kopii zapasowej części systemu ‘{part}’", + "config_validate_color": "Powinien być poprawnym szesnastkowym kodem koloru RGB", + "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 samodzielnie podpisanego upoważnienia do (file: {file})", + "backup_running_hooks": "Uruchamianie kopii zapasowej hooków…", + "backup_permission": "Uprawnienia do tworzenia kopii zapasowej dla aplikacji {app}", + "certmanager_domain_cert_not_selfsigned": "Certyfikat dla domeny {domain} nie jest samopodpisany. Czy na pewno chcesz go zastąpić? (Użyj opcji '--force', aby to zrobić.)", + "config_action_disabled": "Nie można uruchomić akcji '{action}', ponieważ jest ona wyłączona. Upewnij się, że spełnione są jej ograniczenia. Pomoc: {help}", + "config_action_failed": "Nie udało się uruchomić akcji '{action}': {error}", + "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 już zarezerwowane. Nie możesz 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} nie wydaje się być dostępna za pośrednictwem protokołu HTTP. Aby uzyskać więcej informacji, sprawdź kategorię 'Web' w diagnostyce. (Jeśli wiesz, co robisz, użyj '--no-checks', aby wyłączyć te kontrole.)", + "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", + "ask_dyndns_recovery_password_explain": "Proszę wybrać hasło odzyskiwania dla swojej domeny DynDNS, na wypadek gdybyś musiał go później zresetować.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Proszę wprowadzić hasło odzyskiwania dla tej domeny DynDNS.", + "certmanager_unable_to_parse_self_CA_name": "Nie można spasować nazwy organu samopodpisywanego (pliku: {file})", + "app_corrupt_source": "YunoHost był w stanie pobrać zasób ‘{source_id}’ ({url}) dla {app}, ale zasób nie pasuje do oczekiwanego sumy kontrolnej. Może to oznaczać, że na twoim serwerze wystąpiła tymczasowa awaria sieci, LUB zasób został jakoś zmieniony przez dostawcę usługi (lub złośliwego aktora?) i pakowacze YunoHost muszą zbadać sprawę i zaktualizować manifest aplikacji, aby odzwierciedlić tę zmianę. \nOczekiwana suma kontrolna sha256: {expected_sha256} \nPobrana suma kontrolna sha256: {computed_sha256} \nRozmiar pobranego pliku: {size}”", + "ask_dyndns_recovery_password": "Hasło odzyskiwania DynDNS", + "certmanager_hit_rate_limit": "Zbyt wiele certyfikatów zostało ostatnio wydanych dla dokładnie tej grupy domen {domain}. Spróbuj ponownie później. Zobacz https://letsencrypt.org/docs/rate-limits/ aby uzyskać więcej informacji", + "apps_failed_to_upgrade_line": "\n * {app_id} (aby zobaczyć odpowiedni dziennik, wykonaj ‘yunohost log show {operation_logger_name}’)", + "diagnosis_basesystem_ynh_inconsistent_versions": "Używasz niespójnych wersji pakietów YunoHost… najprawdopodobniej z powodu nieudanej lub częściowej aktualizacji.", + "service_removed": "Usunięto usługę '{service}'", + "service_disabled": "Usługa '{service}' nie będzie już uruchamiana podczas uruchamiania systemu.", + "diagnosis_description_web": "Sieć", + "confirm_notifications_read": "OSTRZEŻENIE: Zanim przejdziesz dalej, powinieneś sprawdzić powyższe powiadomienia aplikacji, mogą tam być istotne informacje o których warto wiedzieć. [{answers}]", + "diagnosis_description_services": "Kontrola stanu usług", + "diagnosis_domain_expiration_error": "Niektóre domeny wygasną BARDZO WKRÓTCE!", + "diagnosis_domain_expiration_success": "Twoje domeny są zarejestrowane i nie wygasną w najbliższym czasie.", + "diagnosis_domain_expiration_warning": "Niektóre domeny wkrótce wygasną!", + "diagnosis_dns_specialusedomain": "Domena {domain} opiera się na domenie najwyższego poziomu specjalnego przeznaczenia (TLD), takiej jak .local lub .test i dlatego nie oczekuje się, że będzie zawierać rzeczywiste rekordy DNS.", + "danger": "Zagrożeniæ:", + "config_validate_url": "Powinien być poprawnym adresem internetowym (URL)", + "diagnosis_domain_expires_in": "Domena {domain} wygasa za {days} dni.", + "diagnosis_cant_run_because_of_dep": "Nie można przeprowadzić diagnostyki dla kategorii {category}, ponieważ występują poważne problemy związane z kategorią {dep}.", + "diagnosis_everything_ok": "Wszystko wygląda dobrze dla kategorii {category}!", + "diagnosis_found_errors": "Znaleziono istotne problemy związane z kategorią: {errors}!", + "diagnosis_dns_missing_record": "Zgodnie z zalecaną konfiguracją DNS powinieneś dodać rekord DNS z następującymi informacjami.
Typ: {type}
Nazwa: {name}
Wartość: {value}", + "diagnosis_display_tip": "Aby zobaczyć znalezione problemy, możesz przejść do sekcji Diagnostyka w webadmin lub uruchomić z wiersza poleceń polecenie „yunohost diagnoza show --issues --human-readable”.", + "diagnosis_dns_point_to_doc": "eśli potrzebujesz pomocy w konfiguracji rekordów DNS, sprawdź dokumentację pod adresem https://yunohost.org/dns_config.", + "diagnosis_failed": "Nie udało się pobrać wyniku diagnostyki dla kategorii „{category}”: {error}", + "diagnosis_dns_bad_conf": "Brakuje niektórych rekordów DNS lub są one nieprawidłowe dla domeny {domain} (category {category})", + "diagnosis_dns_discrepancy": "Wydaje się, że następujący rekord DNS nie jest zgodny z zalecaną konfiguracją:
Typ: {type}
Nazwa: {name}
Aktualna wartość: < code>{current}

Oczekiwana wartość: {value}", + "diagnosis_domain_not_found_details": "Domena {domain} nie istnieje w bazie WHOIS lub wygasła!", + "custom_app_url_required": "Aby zaktualizować aplikację niestandardową {app}, musisz podać adres URL", + "diagnosis_description_ports": "Ujawnione porty", + "diagnosis_basesystem_ynh_single_version": "Wersja {package}: {version} ({repo})", + "diagnosis_failed_for_category": "Diagnostyka nie powiodła się dla kategorii „{category}”: {error}", + "diagnosis_basesystem_hardware_model": "Model serwera to {model}", + "service_enabled": "Usługa '{service}' będzie teraz automatycznie uruchamiana podczas uruchamiania systemu.", + "confirm_app_install_warning": "Ostrzeżenie: Ta aplikacja może działać, ale nie jest dobrze zintegrowana z YunoHost. Niektóre funkcje, takie jak jednorazowe logowanie i tworzenie/przywracanie kopii zapasowych mogą być niedostępne. Zainstalować mimo to? [{answers}] ", + "diagnosis_apps_broken": "Ta aplikacja jest obecnie oznaczona jako uszkodzona w katalogu aplikacji YunoHost. Może to być problem tymczasowy, do czasu gdy opiekunowie próbują go naprawić. W międzyczasie aktualizacja tej aplikacji jest wyłączona.", + "diagnosis_apps_not_in_app_catalog": "Ta aplikacja nie znajduje się w katalogu aplikacji YunoHost. Jeśli była tam wcześniej i została usunięta, powinieneś rozważyć odinstalowanie tej aplikacji, ponieważ nie będzie otrzymywać aktualizacji, co może zagrażać integralności i bezpieczeństwu twojego systemu.", + "diagnosis_dns_try_dyndns_update_force": "Konfiguracja DNS tej domeny powinna być automatycznie zarządzana przez YunoHost. Jeśli tak nie jest, możesz spróbować wymusić aktualizację za pomocą yunohost dyndns update --force.", + "diagnosis_apps_bad_quality": "Ta aplikacja jest obecnie oznaczona jako uszkodzona w katalogu aplikacji YunoHost. Może to być problem tymczasowy, do czasu gdy opiekunowie próbują go naprawić. W międzyczasie aktualizacja tej aplikacji jest wyłączona.", + "diagnosis_apps_deprecated_practices": "Zainstalowana wersja tej aplikacji nadal korzysta z bardzo starych i przestarzałych praktyk pakowania. Naprawdę powinieneś rozważyć jego aktualizację.", + "diagnosis_apps_outdated_ynh_requirement": "Zainstalowana wersja tej aplikacji wymaga jedynie yunohost >= 2.x lub 3.x, co sugeruje, że nie jest ona zgodna z zalecanymi praktykami pakowania i narzędziami. Naprawdę powinieneś rozważyć jej aktualizację.", + "service_reloaded": "Usługa '{service}' została ponownie załadowana", + "service_reloaded_or_restarted": "Usługa '{service}' została ponownie załadowana lub uruchomiona ponownie", + "ask_dyndns_recovery_password_explain_unavailable": "Ta domena DynDNS jest już zarejestrowana. Jeśli jesteś osobą, która pierwotnie zarejestrowała tę domenę, możesz wprowadzić hasło odzyskiwania, aby ją odzyskać.", + "diagnosis_domain_expiration_not_found": "Nie udało się sprawdzić daty wygaśnięcia niektórych domen", + "diagnosis_domain_expiration_not_found_details": "Informacje WHOIS dotyczące domeny {domain} wydają się nie zawierać informacji o dacie jej wygaśnięcia?", + "diagnosis_high_number_auth_failures": "Ostatnio wystąpiła podejrzanie duża liczba błędów uwierzytelniania. Możesz upewnić się, że Fail2ban działa i jest poprawnie skonfigurowany, lub użyj niestandardowego portu dla SSH, jak wyjaśniono w https://yunohost.org/security.", + "service_remove_failed": "Nie można usunąć usługi '{service}", + "diagnosis_apps_issue": "Znaleziono problem z aplikacją {app}" } \ No newline at end of file diff --git a/locales/pt.json b/locales/pt.json index 0aa6b8223..b41ecdb4f 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -14,7 +14,7 @@ "ask_main_domain": "Domínio principal", "ask_new_admin_password": "Nova senha de administração", "ask_password": "Senha", - "backup_created": "Backup completo", + "backup_created": "Backup completo: {name}", "backup_output_directory_not_empty": "Você deve escolher um diretório de saída que esteja vazio", "custom_app_url_required": "Deve fornecer um link para atualizar a sua aplicação personalizada {app}", "domain_cert_gen_failed": "Não foi possível gerar o certificado", @@ -23,22 +23,18 @@ "domain_deleted": "Domínio removido com êxito", "domain_deletion_failed": "Não foi possível eliminar o domínio {domain}: {error}", "domain_dyndns_already_subscribed": "Já subscreveu um domínio DynDNS", - "domain_dyndns_root_unknown": "Domínio root (administrador) DynDNS desconhecido", "domain_exists": "O domínio já existe", - "domain_uninstall_app_first": "Existem uma ou mais aplicações instaladas neste domínio. Por favor desinstale-as antes de proceder com a remoção do domínio.", + "domain_uninstall_app_first": "Existem uma ou mais aplicações instaladas neste domínio.\n{apps}\n\nPor favor desinstale-as antes de proceder com a remoção do domínio", "done": "Concluído.", - "downloading": "Transferência em curso...", + "downloading": "Transferência em curso…", "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP para DynDNS", "dyndns_ip_updated": "Endereço IP atualizado com êxito para DynDNS", - "dyndns_key_generating": "A chave DNS está a ser gerada, isto pode demorar um pouco...", - "dyndns_registered": "Dom+inio DynDNS registado com êxito", - "dyndns_registration_failed": "Não foi possível registar o domínio DynDNS: {error}", "dyndns_unavailable": "O domínio '{domain}' não está disponível.", - "extracting": "Extração em curso...", + "extracting": "Extração em curso…", "field_invalid": "Campo inválido '{}'", "firewall_reloaded": "Firewall recarregada com êxito", "installation_complete": "Instalação concluída", - "iptables_unavailable": "Não pode alterar aqui a iptables. Ou o seu kernel não o suporta ou está num espaço reservado.", + "iptables_unavailable": "Não pode alterar aqui a iptables. Ou o seu kernel não o suporta ou está num espaço reservado", "mail_alias_remove_failed": "Não foi possível remover a etiqueta de correio '{mail}'", "mail_domain_unknown": "Domínio de endereço de correio '{domain}' inválido. Por favor, usa um domínio administrado per esse servidor.", "mail_forward_remove_failed": "Não foi possível remover o reencaminhamento de correio '{mail}'", @@ -52,42 +48,42 @@ "pattern_username": "Devem apenas ser carácteres minúsculos alfanuméricos e subtraços", "restore_confirm_yunohost_installed": "Quer mesmo restaurar um sistema já instalado? [{answers}]", "service_add_failed": "Incapaz adicionar serviço '{service}'", - "service_added": "Serviço adicionado com êxito", + "service_added": "Serviço '{service}' adicionado com êxito", "service_already_started": "O serviço '{service}' já está em execussão", "service_already_stopped": "O serviço '{service}' já está parado", "service_cmd_exec_failed": "Incapaz executar o comando '{command}'", - "service_disable_failed": "Incapaz desativar o serviço '{service}'", - "service_disabled": "O serviço '{service}' foi desativado com êxito", - "service_enable_failed": "Incapaz de ativar o serviço '{service}'", - "service_enabled": "Serviço '{service}' ativado com êxito", + "service_disable_failed": "Incapaz desativar o serviço '{service}'\n\nLogs: {logs}", + "service_disabled": "O serviço '{service}' foi desativado com êxito.", + "service_enable_failed": "Incapaz de ativar o serviço '{service}'\n\nLogs: {logs}", + "service_enabled": "Serviço '{service}' ativado com êxito.", "service_remove_failed": "Incapaz de remover o serviço '{service}'", - "service_removed": "Serviço eliminado com êxito", - "service_start_failed": "Não foi possível iniciar o serviço '{service}'", + "service_removed": "Serviço '{service}' eliminado com êxito", + "service_start_failed": "Não foi possível iniciar o serviço '{service}'\n\nLogs: {logs}", "service_started": "O serviço '{service}' foi iniciado com êxito", - "service_stop_failed": "Incapaz parar o serviço '{service}'", + "service_stop_failed": "Incapaz parar o serviço '{service}'\n\nLogs: {logs}", "service_stopped": "O serviço '{service}' foi parado com êxito", "service_unknown": "Serviço desconhecido '{service}'", "ssowat_conf_generated": "Configuração SSOwat gerada com êxito", "system_upgraded": "Sistema atualizado com êxito", "system_username_exists": "O utilizador já existe no registo do sistema", - "unexpected_error": "Ocorreu um erro inesperado", - "updating_apt_cache": "A atualizar a lista de pacotes disponíveis...", + "unexpected_error": "Ocorreu um erro inesperado: {error}", + "updating_apt_cache": "A atualizar a lista de pacotes disponíveis…", "upgrade_complete": "Atualização completa", - "upgrading_packages": "Atualização de pacotes em curso...", + "upgrading_packages": "Atualização de pacotes em curso…", "user_created": "Utilizador criado com êxito", - "user_creation_failed": "Não foi possível criar o utilizador", + "user_creation_failed": "Não foi possível criar o utilizador {user}: {error}", "user_deleted": "Utilizador eliminado com êxito", - "user_deletion_failed": "Incapaz eliminar o utilizador", - "user_unknown": "Utilizador desconhecido", - "user_update_failed": "Não foi possível atualizar o utilizador", + "user_deletion_failed": "Incapaz eliminar o utilizador {user}: {error}", + "user_unknown": "Utilizador desconhecido: {user}", + "user_update_failed": "Não foi possível atualizar o utilizador {user}: {error}", "user_updated": "Utilizador atualizado com êxito", "yunohost_already_installed": "AYunoHost já está instalado", "yunohost_configured": "YunoHost configurada com êxito", - "yunohost_installing": "A instalar a YunoHost...", - "yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'.", + "yunohost_installing": "A instalar a YunoHost…", + "yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'", "app_not_correctly_installed": "{app} parece não estar corretamente instalada", "app_not_properly_removed": "{app} não foi corretamente removido", - "app_requirements_checking": "Verificando os pacotes necessários para {app}...", + "app_requirements_checking": "Verificando os pacotes necessários para {app}…", "app_unsupported_remote_type": "A aplicação não possui suporte ao tipo remoto utilizado", "backup_archive_app_not_found": "Não foi possível encontrar {app} no arquivo de backup", "backup_archive_broken_link": "Não foi possível acessar o arquivo de backup (link quebrado ao {path})", @@ -96,7 +92,7 @@ "backup_cleaning_failed": "Não foi possível limpar o diretório temporário de backup", "backup_creation_failed": "Não foi possível criar o arquivo de backup", "backup_delete_error": "Não foi possível remover '{path}'", - "backup_deleted": "Backup removido", + "backup_deleted": "Backup removido: {name}", "backup_hook_unknown": "O gancho de backup '{hook}' é desconhecido", "backup_nothings_done": "Nada há se salvar", "backup_output_directory_forbidden": "Escolha um diretório de saída diferente. Backups não podem ser criados nos subdiretórios /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", @@ -133,19 +129,19 @@ "app_change_url_success": "A URL agora é {domain}{path}", "apps_catalog_obsolete_cache": "O cache do catálogo de aplicações está vazio ou obsoleto.", "apps_catalog_failed_to_download": "Não foi possível fazer o download do catálogo de aplicações {apps_catalog}: {error}", - "apps_catalog_updating": "Atualizando o catálogo de aplicações...", + "apps_catalog_updating": "Atualizando o catálogo de aplicações…", "apps_catalog_init_success": "Catálogo de aplicações do sistema inicializado!", "apps_already_up_to_date": "Todas as aplicações já estão atualizadas", "app_packaging_format_not_supported": "Essa aplicação não pode ser instalada porque o formato dela não é suportado pela sua versão do YunoHost. Considere atualizar seu sistema.", "app_upgrade_script_failed": "Ocorreu um erro dentro do script de atualização da aplicação", "app_upgrade_several_apps": "As seguintes aplicações serão atualizadas: {apps}", - "app_start_restore": "Restaurando {app}...", - "app_start_backup": "Obtendo os arquivos para fazer o backup de {app}...", - "app_start_remove": "Removendo {app}...", - "app_start_install": "Instalando {app}...", + "app_start_restore": "Restaurando {app}…", + "app_start_backup": "Obtendo os arquivos para fazer o backup de {app}…", + "app_start_remove": "Removendo {app}…", + "app_start_install": "Instalando {app}…", "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_remove_after_failed_install": "Removendo a aplicação após a falha da instalação…", "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", @@ -156,15 +152,15 @@ "app_make_default_location_already_used": "Não foi passível fazer a aplicação '{app}' ser a padrão no domínio, '{domain}' já está sendo usado por '{other_app}'", "backup_archive_writing_error": "Não foi possível adicionar os arquivos '{source}' (nomeados dentro do arquivo '{dest}') ao backup no arquivo comprimido '{archive}'", "backup_archive_corrupted": "Parece que o arquivo de backup '{archive}' está corrompido: {error}", - "backup_archive_cant_retrieve_info_json": "Não foi possível carregar informações para o arquivo '{archive}'... Não foi possível carregar info.json (ou não é um JSON válido).", - "backup_applying_method_copy": "Copiando todos os arquivos para o backup...", - "backup_actually_backuping": "Criando cópia de backup dos arquivos obtidos...", + "backup_archive_cant_retrieve_info_json": "Não foi possível carregar informações para o arquivo '{archive}'… Não foi possível carregar info.json (ou não é um JSON válido).", + "backup_applying_method_copy": "Copiando todos os arquivos para o backup…", + "backup_actually_backuping": "Criando cópia de backup dos arquivos obtidos…", "ask_user_domain": "Domínio para usar para o endereço de email e conta XMPP do usuário", "ask_new_path": "Novo caminho", "ask_new_domain": "Novo domínio", "apps_catalog_update_success": "O catálogo de aplicações foi atualizado!", "backup_no_uncompress_archive_dir": "Não existe tal diretório de arquivo descomprimido", - "backup_mount_archive_for_restore": "Preparando o arquivo para restauração...", + "backup_mount_archive_for_restore": "Preparando o arquivo para restauração…", "backup_method_tar_finished": "Arquivo de backup TAR criado", "backup_method_custom_finished": "Método de backup personalizado '{method}' finalizado", "backup_method_copy_finished": "Cópia de backup finalizada", @@ -179,7 +175,7 @@ "backup_with_no_backup_script_for_app": "A aplicação '{app}' não tem um script de backup. Ignorando.", "backup_unable_to_organize_files": "Não foi possível usar o método rápido de organizar os arquivos no arquivo de backup", "backup_system_part_failed": "Não foi possível fazer o backup da parte do sistema '{part}'", - "backup_running_hooks": "Executando os hooks de backup...", + "backup_running_hooks": "Executando os hooks de backup…", "backup_permission": "Permissão de backup para {app}", "backup_output_symlink_dir_broken": "O diretório de seu arquivo '{path}' é um link simbólico quebrado. Talvez você tenha esquecido de re/montar ou conectar o dispositivo de armazenamento para onde o link aponta.", "backup_output_directory_required": "Você deve especificar um diretório de saída para o backup", @@ -205,7 +201,7 @@ "config_validate_time": "Deve ser um horário válido como HH:MM", "config_validate_url": "Deve ser uma URL válida", "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_basesystem_ynh_inconsistent_versions": "Você está executando versões inconsistentes dos pacotes YunoHost… provavelmente por causa de uma atualização parcial ou que falhou.", "diagnosis_description_basesystem": "Sistema base", "certmanager_cert_signing_failed": "Não foi possível assinar o novo certificado", "certmanager_unable_to_parse_self_CA_name": "Não foi possível processar nome da autoridade de auto-assinatura (arquivo: {file})", @@ -226,20 +222,20 @@ "certmanager_domain_not_diagnosed_yet": "Ainda não há resultado de diagnóstico para o domínio {domain}. Por favor re-execute um diagnóstico para as categorias 'Registros DNS' e 'Web' na seção de diagnósticos para checar se o domínio está pronto para o Let's Encrypt. (Ou, se você souber o que está fazendo, use '--no-checks' para desativar estas checagens.)", "diagnosis_basesystem_host": "O Servidor está rodando Debian {debian_version}", "diagnosis_description_systemresources": "Recursos do sistema", - "certmanager_acme_not_configured_for_domain": "O challenge ACME não pode ser realizado para {domain} porque o código correspondente na configuração do nginx está ausente... Por favor tenha certeza de que sua configuração do nginx está atualizada executando o comando `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "O challenge ACME não pode ser realizado para {domain} porque o código correspondente na configuração do nginx está ausente… Por favor tenha certeza de que sua configuração do nginx está atualizada executando o comando `yunohost tools regen-conf nginx --dry-run --with-diff`.", "certmanager_attempt_to_renew_nonLE_cert": "O certificado para o domínio '{domain}' não foi emitido pelo Let's Encrypt. Não é possível renová-lo automaticamente!", "certmanager_attempt_to_renew_valid_cert": "O certificado para o domínio '{domain}' não esta prestes a expirar! (Você pode usar --force se saber o que está fazendo)", "certmanager_cannot_read_cert": "Algo de errado aconteceu ao tentar abrir o atual certificado para o domínio {domain} (arquivo: {file}), motivo: {reason}", "certmanager_cert_install_success": "Certificado Let's Encrypt foi instalado para o domínio '{domain}'", "certmanager_cert_install_success_selfsigned": "Certificado autoassinado foi instalado para o domínio '{domain}'", - "certmanager_certificate_fetching_or_enabling_failed": "Tentativa de usar o novo certificado para o domínio {domain} não funcionou...", + "certmanager_certificate_fetching_or_enabling_failed": "Tentativa de usar o novo certificado para o domínio {domain} não funcionou…", "certmanager_domain_cert_not_selfsigned": "O certificado para o domínio {domain} não é autoassinado. Você tem certeza que quer substituí-lo? (Use '--force' para fazê-lo)", "certmanager_domain_dns_ip_differs_from_public_ip": "O registro de DNS para o domínio '{domain}' é diferente do IP deste servidor. Por favor cheque a categoria 'Registros DNS' (básico) no diagnóstico para mais informações. Se você modificou recentemente o registro 'A', espere um tempo para ele se propagar (alguns serviços de checagem de propagação de DNS estão disponíveis online). (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)", "certmanager_hit_rate_limit": "Foram emitidos certificados demais para este conjunto de domínios {domain} recentemente. Por favor tente novamente mais tarde. Veja https://letsencrypt.org/docs/rate-limits/ para mais detalhes", "certmanager_no_cert_file": "Não foi possível ler o arquivo de certificado para o domínio {domain} (arquivo: {file})", "certmanager_self_ca_conf_file_not_found": "Não foi possível encontrar o arquivo de configuração para a autoridade de auto-assinatura (arquivo: {file})", - "confirm_app_install_danger": "ATENÇÃO! Sabe-se que esta aplicação ainda é experimental (isso se não que explicitamente não funciona)! Você provavelmente NÃO deve instalar ela a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se esta aplicação não funcionar ou quebrar o seu sistema... Se você está disposto a tomar esse rico de toda forma, digite '{answers}'", - "confirm_app_install_thirdparty": "ATENÇÃO! Essa aplicação não faz parte do catálogo do YunoHost. Instalar aplicações de terceiros pode comprometer a integridade e segurança do seu sistema. Você provavelmente NÃO deve instalá-la a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se este app não funcionar ou quebrar seu sistema... Se você está disposto a tomar este risco de toda forma, digite '{answers}'", + "confirm_app_install_danger": "ATENÇÃO! Sabe-se que esta aplicação ainda é experimental (isso se não que explicitamente não funciona)! Você provavelmente NÃO deve instalar ela a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se esta aplicação não funcionar ou quebrar o seu sistema… Se você está disposto a tomar esse rico de toda forma, digite '{answers}'", + "confirm_app_install_thirdparty": "ATENÇÃO! Essa aplicação não faz parte do catálogo do YunoHost. Instalar aplicações de terceiros pode comprometer a integridade e segurança do seu sistema. Você provavelmente NÃO deve instalá-la a não ser que você saiba o que você está fazendo. NENHUM SUPORTE será fornecido se este app não funcionar ou quebrar seu sistema… Se você está disposto a tomar este risco de toda forma, digite '{answers}'", "diagnosis_description_ports": "Exposição de portas", "diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}", "diagnosis_backports_in_sources_list": "Parece que o apt (o gerenciador de pacotes) está configurado para usar o repositório backport. A não ser que você saiba o que você esteá fazendo, desencorajamos fortemente a instalação de pacotes de backports porque é provável que crie instabilidades ou conflitos no seu sistema.", diff --git a/locales/ru.json b/locales/ru.json index 2c4e703da..09cedd59c 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -18,17 +18,17 @@ "app_not_installed": "{app} не найдено в списке установленных приложений: {all_apps}", "app_not_properly_removed": "{app} удалены неправильно", "app_removed": "{app} удалено", - "app_requirements_checking": "Проверка необходимых пакетов для {app}...", - "app_sources_fetch_failed": "Невозможно получить исходные файлы, проверьте правильность URL", + "app_requirements_checking": "Проверка необходимых пакетов для {app}…", + "app_sources_fetch_failed": "Невозможно получить исходные файлы, проверьте правильность URL?", "app_unknown": "Неизвестное приложение", - "app_upgrade_app_name": "Обновление {app}...", + "app_upgrade_app_name": "Обновление {app}…", "app_upgrade_failed": "Невозможно обновить {app}: {error}", "app_upgrade_some_app_failed": "Некоторые приложения не удалось обновить", "app_upgraded": "{app} обновлено", "installation_complete": "Установка завершена", "password_too_simple_1": "Пароль должен быть не менее 8 символов", "password_listed": "Этот пароль является одним из наиболее часто используемых паролей в мире. Пожалуйста, выберите что-то более уникальное.", - "backup_applying_method_copy": "Копирование всех файлов в резервную копию...", + "backup_applying_method_copy": "Копирование всех файлов в резервную копию…", "domain_dns_conf_is_just_a_recommendation": "Эта страница показывает вам *рекомендуемую* конфигурацию. Она не создаёт для вас конфигурацию DNS. Вы должны сами конфигурировать DNS у вашего регистратора в соответствии с этой рекомендацией.", "good_practices_about_user_password": "Выберите пароль пользователя длиной не менее 8 символов, хотя рекомендуется использовать более длинные (например, парольную фразу) и / или использовать символы различного типа (прописные, строчные буквы, цифры и специальные символы).", "password_too_simple_3": "Пароль должен содержать не менее 8 символов и содержать цифры, заглавные и строчные буквы, а также специальные символы", @@ -44,7 +44,7 @@ "ask_new_domain": "Новый домен", "ask_new_path": "Новый путь", "ask_password": "Пароль", - "app_remove_after_failed_install": "Удаление приложения после сбоя установки...", + "app_remove_after_failed_install": "Удаление приложения после сбоя установки…", "app_upgrade_script_failed": "Внутри скрипта обновления приложения произошла ошибка", "upnp_disabled": "UPnP отключен", "app_manifest_install_ask_domain": "Выберите домен, в котором должно быть установлено это приложение", @@ -56,11 +56,11 @@ "app_full_domain_unavailable": "Извините, это приложение должно быть установлено в собственном домене, но другие приложения уже установлены в домене '{domain}'. Вместо этого вы можете использовать отдельный поддомен для этого приложения.", "app_install_script_failed": "Произошла ошибка в скрипте установки приложения", "apps_catalog_update_success": "Каталог приложений был обновлён!", - "apps_catalog_updating": "Обновление каталога приложений...", - "yunohost_installing": "Установка YunoHost...", - "app_start_remove": "Удаление {app}...", + "apps_catalog_updating": "Обновление каталога приложений…", + "yunohost_installing": "Установка YunoHost…", + "app_start_remove": "Удаление {app}…", "app_label_deprecated": "Эта команда устарела! Пожалуйста, используйте новую команду 'yunohost user permission update', чтобы управлять ярлыком приложения.", - "app_start_restore": "Восстановление {app}...", + "app_start_restore": "Восстановление {app}…", "app_upgrade_several_apps": "Будут обновлены следующие приложения: {apps}", "password_too_simple_2": "Пароль должен содержать не менее 8 символов и включать цифры, заглавные и строчные буквы", "password_too_simple_4": "Пароль должен содержать не менее 12 символов и включать цифры, заглавные и строчные буквы, а также специальные символы", @@ -68,35 +68,35 @@ "user_unknown": "Неизвестный пользователь: {user}", "yunohost_already_installed": "YunoHost уже установлен", "yunohost_configured": "Теперь YunoHost настроен", - "upgrading_packages": "Обновление пакетов...", + "upgrading_packages": "Обновление пакетов…", "app_make_default_location_already_used": "Невозможно сделать '{app}' приложением по умолчанию на домене, '{domain}' уже используется '{other_app}'", "app_config_unable_to_apply": "Не удалось применить значения панели конфигурации.", "app_config_unable_to_read": "Не удалось прочитать значения панели конфигурации.", "app_install_failed": "Невозможно установить {app}: {error}", "apps_catalog_init_success": "Система каталога приложений инициализирована!", "backup_abstract_method": "Этот метод резервного копирования еще не реализован", - "backup_actually_backuping": "Создание резервного архива из собранных файлов...", - "backup_applying_method_custom": "Вызов пользовательского метода резервного копирования {method}'...", + "backup_actually_backuping": "Создание резервного архива из собранных файлов…", + "backup_applying_method_custom": "Вызов пользовательского метода резервного копирования {method}'…", "backup_archive_app_not_found": "Не удалось найти {app} в резервной копии", - "backup_applying_method_tar": "Создание резервной копии в TAR-архиве...", + "backup_applying_method_tar": "Создание резервной копии в TAR-архиве…", "backup_archive_broken_link": "Не удалось получить доступ к резервной копии (неправильная ссылка {path})", "apps_catalog_failed_to_download": "Невозможно загрузить каталог приложений {apps_catalog}: {error}", "apps_catalog_obsolete_cache": "Кэш каталога приложений пуст или устарел.", - "backup_archive_cant_retrieve_info_json": "Не удалось загрузить информацию об архиве '{archive}'... info.json не может быть получен (или не является корректным json).", + "backup_archive_cant_retrieve_info_json": "Не удалось загрузить информацию об архиве '{archive}'… info.json не может быть получен (или не является корректным json).", "app_packaging_format_not_supported": "Это приложение не может быть установлено, поскольку его формат не поддерживается вашей версией YunoHost. Возможно, вам следует обновить систему.", "app_restore_failed": "Не удалось восстановить {app}: {error}", "app_restore_script_failed": "Произошла ошибка внутри сценария восстановления приложения", "ask_user_domain": "Домен, используемый для адреса электронной почты пользователя и учетной записи XMPP", "app_not_upgraded": "Не удалось обновить приложение '{failed_app}', и, как следствие, обновление следующих приложений было отменено: {apps}", - "app_start_backup": "Сбор файлов для резервного копирования {app}...", - "app_start_install": "Устанавливается {app}...", + "app_start_backup": "Сбор файлов для резервного копирования {app}…", + "app_start_install": "Устанавливается {app}…", "backup_app_failed": "Не удалось создать резервную копию {app}", "backup_archive_name_exists": "Резервная копия с таким именем уже существует.", "backup_archive_name_unknown": "Неизвестный локальный архив резервного копирования с именем '{name}'", "backup_archive_open_failed": "Не удалось открыть архив резервной копии", "backup_archive_corrupted": "Похоже, что архив резервной копии '{archive}' поврежден : {error}", "certmanager_cert_install_success_selfsigned": "Самоподписанный сертификат для домена '{domain}' установлен", - "backup_created": "Создана резервная копия", + "backup_created": "Создана резервная копия: {name}", "config_unknown_filter_key": "Ключ фильтра '{filter_key}' неверен.", "config_validate_date": "Должна быть правильная дата в формате YYYY-MM-DD", "config_validate_email": "Должен быть правильный email", @@ -106,8 +106,8 @@ "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": "Должна быть правильная ссылка", - "confirm_app_install_danger": "ОПАСНО! Это приложение все еще является экспериментальным (если не сказать, что оно явно не работает)! Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку... Если вы все равно готовы рискнуть, введите '{answers}'", - "confirm_app_install_thirdparty": "ВАЖНО! Это приложение не входит в каталог приложений YunoHost. Установка сторонних приложений может нарушить целостность и безопасность вашей системы. Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку... Если вы все равно готовы рискнуть, введите '{answers}'", + "confirm_app_install_danger": "ОПАСНО! Это приложение все еще является экспериментальным (если не сказать, что оно явно не работает)! Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку… Если вы все равно готовы рискнуть, введите '{answers}'", + "confirm_app_install_thirdparty": "ВАЖНО! Это приложение не входит в каталог приложений YunoHost. Установка сторонних приложений может нарушить целостность и безопасность вашей системы. Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку… Если вы все равно готовы рискнуть, введите '{answers}'", "config_apply_failed": "Не удалось применить новую конфигурацию: {error}", "config_cant_set_value_on_section": "Вы не можете установить одно значение на весь раздел конфигурации.", "config_forbidden_keyword": "Ключевое слово '{keyword}' зарезервировано, вы не можете создать или использовать панель конфигурации с вопросом с таким id.", @@ -119,11 +119,11 @@ "backup_creation_failed": "Не удалось создать резервную копию", "backup_csv_addition_failed": "Не удалось добавить файлы для резервного копирования в CSV-файл", "backup_csv_creation_failed": "Не удалось создать CSV-файл, необходимый для восстановления", - "backup_deleted": "Резервная копия удалена", + "backup_deleted": "Резервная копия удалена: {name}", "backup_delete_error": "Не удалось удалить '{path}'", "backup_method_copy_finished": "Создание копии бэкапа завершено", "backup_method_tar_finished": "Создан резервный TAR-архив", - "backup_mount_archive_for_restore": "Подготовка архива для восстановления...", + "backup_mount_archive_for_restore": "Подготовка архива для восстановления…", "backup_method_custom_finished": "Пользовательский метод резервного копирования '{method}' завершен", "backup_nothings_done": "Нечего сохранять", "backup_output_directory_required": "Вы должны выбрать каталог для сохранения резервной копии", @@ -153,7 +153,7 @@ "certmanager_cannot_read_cert": "При попытке открыть текущий сертификат для домена {domain}что-то пошло не так (файл: {file}), причина: {reason}", "certmanager_cert_install_success": "Сертификат Let's Encrypt для домена '{domain}' установлен", "certmanager_domain_cert_not_selfsigned": "Сертификат для домена {domain} не самоподписанный. Вы уверены, что хотите заменить его? (Для этого используйте '--force'.)", - "certmanager_certificate_fetching_or_enabling_failed": "Попытка использовать новый сертификат для {domain} не сработала...", + "certmanager_certificate_fetching_or_enabling_failed": "Попытка использовать новый сертификат для {domain} не сработала…", "certmanager_domain_http_not_working": "Похоже, домен {domain} не доступен через HTTP. Пожалуйста, проверьте категорию 'Домены' в диагностике для получения дополнительной информации. (Если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", "certmanager_hit_rate_limit": "Для этого набора доменов {domain} в последнее время было выпущено слишком много сертификатов. Пожалуйста, повторите попытку позже. См. https://letsencrypt.org/docs/rate-limits/ для получения более подробной информации", "certmanager_no_cert_file": "Не удалось прочитать файл сертификата для домена {domain} (файл: {file})", @@ -166,7 +166,7 @@ "diagnosis_description_services": "Проверка статусов сервисов", "config_validate_color": "Должен быть правильный hex цвета RGB", "diagnosis_basesystem_hardware": "Аппаратная архитектура сервера – {virt} {arch}", - "certmanager_acme_not_configured_for_domain": "Задача ACME не может быть запущена для {domain} прямо сейчас, потому что в его nginx conf отсутствует соответствующий фрагмент кода... Пожалуйста, убедитесь, что конфигурация вашего nginx обновлена, используя 'yunohost tools regen-conf nginx --dry-run --with-diff'.", + "certmanager_acme_not_configured_for_domain": "Задача ACME не может быть запущена для {domain} прямо сейчас, потому что в его nginx conf отсутствует соответствующий фрагмент кода… Пожалуйста, убедитесь, что конфигурация вашего nginx обновлена, используя 'yunohost tools regen-conf nginx --dry-run --with-diff'.", "diagnosis_basesystem_ynh_single_version": "{package} версия: {version} ({repo})", "diagnosis_description_mail": "Электронная почта", "diagnosis_basesystem_kernel": "Версия ядра Linux на сервере {kernel_version}", @@ -199,7 +199,7 @@ "domain_deleted": "Домен удален", "backup_custom_backup_error": "Пользовательский метод резервного копирования не смог пройти этап 'backup'", "diagnosis_apps_outdated_ynh_requirement": "Установленная версия этого приложения требует только yunohost >= 2.x, что указывает на то, что оно не соответствует рекомендуемым практикам упаковки и помощникам. Вам следует рассмотреть возможность его обновления.", - "diagnosis_basesystem_ynh_inconsistent_versions": "Вы используете несовместимые версии пакетов YunoHost... скорее всего, из-за неудачного или частичного обновления.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Вы используете несовместимые версии пакетов YunoHost… скорее всего, из-за неудачного или частичного обновления.", "diagnosis_failed_for_category": "Не удалось провести диагностику для категории '{category}': {error}", "diagnosis_cache_still_valid": "(Кэш еще действителен для диагностики {category}. Повторная диагностика пока не проводится!)", "diagnosis_cant_run_because_of_dep": "Невозможно выполнить диагностику для {category}, пока есть важные проблемы, связанные с {dep}.", @@ -224,7 +224,7 @@ "permission_protected": "Разрешение {permission} защищено. Вы не можете добавить или удалить группу посетителей в/из этого разрешения.", "log_domain_config_set": "Обновление конфигурации для домена '{}'", "log_domain_dns_push": "Сделать DNS-записи для домена '{}'", - "other_available_options": "... и {n} других не показанных доступных опций", + "other_available_options": "… и {n} других не показанных доступных опций", "permission_cannot_remove_main": "Удаление основного разрешения не допускается", "permission_require_account": "Разрешение {permission} имеет смысл только для пользователей, имеющих учетную запись, и поэтому не может быть включено для посетителей.", "permission_update_failed": "Не удалось обновить разрешение '{permission}': {error}", @@ -234,7 +234,7 @@ "log_dyndns_subscribe": "Подписаться на субдомен YunoHost '{}'", "pattern_firstname": "Должно быть настоящее имя", "migrations_pending_cant_rerun": "Эти миграции еще не завершены, поэтому не могут быть запущены снова: {ids}", - "migrations_running_forward": "Запуск миграции {id}...", + "migrations_running_forward": "Запуск миграции {id}…", "regenconf_file_backed_up": "Файл конфигурации '{conf}' сохранен в '{backup}'", "regenconf_file_copy_failed": "Не удалось скопировать новый файл конфигурации '{new}' в '{conf}'", "regenconf_file_manually_modified": "Конфигурационный файл '{conf}' был изменен вручную и не будет обновлен", @@ -274,7 +274,7 @@ "log_domain_main_domain": "Сделать '{}' основным доменом", "diagnosis_sshd_config_insecure": "Похоже, что конфигурация SSH была изменена вручную, и она небезопасна, поскольку не содержит директив 'AllowGroups' или 'AllowUsers' для ограничения доступа авторизованных пользователей.", "group_already_exist_on_system": "Группа {group} уже существует в системных группах", - "group_already_exist_on_system_but_removing_it": "Группа {group} уже существует в системных группах, но YunoHost удалит ее...", + "group_already_exist_on_system_but_removing_it": "Группа {group} уже существует в системных группах, но YunoHost удалит ее…", "group_unknown": "Группа '{group}' неизвестна", "log_app_action_run": "Запуск действия приложения '{}'", "log_available_on_yunopaste": "Эти логи теперь доступны через {url}", @@ -286,7 +286,7 @@ "invalid_regex": "Неверный regex:'{regex}'", "regenconf_file_manually_removed": "Конфигурационный файл '{conf}' был удален вручную и не будет создан", "migrations_not_pending_cant_skip": "Эти миграции не ожидаются, поэтому не могут быть пропущены: {ids}", - "migrations_skip_migration": "Пропуск миграции {id}...", + "migrations_skip_migration": "Пропуск миграции {id}…", "invalid_number": "Должна быть цифра", "regenconf_failed": "Не удалось восстановить конфигурацию для категории(й): {categories}", "diagnosis_services_conf_broken": "Конфигурация нарушена для службы {service}!", @@ -312,10 +312,10 @@ "pattern_mailbox_quota": "Должен быть размер с суффиксом b/k/M/G/T или 0, что значит без ограничений", "permission_already_disallowed": "У группы '{group}' уже отключено разрешение '{permission}'", "permission_creation_failed": "Не удалось создать разрешение '{permission}': {error}", - "regenconf_pending_applying": "Применение ожидающей конфигурации для категории '{category}'...", + "regenconf_pending_applying": "Применение ожидающей конфигурации для категории '{category}'…", "user_updated": "Информация о пользователе изменена", "regenconf_need_to_explicitly_specify_ssh": "Конфигурация ssh была изменена вручную, но Вам нужно явно указать категорию 'ssh' с --force, чтобы применить изменения.", - "ldap_server_is_down_restart_it": "Служба LDAP не работает, попытайтесь перезапустить ее...", + "ldap_server_is_down_restart_it": "Служба LDAP не работает, попытайтесь перезапустить ее…", "permission_already_up_to_date": "Разрешение не было обновлено, потому что запросы на добавление/удаление уже соответствуют текущему состоянию.", "group_cannot_edit_primary_group": "Группа '{group}' не может быть отредактирована вручную. Это основная группа, предназначенная для содержания только одного конкретного пользователя.", "log_app_remove": "Удалите приложение '{}'", @@ -325,5 +325,20 @@ "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_allow_ipv6_help": "Разрешить использование IPv6 для получения и отправки почты", + "admins": "Администраторы", + "all_users": "Все пользователи YunoHost", + "app_action_failed": "Не удалось выполнить действие {action} для приложения {app}", + "app_manifest_install_ask_init_main_permission": "Кто должен иметь доступ к этому приложению? (Это может быть изменено позже)", + "app_arch_not_supported": "Это приложение может быть установлено только на архитектуры {required}, но архитектура вашего сервер - {current}", + "app_manifest_install_ask_init_admin_permission": "Кто должен иметь доступ к функциям для администраторов этого приложения? (Это может быть изменено позже)", + "app_change_url_script_failed": "Произошла ошибка внутри скрипта смены URL", + "app_corrupt_source": "YunoHost смог скачать материал «{source_id}» ({url}) для {app}, но материал не соотвествует с ожидаемой контрольной суммой. Это может означать, что на ваше сервере произошла временная сетевая ошибка, ИЛИ материал был каким-либо образом изменён сопровождающим главной ветки (или злоумышленником?) и упаковщикам YunoHost нужно выяснить и, возможно, обновить манифест, чтобы применить изменения.\n Ожидаемая контрольная сумма sha256: {expected_sha256}\n Полученная контрольная сумма sha256: {computed_sha256}\n Размер скачанного файла: {size}", + "app_not_enough_ram": "Это приложение требует {required} ОЗУ для установки/обновления, но сейчас доступно только {current}.", + "app_change_url_failed": "Невозможно изменить URL для {app}: {error}", + "app_not_enough_disk": "Это приложение требует {required} свободного места.", + "app_change_url_require_full_domain": "{app} не может быть перемещено на данный URL, потому что оно требует весь домен (т.е., путь - /)", + "app_failed_to_download_asset": "Не удалось скачать материал «{source_id}» ({url}) для {app}: {out}", + "app_failed_to_upgrade_but_continue": "Не удалось обновить приложение {failed_app}, обновления продолжаются, как запрошено. Выполните «yunohost log show {operation_logger_name}», чтобы увидеть журнал ошибки", + "app_not_upgraded_broken_system": "Не удалось обновить приложение «{failed_app}», система находится в сломанном состоянии, обновления следующих приложений были отменены: {apps}" } \ No newline at end of file diff --git a/locales/sk.json b/locales/sk.json index bead46713..a58b1f960 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -30,7 +30,7 @@ "app_manifest_install_ask_domain": "Vyberte doménu, kam bude táto aplikácia nainštalovaná", "app_manifest_install_ask_is_public": "Má byť táto aplikácia viditeľná pre anonymných návštevníkov?", "app_manifest_install_ask_password": "Vyberte heslo pre správu tejto aplikácie", - "app_manifest_install_ask_path": "Vyberte cestu adresy URL (po názve domény), kde bude táto aplikácia nainštalovaná", + "app_manifest_install_ask_path": "Vyberte cestu adresy URL (po názve domény), kam bude táto aplikácia nainštalovaná", "app_not_correctly_installed": "Zdá sa, že {app} nie je správne nainštalovaná", "app_not_properly_removed": "{app} nebola správne odstránená", "app_packaging_format_not_supported": "Túto aplikáciu nie je možné nainštalovať, pretože formát balíčkov, ktorý používa, nie je podporovaný Vašou verziou YunoHost. Mali by ste zvážiť aktualizovanie Vášho systému.", @@ -87,14 +87,14 @@ "backup_copying_to_organize_the_archive": "Kopírujem {size} MB kvôli preusporiadaniu archívu", "backup_couldnt_bind": "Nepodarilo sa previazať {src} s {dest}.", "backup_create_size_estimation": "Archív bude obsahovať približne {size} údajov.", - "backup_created": "Záloha bola vytvorená", + "backup_created": "Záloha bola vytvorená: {name}", "backup_creation_failed": "Nepodarilo sa vytvoriť archív so zálohou", "backup_csv_addition_failed": "Do CSV súboru sa nepodarilo pridať súbory na zálohovanie", "backup_csv_creation_failed": "Nepodarilo sa vytvoriť súbor CSV potrebný pre obnovu zo zálohy", "backup_custom_backup_error": "Vlastná metóda zálohovania sa nedostala za krok 'záloha'", "backup_custom_mount_error": "Vlastná metóda zálohovania sa nedostala za krok 'pripojenie'", "backup_delete_error": "Nepodarilo sa odstrániť '{path}'", - "backup_deleted": "Záloha bola odstránená", + "backup_deleted": "Záloha bola odstránená: {name}", "backup_method_copy_finished": "Dokončené kopírovanie zálohy", "backup_method_custom_finished": "Vlastná metóda zálohovania '{method}' skončila", "backup_method_tar_finished": "Bol vytvorený TAR archív so zálohou", @@ -114,7 +114,7 @@ "certmanager_attempt_to_replace_valid_cert": "Chystáte sa prepísať správny a platný certifikát pre doménu {domain}! (Použite --force na vynútenie)", "certmanager_cannot_read_cert": "Počas otvárania aktuálneho certifikátu pre doménu {domain} došlo k neznámej chybe (súbor: {file}), príčina: {reason}", "certmanager_cert_install_success": "Pre doménu '{domain}' bol práve nainštalovaný certifikát od Let's Encrypt", - "certmanager_cert_install_success_selfsigned": "Pre doménu '{domain}' bol práve nainštalovaný vlastnoručne podpísany (self-signed) certifikát", + "certmanager_cert_install_success_selfsigned": "Pre doménu '{domain}' bol práve nainštalovaný vlastnoručne podpísaný (self-signed) certifikát", "certmanager_cert_renew_success": "Certifikát od Let's Encrypt pre doménu '{domain}' bol úspešne obnovený", "certmanager_cert_signing_failed": "Nepodarilo sa podpísať nový certifikát", "certmanager_domain_cert_not_selfsigned": "Certifikát pre doménu {domain} nie je vlastnoručne podpísaný (self-signed). Naozaj ho chcete nahradiť? (Použite '--force', ak to chcete urobiť.)", @@ -215,11 +215,11 @@ "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_no_ipv6_tip": "Váš server bude fungovať aj bez IPv6, no pre celkové zdravie internetu je lepšie ho nastaviť. V prípade, že je IPv6 dostupné, systém alebo váš poskytovateľ by ho mal automaticky nakonfigurovať. V opačnom prípade budete možno musieť nastaviť zopár vecí ručne tak, ako je vysvetlené v dokumentácii na https://yunohost.org/ipv6. Ak nemôžete povoliť IPv6 alebo je to na vás príliš technicky náročné, môžete pokojne toto upozornenie ignorovať.", "diagnosis_ip_broken_dnsresolution": "Zdá sa, že z nejakého dôvodu nefunguje prekladanie názvov domén… Blokuje vaša brána firewall DNS požiadavky?", "diagnosis_ip_broken_resolvconf": "Zdá sa, že na vašom serveri nefunguje prekladanie názvov domén, čo môže súvisieť s tým, že /etc/resolv.conf neukazuje na 127.0.0.1.", - "diagnosis_ip_connected_ipv4": "Server nie je pripojený k internetu prostredníctvom IPv4!", - "diagnosis_ip_connected_ipv6": "Server nie je pripojený k internetu prostredníctvom IPv6!", + "diagnosis_ip_connected_ipv4": "Server je pripojený k internetu prostredníctvom IPv4!", + "diagnosis_ip_connected_ipv6": "Server je pripojený k internetu prostredníctvom IPv6!", "diagnosis_ip_dnsresolution_working": "Preklad názvov domén nefunguje!", "diagnosis_ip_global": "Globálna IP adresa: {global}", "diagnosis_ip_local": "Miestna IP adresa: {local}", @@ -230,7 +230,7 @@ "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_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ý.", @@ -259,5 +259,24 @@ "app_change_url_script_failed": "Vo skripte na zmenu URL adresy sa vyskytla chyba", "app_not_enough_disk": "Táto aplikácia vyžaduje {required} voľného miesta.", "app_not_enough_ram": "Táto aplikácia vyžaduje {required} pamäte na inštaláciu/aktualizáciu, ale k dispozícii je momentálne iba {current}.", - "apps_failed_to_upgrade": "Nasledovné aplikácie nebolo možné aktualizovať: {apps}" -} + "apps_failed_to_upgrade": "Nasledovné aplikácie nebolo možné aktualizovať: {apps}", + "global_settings_setting_security_experimental_enabled": "Experimentálne bezpečnostné funkcie", + "global_settings_setting_security_experimental_enabled_help": "Povoliť experimentálne bezpečnostné funkcie (nezapínajte túto možnosť, ak neviete, čo môže spôsobiť!)", + "service_description_rspamd": "Filtruje spam a iné funkcie týkajúce sa e-mailu", + "log_letsencrypt_cert_renew": "Obnoviť '{}' certifikát Let's Encrypt", + "domain_config_cert_summary_selfsigned": "UPOZORNENIE: Aktuálny certifikát je vlastnoručne podpísaný. Prehliadače budú návštevníkom zobrazovať strašidelné varovanie!", + "global_settings_setting_ssowat_panel_overlay_enabled": "Povoliť malú štvorcovú ikonu portálu „YunoHost“ na aplikáciach", + "domain_config_mail_out": "Odchádzajúce e-maily", + "domain_config_default_app": "Predvolená aplikácia", + "domain_config_xmpp_help": "Pozor: niektoré funkcie XMPP vyžadujú aktualizáciu vašich DNS záznamov a obnovenie Lets Encrypt certifikátu pred tým, ako je ich možné zapnúť", + "domain_config_default_app_help": "Návštevníci budú pri návšteve tejto domény automaticky presmerovaní na túto doménu. Ak nenastavíte žiadnu aplikáciu, zobrazí sa stránka s prihlasovacím formulárom na portál.", + "registrar_infos": "Informácie o registrátorovi", + "domain_dns_registrar_managed_in_parent_domain": "Táto doména je subdoména {parent_domain_link}. Nastavenie DNS registrátora je spravovaná v konfiguračnom paneli {parent_domain}.", + "log_letsencrypt_cert_install": "Inštalovať certifikát Let's Encrypt na doménu '{}'", + "domain_config_cert_no_checks": "Ignorovať kontroly diagnostiky", + "domain_config_cert_install": "Nainštalovať certifikát Let's Encrypt", + "domain_config_mail_in": "Prichádzajúce e-maily", + "domain_config_cert_summary": "Stav certifikátu", + "domain_config_xmpp": "Krátke správy (XMPP)", + "log_app_makedefault": "Nastaviť '{}' ako predvolenú aplikáciu" +} \ No newline at end of file diff --git a/locales/te.json b/locales/te.json index 7a06f88ef..534a3860f 100644 --- a/locales/te.json +++ b/locales/te.json @@ -18,12 +18,12 @@ "app_install_script_failed": "యాప్ ఇన్‌స్టాలేషన్ స్క్రిప్ట్‌లో లోపం సంభవించింది", "app_manifest_install_ask_domain": "ఈ యాప్‌ను ఇన్‌స్టాల్ చేయాల్సిన డొమైన్‌ను ఎంచుకోండి", "app_manifest_install_ask_password": "ఈ యాప్‌కు అడ్మినిస్ట్రేషన్ పాస్‌వర్డ్‌ను ఎంచుకోండి", - "app_not_installed": "ఇన్‌స్టాల్ చేసిన యాప్‌ల జాబితాలో {app}ని కనుగొనడం సాధ్యపడలేదు: {all apps}", + "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_start_backup": "{app} కోసం బ్యాకప్ చేయాల్సిన ఫైల్‌లను సేకరిస్తోంది…", + "app_start_install": "{app}ని ఇన్‌స్టాల్ చేస్తోంది…", + "app_start_restore": "{app}ని పునరుద్ధరిస్తోంది…", "app_unknown": "తెలియని యాప్", "app_upgrade_failed": "అప్‌గ్రేడ్ చేయడం సాధ్యపడలేదు {app}: {error}", "app_manifest_install_ask_admin": "ఈ యాప్ కోసం నిర్వాహక వినియోగదారుని ఎంచుకోండి", @@ -34,11 +34,11 @@ "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_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_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 1af0ffd54..e51aa5efa 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -15,5 +15,10 @@ "additional_urls_already_added": "Ek URL '{url}' zaten '{permission}' izni için ek URL'ye eklendi", "additional_urls_already_removed": "Ek URL '{url}', '{permission}' izni için ek URL'de zaten kaldırıldı", "app_action_cannot_be_ran_because_required_services_down": "Bu eylemi gerçekleştirmek için şu servisler çalışıyor olmalıdır: {services}. Devam etmek için onları yeniden başlatın (ve muhtemelen neden çalışmadığını araştırın).", - "app_arch_not_supported": "Bu uygulama yalnızca {required} işlemci mimarisi üzerine kurulabilir ancak sunucunuzun işlemci mimarisi {current}." + "app_arch_not_supported": "Bu uygulama yalnızca {required} işlemci mimarisi üzerine kurulabilir ancak sunucunuzun işlemci mimarisi {current}", + "app_argument_choice_invalid": "'{name}'' için geçerli bir değer giriniz '{value}' mevcut seçimlerin arasında değil ({choices})", + "app_change_url_failed": "{app}: {error} için url değiştirilemedi", + "app_argument_required": "'{name}' değeri gerekli", + "app_argument_invalid": "'{name}': {error} için geçerli bir değer giriniz", + "app_argument_password_no_default": "'{name}': çözümlenirken bir hata meydana geldi. Parola argümanı güvenlik nedeniyle varsayılan değer alamaz" } \ No newline at end of file diff --git a/locales/uk.json b/locales/uk.json index fca0ea360..04640d1b4 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -23,8 +23,8 @@ "app_action_cannot_be_ran_because_required_services_down": "Для виконання цієї дії повинні бути запущені наступні необхідні служби: {services}. Спробуйте перезапустити їх, щоб продовжити (і, можливо, з'ясувати, чому вони не працюють).", "already_up_to_date": "Нічого не потрібно робити. Все вже актуально.", "admin_password": "Пароль адмініструванні", - "additional_urls_already_removed": "Додаткова URL-адреса '{url}' вже видалена в додатковій URL-адресі для дозволу '{permission}'", - "additional_urls_already_added": "Додаткова URL-адреса '{url}' вже додана в додаткову URL-адресу для дозволу '{permission}'", + "additional_urls_already_removed": "Додаткову URL-адресу '{url}' вже видалено для дозволу '{permission}'", + "additional_urls_already_added": "Додаткову URL-адресу '{url}' вже додано для дозволу '{permission}'", "action_invalid": "Неприпустима дія '{action}'", "aborting": "Переривання.", "diagnosis_description_web": "Мережа", @@ -69,8 +69,8 @@ "restore_running_app_script": "Відновлення застосунку '{app}'…", "restore_removing_tmp_dir_failed": "Неможливо видалити старий тимчасовий каталог", "restore_nothings_done": "Нічого не було відновлено", - "restore_not_enough_disk_space": "Недостатньо місця (простір: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin: d} Б)", - "restore_may_be_not_enough_disk_space": "Схоже, у вашій системі недостатньо місця (вільно: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin: d} Б)", + "restore_not_enough_disk_space": "Недостатньо місця (простір: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin} Б)", + "restore_may_be_not_enough_disk_space": "Схоже, у вашій системі недостатньо місця (вільно: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin} Б)", "restore_hook_unavailable": "Скрипт відновлення для '{part}' недоступний у вашій системі і в архіві його теж немає", "restore_failed": "Не вдалося відновити систему", "restore_extracting": "Витягнення необхідних файлів з архіву…", @@ -83,7 +83,7 @@ "regex_with_only_domain": "Ви не можете використовувати regex для домену, тільки для шляху", "regex_incompatible_with_tile": "/! \\ Packagers! Дозвіл '{permission}' має значення show_tile 'true', тому ви не можете визначити regex URL в якості основної URL", "regenconf_need_to_explicitly_specify_ssh": "Конфігурація ssh була змінена вручну, але вам потрібно явно вказати категорію 'ssh' з --force, щоб застосувати зміни.", - "regenconf_pending_applying": "Застосування очікує конфігурації для категорії '{category}'...", + "regenconf_pending_applying": "Застосування очікує конфігурації для категорії '{category}'…", "regenconf_failed": "Не вдалося відновити конфігурацію для категорії (категорій): {categories}", "regenconf_dry_pending_applying": "Перевірка очікує конфігурації, яка була б застосована для категорії '{category}'…", "regenconf_would_be_updated": "Конфігурація була б оновлена для категорії '{category}'", @@ -138,8 +138,8 @@ "not_enough_disk_space": "Недостатньо вільного місця на '{path}'", "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадмініструванні або виконайте команду `yunohost tools migrations run`.", "migrations_success_forward": "Міграцію {id} завершено", - "migrations_skip_migration": "Пропускання міграції {id}...", - "migrations_running_forward": "Виконання міграції {id}...", + "migrations_skip_migration": "Пропускання міграції {id}…", + "migrations_running_forward": "Виконання міграції {id}…", "migrations_pending_cant_rerun": "Наступні міграції ще не завершені, тому не можуть бути запущені знову: {ids}", "migrations_not_pending_cant_skip": "Наступні міграції не очікують виконання, тому не можуть бути пропущені: {ids}", "migrations_no_such_migration": "Не існує міграції під назвою '{id}'", @@ -147,14 +147,14 @@ "migrations_need_to_accept_disclaimer": "Щоб запустити міграцію {id}, ви повинні прийняти наступну відмову від відповідальності:\n---\n{disclaimer}\n---\nЯкщо ви згодні запустити міграцію, будь ласка, повторіть команду з опцією '--accept-disclaimer'.", "migrations_must_provide_explicit_targets": "Ви повинні вказати явні цілі при використанні '--skip' або '--force-rerun'", "migrations_migration_has_failed": "Міграція {id} не завершена, перериваємо. Помилка: {exception}", - "migrations_loading_migration": "Завантаження міграції {id}...", + "migrations_loading_migration": "Завантаження міграції {id}…", "migrations_list_conflict_pending_done": "Ви не можете одночасно використовувати '--previous' і '--done'.", "migrations_exclusive_options": "'--auto', '--skip', і '--force-rerun' є взаємовиключними опціями.", "migrations_failed_to_load_migration": "Не вдалося завантажити міграцію {id}: {error}", "migrations_dependencies_not_satisfied": "Запустіть ці міграції: '{dependencies_id}', перед міграцією {id}.", "migrations_already_ran": "Наступні міграції вже виконано: {ids}", "migration_ldap_rollback_success": "Система відкотилася.", - "migration_ldap_migration_failed_trying_to_rollback": "Не вдалося виконати міграцію... Пробуємо відкотити систему.", + "migration_ldap_migration_failed_trying_to_rollback": "Не вдалося виконати міграцію… Пробуємо відкотити систему.", "migration_ldap_can_not_backup_before_migration": "Не вдалося завершити резервне копіювання системи перед невдалою міграцією. Помилка: {error}", "migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і налаштування застосунків перед фактичною міграцією.", "main_domain_changed": "Основний домен було змінено", @@ -230,11 +230,11 @@ "group_cannot_edit_all_users": "Група 'all_users' не може бути відредагована вручну. Це спеціальна група, призначена для всіх користувачів, зареєстрованих в YunoHost", "group_creation_failed": "Не вдалося створити групу '{group}': {error}", "group_created": "Групу '{group}' створено", - "group_already_exist_on_system_but_removing_it": "Група {group} вже існує в групах системи, але YunoHost вилучить її...", + "group_already_exist_on_system_but_removing_it": "Група {group} вже існує в групах системи, але YunoHost вилучить її…", "group_already_exist_on_system": "Група {group} вже існує в групах системи", "group_already_exist": "Група {group} вже існує", "good_practices_about_user_password": "Зараз ви збираєтеся поставити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", - "good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрування. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).", + "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-ретрансляції", @@ -244,20 +244,17 @@ "firewall_reload_failed": "Не вдалося перезавантажити фаєрвол", "file_does_not_exist": "Файл {path} не існує.", "field_invalid": "Неприпустиме поле '{}'", - "extracting": "Витягнення...", + "extracting": "Витягнення…", "dyndns_unavailable": "Домен '{domain}' недоступний.", "dyndns_domain_not_provided": "DynDNS провайдер {provider} не може надати домен {domain}.", - "dyndns_registration_failed": "Не вдалося зареєструвати домен DynDNS: {error}", - "dyndns_registered": "Домен DynDNS зареєстровано", "dyndns_provider_unreachable": "Неможливо зв'язатися з провайдером DynDNS {provider}: або ваш YunoHost неправильно під'єднано до Інтернету, або сервер dynette не працює.", "dyndns_no_domain_registered": "Домен не зареєстровано в DynDNS", "dyndns_key_not_found": "DNS-ключ для домену не знайдено", - "dyndns_key_generating": "Утворення DNS-ключа... Це може зайняти деякий час.", "dyndns_ip_updated": "Вашу IP-адресу в DynDNS оновлено", "dyndns_ip_update_failed": "Не вдалося оновити IP-адресу в DynDNS", "dyndns_could_not_check_available": "Не вдалося перевірити, чи {domain} доступний у {provider}.", "dpkg_lock_not_available": "Ця команда не може бути виконана прямо зараз, тому що інша програма, схоже, використовує блокування dpkg (системного менеджера пакетів)", - "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, під'єднавшись через SSH і виконавши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`та/або `sudo dpkg --audit`.", + "dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані… Ви можете спробувати вирішити цю проблему, під'єднавшись через SSH і виконавши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`та/або `sudo dpkg --audit`.", "downloading": "Завантаження…", "done": "Готово", "domains_available": "Доступні домени:", @@ -265,7 +262,6 @@ "domain_remove_confirm_apps_removal": "Вилучення цього домену призведе до вилучення таких застосунків:\n{apps}\n\nВи впевнені, що хочете це зробити? [{answers}]", "domain_hostname_failed": "Неможливо встановити нову назву хоста. Це може викликати проблеми в подальшому (можливо, все буде в порядку).", "domain_exists": "Цей домен уже існує", - "domain_dyndns_root_unknown": "Невідомий кореневий домен DynDNS", "domain_dyndns_already_subscribed": "Ви вже підписалися на домен DynDNS", "domain_dns_conf_is_just_a_recommendation": "Ця команда показує *рекомендовану* конфігурацію. Насправді вона не встановлює конфігурацію DNS для вас. Ви самі повинні налаштувати свою зону DNS у реєстратора відповідно до цих рекомендацій.", "domain_deletion_failed": "Неможливо видалити домен {domain}: {error}", @@ -273,7 +269,7 @@ "domain_creation_failed": "Неможливо створити домен {domain}: {error}", "domain_created": "Домен створено", "domain_cert_gen_failed": "Не вдалося утворити сертифікат", - "domain_cannot_remove_main_add_new_one": "Ви не можете видалити '{domain}', так як це основний домен і ваш єдиний домен, вам потрібно спочатку додати інший домен за допомогою 'yunohost domain add ', потім встановити його як основний домен за допомогою 'yunohost domain main-domain -n ' і потім ви можете вилучити домен '{domain}' за допомогою 'yunohost domain remove {domain}'.'", + "domain_cannot_remove_main_add_new_one": "Ви не можете видалити '{domain}', так як це основний домен і ваш єдиний домен, вам потрібно спочатку додати інший домен за допомогою 'yunohost domain add ', потім встановити його як основний домен за допомогою 'yunohost domain main-domain -n ' і потім ви можете вилучити домен '{domain}' за допомогою 'yunohost domain remove {domain}'.", "domain_cannot_add_xmpp_upload": "Ви не можете додавати домени, що починаються з 'xmpp-upload.'. Таку назву зарезервовано для функції XMPP upload, вбудованої в YunoHost.", "domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}", "disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку", @@ -314,7 +310,7 @@ "diagnosis_security_vulnerable_to_meltdown_details": "Щоб виправити це, вам слід оновити систему і перезавантажитися, щоб завантажити нове ядро Linux (або звернутися до вашого серверного провайдера, якщо це не спрацює). Докладніше див. на сайті https://meltdownattack.com/.", "diagnosis_security_vulnerable_to_meltdown": "Схоже, що ви вразливі до критичної вразливості безпеки Meltdown", "diagnosis_rootfstotalspace_critical": "Коренева файлова система має тільки {space}, що дуже тривожно! Скоріше за все, дисковий простір закінчиться дуже скоро! Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.", - "diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {space}. Можливо це нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися... Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.", + "diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {space}. Можливо це нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися… Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.", "diagnosis_regenconf_manually_modified_details": "Можливо це нормально, якщо ви знаєте, що робите! YunoHost перестане оновлювати цей файл автоматично. Але врахуйте, що оновлення YunoHost можуть містити важливі рекомендовані зміни. Якщо хочете, ви можете перевірити відмінності за допомогою команди yunohost tools regen-conf {category} --dry-run --with-diff і примусово повернути рекомендовану конфігурацію за допомогою команди yunohost tools regen-conf {category} --force", "diagnosis_regenconf_manually_modified": "Конфігураційний файл {file}, схоже, було змінено вручну.", "diagnosis_regenconf_allgood": "Усі конфігураційні файли відповідають рекомендованій конфігурації!", @@ -328,8 +324,8 @@ "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 \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_alternatives_6": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана…). Якщо ваш зворотний DNS правильно налаштований для IPv4, ви можете спробувати вимкнути використання IPv6 при надсиланні листів, виконавши команду yunohost settings set email.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. Деякі листи можуть не доставлятися або позначатися як спам.", "diagnosis_mail_fcrdns_ok": "Ваш зворотний DNS налаштовано правильно!", @@ -342,13 +338,13 @@ "diagnosis_mail_ehlo_unreachable_details": "Не вдалося відкрити з'єднання за портом 25 з вашим сервером на IPv{ipversion}. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер.
2. Ви також повинні переконатися, що служба postfix запущена.
3. На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.", "diagnosis_mail_ehlo_unreachable": "Поштовий сервер SMTP недоступний ззовні по IPv{ipversion}. Він не зможе отримувати листи електронної пошти.", "diagnosis_mail_ehlo_ok": "Поштовий сервер SMTP доступний ззовні і тому може отримувати електронні листи!", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Деякі провайдери не дозволять вам розблокувати вихідний порт 25, тому що вони не піклуються про мережевий нейтралітет (Net Neutrality).
- Деякі з них пропонують альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу такого роду обмежень. Дивіться https://yunohost.org/#/vpn_advantage
- Ви також можете розглянути можливість переходу на більш дружнього до мережевого нейтралітету провайдера", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Деякі провайдери не дозволять вам розблокувати вихідний порт 25, тому що вони не піклуються про мережевий нейтралітет (Net Neutrality).
- Деякі з них пропонують альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу такого роду обмежень. Дивіться https://yunohost.org/vpn_advantage
- Ви також можете розглянути можливість переходу на більш дружнього до мережевого нейтралітету провайдера", "diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).", "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.", "app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок", "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_installing": "Установлення YunoHost…", "yunohost_configured": "YunoHost вже налаштовано", "yunohost_already_installed": "YunoHost вже встановлено", "user_updated": "Відомості про користувача змінено", @@ -364,9 +360,9 @@ "upnp_enabled": "UPnP увімкнено", "upnp_disabled": "UPnP вимкнено", "upnp_dev_not_found": "UPnP-пристрій не знайдено", - "upgrading_packages": "Оновлення пакетів...", + "upgrading_packages": "Оновлення пакетів…", "upgrade_complete": "Оновлення завершено", - "updating_apt_cache": "Завантаження доступних оновлень для системних пакетів...", + "updating_apt_cache": "Завантаження доступних оновлень для системних пакетів…", "update_apt_cache_warning": "Щось пішло не так при оновленні кеша APT (менеджера пакунків Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}", "update_apt_cache_failed": "Неможливо оновити кеш APT (менеджер пакетів Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}", "unrestore_app": "{app} не буде оновлено", @@ -374,7 +370,7 @@ "unknown_main_domain_path": "Невідомий домен або шлях для '{app}'. Вам необхідно вказати домен і шлях, щоб мати можливість вказати URL для дозволу.", "unexpected_error": "Щось пішло не так: {error}", "unbackup_app": "{app} НЕ буде збережено", - "this_action_broke_dpkg": "Ця дія порушила dpkg/APT (системні менеджери пакетів)... Ви можете спробувати вирішити цю проблему, під'єднавшись по SSH і запустивши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`.", + "this_action_broke_dpkg": "Ця дія порушила dpkg/APT (системні менеджери пакетів)… Ви можете спробувати вирішити цю проблему, під'єднавшись по SSH і запустивши `sudo apt install --fix-broken` та/або `sudo dpkg --configure -a`.", "system_username_exists": "Ім'я користувача вже існує в списку користувачів системи", "system_upgraded": "Систему оновлено", "ssowat_conf_generated": "Конфігурацію SSOwat перестворено", @@ -417,12 +413,12 @@ "diagnosis_ip_weird_resolvconf_details": "Файл /etc/resolv.conf повинен бути символічним посиланням на /etc/resolvconf/run/resolv.conf, що вказує на 127.0.0.1(dnsmasq). Якщо ви хочете вручну налаштувати DNS вирішувачі (resolvers), відредагуйте /etc/resolv.dnsmasq.conf.", "diagnosis_ip_weird_resolvconf": "Роздільність DNS, схоже, працює, але схоже, що ви використовуєте користувацьку /etc/resolv.conf.", "diagnosis_ip_broken_resolvconf": "Схоже, що роздільність доменних імен на вашому сервері порушено, що пов'язано з тим, що /etc/resolv.conf не вказує на 127.0.0.1.", - "diagnosis_ip_broken_dnsresolution": "Роздільність доменних імен, схоже, з якоїсь причини не працює... Фаєрвол блокує DNS-запити?", + "diagnosis_ip_broken_dnsresolution": "Роздільність доменних імен, схоже, з якоїсь причини не працює… Фаєрвол блокує DNS-запити?", "diagnosis_ip_dnsresolution_working": "Роздільність доменних імен працює!", "diagnosis_ip_not_connected_at_all": "Здається, сервер взагалі не під'єднаний до Інтернету!?", "diagnosis_ip_local": "Локальний IP: {local}", "diagnosis_ip_global": "Глобальний IP: {global}", - "diagnosis_ip_no_ipv6_tip": "Наявність робочого IPv6 не є обов'язковим для роботи вашого сервера, але це краще для здоров'я Інтернету в цілому. IPv6 зазвичай автоматично налаштовується системою або вашим провайдером, якщо він доступний. В іншому випадку вам, можливо, доведеться налаштувати деякі речі вручну, як пояснюється в документації тут: https://yunohost.org/#/ipv6. Якщо ви не можете увімкнути IPv6 або якщо це здається вам занадто технічним, ви також можете сміливо нехтувати цим попередженням.", + "diagnosis_ip_no_ipv6_tip": "Наявність робочого IPv6 не є обов'язковим для роботи вашого сервера, але це краще для здоров'я Інтернету в цілому. IPv6 зазвичай автоматично налаштовується системою або вашим провайдером, якщо він доступний. В іншому випадку вам, можливо, доведеться налаштувати деякі речі вручну, як пояснюється в документації тут: https://yunohost.org/ipv6. Якщо ви не можете увімкнути IPv6 або якщо це здається вам занадто технічним, ви також можете сміливо нехтувати цим попередженням.", "diagnosis_ip_no_ipv6": "Сервер не має робочого IPv6.", "diagnosis_ip_connected_ipv6": "Сервер під'єднаний до Інтернету через IPv6!", "diagnosis_ip_no_ipv4": "Сервер не має робочого IPv4.", @@ -441,7 +437,7 @@ "diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені зі стороннього репозиторію під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили застосунки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити це становище, спробуйте виконати наступну команду: {cmd_to_fix}", "diagnosis_package_installed_from_sury": "Деякі системні пакети мають бути зістарені у версії", "diagnosis_backports_in_sources_list": "Схоже, що apt (менеджер пакетів) налаштований на використання репозиторія backports. Якщо ви не знаєте, що робите, ми наполегливо не радимо встановлювати пакети з backports, тому що це може привести до нестабільності або конфліктів у вашій системі.", - "diagnosis_basesystem_ynh_inconsistent_versions": "Ви використовуєте несумісні версії пакетів YunoHost... швидше за все, через невдале або часткове оновлення.", + "diagnosis_basesystem_ynh_inconsistent_versions": "Ви використовуєте несумісні версії пакетів YunoHost… швидше за все, через невдале або часткове оновлення.", "diagnosis_basesystem_ynh_main_version": "Сервер працює під управлінням YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_single_version": "{package} версія: {version} ({repo})", "diagnosis_basesystem_kernel": "Сервер працює під управлінням ядра Linux {kernel_version}", @@ -449,19 +445,19 @@ "diagnosis_basesystem_hardware_model": "Модель сервера - {model}", "diagnosis_basesystem_hardware": "Архітектура апаратного забезпечення сервера - {virt} {arch}", "custom_app_url_required": "Ви повинні надати URL-адресу для оновлення вашого користувацького застосунку {app}", - "confirm_app_install_thirdparty": "НЕБЕЗПЕЧНО! Цей застосунок не входить у каталог застосунків YunoHost. Установлення сторонніх застосунків може порушити цілісність і безпеку вашої системи. Вам не слід встановлювати його, якщо ви не знаєте, що робите. НІЯКОЇ ПІДТРИМКИ НЕ БУДЕ, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові піти на такий ризик, введіть '{answers}'", - "confirm_app_install_danger": "НЕБЕЗПЕЧНО! Відомо, що цей застосунок все ще експериментальний (якщо не сказати, що він явно не працює)! Вам не слід встановлювати його, якщо ви не знаєте, що робите. Ніякої підтримки не буде надано, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові ризикнути, введіть '{answers}'", + "confirm_app_install_thirdparty": "НЕБЕЗПЕЧНО! Цей застосунок не входить у каталог застосунків YunoHost. Установлення сторонніх застосунків може порушити цілісність і безпеку вашої системи. Вам не слід встановлювати його, якщо ви не знаєте, що робите. НІЯКОЇ ПІДТРИМКИ НЕ БУДЕ, якщо цей застосунок не буде працювати або зламає вашу систему… Якщо ви все одно готові піти на такий ризик, введіть '{answers}'", + "confirm_app_install_danger": "НЕБЕЗПЕЧНО! Відомо, що цей застосунок все ще експериментальний (якщо не сказати, що він явно не працює)! Вам не слід встановлювати його, якщо ви не знаєте, що робите. Ніякої підтримки не буде надано, якщо цей застосунок не буде працювати або зламає вашу систему… Якщо ви все одно готові ризикнути, введіть '{answers}'", "confirm_app_install_warning": "Попередження: Цей застосунок може працювати, але він не дуже добре інтегрований в YunoHost. Деякі функції, такі як єдина реєстрація та резервне копіювання/відновлення, можуть бути недоступні. Все одно встановити? [{answers}]. ", "certmanager_unable_to_parse_self_CA_name": "Не вдалося розібрати назву самопідписного центру (файл: {file})", "certmanager_self_ca_conf_file_not_found": "Не вдалося знайти файл конфігурації для самопідписного центру (файл: {file})", "certmanager_no_cert_file": "Не вдалося розпізнати файл сертифіката для домену {domain} (файл: {file})", "certmanager_hit_rate_limit": "Для цього набору доменів {domain} недавно було випущено дуже багато сертифікатів. Будь ласка, спробуйте ще раз пізніше. Див. https://letsencrypt.org/docs/rate-limits/ для отримання подробиць", "certmanager_warning_subdomain_dns_record": "Піддомен '{subdomain}' не дозволяється на тій же IP-адресі, що і '{domain}'. Деякі функції будуть недоступні, поки ви не виправите це і не перестворите сертифікат.", - "certmanager_domain_http_not_working": "Домен {domain}, схоже, не доступний через HTTP. Будь ласка, перевірте категорію 'Мережа' в діагностиці для отримання додаткових даних. (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).", - "certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домену '{domain}' відрізняються від IP цього сервера. Будь ласка, перевірте категорію 'DNS-записи' (основні) в діагностиці для отримання додаткових даних. Якщо ви недавно змінили запис A, будь ласка, зачекайте, поки він пошириться (деякі програми перевірки поширення DNS доступні в Інтернеті). (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).", - "certmanager_domain_cert_not_selfsigned": "Сертифікат для домену {domain} не є самопідписаним. Ви впевнені, що хочете замінити його? (Для цього використовуйте '--force').", - "certmanager_domain_not_diagnosed_yet": "Поки немає результатів діагностики для домену {domain}. Будь ласка, повторно проведіть діагностику для категорій 'DNS-записи' і 'Мережа' в розділі діагностики, щоб перевірити, чи готовий домен до Let's Encrypt. (Або, якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).", - "certmanager_certificate_fetching_or_enabling_failed": "Спроба використовувати новий сертифікат для {domain} не спрацювала...", + "certmanager_domain_http_not_working": "Домен {domain}, схоже, не доступний через HTTP. Будь ласка, перевірте категорію 'Мережа' в діагностиці для отримання додаткових даних. (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домену '{domain}' відрізняються від IP цього сервера. Будь ласка, перевірте категорію 'DNS-записи' (основні) в діагностиці для отримання додаткових даних. Якщо ви недавно змінили запис A, будь ласка, зачекайте, поки він пошириться (деякі програми перевірки поширення DNS доступні в Інтернеті). (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки.)", + "certmanager_domain_cert_not_selfsigned": "Сертифікат для домену {domain} не є самопідписаним. Ви впевнені, що хочете замінити його? (Для цього використовуйте '--force'.)", + "certmanager_domain_not_diagnosed_yet": "Поки немає результатів діагностики для домену {domain}. Будь ласка, повторно проведіть діагностику для категорій 'DNS-записи' і 'Мережа' в розділі діагностики, щоб перевірити, чи готовий домен до Let's Encrypt. (Або, якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки.)", + "certmanager_certificate_fetching_or_enabling_failed": "Спроба використовувати новий сертифікат для {domain} не спрацювала…", "certmanager_cert_signing_failed": "Не вдалося підписати новий сертифікат", "certmanager_cert_renew_success": "Сертифікат Let's Encrypt оновлений для домену '{domain}'", "certmanager_cert_install_success_selfsigned": "Самопідписаний сертифікат тепер встановлений для домену '{domain}'", @@ -470,12 +466,12 @@ "certmanager_attempt_to_replace_valid_cert": "Ви намагаєтеся перезаписати хороший дійсний сертифікат для домену {domain}! (Використовуйте --force для обходу)", "certmanager_attempt_to_renew_valid_cert": "Строк дії сертифіката для домена '{domain}' не закінчується! (Ви можете використовувати --force, якщо знаєте, що робите)", "certmanager_attempt_to_renew_nonLE_cert": "Сертифікат для домену '{domain}' не випущено Let's Encrypt. Неможливо продовжити його автоматично!", - "certmanager_acme_not_configured_for_domain": "Завдання ACME не може бути запущене для {domain} прямо зараз, тому що в його nginx-конфігурації відсутній відповідний фрагмент коду... Будь ласка, переконайтеся, що конфігурація nginx оновлена за допомогою `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_acme_not_configured_for_domain": "Завдання ACME не може бути запущене для {domain} прямо зараз, тому що в його nginx-конфігурації відсутній відповідний фрагмент коду… Будь ласка, переконайтеся, що конфігурація nginx оновлена за допомогою `yunohost tools regen-conf nginx --dry-run --with-diff`.", "backup_with_no_restore_script_for_app": "{app} не має скрипта відновлення, ви не зможете автоматично відновити резервну копію цього застосунку.", "backup_with_no_backup_script_for_app": "Застосунок '{app}' не має скрипта резервного копіювання. Нехтую ним.", "backup_unable_to_organize_files": "Неможливо використовувати швидкий спосіб для організації файлів в архіві", "backup_system_part_failed": "Не вдалося створити резервну копію системної частини '{part}'", - "backup_running_hooks": "Запуск гачків (hook) резервного копіювання...", + "backup_running_hooks": "Запуск гачків (hook) резервного копіювання…", "backup_permission": "Дозвіл на резервне копіювання для {app}", "backup_output_symlink_dir_broken": "Ваш архівний каталог '{path}' є неробочим символічним посиланням. Можливо, ви забули перемонтувати або підключити носій, на який вона вказує.", "backup_output_directory_required": "Ви повинні вказати вихідний каталог для резервного копіювання", @@ -483,7 +479,7 @@ "backup_output_directory_forbidden": "Виберіть інший вихідний каталог. Резервні копії не можуть бути створені в підкаталогах /bin,/boot,/dev,/etc,/lib,/root,/run,/sbin,/sys,/usr,/var або /home/yunohost.backup/archives", "backup_nothings_done": "Нема що зберігати", "backup_no_uncompress_archive_dir": "Немає такого каталогу нестислого архіву", - "backup_mount_archive_for_restore": "Підготовлення архіву для відновлення...", + "backup_mount_archive_for_restore": "Підготовлення архіву для відновлення…", "backup_method_tar_finished": "Створено архів резервного копіювання TAR", "backup_method_custom_finished": "Користувацький спосіб резервного копіювання '{method}' завершено", "backup_method_copy_finished": "Резервне копіювання завершено", @@ -501,21 +497,21 @@ "backup_copying_to_organize_the_archive": "Копіювання {size} МБ для організації архіву", "backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання", "backup_cant_mount_uncompress_archive": "Не вдалося змонтувати нестислий архів як захищений від запису", - "backup_ask_for_copying_if_needed": "Ви бажаєте тимчасово виконати резервне копіювання з використанням {size} МБ? (Цей спосіб використовується, оскільки деякі файли не можуть бути підготовлені дієвіше).", + "backup_ask_for_copying_if_needed": "Ви бажаєте тимчасово виконати резервне копіювання з використанням {size} МБ? (Цей спосіб використовується, оскільки деякі файли не можуть бути підготовлені дієвіше.)", "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": "Архів резервного копіювання з такою назвою вже існує.", + "backup_archive_name_exists": "Архів резервної копії з назвою '{name}' вже існує.", "backup_archive_broken_link": "Не вдалося отримати доступ до архіву резервного копіювання (неробоче посилання на {path})", "backup_archive_app_not_found": "Не вдалося знайти {app} в архіві резервного копіювання", - "backup_applying_method_tar": "Створення резервного TAR-архіву...", - "backup_applying_method_custom": "Виклик користувацького способу резервного копіювання '{method}'...", - "backup_applying_method_copy": "Копіювання всіх файлів у резервну копію...", + "backup_applying_method_tar": "Створення резервного TAR-архіву…", + "backup_applying_method_custom": "Виклик користувацького способу резервного копіювання '{method}'…", + "backup_applying_method_copy": "Копіювання всіх файлів у резервну копію…", "backup_app_failed": "Не вдалося створити резервну копію {app}", - "backup_actually_backuping": "Створення резервного архіву з зібраних файлів...", + "backup_actually_backuping": "Створення резервного архіву з зібраних файлів…", "backup_abstract_method": "Цей спосіб резервного копіювання ще не реалізований", "ask_password": "Пароль", "ask_new_path": "Новий шлях", @@ -534,19 +530,19 @@ "app_upgrade_some_app_failed": "Деякі застосунки не можуть бути оновлені", "app_upgrade_script_failed": "Сталася помилка в скрипті оновлення застосунку", "app_upgrade_failed": "Не вдалося оновити {app}: {error}", - "app_upgrade_app_name": "Зараз оновлюємо {app}...", + "app_upgrade_app_name": "Зараз оновлюємо {app}…", "app_upgrade_several_apps": "Наступні застосунки буде оновлено: {apps}", "app_unsupported_remote_type": "Для застосунку використовується непідтримуваний віддалений тип", "app_unknown": "Невідомий застосунок", - "app_start_restore": "Відновлення {app}...", - "app_start_backup": "Збирання файлів для резервного копіювання {app}...", - "app_start_remove": "Вилучення {app}...", - "app_start_install": "Установлення {app}...", + "app_start_restore": "Відновлення {app}…", + "app_start_backup": "Збирання файлів для резервного копіювання {app}…", + "app_start_remove": "Вилучення {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_checking": "Перевіряння необхідних пакунків для {app}...", + "app_remove_after_failed_install": "Вилучення застосунку після збою встановлення…", + "app_requirements_checking": "Перевіряння необхідних пакунків для {app}…", "app_removed": "{app} видалено", "app_not_properly_removed": "{app} не було видалено належним чином", "app_not_installed": "Не вдалося знайти {app} в списку встановлених застосунків: {all_apps}", @@ -564,7 +560,7 @@ "user_import_bad_file": "Ваш файл CSV неправильно відформатовано, він буде знехтуваний, щоб уникнути потенційної втрати даних", "user_import_bad_line": "Неправильний рядок {line}: {details}", "log_user_import": "Імпорт користувачів", - "ldap_server_is_down_restart_it": "Службу LDAP вимкнено, спробуйте перезапустити її...", + "ldap_server_is_down_restart_it": "Службу LDAP вимкнено, спробуйте перезапустити її…", "ldap_server_down": "Не вдається під'єднатися до сервера LDAP", "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надто застарілі практики упакування. Вам дійсно варто подумати про його оновлення.", "diagnosis_apps_outdated_ynh_requirement": "Установлена версія цього застосунку вимагає лише Yunohost >= 2.x чи 3.х, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.", @@ -620,19 +616,19 @@ "domain_config_auth_application_secret": "Таємний ключ застосунку", "log_domain_config_set": "Оновлення конфігурації для домену '{}'", "log_domain_dns_push": "Передавання записів DNS для домену '{}'", - "other_available_options": "...і {n} інших доступних опцій, які не показано", - "domain_dns_pushing": "Передання записів DNS...", + "other_available_options": "…і {n} інших доступних опцій, які не показано", + "domain_dns_pushing": "Передання записів DNS…", "ldap_attribute_already_exists": "Атрибут LDAP '{attribute}' вже існує зі значенням '{value}'", "domain_dns_push_already_up_to_date": "Записи вже оновлені, нічого не потрібно робити.", "domain_unknown": "Домен '{domain}' є невідомим", "migration_0021_start": "Початок міграції на Bullseye", - "migration_0021_patching_sources_list": "Виправлення sources.lists...", - "migration_0021_main_upgrade": "Початок основного оновлення...", - "migration_0021_yunohost_upgrade": "Початок оновлення ядра YunoHost...", + "migration_0021_patching_sources_list": "Виправлення sources.lists…", + "migration_0021_main_upgrade": "Початок основного оновлення…", + "migration_0021_yunohost_upgrade": "Початок оновлення ядра YunoHost…", "migration_0021_problematic_apps_warning": "Зверніть увагу, що були виявлені наступні, ймовірно проблемні встановлені застосунки. Схоже, що вони не були встановлені з каталогу застосунків YunoHost або не зазначені як «робочі». Отже, не можна гарантувати, що вони будуть працювати після оновлення: {problematic_apps}", "migration_0021_modified_files": "Зверніть увагу, що такі файли були змінені вручну і можуть бути перезаписані після оновлення: {manually_modified_files}", - "migration_0021_cleaning_up": "Очищення кеш-пам'яті і пакетів, які більше не потрібні...", - "migration_0021_patch_yunohost_conflicts": "Застосування виправлення для вирішення проблеми конфлікту...", + "migration_0021_cleaning_up": "Очищення кеш-пам'яті і пакетів, які більше не потрібні…", + "migration_0021_patch_yunohost_conflicts": "Застосування виправлення для вирішення проблеми конфлікту…", "migration_0021_still_on_buster_after_main_upgrade": "Щось пішло не так під час основного оновлення, здається, що система все ще працює на Debian Buster", "migration_0021_not_enough_free_space": "Вільного місця в /var/ досить мало! У вас повинно бути не менше 1 ГБ вільного місця, щоб запустити цю міграцію.", "migration_0021_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Bullseye.", @@ -640,7 +636,7 @@ "migration_description_0021_migrate_to_bullseye": "Оновлення системи до Debian Bullseye і YunoHost 11.x", "service_description_postgresql": "Зберігає дані застосунків (база даних SQL)", "domain_config_default_app": "Типовий застосунок", - "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 встановлено, але не PostgreSQL 13!? У вашій системі могло статися щось неприємне :(...", + "migration_0023_postgresql_13_not_installed": "PostgreSQL 11 встановлено, але не PostgreSQL 13!? У вашій системі могло статися щось неприємне :(…", "migration_description_0023_postgresql_11_to_13": "Перенесення баз даних з PostgreSQL 11 на 13", "tools_upgrade": "Оновлення системних пакетів", "tools_upgrade_failed": "Не вдалося оновити наступні пакети: {packages_list}", @@ -755,10 +751,34 @@ "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.", + "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}')" + "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}", + "ask_dyndns_recovery_password_explain_unavailable": "Цей домен DynDNS вже зареєстрований. Якщо ви особисто зареєстрували цей домен, можете ввести пароль для відновлення домену.", + "dyndns_too_many_requests": "Сервіс DynDNS YunoHost отримала від вас занадто багато запитів, зачекайте приблизно 1 годину, перш ніж спробувати ще раз.", + "ask_dyndns_recovery_password_explain": "Будь ласка, виберіть пароль для відновлення вашого домену DynDNS, на випадок, якщо вам знадобиться скинути його пізніше.", + "ask_dyndns_recovery_password": "Пароль відновлення DynDNS", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Будь ласка, введіть пароль відновлення для цього домену DynDNS.", + "dyndns_no_recovery_password": "Не вказано пароль для відновлення! У разі втрати контролю над цим доменом вам необхідно звернутися до адміністратора команди YunoHost!", + "dyndns_subscribed": "Домен DynDNS зареєстровано", + "dyndns_subscribe_failed": "Не вдалося підписатися на домен DynDNS: {error}", + "dyndns_unsubscribe_failed": "Не вдалося скасувати підписку на домен DynDNS: {error}", + "dyndns_unsubscribed": "Домен DynDNS відписано", + "dyndns_unsubscribe_denied": "Не вдалося відписати домен: невірні облікові дані", + "dyndns_unsubscribe_already_unsubscribed": "Домен вже відписаний", + "dyndns_set_recovery_password_denied": "Не вдалося встановити пароль відновлення: невірний ключ", + "dyndns_set_recovery_password_unknown_domain": "Не вдалося встановити пароль відновлення: домен не зареєстровано", + "dyndns_set_recovery_password_invalid_password": "Не вдалося встановити пароль для відновлення: пароль недостатньо надійний", + "dyndns_set_recovery_password_failed": "Не вдалося встановити пароль для відновлення: {error}", + "dyndns_set_recovery_password_success": "Пароль для відновлення встановлено!", + "log_dyndns_unsubscribe": "Скасувати підписку на субдомен YunoHost '{}'" } \ No newline at end of file diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index 18c6430c0..a80a9cc0c 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -40,7 +40,7 @@ "certmanager_domain_dns_ip_differs_from_public_ip": "域'{domain}' 的DNS记录与此服务器的IP不同。请检查诊断中的“ DNS记录”(基本)类别,以获取更多信息。 如果您最近修改了A记录,请等待它传播(某些DNS传播检查器可在线获得)。 (如果您知道自己在做什么,请使用“ --no-checks”关闭这些检查。)", "certmanager_domain_cert_not_selfsigned": "域 {domain} 的证书不是自签名的, 您确定要更换它吗?(使用“ --force”这样做。)", "certmanager_domain_not_diagnosed_yet": "尚无域{domain} 的诊断结果。请在诊断部分中针对“ DNS记录”和“ Web”类别重新运行诊断,以检查该域是否已准备好安装“Let's Encrypt”证书。(或者,如果您知道自己在做什么,请使用“ --no-checks”关闭这些检查。)", - "certmanager_certificate_fetching_or_enabling_failed": "尝试将新证书用于 {domain}无效...", + "certmanager_certificate_fetching_or_enabling_failed": "尝试将新证书用于 {domain}无效…", "certmanager_cert_signing_failed": "无法签署新证书", "certmanager_cert_install_success_selfsigned": "为域 '{domain}'安装了自签名证书", "certmanager_cert_renew_success": "为域 '{domain}'续订“Let's Encrypt”证书", @@ -49,12 +49,12 @@ "certmanager_attempt_to_replace_valid_cert": "您正在尝试覆盖域{domain}的有效证书!(使用--force绕过)", "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配置是最新的。", + "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_backup_script_for_app": "应用'{app}'没有备份脚本。无视。", "backup_unable_to_organize_files": "无法使用快速方法来组织档案中的文件", "backup_system_part_failed": "无法备份'{part}'系统部分", - "backup_running_hooks": "正在运行备份挂钩...", + "backup_running_hooks": "正在运行备份挂钩…", "backup_permission": "{app}的备份权限", "backup_output_symlink_dir_broken": "您的存档目录'{path}' 是断开的符号链接。 也许您忘记了重新安装/装入或插入它指向的存储介质。", "backup_output_directory_required": "您必须提供备份的输出目录", @@ -62,7 +62,7 @@ "backup_output_directory_forbidden": "选择一个不同的输出目录。无法在/bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var或/home/yunohost.backup/archives子文件夹中创建备份", "backup_nothings_done": "没什么可保存的", "backup_no_uncompress_archive_dir": "没有这样的未压缩存档目录", - "backup_mount_archive_for_restore": "正在准备存档以进行恢复...", + "backup_mount_archive_for_restore": "正在准备存档以进行恢复…", "backup_method_tar_finished": "TAR备份存档已创建", "backup_method_custom_finished": "自定义备份方法'{method}' 已完成", "backup_method_copy_finished": "备份副本已完成", @@ -83,17 +83,17 @@ "backup_archive_writing_error": "无法将要备份的文件'{source}'(在归档文'{dest}'中命名)添加到压缩归档文件 '{archive}'s}”中", "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": "具有该名称的备份存档已经存在。", "backup_archive_broken_link": "无法访问备份存档(指向{path}的链接断开)", "backup_archive_app_not_found": "在备份档案中找不到 {app}", - "backup_applying_method_tar": "创建备份TAR存档...", - "backup_applying_method_custom": "调用自定义备份方法'{method}'...", - "backup_applying_method_copy": "正在将所有文件复制到备份...", + "backup_applying_method_tar": "创建备份TAR存档…", + "backup_applying_method_custom": "调用自定义备份方法'{method}'…", + "backup_applying_method_copy": "正在将所有文件复制到备份…", "backup_app_failed": "无法备份{app}", - "backup_actually_backuping": "根据收集的文件创建备份档案...", + "backup_actually_backuping": "根据收集的文件创建备份档案…", "backup_abstract_method": "此备份方法尚未实现", "ask_password": "密码", "ask_new_path": "新路径", @@ -111,16 +111,16 @@ "app_upgraded": "{app}upgraded", "app_upgrade_some_app_failed": "某些应用无法升级", "app_upgrade_script_failed": "应用升级脚本内部发生错误", - "app_upgrade_app_name": "现在升级{app} ...", + "app_upgrade_app_name": "现在升级{app}…", "app_upgrade_several_apps": "以下应用将被升级: {apps}", "app_unsupported_remote_type": "应用使用的远程类型不受支持", - "app_start_backup": "正在收集要备份的文件,用于{app} ...", - "app_start_install": "{app}安装中...", + "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_checking": "正在检查{app}所需的软件包...", + "app_remove_after_failed_install": "安装失败后删除应用…", + "app_requirements_checking": "正在检查{app}所需的软件包…", "app_removed": "{app} 已卸载", "app_not_properly_removed": "{app} 未正确删除", "app_not_correctly_installed": "{app} 似乎安装不正确", @@ -230,7 +230,7 @@ "service_enable_failed": "无法使服务 '{service}'在启动时自动启动。\n\n最近的服务日志:{logs}", "service_disabled": "系统启动时,服务 '{service}' 将不再启动。", "service_disable_failed": "服务'{service}'在启动时无法启动。\n\n最近的服务日志:{logs}", - "this_action_broke_dpkg": "此操作破坏了dpkg / APT(系统软件包管理器)...您可以尝试通过SSH连接并运行`sudo apt install --fix-broken`和/或`sudo dpkg --configure -a`来解决此问题。", + "this_action_broke_dpkg": "此操作破坏了dpkg / APT(系统软件包管理器)…您可以尝试通过SSH连接并运行`sudo apt install --fix-broken`和/或`sudo dpkg --configure -a`来解决此问题。", "system_username_exists": "用户名已存在于系统用户列表中", "system_upgraded": "系统升级", "ssowat_conf_generated": "SSOwat配置已重新生成", @@ -240,9 +240,9 @@ "service_stopped": "服务'{service}' 已停止", "service_stop_failed": "无法停止服务'{service}'\n\n最近的服务日志:{logs}", "upnp_dev_not_found": "找不到UPnP设备", - "upgrading_packages": "升级程序包...", + "upgrading_packages": "升级程序包…", "upgrade_complete": "升级完成", - "updating_apt_cache": "正在获取系统软件包的可用升级...", + "updating_apt_cache": "正在获取系统软件包的可用升级…", "update_apt_cache_warning": "更新APT缓存(Debian的软件包管理器)时出了点问题。这是sources.list行的转储,这可能有助于确定有问题的行:\n{sourceslist}", "update_apt_cache_failed": "无法更新APT的缓存(Debian的软件包管理器)。这是sources.list行的转储,这可能有助于确定有问题的行:\n{sourceslist}", "unrestore_app": "{app} 将不会恢复", @@ -250,13 +250,13 @@ "unknown_main_domain_path": "'{app}'的域或路径未知。您需要指定一个域和一个路径,以便能够指定用于许可的URL。", "unexpected_error": "出乎意料的错误: {error}", "unbackup_app": "{app} 将不会保存", - "yunohost_installing": "正在安装YunoHost ...", + "yunohost_installing": "正在安装YunoHost…", "yunohost_configured": "现在已配置YunoHost", "yunohost_already_installed": "YunoHost已经安装", "user_updated": "用户信息已更改", "user_update_failed": "无法更新用户{user}: {error}", "user_unknown": "未知用户: {user}", - "user_home_creation_failed": "无法为用户创建'home'文件夹", + "user_home_creation_failed": "无法为用户创建'{home}'文件夹", "user_deletion_failed": "无法删除用户 {user}: {error}", "user_deleted": "用户已删除", "user_creation_failed": "无法创建用户 {user}: {error}", @@ -266,7 +266,7 @@ "upnp_enabled": "UPnP已启用", "upnp_disabled": "UPnP已禁用", "yunohost_not_installed": "YunoHost没有正确安装,请运行 'yunohost tools postinstall'", - "yunohost_postinstall_end_tip": "后期安装完成! 为了最终完成您的设置,请考虑:\n -通过webadmin的“用户”部分添加第一个用户(或在命令行中'yunohost user create ' );\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解YunoHost”部分: https://yunohost.org/admindoc.", + "yunohost_postinstall_end_tip": "后期安装完成! 为了最终完成您的设置,请考虑:\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解YunoHost”部分: https://yunohost.org/admindoc.", "operation_interrupted": "该操作是否被手动中断?", "invalid_regex": "无效的正则表达式:'{regex}'", "installation_complete": "安装完成", @@ -288,7 +288,7 @@ "group_cannot_edit_all_users": "组“ all_users”不能手动编辑。这是一个特殊的组,旨在包含所有在YunoHost中注册的用户", "group_creation_failed": "无法创建组'{group}': {error}", "group_created": "创建了 '{group}'组", - "group_already_exist_on_system_but_removing_it": "系统组中已经存在组{group},但是YunoHost会将其删除...", + "group_already_exist_on_system_but_removing_it": "系统组中已经存在组{group},但是YunoHost会将其删除…", "group_already_exist_on_system": "系统组中已经存在组{group}", "group_already_exist": "群组{group}已经存在", "good_practices_about_admin_password": "现在,您将设置一个新的管理员密码。 密码至少应包含8个字符。并且出于安全考虑建议使用较长的密码同时尽可能使用各种字符(大写,小写,数字和特殊字符)。", @@ -301,15 +301,12 @@ "firewall_reload_failed": "无法重新加载防火墙", "file_does_not_exist": "文件{path} 不存在。", "field_invalid": "无效的字段'{}'", - "extracting": "提取中...", + "extracting": "提取中…", "dyndns_unavailable": "域'{domain}' 不可用。", "dyndns_domain_not_provided": "DynDNS提供者 {provider} 无法提供域 {domain}。", - "dyndns_registration_failed": "无法注册DynDNS域: {error}", - "dyndns_registered": "DynDNS域已注册", "dyndns_provider_unreachable": "无法联系DynDNS提供者 {provider}: 您的YunoHost未正确连接到Internet或dynette服务器已关闭。", "dyndns_no_domain_registered": "没有在DynDNS中注册的域", "dyndns_key_not_found": "找不到该域的DNS密钥", - "dyndns_key_generating": "正在生成DNS密钥...可能需要一段时间。", "dyndns_ip_updated": "在DynDNS上更新了您的IP", "dyndns_ip_update_failed": "无法将IP地址更新到DynDNS", "dyndns_could_not_check_available": "无法检查{provider}上是否可用 {domain}。", @@ -322,7 +319,6 @@ "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_deletion_failed": "无法删除域 {domain}: {error}", @@ -408,12 +404,12 @@ "diagnosis_ip_weird_resolvconf_details": "文件 /etc/resolv.conf 应该是指向 /etc/resolvconf/run/resolv.conf 本身的符号链接,指向 127.0.0.1 (dnsmasq)。如果要手动配置DNS解析器,请编辑 /etc/resolv.dnsmasq.conf。", "diagnosis_ip_weird_resolvconf": "DNS解析似乎可以正常工作,但是您似乎正在使用自定义的 /etc/resolv.conf 。", "diagnosis_ip_broken_resolvconf": "域名解析在您的服务器上似乎已损坏,这似乎与 /etc/resolv.conf 有关,但未指向 127.0.0.1 。", - "diagnosis_ip_broken_dnsresolution": "域名解析似乎由于某种原因而被破坏...防火墙是否阻止了DNS请求?", + "diagnosis_ip_broken_dnsresolution": "域名解析似乎由于某种原因而被破坏…防火墙是否阻止了DNS请求?", "diagnosis_ip_dnsresolution_working": "域名解析正常!", "diagnosis_ip_not_connected_at_all": "服务器似乎根本没有连接到Internet?", "diagnosis_ip_local": "本地IP:{local}", "diagnosis_ip_global": "全局IP: {global}", - "diagnosis_ip_no_ipv6_tip": "正常运行的IPv6并不是服务器正常运行所必需的,但是对于整个Internet的健康而言,则更好。通常,IPv6应该由系统或您的提供商自动配置(如果可用)。否则,您可能需要按照此处的文档中的说明手动配置一些内容: https://yunohost.org/#/ipv6。如果您无法启用IPv6或对您来说太过困难,也可以安全地忽略此警告。", + "diagnosis_ip_no_ipv6_tip": "正常运行的IPv6并不是服务器正常运行所必需的,但是对于整个Internet的健康而言,则更好。通常,IPv6应该由系统或您的提供商自动配置(如果可用)。否则,您可能需要按照此处的文档中的说明手动配置一些内容: https://yunohost.org/ipv6。如果您无法启用IPv6或对您来说太过困难,也可以安全地忽略此警告。", "diagnosis_ip_no_ipv6": "服务器没有可用的IPv6。", "diagnosis_ip_connected_ipv6": "服务器通过IPv6连接到Internet!", "diagnosis_ip_no_ipv4": "服务器没有可用的IPv4。", @@ -435,13 +431,13 @@ "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_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_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}中)", @@ -510,8 +506,8 @@ "not_enough_disk_space": "'{path}'上的可用空间不足", "migrations_to_be_ran_manually": "迁移{id}必须手动运行。请转到webadmin页面上的工具→迁移,或运行`yunohost tools migrations run`。", "migrations_success_forward": "迁移 {id} 已完成", - "migrations_skip_migration": "正在跳过迁移{id}...", - "migrations_running_forward": "正在运行迁移{id}...", + "migrations_skip_migration": "正在跳过迁移{id}…", + "migrations_running_forward": "正在运行迁移{id}…", "migrations_pending_cant_rerun": "这些迁移仍处于待处理状态,因此无法再次运行: {ids}", "migrations_not_pending_cant_skip": "这些迁移没有待处理,因此不能跳过: {ids}", "migrations_no_such_migration": "没有称为 '{id}'的迁移", @@ -519,14 +515,14 @@ "migrations_need_to_accept_disclaimer": "要运行迁移{id},您必须接受以下免责声明:\n---\n{disclaimer}\n---\n如果您接受并继续运行迁移,请使用选项'--accept-disclaimer'重新运行该命令。", "migrations_must_provide_explicit_targets": "使用'--skip'或'--force-rerun'时必须提供明确的目标", "migrations_migration_has_failed": "迁移{id}尚未完成,正在中止。错误: {exception}", - "migrations_loading_migration": "正在加载迁移{id}...", + "migrations_loading_migration": "正在加载迁移{id}…", "migrations_list_conflict_pending_done": "您不能同时使用'--previous' 和'--done'。", "migrations_exclusive_options": "'--auto', '--skip',和'--force-rerun'是互斥的选项。", "migrations_failed_to_load_migration": "无法加载迁移{id}: {error}", "migrations_dependencies_not_satisfied": "在迁移{id}之前运行以下迁移: '{dependencies_id}'。", "migrations_already_ran": "这些迁移已经完成: {ids}", "migration_ldap_rollback_success": "系统回滚。", - "migration_ldap_migration_failed_trying_to_rollback": "无法迁移...试图回滚系统。", + "migration_ldap_migration_failed_trying_to_rollback": "无法迁移…试图回滚系统。", "migration_ldap_can_not_backup_before_migration": "迁移失败之前,无法完成系统的备份。错误: {error}", "migration_ldap_backup_before_migration": "在实际迁移之前,请创建LDAP数据库和应用设置的备份。", "main_domain_changed": "主域已更改", @@ -573,7 +569,7 @@ "danger": "警告:", "diagnosis_apps_allgood": "所有已安装的应用都遵守基本的打包原则", "diagnosis_apps_deprecated_practices": "此应用的安装 版本仍然使用一些超旧的弃用打包原则。推荐您升级它。", - "diagnosis_apps_issue": "发现应用{ app } 存在问题", + "diagnosis_apps_issue": "发现应用{app} 存在问题", "diagnosis_description_apps": "应用", "global_settings_setting_backup_compress_tar_archives_help": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量CPU。", "global_settings_setting_nginx_compatibility_help": "Web服务器NGINX的兼容性与安全性的权衡,影响密码(以及其他与安全性有关的方面)", diff --git a/maintenance/autofix_locale_format.py b/maintenance/autofix_locale_format.py index caa36f9f2..5fa34ad5e 100644 --- a/maintenance/autofix_locale_format.py +++ b/maintenance/autofix_locale_format.py @@ -109,15 +109,16 @@ def autofix_orthotypography_and_standardized_words(): "\u2008", "\u2009", "\u200A", - "\u202f", - "\u202F", + # "\u202f", + # "\u202F", "\u3000", ] transformations = {s: " " for s in godamn_spaces_of_hell} transformations.update( { - "…": "...", + r"\.\.\.": "…", + "https ://": "https://", } ) diff --git a/maintenance/make_changelog.sh b/maintenance/make_changelog.sh index f5d1572a6..a24ccb08c 100644 --- a/maintenance/make_changelog.sh +++ b/maintenance/make_changelog.sh @@ -1,9 +1,9 @@ VERSION="?" -RELEASE="testing" +RELEASE="stable" REPO=$(basename $(git rev-parse --show-toplevel)) -REPO_URL="https://github.com/yunohost/yunohost" -ME=$(git config --global --get user.name) -EMAIL=$(git config --global --get user.email) +REPO_URL=$(git remote get-url origin) +ME=$(git config --get user.name) +EMAIL=$(git config --get user.email) LAST_RELEASE=$(git tag --list 'debian/11.*' --sort="v:refname" | tail -n 1) @@ -11,7 +11,8 @@ 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" \ +| sed -E "s&Merge .*#([0-9]+).*\$& \([#\1]\(http://github.com/YunoHost/$REPO/pull/\1\)\)&g" \ +| sed -E "/Co-authored-by: .* <.*>/d" \ | grep -v "Translations update from Weblate" \ | tac @@ -22,7 +23,7 @@ TRANSLATIONS=$(git log $LAST_RELEASE... -n 10000 --pretty=format:"%s" \ [[ -z "$TRANSLATIONS" ]] || echo " - [i18n] Translations updated for $TRANSLATIONS" echo "" -CONTRIBUTORS=$(git logc $LAST_RELEASE... -n 10000 --pretty=format:"%an" \ +CONTRIBUTORS=$(git log -n10 --pretty=format:'%Cred%h%Creset %C(bold blue)(%an) %Creset%Cgreen(%cr)%Creset - %s %C(yellow)%d%Creset' --abbrev-commit $LAST_RELEASE... -n 10000 --pretty=format:"%an" \ | 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)" diff --git a/maintenance/missing_i18n_keys.py b/maintenance/missing_i18n_keys.py index 2ed7fd141..25f74bee2 100644 --- a/maintenance/missing_i18n_keys.py +++ b/maintenance/missing_i18n_keys.py @@ -32,6 +32,7 @@ def find_expected_string_keys(): python_files = glob.glob(ROOT + "src/*.py") python_files.extend(glob.glob(ROOT + "src/utils/*.py")) python_files.extend(glob.glob(ROOT + "src/migrations/*.py")) + python_files.extend(glob.glob(ROOT + "src/migrations/*.py.disabled")) python_files.extend(glob.glob(ROOT + "src/authenticators/*.py")) python_files.extend(glob.glob(ROOT + "src/diagnosers/*.py")) python_files.append(ROOT + "bin/yunohost") @@ -135,6 +136,13 @@ def find_expected_string_keys(): # Domain config panel domain_config = toml.load(open(ROOT + "share/config_domain.toml")) + domain_settings_with_help_key = [ + "portal_logo", + "portal_public_intro", + "portal_theme", + "portal_user_intro", + "search_engine", + ] for panel in domain_config.values(): if not isinstance(panel, dict): continue @@ -145,6 +153,8 @@ def find_expected_string_keys(): if not isinstance(values, dict): continue yield f"domain_config_{key}" + if key in domain_settings_with_help_key: + yield f"domain_config_{key}_help" # Global settings global_config = toml.load(open(ROOT + "share/config_global.toml")) @@ -155,7 +165,6 @@ def find_expected_string_keys(): "smtp_relay_password", "smtp_relay_port", "smtp_relay_user", - "ssh_port", "ssowat_panel_overlay_enabled", "root_password", "root_access_explain", diff --git a/share/actionsmap-portal.yml b/share/actionsmap-portal.yml new file mode 100644 index 000000000..6b02a061d --- /dev/null +++ b/share/actionsmap-portal.yml @@ -0,0 +1,92 @@ +_global: + namespace: yunohost + authentication: + api: ldap_ynhuser + cli: null + lock: false + cache: false + +portal: + category_help: Portal routes + actions: + + ### portal_me() + me: + action_help: Allow user to fetch their own infos + api: GET /me + + ### portal_apps() + apps: + action_help: Allow users to fetch lit of apps they have access to + api: GET /me/apps + + ### portal_update() + update: + action_help: Allow user to update their infos (display name, mail aliases/forward, password, ...) + api: PUT /update + arguments: + --fullname: + help: The full name of the user. For example 'Camille Dupont' + extra: + pattern: &pattern_fullname + - !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$ + - "pattern_fullname" + --mailforward: + help: Mailforward addresses to add + nargs: "*" + metavar: MAIL + extra: + pattern: &pattern_email_forward + - !!str ^[\w\+.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ + - "pattern_email_forward" + --mailalias: + help: Mail aliases to add + nargs: "*" + metavar: MAIL + extra: + pattern: &pattern_email + - !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ + - "pattern_email" + --currentpassword: + help: Current password + nargs: "?" + --newpassword: + help: New password to set + nargs: "?" + + ### portal_update_password() + # update_password: + # action_help: Allow user to change their password + # api: PUT /me/update_password + # arguments: + # -c: + # full: --current + # help: Current password + # -p: + # full: --password + # help: New password to set + + ### portal_reset_password() + reset_password: + action_help: Allow user to update their infos (display name, mail aliases/forward, ...) + api: PUT /me/reset_password + authentication: + # FIXME: to be implemented ? + api: reset_password_token + # FIXME: add args etc + + ### portal_register() + register: + action_help: Allow user to register using an invite token or ??? + api: POST /me + authentication: + # FIXME: to be implemented ? + api: register_invite_token + # FIXME: add args etc + + ### portal_public() + public: + action_help: Allow anybody to list public apps and other infos regarding the public portal + api: GET /public + authentication: + api: null diff --git a/share/actionsmap.yml b/share/actionsmap.yml old mode 100644 new mode 100755 index 58787790c..6c81e58ca --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -63,33 +63,17 @@ user: help: The unique username to create extra: pattern: &pattern_username - - !!str ^[a-z0-9_]+$ + - !!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 + required: True pattern: &pattern_fullname - !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$ - "pattern_fullname" - -f: - full: --firstname - help: Deprecated. Use --fullname instead. - extra: - required: False - pattern: &pattern_firstname - - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - - "pattern_firstname" - -l: - full: --lastname - help: Deprecated. Use --fullname instead. - extra: - required: False - pattern: &pattern_lastname - - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - - "pattern_lastname" -p: full: --password help: User password @@ -102,7 +86,7 @@ user: comment: good_practices_about_user_password -d: full: --domain - help: Domain for the email address and xmpp account + help: Domain for the email address extra: pattern: &pattern_domain - !!str ^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ @@ -147,16 +131,6 @@ user: help: The full name of the user. For example 'Camille Dupont' extra: pattern: *pattern_fullname - -f: - full: --firstname - help: Deprecated. Use --fullname instead. - extra: - pattern: *pattern_firstname - -l: - full: --lastname - help: Deprecated. Use --fullname instead. - extra: - pattern: *pattern_lastname -m: full: --mail extra: @@ -493,7 +467,7 @@ domain: help: Display domains as a tree action: store_true --features: - help: List only domains with features enabled (xmpp, mail_in, mail_out) + help: List only domains with features enabled (mail_in, mail_out) nargs: "*" ### domain_info() @@ -515,10 +489,16 @@ domain: help: Domain name to add extra: pattern: *pattern_domain - -d: - full: --dyndns - help: Subscribe to the DynDNS service + --ignore-dyndns: + help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true + --dyndns-recovery-password: + metavar: PASSWORD + nargs: "?" + const: 0 + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain + extra: + pattern: *pattern_password ### domain_remove() remove: @@ -537,17 +517,16 @@ domain: full: --force help: Do not ask confirmation to remove apps action: store_true - - - ### domain_dns_conf() - dns-conf: - deprecated: true - action_help: Generate sample DNS configuration for a domain - arguments: - domain: - help: Target domain + --ignore-dyndns: + help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service + action: store_true + --dyndns-recovery-password: + metavar: PASSWORD + nargs: "?" + const: 0 + help: If removing a DynDNS domain, unsubscribe from the DynDNS service with a password extra: - pattern: *pattern_domain + pattern: *pattern_password ### domain_maindomain() main-domain: @@ -562,54 +541,6 @@ domain: extra: pattern: *pattern_domain - ### certificate_status() - cert-status: - deprecated: true - action_help: List status of current certificates (all by default). - arguments: - domain_list: - help: Domains to check - nargs: "*" - --full: - help: Show more details - action: store_true - - ### certificate_install() - cert-install: - deprecated: true - action_help: Install Let's Encrypt certificates for given domains (all by default). - arguments: - domain_list: - help: Domains for which to install the certificates - nargs: "*" - --force: - help: Install even if current certificate is not self-signed - action: store_true - --no-checks: - help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) - action: store_true - --self-signed: - help: Install self-signed certificate instead of Let's Encrypt - action: store_true - - ### certificate_renew() - cert-renew: - deprecated: true - action_help: Renew the Let's Encrypt certificates for given domains (all by default). - arguments: - domain_list: - help: Domains for which to renew the certificates - nargs: "*" - --force: - help: Ignore the validity threshold (30 days) - action: store_true - --email: - help: Send an email to root with logs if some renewing fails - action: store_true - --no-checks: - help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) - action: store_true - ### domain_url_available() url-available: hide_in_help: True @@ -622,6 +553,7 @@ domain: pattern: *pattern_domain path: help: The path to check (e.g. /coffee) + ### domain_action_run() action-run: @@ -638,11 +570,61 @@ domain: help: Serialized arguments for action (i.e. "foo=bar&lorem=ipsum") subcategories: + dyndns: + subcategory_help: Subscribe and Update DynDNS Hosts + actions: + ### domain_dyndns_subscribe() + subscribe: + action_help: Subscribe to a DynDNS service + arguments: + domain: + help: Domain to subscribe to the DynDNS service + extra: + pattern: *pattern_domain + -p: + full: --recovery-password + nargs: "?" + const: 0 + help: Password used to later recover the domain if needed + extra: + pattern: *pattern_password + + ### domain_dyndns_unsubscribe() + unsubscribe: + action_help: Unsubscribe from a DynDNS service + arguments: + domain: + help: Domain to unsubscribe from the DynDNS service + extra: + pattern: *pattern_domain + required: True + -p: + full: --recovery-password + nargs: "?" + const: 0 + help: Recovery password used to delete the domain + extra: + pattern: *pattern_password + + ### domain_dyndns_set_recovery_password() + set-recovery-password: + action_help: Set recovery password + arguments: + domain: + help: Domain to set recovery password for + extra: + pattern: *pattern_domain + required: True + -p: + full: --recovery-password + help: The new recovery password + extra: + password: ask_dyndns_recovery_password + pattern: *pattern_password config: subcategory_help: Domain settings actions: - ### domain_config_get() get: action_help: Display a domain configuration @@ -758,7 +740,7 @@ domain: help: Domains for which to renew the certificates nargs: "*" --force: - help: Ignore the validity threshold (30 days) + help: Ignore the validity threshold (15 days) action: store_true --email: help: Send an email to root with logs if some renewing fails @@ -867,14 +849,14 @@ app: help: Custom name for the app -a: full: --args - help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") + help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path&init_main_permission=visitors") -n: full: --no-remove-on-failure help: Debug option to avoid removing the app on a failed installation action: store_true -f: full: --force - help: Do not ask confirmation if the app is not safe to use (low quality, experimental or 3rd party) + help: Do not ask confirmation if the app is not safe to use (low quality, experimental or 3rd party), or when the app displays a post-install notification action: store_true ### app_remove() @@ -913,7 +895,7 @@ app: action: store_true -c: full: --continue-on-failure - help: Continue to upgrade apps event if one or more upgrade failed + help: Continue to upgrade apps even if one or more upgrade failed action: store_true ### app_change_url() @@ -954,6 +936,14 @@ 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: @@ -1126,7 +1116,7 @@ backup: api: PUT /backups//restore arguments: name: - help: Name of the local backup archive + help: Name or path of the backup archive --system: help: List of system parts to restore (or all if none is given) nargs: "*" @@ -1157,7 +1147,7 @@ backup: api: GET /backups/ arguments: name: - help: Name of the local backup archive + help: Name or path of the backup archive -d: full: --with-details help: Show additional backup information @@ -1508,22 +1498,27 @@ firewall: # DynDNS # ############################# dyndns: - category_help: Subscribe and Update DynDNS Hosts + category_help: Subscribe and Update DynDNS Hosts ( deprecated, use 'yunohost domain dyndns' instead ) actions: ### dyndns_subscribe() subscribe: action_help: Subscribe to a DynDNS service + deprecated: true arguments: -d: full: --domain - help: Full domain to subscribe with + help: Full domain to subscribe with ( deprecated, use 'yunohost domain dyndns subscribe' instead ) extra: pattern: *pattern_domain - -k: - full: --key - help: Public DNS key - + -p: + full: --recovery-password + nargs: "?" + const: 0 + help: Password used to later recover the domain if needed + extra: + pattern: *pattern_password + ### dyndns_update() update: action_help: Update IP on DynDNS platform @@ -1611,8 +1606,15 @@ tools: required: True comment: good_practices_about_admin_password --ignore-dyndns: - help: Do not subscribe domain to a DynDNS service + help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service action: store_true + --dyndns-recovery-password: + metavar: PASSWORD + nargs: "?" + const: 0 + help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later recover the domain if needed + extra: + pattern: *pattern_password --force-diskspace: help: Use this if you really want to install YunoHost on a setup with less than 10 GB on the root filesystem action: store_true @@ -1652,6 +1654,10 @@ tools: help: python command to execute full: --command + ### tools_basic_space_cleanup() + basic-space-cleanup: + action_help: Basic space cleanup (apt, journalctl, logs, ...) + ### tools_shutdown() shutdown: action_help: Shutdown the server @@ -1885,7 +1891,7 @@ log: - display arguments: path: - help: Log file which to display the content + help: Log file which to display the content. 'last' or 'last-X' selects the last but X log file -n: full: --number help: Number of lines to display @@ -1976,7 +1982,7 @@ diagnosis: api: PUT /diagnosis/ignore arguments: --filter: - help: "Add a filter. The first element should be a diagnosis category, and other criterias can be provided using the infos from the 'meta' sections in 'yunohost diagnosis show'. For example: 'dnsrecords domain=yolo.test category=xmpp'" + help: "Add a filter. The first element should be a diagnosis category, and other criterias can be provided using the infos from the 'meta' sections in 'yunohost diagnosis show'. For example: 'dnsrecords domain=yolo.test category=mail'" nargs: "*" metavar: CRITERIA --list: @@ -1988,6 +1994,6 @@ diagnosis: api: PUT /diagnosis/unignore arguments: --filter: - help: Remove a filter (it should be an existing filter as listed with --list) + help: Remove a filter (it should be an existing filter as listed with "ignore --list") nargs: "*" metavar: CRITERIA diff --git a/share/config_domain.toml b/share/config_domain.toml index 82ef90c32..e9ec5a437 100644 --- a/share/config_domain.toml +++ b/share/config_domain.toml @@ -4,11 +4,51 @@ i18n = "domain_config" [feature] name = "Features" + [feature.portal] + # Only available for "topest" domains + name = "Portal" + + [feature.portal.show_other_domains_apps] + type = "boolean" + default = false + + [feature.portal.portal_title] + type = "string" + default = "YunoHost" + + [feature.portal.portal_logo] + type = "file" + accept = ["image/png", "image/jpeg", "image/svg+xml"] + mode = "python" + bind = "/usr/share/yunohost/portal/customassets/{filename}{ext}" + + [feature.portal.portal_theme] + type = "select" + choices = ["system", "light", "dark", "cupcake", "bumblebee", "emerald", "corporate", "synthwave", "retro", "cyberpunk", "valentine", "halloween", "garden", "forest", "aqua", "lofi", "pastel", "fantasy", "wireframe", "black", "luxury", "dracula", "cmyk", "autumn", "business", "acid", "lemonade", "night", "coffee", "winter"] + default = "system" + + [feature.portal.search_engine] + type = "url" + default = "" + + [feature.portal.search_engine_name] + type = "string" + visible = "search_engine" + + [feature.portal.portal_user_intro] + type = "text" + + [feature.portal.portal_public_intro] + type = "text" + + # FIXME link to GCU + [feature.app] [feature.app.default_app] type = "app" filter = "is_webapp" default = "_none" + add_yunohost_portal_to_choices = true [feature.mail] @@ -20,12 +60,6 @@ name = "Features" type = "boolean" default = 1 - [feature.xmpp] - - [feature.xmpp.xmpp] - type = "boolean" - default = 0 - [dns] name = "DNS" diff --git a/share/config_global.toml b/share/config_global.toml index 40b71ab19..701c444de 100644 --- a/share/config_global.toml +++ b/share/config_global.toml @@ -107,7 +107,7 @@ name = "Email" [email.pop3.pop3_enabled] type = "boolean" default = false - + [email.smtp] name = "SMTP" [email.smtp.smtp_allow_ipv6] @@ -144,16 +144,6 @@ name = "Email" [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" diff --git a/share/helpers b/share/helpers deleted file mode 100644 index 04f7b538c..000000000 --- a/share/helpers +++ /dev/null @@ -1,8 +0,0 @@ -# -*- shell-script -*- - -readonly XTRACE_ENABLE=$(set +o | grep xtrace) # This is a trick to later only restore set -x if it was set when calling this script -set +x -for helper in $(run-parts --list /usr/share/yunohost/helpers.d 2>/dev/null) ; do - [ -r $helper ] && . $helper || true -done -eval "$XTRACE_ENABLE" diff --git a/share/registrar_list.toml b/share/registrar_list.toml index 3f478a03f..47218c9e3 100644 --- a/share/registrar_list.toml +++ b/share/registrar_list.toml @@ -227,7 +227,7 @@ redact = true [gandi.api_protocol] - type = "string" + type = "select" choices.rpc = "RPC" choices.rest = "REST" default = "rest" diff --git a/src/__init__.py b/src/__init__.py index 28b951801..09dbabf65 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,6 @@ #! /usr/bin/python # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -50,6 +50,13 @@ def cli(debug, quiet, output_as, timeout, args, parser): def api(debug, host, port): + + allowed_cors_origins = [] + allowed_cors_origins_file = "/etc/yunohost/.admin-api-allowed-cors-origins" + + if os.path.exists(allowed_cors_origins_file): + allowed_cors_origins = open(allowed_cors_origins_file).read().strip().split(",") + init_logging(interface="api", debug=debug) def is_installed_api(): @@ -64,6 +71,28 @@ def api(debug, host, port): actionsmap="/usr/share/yunohost/actionsmap.yml", locales_dir="/usr/share/yunohost/locales/", routes={("GET", "/installed"): is_installed_api}, + allowed_cors_origins=allowed_cors_origins, + ) + sys.exit(ret) + + +def portalapi(debug, host, port): + + allowed_cors_origins = [] + allowed_cors_origins_file = "/etc/yunohost/.portal-api-allowed-cors-origins" + + if os.path.exists(allowed_cors_origins_file): + allowed_cors_origins = open(allowed_cors_origins_file).read().strip().split(",") + + # FIXME : is this the logdir we want ? (yolo to work around permission issue) + init_logging(interface="portalapi", debug=debug, logdir="/var/log") + + ret = moulinette.api( + host=host, + port=port, + actionsmap="/usr/share/yunohost/actionsmap-portal.yml", + locales_dir="/usr/share/yunohost/locales/", + allowed_cors_origins=allowed_cors_origins, ) sys.exit(ret) @@ -117,17 +146,11 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun "version": 1, "disable_existing_loggers": True, "formatters": { - "console": { - "format": "%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" + "tty-debug": { + "format": "%(relativeCreated)-4d %(level_with_color)s %(message)s" }, - "tty-debug": {"format": "%(relativeCreated)-4d %(fmessage)s"}, "precise": { - "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" - }, - }, - "filters": { - "action": { - "()": "moulinette.utils.log.ActionFilter", + "format": "%(asctime)-15s %(levelname)-8s %(name)s.%(funcName)s - %(message)s" }, }, "handlers": { @@ -140,7 +163,6 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun "class": "logging.FileHandler", "formatter": "precise", "filename": logfile, - "filters": ["action"], }, }, @@ -163,7 +185,7 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun } # Logging configuration for CLI (or any other interface than api...) # - if interface != "api": + if interface not in ["api", "portalapi"]: configure_logging(logging_configuration) # Logging configuration for API # diff --git a/src/app.py b/src/app.py index dff53df6d..e477af109 100644 --- a/src/app.py +++ b/src/app.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -17,29 +17,26 @@ # along with this program. If not, see . # +import time import glob import os -import toml -import json import shutil import yaml -import time import re import subprocess import tempfile import copy -from collections import OrderedDict -from typing import List, Tuple, Dict, Any, Iterator, Optional +from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Iterator, Optional, Union from packaging import version +from logging import getLogger +from pathlib import Path from moulinette import Moulinette, m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.process import run_commands, check_output from moulinette.utils.filesystem import ( read_file, read_json, read_toml, - read_yaml, write_to_file, write_to_json, cp, @@ -48,12 +45,6 @@ from moulinette.utils.filesystem import ( chmod, ) -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.system import ( @@ -74,7 +65,13 @@ from yunohost.app_catalog import ( # noqa APPS_CATALOG_LOGOS, ) -logger = getActionLogger("yunohost.app") +if TYPE_CHECKING: + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + + from yunohost.utils.configpanel import RawSettings + from yunohost.utils.form import FormModel + +logger = getLogger("yunohost.app") APPS_SETTING_PATH = "/etc/yunohost/apps/" APP_TMP_WORKDIRS = "/var/cache/yunohost/app_tmp_work_dirs" @@ -84,7 +81,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 = [ @@ -99,6 +96,8 @@ APP_FILES_TO_COPY = [ "doc", ] +PORTAL_SETTINGS_DIR = "/etc/yunohost/portal" + def app_list(full=False, upgradable=False): """ @@ -124,8 +123,8 @@ def app_info(app, full=False, upgradable=False): """ Get info for a specific app """ + from yunohost.domain import _get_raw_domain_settings from yunohost.permission import user_permission_list - from yunohost.domain import domain_config_get _assert_is_installed(app) @@ -189,11 +188,17 @@ def app_info(app, full=False, upgradable=False): ret["from_catalog"] = from_catalog # Hydrate app notifications and doc + rendered_doc = {} for pagename, content_per_lang in ret["manifest"]["doc"].items(): for lang, content in content_per_lang.items(): - ret["manifest"]["doc"][pagename][lang] = _hydrate_app_template( - content, settings - ) + rendered_content = _hydrate_app_template(content, settings) + # Rendered content may be empty because of conditional blocks + if not rendered_content: + continue + if pagename not in rendered_doc: + rendered_doc[pagename] = {} + rendered_doc[pagename][lang] = rendered_content + ret["manifest"]["doc"] = rendered_doc # Filter dismissed notification ret["manifest"]["notifications"] = { @@ -204,15 +209,22 @@ def app_info(app, full=False, upgradable=False): # Hydrate notifications (also filter uneeded post_upgrade notification based on version) for step, notifications in ret["manifest"]["notifications"].items(): + rendered_notifications = {} for name, content_per_lang in notifications.items(): for lang, content in content_per_lang.items(): - notifications[name][lang] = _hydrate_app_template(content, settings) + rendered_content = _hydrate_app_template(content, settings) + if not rendered_content: + continue + if name not in rendered_notifications: + rendered_notifications[name] = {} + rendered_notifications[name][lang] = rendered_content + ret["manifest"]["notifications"][step] = rendered_notifications - ret["is_webapp"] = "domain" in settings and "path" in settings + ret["is_webapp"] = "domain" in settings and settings["domain"] and "path" in settings if ret["is_webapp"]: ret["is_default"] = ( - domain_config_get(settings["domain"], "feature.app.default_app") == app + _get_raw_domain_settings(settings["domain"]).get("default_app") == app ) ret["supports_change_url"] = os.path.exists( @@ -227,6 +239,10 @@ def app_info(app, full=False, upgradable=False): ret["supports_config_panel"] = os.path.exists( os.path.join(setting_path, "config_panel.toml") ) + ret["supports_purge"] = ( + local_manifest["packaging_format"] >= 2 + and local_manifest["resources"].get("data_dir") is not None + ) ret["permissions"] = permissions ret["label"] = permissions.get(app + ".main", {}).get("label") @@ -241,8 +257,8 @@ def _app_upgradable(app_infos): # Determine upgradability app_in_catalog = app_infos.get("from_catalog") - installed_version = version.parse(app_infos.get("version", "0~ynh0")) - version_in_catalog = version.parse( + installed_version = _parse_app_version(app_infos.get("version", "0~ynh0")) + version_in_catalog = _parse_app_version( app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0") ) @@ -257,25 +273,7 @@ def _app_upgradable(app_infos): ): return "bad_quality" - # If the app uses the standard version scheme, use it to determine - # upgradability - if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog): - if installed_version < version_in_catalog: - return "yes" - else: - return "no" - - # Legacy stuff for app with old / non-standard version numbers... - - # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded - if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[ - "from_catalog" - ].get("git"): - return "url_required" - - settings = app_infos["settings"] - local_update_time = settings.get("update_time", settings.get("install_time", 0)) - if app_infos["from_catalog"]["lastUpdate"] > local_update_time: + if installed_version < version_in_catalog: return "yes" else: return "no" @@ -411,6 +409,7 @@ def app_change_url(operation_logger, app, domain, path): path -- New path at which the application will be move """ + from yunohost.utils.form import DomainOption, WebPathOption from yunohost.hook import hook_exec_with_script_debug_if_failure, hook_callback from yunohost.service import service_reload_or_restart @@ -464,6 +463,9 @@ def app_change_url(operation_logger, app, domain, path): env_dict["old_path"] = old_path env_dict["new_domain"] = domain env_dict["new_path"] = path + env_dict["domain"] = domain + env_dict["path"] = path + env_dict["path_url"] = path env_dict["change_path"] = "1" if old_path != path else "0" env_dict["change_domain"] = "1" if old_domain != domain else "0" @@ -559,7 +561,7 @@ def app_upgrade( ) 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.utils.legacy import _patch_legacy_helpers from yunohost.backup import ( backup_list, backup_create, @@ -620,9 +622,11 @@ def app_upgrade( # Manage upgrade type and avoid any upgrade if there is nothing to do upgrade_type = "UNKNOWN" # Get current_version and new version - app_new_version = version.parse(manifest.get("version", "?")) - app_current_version = version.parse(app_dict.get("version", "?")) - if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version): + app_new_version_raw = manifest.get("version", "?") + app_current_version_raw = app_dict.get("version", "?") + app_new_version = _parse_app_version(app_new_version_raw) + app_current_version = _parse_app_version(app_current_version_raw) + if "~ynh" in str(app_current_version_raw) and "~ynh" in str(app_new_version_raw): if app_current_version >= app_new_version and not force: # In case of upgrade from file or custom repository # No new version available @@ -636,23 +640,18 @@ def app_upgrade( manifest.get("remote", {}).get("revision", "?"), ) continue - elif app_current_version > app_new_version: - upgrade_type = "DOWNGRADE_FORCED" + + if app_current_version > app_new_version: + upgrade_type = "DOWNGRADE" elif app_current_version == app_new_version: - upgrade_type = "UPGRADE_FORCED" + upgrade_type = "UPGRADE_SAME" else: - app_current_version_upstream, app_current_version_pkg = str( - app_current_version - ).split("~ynh") - app_new_version_upstream, app_new_version_pkg = str( - app_new_version - ).split("~ynh") + app_current_version_upstream, _ = str(app_current_version_raw).split("~ynh") + app_new_version_upstream, _ = str(app_new_version_raw).split("~ynh") if app_current_version_upstream == app_new_version_upstream: upgrade_type = "UPGRADE_PACKAGE" - elif app_current_version_pkg == app_new_version_pkg: - upgrade_type = "UPGRADE_APP" else: - upgrade_type = "UPGRADE_FULL" + upgrade_type = "UPGRADE_APP" # Check requirements for name, passed, values, err in _check_manifest_requirements( @@ -675,7 +674,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["PRE_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) _display_notifications(notifications, force=force) @@ -697,9 +696,17 @@ def app_upgrade( safety_backup_name = f"{app_instance_name}-pre-upgrade2" other_safety_backup_name = f"{app_instance_name}-pre-upgrade1" - backup_create( - name=safety_backup_name, apps=[app_instance_name], system=None - ) + tweaked_backup_core_only = False + if "BACKUP_CORE_ONLY" not in os.environ: + tweaked_backup_core_only = True + os.environ["BACKUP_CORE_ONLY"] = "1" + try: + backup_create( + name=safety_backup_name, apps=[app_instance_name], system=None + ) + finally: + if tweaked_backup_core_only: + del os.environ["BACKUP_CORE_ONLY"] if safety_backup_name in backup_list()["archives"]: # if the backup suceeded, delete old safety backup to save space @@ -722,9 +729,6 @@ def app_upgrade( # 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" @@ -732,8 +736,8 @@ def app_upgrade( env_dict_more = { "YNH_APP_UPGRADE_TYPE": upgrade_type, - "YNH_APP_MANIFEST_VERSION": str(app_new_version), - "YNH_APP_CURRENT_VERSION": str(app_current_version), + "YNH_APP_MANIFEST_VERSION": str(app_new_version_raw), + "YNH_APP_CURRENT_VERSION": str(app_current_version_raw), } if manifest["packaging_format"] < 2: @@ -750,7 +754,10 @@ def app_upgrade( from yunohost.utils.resources import AppResourceManager AppResourceManager( - app_instance_name, wanted=manifest, current=app_dict["manifest"] + app_instance_name, + wanted=manifest, + current=app_dict["manifest"], + workdir=extracted_app_folder, ).apply( rollback_and_raise_exception_if_failure=True, operation_logger=operation_logger, @@ -791,7 +798,7 @@ def app_upgrade( and not no_safety_backup ): logger.warning( - "Upgrade failed ... attempting to restore the satefy backup (Yunohost first need to remove the app for this) ..." + "Upgrade failed ... attempting to restore the safety backup (Yunohost first need to remove the app for this) ..." ) app_remove(app_instance_name, force_workdir=extracted_app_folder) @@ -826,6 +833,41 @@ def app_upgrade( + "\n -".join(manually_modified_files_by_app) ) + # If the upgrade didnt fail, update the revision and app files (even if it broke the system, otherwise we end up in a funky intermediate state where the app files don't match the installed version or settings, for example for v1->v2 upgrade marked as "broke the system" for some reason) + if not upgrade_failed: + now = int(time.time()) + app_setting(app_instance_name, "update_time", now) + app_setting( + app_instance_name, + "current_revision", + manifest.get("remote", {}).get("revision", "?"), + ) + + # Clean hooks and add new ones + hook_remove(app_instance_name) + if "hooks" in os.listdir(extracted_app_folder): + for hook in os.listdir(extracted_app_folder + "/hooks"): + hook_add( + app_instance_name, extracted_app_folder + "/hooks/" + hook + ) + + # Replace scripts and manifest and conf (if exists) + # Move scripts and manifest to the right place + for file_to_copy in APP_FILES_TO_COPY: + rm(f"{app_setting_path}/{file_to_copy}", recursive=True, force=True) + if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): + cp( + f"{extracted_app_folder}/{file_to_copy}", + f"{app_setting_path}/{file_to_copy}", + recursive=True, + ) + + # Clean and set permissions + shutil.rmtree(extracted_app_folder) + chmod(app_setting_path, 0o600) + chmod(f"{app_setting_path}/settings.yml", 0o400) + chown(app_setting_path, "root", recursive=True) + # If upgrade failed or broke the system, # raise an error and interrupt all other pending upgrades if upgrade_failed or broke_the_system: @@ -876,36 +918,6 @@ def app_upgrade( ) # Otherwise we're good and keep going ! - now = int(time.time()) - app_setting(app_instance_name, "update_time", now) - app_setting( - app_instance_name, - "current_revision", - manifest.get("remote", {}).get("revision", "?"), - ) - - # Clean hooks and add new ones - hook_remove(app_instance_name) - if "hooks" in os.listdir(extracted_app_folder): - for hook in os.listdir(extracted_app_folder + "/hooks"): - hook_add(app_instance_name, extracted_app_folder + "/hooks/" + hook) - - # Replace scripts and manifest and conf (if exists) - # Move scripts and manifest to the right place - for file_to_copy in APP_FILES_TO_COPY: - rm(f"{app_setting_path}/{file_to_copy}", recursive=True, force=True) - if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): - cp( - f"{extracted_app_folder}/{file_to_copy}", - f"{app_setting_path}/{file_to_copy}", - recursive=True, - ) - - # Clean and set permissions - shutil.rmtree(extracted_app_folder) - chmod(app_setting_path, 0o600) - chmod(f"{app_setting_path}/settings.yml", 0o400) - chown(app_setting_path, "root", recursive=True) # So much win logger.success(m18n.n("app_upgraded", app=app_instance_name)) @@ -916,7 +928,7 @@ def app_upgrade( settings = _get_app_settings(app_instance_name) notifications = _filter_and_hydrate_notifications( manifest["notifications"]["POST_UPGRADE"], - current_version=app_current_version, + current_version=app_current_version_raw, data=settings, ) if Moulinette.interface.type == "cli": @@ -951,10 +963,11 @@ def app_upgrade( def app_manifest(app, with_screenshot=False): + from yunohost.utils.form import parse_raw_options + manifest, extracted_app_folder = _extract_app(app) - raw_questions = manifest.get("install", {}).values() - manifest["install"] = hydrate_questions_with_choices(raw_questions) + manifest["install"] = parse_raw_options(manifest.get("install", {}), serialize=True) # Add a base64 image to be displayed in web-admin if with_screenshot and Moulinette.interface.type == "api": @@ -970,9 +983,9 @@ def app_manifest(app, with_screenshot=False): 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}" + manifest["screenshot"] = ( + f"data:image/{ext};charset=utf-8;base64,{data}" + ) break shutil.rmtree(extracted_app_folder) @@ -1047,7 +1060,9 @@ def app_install( permission_sync_to_user, ) from yunohost.regenconf import manually_modified_files - from yunohost.utils.legacy import _patch_legacy_php_versions, _patch_legacy_helpers + from yunohost.utils.legacy import _patch_legacy_helpers + from yunohost.utils.form import ask_questions_and_parse_answers + from yunohost.user import user_list # Check if disk space available if free_space_in_directory("/") <= 512 * 1000 * 1000: @@ -1071,6 +1086,11 @@ def app_install( app_id = manifest["id"] + if app_id in user_list()["users"].keys(): + raise YunohostValidationError( + f"There is already a YunoHost user called {app_id} ...", raw_msg=True + ) + # Check requirements for name, passed, values, err in _check_manifest_requirements( manifest, action="install" @@ -1096,13 +1116,9 @@ def app_install( app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) # Retrieve arguments list for install script - raw_questions = manifest["install"] - questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args) - args = { - question.name: question.value - for question in questions - if question.value is not None - } + raw_options = manifest["install"] + options, form = ask_questions_and_parse_answers(raw_options, prefilled_answers=args) + args = form.dict(exclude_none=True) # Validate domain / path availability for webapps # (ideally this should be handled by the resource system for manifest v >= 2 @@ -1113,9 +1129,6 @@ def app_install( # 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) - # We'll check that the app didn't brutally edit some system configuration manually_modified_files_before_install = manually_modified_files() @@ -1139,15 +1152,18 @@ def app_install( "current_revision": manifest.get("remote", {}).get("revision", "?"), } - # If packaging_format v2+, save all install questions as settings + # If packaging_format v2+, save all install options as settings if packaging_format >= 2: - for question in questions: + for option in options: + # Except readonly "questions" that don't even have a value + if option.readonly: + continue # Except user-provider passwords # ... which we need to reinject later in the env_dict - if question.type == "password": + if option.type == "password": continue - app_settings[question.name] = question.value + app_settings[option.id] = form[option.id] _set_app_settings(app_instance_name, app_settings) @@ -1160,6 +1176,10 @@ def app_install( recursive=True, ) + # Hotfix for bug in the webadmin while we fix the actual issue :D + if label == "undefined": + label = None + # Override manifest name by given label # This info is also later picked-up by the 'permission' resource initialization if label: @@ -1196,22 +1216,23 @@ def app_install( app_instance_name, args=args, workdir=extracted_app_folder, action="install" ) - # If packaging_format v2+, save all install questions as settings + # If packaging_format v2+, save all install options as settings if packaging_format >= 2: - for question in questions: + for option in options: # Reinject user-provider passwords which are not in the app settings # (cf a few line before) - if question.type == "password": - env_dict[question.name] = question.value + if option.type == "password": + env_dict[option.id] = form[option.id] # We want to hav the env_dict in the log ... but not password values env_dict_for_logging = env_dict.copy() - for question in questions: - # Or should it be more generally question.redact ? - if question.type == "password": - 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] + for option in options: + # Or should it be more generally option.redact ? + if option.type == "password": + if f"YNH_APP_ARG_{option.id.upper()}" in env_dict_for_logging: + del env_dict_for_logging[f"YNH_APP_ARG_{option.id.upper()}"] + if option.id in env_dict_for_logging: + del env_dict_for_logging[option.id] operation_logger.extra.update({"env": env_dict_for_logging}) @@ -1373,14 +1394,14 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None): 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.utils.legacy import _patch_legacy_helpers from yunohost.hook import hook_exec, hook_remove, hook_callback from yunohost.permission import ( user_permission_list, permission_delete, permission_sync_to_user, ) - from yunohost.domain import domain_list, domain_config_get, domain_config_set + from yunohost.domain import domain_list, domain_config_set, _get_raw_domain_settings if not _is_installed(app): raise YunohostValidationError( @@ -1395,10 +1416,6 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None): # Attempt to patch legacy helpers ... _patch_legacy_helpers(app_setting_path) - # Apply dirty patch to make php5 apps compatible with php7 (e.g. the remove - # script might date back from jessie install) - _patch_legacy_php_versions(app_setting_path) - 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 @@ -1452,13 +1469,16 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None): for permission_name in user_permission_list(apps=[app])["permissions"].keys(): permission_delete(permission_name, force=True, sync_perm=False) + if purge and os.path.exists(f"/var/log/{app}"): + shutil.rmtree(f"/var/log/{app}") + if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) hook_remove(app) for domain in domain_list()["domains"]: - if domain_config_get(domain, "feature.app.default_app") == app: + if _get_raw_domain_settings(domain).get("default_app") == app: domain_config_set(domain, "feature.app.default_app", "_none") if ret == 0: @@ -1514,119 +1534,6 @@ def app_setting(app, key, value=None, delete=False): """ app_settings = _get_app_settings(app) or {} - # - # Legacy permission setting management - # (unprotected, protected, skipped_uri/regex) - # - - is_legacy_permission_setting = any( - key.startswith(word + "_") for word in ["unprotected", "protected", "skipped"] - ) - - if is_legacy_permission_setting: - from yunohost.permission import ( - user_permission_list, - user_permission_update, - permission_create, - permission_delete, - permission_url, - ) - - permissions = user_permission_list(full=True, apps=[app])["permissions"] - key_ = key.split("_")[0] - permission_name = f"{app}.legacy_{key_}_uris" - permission = permissions.get(permission_name) - - # GET - if value is None and not delete: - return ( - ",".join(permission.get("uris", []) + permission["additional_urls"]) - if permission - else None - ) - - # DELETE - if delete: - # If 'is_public' setting still exists, we interpret this as - # coming from a legacy app (because new apps shouldn't manage the - # is_public state themselves anymore...) - # - # In that case, we interpret the request for "deleting - # unprotected/skipped" setting as willing to make the app - # private - if ( - "is_public" in app_settings - and "visitors" in permissions[app + ".main"]["allowed"] - ): - if key.startswith("unprotected_") or key.startswith("skipped_"): - user_permission_update(app + ".main", remove="visitors") - - if permission: - permission_delete(permission_name) - - # SET - else: - urls = value - # If the request is about the root of the app (/), ( = the vast majority of cases) - # we interpret this as a change for the main permission - # (i.e. allowing/disallowing visitors) - if urls == "/": - if key.startswith("unprotected_") or key.startswith("skipped_"): - permission_url(app + ".main", url="/", sync_perm=False) - user_permission_update(app + ".main", add="visitors") - else: - user_permission_update(app + ".main", remove="visitors") - else: - urls = urls.split(",") - if key.endswith("_regex"): - urls = ["re:" + url for url in urls] - - if permission: - # In case of new regex, save the urls, to add a new time in the additional_urls - # In case of new urls, we do the same thing but inversed - if key.endswith("_regex"): - # List of urls to save - current_urls_or_regex = [ - url - for url in permission["additional_urls"] - if not url.startswith("re:") - ] - else: - # List of regex to save - current_urls_or_regex = [ - url - for url in permission["additional_urls"] - if url.startswith("re:") - ] - - new_urls = urls + current_urls_or_regex - # We need to clear urls because in the old setting the new setting override the old one and dont just add some urls - permission_url(permission_name, clear_urls=True, sync_perm=False) - permission_url(permission_name, add_url=new_urls) - else: - from yunohost.utils.legacy import legacy_permission_label - - # Let's create a "special" permission for the legacy settings - permission_create( - permission=permission_name, - # FIXME find a way to limit to only the user allowed to the main permission - allowed=["all_users"] - if key.startswith("protected_") - else ["all_users", "visitors"], - url=None, - additional_urls=urls, - auth_header=not key.startswith("skipped_"), - label=legacy_permission_label(app, key.split("_")[0]), - show_tile=False, - protected=True, - ) - - return - - # - # Regular setting management - # - # GET if value is None and not delete: return app_settings.get(key, None) @@ -1635,16 +1542,34 @@ def app_setting(app, key, value=None, delete=False): if delete: if key in app_settings: del app_settings[key] + else: + # Don't call _set_app_settings to avoid unecessary writes... + return # SET else: - if key in ["redirected_urls", "redirected_regex"]: - value = yaml.safe_load(value) app_settings[key] = value _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 @@ -1654,6 +1579,7 @@ def app_register_url(app, domain, path): domain -- The domain on which the app should be registered (e.g. your.domain.tld) path -- The path to be registered (e.g. /coffee) """ + from yunohost.utils.form import DomainOption, WebPathOption from yunohost.permission import ( permission_url, user_permission_update, @@ -1694,12 +1620,15 @@ def app_ssowatconf(): """ - from yunohost.domain import domain_list, _get_maindomain, domain_config_get + from yunohost.domain import ( + domain_list, + _get_raw_domain_settings, + _get_domain_portal_dict, + ) from yunohost.permission import user_permission_list - from yunohost.settings import settings_get - main_domain = _get_maindomain() domains = domain_list()["domains"] + portal_domains = domain_list(exclude_subdomains=True)["domains"] all_permissions = user_permission_list( full=True, ignore_system_perms=True, absolute_urls=True )["permissions"] @@ -1707,49 +1636,25 @@ def app_ssowatconf(): permissions = { "core_skipped": { "users": [], - "label": "Core permissions - skipped", - "show_tile": False, "auth_header": False, "public": True, "uris": [domain + "/yunohost/admin" for domain in domains] + [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.*$", + r"re:^[^/]*/502\.html$", + r"re:^[^/]*/\.well-known/ynh-diagnosis/.*$", + r"re:^[^/]*/\.well-known/acme-challenge/.*$", + r"re:^[^/]*/\.well-known/autoconfig/mail/config-v1\.1\.xml.*$", ], } } - redirected_regex = { - main_domain + r"/yunohost[\/]?$": "https://" + main_domain + "/yunohost/sso/" - } + + # FIXME : this could be handled by nginx's regen conf to further simplify ssowat's code ... redirected_urls = {} - - apps_using_remote_user_var_in_nginx = ( - check_output( - "grep -nri '$remote_user' /etc/yunohost/apps/*/conf/*nginx*conf | awk -F/ '{print $5}' || true" - ) - .strip() - .split("\n") - ) - - for app in _installed_apps(): - app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") or {} - - # Redirected - redirected_urls.update(app_settings.get("redirected_urls", {})) - redirected_regex.update(app_settings.get("redirected_regex", {})) - - from .utils.legacy import ( - translate_legacy_default_app_in_ssowant_conf_json_persistent, - ) - - translate_legacy_default_app_in_ssowant_conf_json_persistent() - for domain in domains: - default_app = domain_config_get(domain, "feature.app.default_app") - if default_app != "_none" and _is_installed(default_app): + default_app = _get_raw_domain_settings(domain).get("default_app") + if default_app not in ["_none", None] and _is_installed(default_app): app_settings = _get_app_settings(default_app) app_domain = app_settings["domain"] app_path = app_settings["path"] @@ -1758,6 +1663,10 @@ def app_ssowatconf(): if domain + "/" != app_domain + app_path: redirected_urls[domain + "/"] = app_domain + app_path + # Will organize apps by portal domain + portal_domains_apps = {domain: {} for domain in portal_domains} + apps_catalog = _load_apps_catalog()["apps"] + # New permission system for perm_name, perm_info in all_permissions.items(): uris = ( @@ -1771,38 +1680,98 @@ def app_ssowatconf(): continue app_id = perm_name.split(".")[0] + app_settings = _get_app_settings(app_id) + + # Stupid hard-coded hack until we properly propagate this to apps ... + apps_that_need_password_in_auth_header = ["nextcloud"] + + if perm_info["auth_header"]: + if app_id in apps_that_need_password_in_auth_header: + auth_header = "basic-with-password" + elif app_settings.get("auth_header"): + auth_header = app_settings.get("auth_header") + assert auth_header in ["basic-with-password", "basic-without-password"] + else: + auth_header = "basic-without-password" + else: + auth_header = False 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"] - and perm_info["url"] - and (not perm_info["url"].startswith("re:")), - "auth_header": perm_info["auth_header"], + "auth_header": auth_header, "public": "visitors" in perm_info["allowed"], "uris": uris, } + # Apps can opt out of the auth spoofing protection using this if they really need to, + # but that's a huge security hole and ultimately should never happen... + if app_settings.get("protect_against_basic_auth_spoofing", True) in [False, "False", "false", "0", 0]: + permissions[perm_name]["protect_against_basic_auth_spoofing"] = False + + # Next: portal related + # No need to keep apps that aren't supposed to be displayed in portal + if not perm_info.get("show_tile", False): + continue + + setting_path = os.path.join(APPS_SETTING_PATH, app_id) + local_manifest = _get_manifest_of_app(setting_path) + + app_domain = uris[0].split("/")[0] + # get "topest" domain + app_portal_domain = next( + domain for domain in portal_domains if domain in app_domain + ) + app_portal_info = { + "label": perm_info["label"], + "users": perm_info["corresponding_users"], + "public": "visitors" in perm_info["allowed"], + "url": uris[0], + "description": local_manifest["description"], + } + + # FIXME : find a smarter way to get this info ? (in the settings maybe..) + # Also ideally we should not rely on the webadmin route for this, maybe expose these through a different route in nginx idk + # Also related to "people will want to customize those.." + app_catalog_info = apps_catalog.get(app_id.split("__")[0]) + if app_catalog_info and "logo_hash" in app_catalog_info: + app_portal_info["logo"] = f"//{app_portal_domain}/yunohost/sso/applogos/{app_catalog_info['logo_hash']}.png" + + portal_domains_apps[app_portal_domain][app_id] = app_portal_info + conf_dict = { - "theme": settings_get("misc.portal.portal_theme"), - "portal_domain": main_domain, - "portal_path": "/yunohost/sso/", - "additional_headers": { - "Auth-User": "uid", - "Remote-User": "uid", - "Name": "cn", - "Email": "mail", - }, - "domains": domains, + "cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret", + "session_folder": "/var/cache/yunohost-portal/sessions", + "cookie_name": "yunohost.portal", "redirected_urls": redirected_urls, - "redirected_regex": redirected_regex, + "domain_portal_urls": _get_domain_portal_dict(), "permissions": permissions, } write_to_json("/etc/ssowat/conf.json", conf_dict, sort_keys=True, indent=4) + # Generate a file per possible portal with available apps + for domain, apps in portal_domains_apps.items(): + portal_settings = {} + + portal_settings_path = Path(PORTAL_SETTINGS_DIR) / f"{domain}.json" + if portal_settings_path.exists(): + portal_settings.update(read_json(str(portal_settings_path))) + + # Do no override anything else than "apps" since the file is shared + # with domain's config panel "portal" options + portal_settings["apps"] = apps + + write_to_json( + str(portal_settings_path), portal_settings, sort_keys=True, indent=4 + ) + + # Cleanup old files from possibly old domains + for setting_file in Path(PORTAL_SETTINGS_DIR).iterdir(): + if setting_file.name.endswith(".json"): + domain = setting_file.name[:-len(".json")] + if domain not in portal_domains_apps: + setting_file.unlink() + logger.debug(m18n.n("ssowat_conf_generated")) @@ -1823,11 +1792,13 @@ def app_change_label(app, new_label): def app_action_list(app): + AppConfigPanel = _get_AppConfigPanel() return AppConfigPanel(app).list_actions() @is_unit_operation() def app_action_run(operation_logger, app, action, args=None, args_file=None): + AppConfigPanel = _get_AppConfigPanel() return AppConfigPanel(app).run_action( action, args=args, args_file=args_file, operation_logger=operation_logger ) @@ -1849,6 +1820,7 @@ def app_config_get(app, key="", full=False, export=False): else: mode = "classic" + AppConfigPanel = _get_AppConfigPanel() try: config_ = AppConfigPanel(app) return config_.get(key, mode) @@ -1868,107 +1840,87 @@ def app_config_set( Apply a new app configuration """ + AppConfigPanel = _get_AppConfigPanel() config_ = AppConfigPanel(app) return config_.set(key, value, args, args_file, operation_logger=operation_logger) -class AppConfigPanel(ConfigPanel): - entity_type = "app" - save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml") - config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml") +def _get_AppConfigPanel(): + from yunohost.utils.configpanel import ConfigPanel - def _run_action(self, action): - env = {key: str(value) for key, value in self.new_values.items()} - self._call_config_script(action, env=env) + class AppConfigPanel(ConfigPanel): + entity_type = "app" + save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml") + config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml") + settings_must_be_defined: bool = True - def _get_raw_settings(self): - self.values = self._call_config_script("show") + def _get_raw_settings(self) -> "RawSettings": + return self._call_config_script("show") - def _apply(self): - env = {key: str(value) for key, value in self.new_values.items()} - return_content = self._call_config_script("apply", env=env) + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ) -> None: + env = {key: str(value) for key, value in form.dict().items()} + return_content = self._call_config_script("apply", env=env) - # If the script returned validation error - # raise a ValidationError exception using - # the first key - if return_content: - for key, message in return_content.get("validation_errors").items(): - raise YunohostValidationError( - "app_argument_invalid", - name=key, - error=message, - ) + # If the script returned validation error + # raise a ValidationError exception using + # the first key + errors = return_content.get("validation_errors") + if errors: + for key, message in errors.items(): + raise YunohostValidationError( + "app_argument_invalid", + name=key, + error=message, + ) - def _call_config_script(self, action, env=None): - from yunohost.hook import hook_exec + def _run_action(self, form: "FormModel", action_id: str) -> None: + env = {key: str(value) for key, value in form.dict().items()} + self._call_config_script(action_id, env=env) - if env is None: - env = {} + def _call_config_script( + self, action: str, env: Union[dict[str, Any], None] = None + ) -> dict[str, Any]: + from yunohost.hook import hook_exec - # Add default config script if needed - config_script = os.path.join( - APPS_SETTING_PATH, self.entity, "scripts", "config" - ) - if not os.path.exists(config_script): - logger.debug("Adding a default config script") - default_script = """#!/bin/bash + if env is None: + env = {} + + # Add default config script if needed + config_script = os.path.join( + APPS_SETTING_PATH, self.entity, "scripts", "config" + ) + if not os.path.exists(config_script): + logger.debug("Adding a default config script") + default_script = """#!/bin/bash source /usr/share/yunohost/helpers ynh_abort_if_errors ynh_app_config_run $1 """ - write_to_file(config_script, default_script) + write_to_file(config_script, default_script) - # Call config script to extract current values - logger.debug(f"Calling '{action}' action from config script") - app = self.entity - app_id, app_instance_nb = _parse_app_instance_name(app) - settings = _get_app_settings(app) - env.update( - { - "app_id": app_id, - "app": app, - "app_instance_nb": str(app_instance_nb), - "final_path": settings.get("final_path", ""), - "install_dir": settings.get("install_dir", ""), - "YNH_APP_BASEDIR": os.path.join(APPS_SETTING_PATH, app), - } - ) + # Call config script to extract current values + logger.debug(f"Calling '{action}' action from config script") + app = self.entity + app_setting_path = os.path.join(APPS_SETTING_PATH, self.entity) + env = _make_environment_for_app_script(app, workdir=app_setting_path) - ret, values = hook_exec(config_script, args=[action], env=env) - if ret != 0: - if action == "show": - raise YunohostError("app_config_unable_to_read") - elif action == "apply": - raise YunohostError("app_config_unable_to_apply") - else: - raise YunohostError("app_action_failed", action=action, app=app) - return values + ret, values = hook_exec(config_script, args=[action], env=env) + if ret != 0: + if action == "show": + raise YunohostError("app_config_unable_to_read") + elif action == "apply": + raise YunohostError("app_config_unable_to_apply") + else: + raise YunohostError("app_action_failed", action=action, app=app) + return values - -def _get_app_actions(app_id): - "Get app config panel stored in json or in toml" - actions_toml_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.toml") - actions_json_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.json") - - if os.path.exists(actions_toml_path): - toml_actions = toml.load(open(actions_toml_path, "r"), _dict=OrderedDict) - - # transform toml format into json format - actions = [] - - for key, value in toml_actions.items(): - action = dict(**value) - action["id"] = key - action["arguments"] = value.get("arguments", {}) - actions.append(action) - - return actions - - elif os.path.exists(actions_json_path): - return json.load(open(actions_json_path)) - - return None + return AppConfigPanel def _get_app_settings(app): @@ -2022,6 +1974,20 @@ def _set_app_settings(app, settings): yaml.safe_dump(settings, f, default_flow_style=False) +def _parse_app_version(v): + + if v in ["?", "-"]: + return (0, 0) + + try: + if "~" in v: + return (version.parse(v.split("~")[0]), int(v.split("~")[1].replace("ynh", ""))) + else: + return (version.parse(v), 0) + except Exception as e: + raise YunohostError(f"Failed to parse app version '{v}' : {e}", raw_msg=True) + + def _get_manifest_of_app(path): "Get app manifest stored in json or in toml" @@ -2219,6 +2185,13 @@ def _parse_app_doc_and_notifications(path): def _hydrate_app_template(template, data): + # Apply jinja for stuff like {% if .. %} blocks, + # but only if there's indeed an if block (to try to reduce overhead or idk) + if "{%" in template: + from jinja2 import Template + + template = Template(template).render(**data) + stuff_to_replace = set(re.findall(r"__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__", template)) for stuff in stuff_to_replace: @@ -2227,7 +2200,7 @@ def _hydrate_app_template(template, data): if varname in data: template = template.replace(stuff, str(data[varname])) - return template + return template.strip() def _convert_v1_manifest_to_v2(manifest): @@ -2344,18 +2317,16 @@ def _set_default_ask_questions(questions, script_name="install"): ), # i18n: app_manifest_install_ask_init_admin_permission ] - for question_name, question in questions.items(): - question["name"] = question_name + for question_id, question in questions.items(): + question["id"] = question_id # If this question corresponds to a question with default ask message... if any( - (question.get("type"), question["name"]) == question_with_default + (question.get("type"), question["id"]) == question_with_default for question_with_default in questions_with_default ): # The key is for example "app_manifest_install_ask_domain" - question["ask"] = m18n.n( - f"app_manifest_{script_name}_ask_{question['name']}" - ) + question["ask"] = m18n.n(f"app_manifest_{script_name}_ask_{question['id']}") # Also it in fact doesn't make sense for any of those questions to have an example value nor a default value... if question.get("type") in ["domain", "user", "password"]: @@ -2750,10 +2721,21 @@ def _check_manifest_requirements( ram_requirement["runtime"] ) + # Some apps have a higher runtime value than build ... + if ram_requirement["build"] != "?" and ram_requirement["runtime"] != "?": + max_build_runtime = ( + ram_requirement["build"] + if human_to_binary(ram_requirement["build"]) + > human_to_binary(ram_requirement["runtime"]) + else ram_requirement["runtime"] + ) + else: + max_build_runtime = ram_requirement["build"] + yield ( "ram", can_build and can_run, - {"current": binary_to_human(ram), "required": ram_requirement["build"]}, + {"current": binary_to_human(ram), "required": max_build_runtime}, "app_not_enough_ram", # i18n: app_not_enough_ram ) @@ -2838,6 +2820,7 @@ def _get_conflicting_apps(domain, path, ignore_app=None): """ from yunohost.domain import _assert_domain_exists + from yunohost.utils.form import DomainOption, WebPathOption domain = DomainOption.normalize(domain) path = WebPathOption.normalize(path) @@ -2892,11 +2875,16 @@ def _make_environment_for_app_script( app_id, app_instance_nb = _parse_app_instance_name(app) env_dict = { + "YNH_DEFAULT_PHP_VERSION": "8.2", "YNH_APP_ID": app_id, "YNH_APP_INSTANCE_NAME": app, "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), "YNH_APP_PACKAGING_FORMAT": str(manifest["packaging_format"]), + "YNH_HELPERS_VERSION": str( + manifest.get("integration", {}).get("helpers_version") + or manifest["packaging_format"] + ).replace(".0", ""), "YNH_ARCH": system_arch(), "YNH_DEBIAN_VERSION": debian_version(), } @@ -2914,19 +2902,43 @@ def _make_environment_for_app_script( # If packaging format v2, load all settings if manifest["packaging_format"] >= 2 or force_include_app_settings: env_dict["app"] = app + data_to_redact = [] + prefixes_or_suffixes_to_redact = [ + "pwd", + "pass", + "passwd", + "password", + "passphrase", + "secret", + "key", + "token", + ] + 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) + setting_value = str(setting_value) + env_dict[setting_name] = setting_value + + # Check if we should redact this setting value + # (the check on the setting length exists to prevent stupid stuff like redacting empty string or something which is actually just 0/1, true/false, ... + if len(setting_value) > 6 and any( + setting_name.startswith(p) or setting_name.endswith(p) + for p in prefixes_or_suffixes_to_redact + ): + data_to_redact.append(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"] + for operation_logger in OperationLogger._instances: + operation_logger.data_to_redact.extend(data_to_redact) + return env_dict @@ -3028,27 +3040,12 @@ def _assert_system_is_sane_for_app(manifest, when): logger.debug("Checking that required services are up and running...") - services = manifest.get("services", []) + # FIXME: in the past we had more elaborate checks about mariadb/php/postfix + # though they werent very formalized. Ideally we should rework this in the + # context of packaging v2, which implies deriving what services are + # relevant to check from the manifst - # Some apps use php-fpm, php5-fpm or php7.x-fpm which is now php8.2-fpm - def replace_alias(service): - if service in ["php-fpm", "php5-fpm", "php7.0-fpm", "php7.3-fpm", "php7.4-fpm"]: - return "php8.2-fpm" - else: - return service - - services = [replace_alias(s) for s in services] - - # We only check those, mostly to ignore "custom" services - # (added by apps) and because those are the most popular - # services - service_filter = ["nginx", "php8.2-fpm", "mysql", "postfix"] - services = [str(s) for s in services if s in service_filter] - - if "nginx" not in services: - services = ["nginx"] + services - if "fail2ban" not in services: - services.append("fail2ban") + services = ["nginx", "fail2ban"] # Wait if a service is reloading test_nb = 0 @@ -3118,14 +3115,9 @@ def _notification_is_dismissed(name, settings): def _filter_and_hydrate_notifications(notifications, current_version=None, data={}): def is_version_more_recent_than_current_version(name, current_version): current_version = str(current_version) - # Boring code to handle the fact that "0.1 < 9999~ynh1" is False + return _parse_app_version(name) > _parse_app_version(current_version) - if "~" in name: - return version.parse(name) > version.parse(current_version) - else: - return version.parse(name) > version.parse(current_version.split("~")[0]) - - return { + out = { # Should we render the markdown maybe? idk name: _hydrate_app_template(_value_for_locale(content_per_lang), data) for name, content_per_lang in notifications.items() @@ -3134,6 +3126,11 @@ def _filter_and_hydrate_notifications(notifications, current_version=None, data= or is_version_more_recent_than_current_version(name, current_version) } + # Filter out empty notifications (notifications may be empty because of if blocks) + return { + name: content for name, content in out.items() if content and content.strip() + } + def _display_notifications(notifications, force=False): if not notifications: @@ -3191,3 +3188,48 @@ def _ask_confirmation( if not answer: raise YunohostError("aborting") + + +def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): + dovecot = True if only in [None, "dovecot"] else False + postfix = True if only in [None, "postfix"] else False + + from yunohost.utils.password import _hash_user_password + + postfix_map = [] + dovecot_passwd = [] + for app in _installed_apps(): + settings = _get_app_settings(app) + + if "domain" not in settings or "mail_pwd" not in settings: + continue + + mail_user = settings.get("mail_user", app) + mail_domain = settings.get("mail_domain", settings["domain"]) + + if dovecot: + hashed_password = _hash_user_password(settings["mail_pwd"]) + dovecot_passwd.append( + f"{app}:{hashed_password}::::::allow_nets=::1,127.0.0.1/24,local,mail={mail_user}@{mail_domain}" + ) + if postfix: + postfix_map.append(f"{mail_user}@{mail_domain} {app}") + + if dovecot: + app_senders_passwd = "/etc/dovecot/app-senders-passwd" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += "\n" + "\n".join(dovecot_passwd) + write_to_file(app_senders_passwd, content) + chmod(app_senders_passwd, 0o440) + chown(app_senders_passwd, "root", "dovecot") + + if postfix: + app_senders_map = "/etc/postfix/app_senders_login_maps" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += "\n" + "\n".join(postfix_map) + write_to_file(app_senders_map, content) + chmod(app_senders_map, 0o440) + chown(app_senders_map, "postfix", "root") + os.system(f"postmap {app_senders_map} 2>/dev/null") + chmod(app_senders_map + ".db", 0o640) + chown(app_senders_map + ".db", "postfix", "root") diff --git a/src/app_catalog.py b/src/app_catalog.py index 9fb662845..2f3076eed 100644 --- a/src/app_catalog.py +++ b/src/app_catalog.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -19,28 +19,28 @@ import os import re import hashlib +from logging import getLogger from moulinette import m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.network import download_json from moulinette.utils.filesystem import ( read_json, read_yaml, write_to_json, - write_to_yaml, mkdir, ) from yunohost.utils.i18n import _value_for_locale from yunohost.utils.error import YunohostError -logger = getActionLogger("yunohost.app_catalog") +logger = getLogger("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 = 3 APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default" +DEFAULT_APPS_CATALOG_LIST = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}] def app_catalog(full=False, with_categories=False, with_antifeatures=False): @@ -120,33 +120,21 @@ def app_search(string): return matching_apps -def _initialize_apps_catalog_system(): - """ - This function is meant to intialize the apps_catalog system with YunoHost's default app catalog. - """ - - default_apps_catalog_list = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}] - - try: - logger.debug( - "Initializing apps catalog system with YunoHost's default app list" - ) - write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list) - except Exception as e: - raise YunohostError( - f"Could not initialize the apps catalog system... : {e}", raw_msg=True - ) - - logger.success(m18n.n("apps_catalog_init_success")) - - def _read_apps_catalog_list(): """ Read the json corresponding to the list of apps catalogs """ + if not os.path.exists(APPS_CATALOG_CONF): + return DEFAULT_APPS_CATALOG_LIST + try: list_ = read_yaml(APPS_CATALOG_CONF) + if list_ == DEFAULT_APPS_CATALOG_LIST: + try: + os.remove(APPS_CATALOG_CONF) + except Exception: + pass # Support the case where file exists but is empty # by returning [] if list_ is None return list_ if list_ else [] diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py index b1b550bc0..36bde5452 100644 --- a/src/authenticators/ldap_admin.py +++ b/src/authenticators/ldap_admin.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -16,11 +16,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # +import jwt import os import logging import ldap import ldap.sasl import time +import hashlib +from pathlib import Path from moulinette import m18n from moulinette.authentication import BaseAuthenticator @@ -29,14 +32,32 @@ from moulinette.utils.text import random_ascii from yunohost.utils.error import YunohostError, YunohostAuthenticationError from yunohost.utils.ldap import _get_ldap_interface -session_secret = random_ascii() logger = logging.getLogger("yunohost.authenticators.ldap_admin") + +def SESSION_SECRET(): + # Only load this once actually requested to avoid boring issues like + # "secret doesnt exists yet" (before postinstall) and therefore service + # miserably fail to start + if not SESSION_SECRET.value: + SESSION_SECRET.value = open("/etc/yunohost/.admin_cookie_secret").read().strip() + assert SESSION_SECRET.value + return SESSION_SECRET.value + + +SESSION_SECRET.value = None # type: ignore +SESSION_FOLDER = "/var/cache/yunohost/sessions" +SESSION_VALIDITY = 3 * 24 * 3600 # 3 days + LDAP_URI = "ldap://localhost:389" ADMIN_GROUP = "cn=admins,ou=groups" AUTH_DN = "uid={uid},ou=users,dc=yunohost,dc=org" +def short_hash(data): + return hashlib.shake_256(data.encode()).hexdigest(20) + + class Authenticator(BaseAuthenticator): name = "ldap_admin" @@ -122,55 +143,87 @@ class Authenticator(BaseAuthenticator): if con: con.unbind_s() + return {"user": uid} + def set_session_cookie(self, infos): from bottle import response assert isinstance(infos, dict) + assert "user" in infos - # This allows to generate a new session id or keep the existing one - current_infos = self.get_session_cookie(raise_if_no_session_exists=False) - new_infos = {"id": current_infos["id"]} - new_infos.update(infos) + # Create a session id, built as + some random ascii + # Prefixing with the user hash is meant to provide the ability to invalidate all this user's session + # (eg because the user gets deleted, or password gets changed) + # User hashing not really meant for security, just to sort of anonymize/pseudonymize the session file name + infos["id"] = short_hash(infos['user']) + random_ascii(20) response.set_cookie( "yunohost.admin", - new_infos, + jwt.encode(infos, SESSION_SECRET(), algorithm="HS256"), secure=True, - secret=session_secret, httponly=True, - # samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions + path="/yunohost/api", + samesite="strict", ) + # Create the session file (expiration mechanism) + session_file = f'{SESSION_FOLDER}/{infos["id"]}' + os.system(f'touch "{session_file}"') + def get_session_cookie(self, raise_if_no_session_exists=True): - from bottle import request + from bottle import request, response try: - # N.B. : here we implicitly reauthenticate the cookie - # because it's signed via the session_secret - # If no session exists (or if session is invalid?) - # it's gonna return the default empty dict, - # which we interpret as an authentication failure - infos = request.get_cookie( - "yunohost.admin", secret=session_secret, default={} + token = request.get_cookie("yunohost.admin", default="").encode() + infos = jwt.decode( + token, + SESSION_SECRET(), + algorithms="HS256", + options={"require": ["id", "user"]}, ) except Exception: - if not raise_if_no_session_exists: - return {"id": random_ascii()} raise YunohostAuthenticationError("unable_authenticate") - if not infos and raise_if_no_session_exists: + if not infos: raise YunohostAuthenticationError("unable_authenticate") - if "id" not in infos: - infos["id"] = random_ascii() + self.purge_expired_session_files() + session_file = f'{SESSION_FOLDER}/{infos["id"]}' + if not os.path.exists(session_file): + response.delete_cookie("yunohost.admin", path="/yunohost/api") + raise YunohostAuthenticationError("session_expired") - # FIXME: Here, maybe we want to re-authenticate the session via the authenticator - # For example to check that the username authenticated is still in the admin group... + # Otherwise, we 'touch' the file to extend the validity + os.system(f'touch "{session_file}"') return infos def delete_session_cookie(self): from bottle import response - response.set_cookie("yunohost.admin", "", max_age=-1) - response.delete_cookie("yunohost.admin") + try: + infos = self.get_session_cookie() + session_file = f'{SESSION_FOLDER}/{infos["id"]}' + os.remove(session_file) + except Exception as e: + logger.debug(f"User logged out, but failed to properly invalidate the session : {e}") + + response.delete_cookie("yunohost.admin", path="/yunohost/api") + + def purge_expired_session_files(self): + + for session_file in Path(SESSION_FOLDER).iterdir(): + if abs(session_file.stat().st_mtime - time.time()) > SESSION_VALIDITY: + try: + session_file.unlink() + except Exception as e: + logger.debug(f"Failed to delete session file {session_file} ? {e}") + + @staticmethod + def invalidate_all_sessions_for_user(user): + + for file in Path(SESSION_FOLDER).glob(f"{short_hash(user)}*"): + try: + file.unlink() + except Exception as e: + logger.debug(f"Failed to delete session file {file} ? {e}") diff --git a/src/authenticators/ldap_ynhuser.py b/src/authenticators/ldap_ynhuser.py new file mode 100644 index 000000000..941797705 --- /dev/null +++ b/src/authenticators/ldap_ynhuser.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- + +import time +import jwt +import logging +import ldap +import ldap.sasl +import base64 +import os +import hashlib +from pathlib import Path + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + +from moulinette import m18n +from moulinette.authentication import BaseAuthenticator +from moulinette.utils.text import random_ascii +from moulinette.utils.filesystem import read_json +from yunohost.utils.error import YunohostError, YunohostAuthenticationError +from yunohost.utils.ldap import _get_ldap_interface + +logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser") + + +def SESSION_SECRET(): + # Only load this once actually requested to avoid boring issues like + # "secret doesnt exists yet" (before postinstall) and therefore service + # miserably fail to start + if not SESSION_SECRET.value: + SESSION_SECRET.value = open("/etc/yunohost/.ssowat_cookie_secret").read().strip() + assert SESSION_SECRET.value + return SESSION_SECRET.value + + +SESSION_SECRET.value = None # type: ignore +SESSION_FOLDER = "/var/cache/yunohost-portal/sessions" +SESSION_VALIDITY = 3 * 24 * 3600 # 3 days + +URI = "ldap://localhost:389" +USERDN = "uid={username},ou=users,dc=yunohost,dc=org" + +# Cache on-disk settings to RAM for faster access +DOMAIN_USER_ACL_DICT: dict[str, dict] = {} +PORTAL_SETTINGS_DIR = "/etc/yunohost/portal" + +# Should a user have *minimal* access to a domain? +# - if the user has permission for an application with a URI on the domain, yes +# - if the user is an admin, yes +# - if the user has an email on the domain, yes +# - otherwise, no +def user_is_allowed_on_domain(user: str, domain: str) -> bool: + + assert "/" not in domain + + portal_settings_path = Path(PORTAL_SETTINGS_DIR) / f"{domain}.json" + + if not portal_settings_path.exists(): + if "." not in domain: + return False + parent_domain = domain.split(".", 1)[-1] + return user_is_allowed_on_domain(user, parent_domain) + + # Check that the domain permissions haven't changed on-disk since we read them + # by comparing file mtime. If we haven't read the file yet, read it for the first time. + # We compare mtime by equality not superiority because maybe the system clock has changed. + mtime = portal_settings_path.stat().st_mtime + if domain not in DOMAIN_USER_ACL_DICT or DOMAIN_USER_ACL_DICT[domain]["mtime"] != mtime: + users: set[str] = set() + for infos in read_json(str(portal_settings_path))["apps"].values(): + users = users.union(infos["users"]) + DOMAIN_USER_ACL_DICT[domain] = {} + DOMAIN_USER_ACL_DICT[domain]["mtime"] = mtime + DOMAIN_USER_ACL_DICT[domain]["users"] = users + + if user in DOMAIN_USER_ACL_DICT[domain]["users"]: + # A user with explicit permission to an application is certainly welcome + return True + + ADMIN_GROUP = "cn=admins,ou=groups" + try: + admins = ( + _get_ldap_interface() + .search(ADMIN_GROUP, attrs=["memberUid"])[0] + .get("memberUid", []) + ) + except Exception as e: + logger.error(f"Failed to list admin users: {e}") + return False + if user in admins: + # Admins can access everything + return True + + try: + user_result = _get_ldap_interface().search("ou=users", f"uid={user}", [ "mail" ]) + if len(user_result) != 1: + logger.error(f"User not found or many users found for {user}. How is this possible after so much validation?") + return False + + user_mail = user_result[0]["mail"] + if len(user_mail) != 1: + logger.error(f"User {user} found, but has the wrong number of email addresses: {user_mail}") + return False + + user_mail = user_mail[0] + if not "@" in user_mail: + logger.error(f"Invalid email address for {user}: {user_mail}") + return False + + if user_mail.split("@")[1] == domain: + # A user from that domain is welcome + return True + + # Users from other domains don't belong here + return False + except Exception as e: + logger.error(f"Failed to get email info for {user}: {e}") + return False + +# We want to save the password in the cookie, but we should do so in an encrypted fashion +# This is needed because the SSO later needs to possibly inject the Basic Auth header +# which includes the user's password +# It's also needed because we need to be able to open LDAP sessions, authenticated as the user, +# which requires the user's password +# +# To do so, we use AES-256-CBC. As it's a block encryption algorithm, it requires an IV, +# which we need to keep around for decryption on SSOwat'side. +# +# SESSION_SECRET is used as the encryption key, which implies it must be exactly 32-char long (256/8) +# +# The result is a string formatted as | +# For example: ctl8kk5GevYdaA5VZ2S88Q==|yTAzCx0Gd1+MCit4EQl9lA== +def encrypt(data): + alg = algorithms.AES(SESSION_SECRET().encode()) + iv = os.urandom(int(alg.block_size / 8)) + + E = Cipher(alg, modes.CBC(iv), default_backend()).encryptor() + p = padding.PKCS7(alg.block_size).padder() + data_padded = p.update(data.encode()) + p.finalize() + data_enc = E.update(data_padded) + E.finalize() + data_enc_b64 = base64.b64encode(data_enc).decode() + iv_b64 = base64.b64encode(iv).decode() + return data_enc_b64 + "|" + iv_b64 + + +def decrypt(data_enc_and_iv_b64): + data_enc_b64, iv_b64 = data_enc_and_iv_b64.split("|") + data_enc = base64.b64decode(data_enc_b64) + iv = base64.b64decode(iv_b64) + + alg = algorithms.AES(SESSION_SECRET().encode()) + D = Cipher(alg, modes.CBC(iv), default_backend()).decryptor() + p = padding.PKCS7(alg.block_size).unpadder() + data_padded = D.update(data_enc) + data = p.update(data_padded) + p.finalize() + return data.decode() + + +def short_hash(data): + return hashlib.shake_256(data.encode()).hexdigest(20) + + +class Authenticator(BaseAuthenticator): + name = "ldap_ynhuser" + + def _authenticate_credentials(self, credentials=None): + from bottle import request + + try: + username, password = credentials.split(":", 1) + except ValueError: + raise YunohostError("invalid_credentials") + + def _reconnect(): + con = ldap.ldapobject.ReconnectLDAPObject(URI, retry_max=2, retry_delay=0.5) + con.simple_bind_s(USERDN.format(username=username), password) + return con + + try: + con = _reconnect() + except ldap.INVALID_CREDENTIALS: + # FIXME FIXME FIXME : this should be properly logged and caught by Fail2ban ! ! ! ! ! ! ! + raise YunohostError("invalid_password") + except ldap.SERVER_DOWN: + logger.warning(m18n.n("ldap_server_down")) + + # Check that we are indeed logged in with the expected identity + try: + # whoami_s return dn:..., then delete these 3 characters + who = con.whoami_s()[3:] + except Exception as e: + logger.warning("Error during ldap authentication process: %s", e) + raise + else: + if who != USERDN.format(username=username): + raise YunohostError( + "Not logged with the appropriate identity ?!", + raw_msg=True, + ) + finally: + # Free the connection, we don't really need it to keep it open as the point is only to check authentication... + if con: + con.unbind_s() + + if not user_is_allowed_on_domain(username, request.get_header("host")): + raise YunohostAuthenticationError("unable_authenticate") + + return {"user": username, "pwd": encrypt(password)} + + def set_session_cookie(self, infos): + from bottle import response, request + + assert isinstance(infos, dict) + assert "user" in infos + assert "pwd" in infos + + # Create a session id, built as + some random ascii + # Prefixing with the user hash is meant to provide the ability to invalidate all this user's session + # (eg because the user gets deleted, or password gets changed) + # User hashing not really meant for security, just to sort of anonymize/pseudonymize the session file name + infos["id"] = short_hash(infos['user']) + random_ascii(20) + infos["host"] = request.get_header("host") + + is_dev = Path("/etc/yunohost/.portal-api-allowed-cors-origins").exists() + + response.set_cookie( + "yunohost.portal", + jwt.encode(infos, SESSION_SECRET(), algorithm="HS256"), + secure=True, + httponly=True, + path="/", + # Doesn't this cause issues ? May cause issue if the portal is on different subdomain than the portal API ? Will surely cause issue for development similar to CORS ? + samesite="strict" if not is_dev else None, + domain=f".{request.get_header('host')}", + ) + + # Create the session file (expiration mechanism) + session_file = f'{SESSION_FOLDER}/{infos["id"]}' + os.system(f'touch "{session_file}"') + + def get_session_cookie(self, decrypt_pwd=False): + from bottle import request, response + + try: + token = request.get_cookie("yunohost.portal", default="").encode() + infos = jwt.decode( + token, + SESSION_SECRET(), + algorithms="HS256", + options={"require": ["id", "host", "user", "pwd"]}, + ) + except Exception: + raise YunohostAuthenticationError("unable_authenticate") + + if not infos: + raise YunohostAuthenticationError("unable_authenticate") + + if infos["host"] != request.get_header("host"): + raise YunohostAuthenticationError("unable_authenticate") + + if not user_is_allowed_on_domain(infos["user"], infos["host"]): + raise YunohostAuthenticationError("unable_authenticate") + + self.purge_expired_session_files() + session_file = Path(SESSION_FOLDER) / infos["id"] + if not session_file.exists(): + response.delete_cookie("yunohost.portal", path="/") + raise YunohostAuthenticationError("session_expired") + + # Otherwise, we 'touch' the file to extend the validity + session_file.touch() + + if decrypt_pwd: + infos["pwd"] = decrypt(infos["pwd"]) + + return infos + + def delete_session_cookie(self): + from bottle import response + + try: + infos = self.get_session_cookie() + session_file = Path(SESSION_FOLDER) / infos["id"] + session_file.unlink() + except Exception as e: + logger.debug(f"User logged out, but failed to properly invalidate the session : {e}") + + response.delete_cookie("yunohost.portal", path="/") + + def purge_expired_session_files(self): + + for session_file in Path(SESSION_FOLDER).iterdir(): + print(session_file.stat().st_mtime - time.time()) + if abs(session_file.stat().st_mtime - time.time()) > SESSION_VALIDITY: + try: + session_file.unlink() + except Exception as e: + logger.debug(f"Failed to delete session file {session_file} ? {e}") + + @staticmethod + def invalidate_all_sessions_for_user(user): + + for file in Path(SESSION_FOLDER).glob(f"{short_hash(user)}*"): + try: + file.unlink() + except Exception as e: + logger.debug(f"Failed to delete session file {file} ? {e}") diff --git a/src/backup.py b/src/backup.py index a23a8d8e0..76eb560b1 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -30,10 +30,10 @@ from glob import glob from collections import OrderedDict from functools import reduce from packaging import version +from logging import getLogger from moulinette import Moulinette, m18n from moulinette.utils.text import random_ascii -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import ( read_file, mkdir, @@ -76,7 +76,6 @@ from yunohost.utils.system import ( binary_to_human, space_used_by_directory, ) -from yunohost.settings import settings_get BACKUP_PATH = "/home/yunohost.backup" ARCHIVES_PATH = f"{BACKUP_PATH}/archives" @@ -84,11 +83,10 @@ APP_MARGIN_SPACE_SIZE = 100 # In MB CONF_MARGIN_SPACE_SIZE = 10 # IN MB POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB MB_ALLOWED_TO_ORGANIZE = 10 -logger = getActionLogger("yunohost.backup") +logger = getLogger("yunohost.backup") class BackupRestoreTargetsManager: - """ BackupRestoreTargetsManager manage the targets in BackupManager and RestoreManager @@ -211,7 +209,6 @@ class BackupRestoreTargetsManager: class BackupManager: - """ This class collect files to backup in a list and apply one or several backup method on it. @@ -825,7 +822,6 @@ class BackupManager: class RestoreManager: - """ RestoreManager allow to restore a past backup archive @@ -1189,9 +1185,6 @@ class RestoreManager: try: self._postinstall_if_needed() - # Apply dirty patch to redirect php5 file on php7 - self._patch_legacy_php_versions_in_csv_file() - self._restore_system() self._restore_apps() except Exception as e: @@ -1202,39 +1195,6 @@ class RestoreManager: finally: self.clean() - def _patch_legacy_php_versions_in_csv_file(self): - """ - Apply dirty patch to redirect php5 and php7.x files to php8.2 - """ - from yunohost.utils.legacy import LEGACY_PHP_VERSION_REPLACEMENTS - - backup_csv = os.path.join(self.work_dir, "backup.csv") - - if not os.path.isfile(backup_csv): - return - - replaced_something = False - with open(backup_csv) as csvfile: - reader = csv.DictReader(csvfile, fieldnames=["source", "dest"]) - newlines = [] - for row in reader: - for pattern, replace in LEGACY_PHP_VERSION_REPLACEMENTS: - if pattern in row["source"]: - replaced_something = True - row["source"] = row["source"].replace(pattern, replace) - - newlines.append(row) - - if not replaced_something: - return - - with open(backup_csv, "w") as csvfile: - writer = csv.DictWriter( - csvfile, fieldnames=["source", "dest"], quoting=csv.QUOTE_ALL - ) - for row in newlines: - writer.writerow(row) - def _restore_system(self): """Restore user and system parts""" @@ -1328,9 +1288,11 @@ class RestoreManager: url=permission_infos["url"], additional_urls=permission_infos["additional_urls"], auth_header=permission_infos["auth_header"], - label=permission_infos["label"] - if perm_name == "main" - else permission_infos["sublabel"], + label=( + permission_infos["label"] + if perm_name == "main" + else permission_infos["sublabel"] + ), show_tile=permission_infos["show_tile"], protected=permission_infos["protected"], sync_perm=False, @@ -1369,8 +1331,6 @@ class RestoreManager: name should be already install) """ from yunohost.utils.legacy import ( - _patch_legacy_php_versions, - _patch_legacy_php_versions_in_settings, _patch_legacy_helpers, ) from yunohost.user import user_group_list @@ -1409,10 +1369,6 @@ class RestoreManager: # Attempt to patch legacy helpers... _patch_legacy_helpers(app_settings_in_archive) - # Apply dirty patch to make php5 apps compatible with php7 - _patch_legacy_php_versions(app_settings_in_archive) - _patch_legacy_php_versions_in_settings(app_settings_in_archive) - # Delete _common.sh file in backup common_file = os.path.join(app_backup_in_archive, "_common.sh") rm(common_file, force=True) @@ -1468,9 +1424,11 @@ class RestoreManager: url=permission_infos.get("url"), additional_urls=permission_infos.get("additional_urls"), auth_header=permission_infos.get("auth_header"), - label=permission_infos.get("label") - if perm_name == "main" - else permission_infos.get("sublabel"), + label=( + permission_infos.get("label") + if perm_name == "main" + else permission_infos.get("sublabel") + ), show_tile=permission_infos.get("show_tile", True), protected=permission_infos.get("protected", False), sync_perm=False, @@ -1554,6 +1512,10 @@ class RestoreManager: if not restore_failed: self.targets.set_result("apps", app_instance_name, "Success") operation_logger.success() + + # Call post_app_restore hook + env_dict = _make_environment_for_app_script(app_instance_name) + hook_callback("post_app_restore", env=env_dict) else: self.targets.set_result("apps", app_instance_name, "Error") @@ -1566,7 +1528,6 @@ class RestoreManager: # Backup methods # # class BackupMethod: - """ BackupMethod is an abstract class that represents a way to backup and restore a list of files. @@ -1857,7 +1818,6 @@ class BackupMethod: class CopyBackupMethod(BackupMethod): - """ This class just do an uncompress copy of each file in a location, and could be the inverse for restoring @@ -1920,6 +1880,11 @@ class TarBackupMethod(BackupMethod): @property def _archive_file(self): + from yunohost.settings import settings_get + + if isinstance(self.manager, RestoreManager): + return self.manager.archive_path + if isinstance(self.manager, BackupManager) and settings_get( "misc.backup.backup_compress_tar_archives" ): @@ -2089,7 +2054,6 @@ class TarBackupMethod(BackupMethod): class CustomBackupMethod(BackupMethod): - """ This class use a bash script/hook "backup_method" to do the backup/restore operations. A user can add his own hook inside @@ -2202,7 +2166,7 @@ def backup_create( # Validate there is no archive with the same name if name and name in backup_list()["archives"]: - raise YunohostValidationError("backup_archive_name_exists") + raise YunohostValidationError("backup_archive_name_exists", name=name) # By default we backup using the tar method if not methods: @@ -2312,11 +2276,6 @@ def backup_restore(name, system=[], apps=[], force=False): # Initialize # # - if name.endswith(".tar.gz"): - name = name[: -len(".tar.gz")] - elif name.endswith(".tar"): - name = name[: -len(".tar")] - restore_manager = RestoreManager(name) restore_manager.set_system_targets(system) @@ -2328,8 +2287,10 @@ def backup_restore(name, system=[], apps=[], force=False): # Add validation if restoring system parts on an already-installed system # - if restore_manager.targets.targets["system"] != [] and os.path.isfile( - "/etc/yunohost/installed" + if ( + restore_manager.info["system"] != {} + and restore_manager.targets.targets["system"] != [] + and os.path.isfile("/etc/yunohost/installed") ): logger.warning(m18n.n("yunohost_already_installed")) if not force: @@ -2449,6 +2410,7 @@ def backup_info(name, with_details=False, human_readable=False): human_readable -- Print sizes in human readable format """ + original_name = name if name.endswith(".tar.gz"): name = name[: -len(".tar.gz")] @@ -2461,7 +2423,10 @@ def backup_info(name, with_details=False, human_readable=False): if not os.path.lexists(archive_file): archive_file += ".gz" if not os.path.lexists(archive_file): - raise YunohostValidationError("backup_archive_name_unknown", name=name) + # Maybe the user provided a path to the backup? + archive_file = original_name + if not os.path.lexists(archive_file): + raise YunohostValidationError("backup_archive_name_unknown", name=name) # If symlink, retrieve the real path if os.path.islink(archive_file): @@ -2548,9 +2513,6 @@ def backup_info(name, with_details=False, human_readable=False): 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): - key_info = key_info.keys() info[category][name] = key_info = {"paths": key_info} else: info[category][name] = key_info diff --git a/src/certificate.py b/src/certificate.py index 52e0d8c1b..0fc840532 100644 --- a/src/certificate.py +++ b/src/certificate.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -21,11 +21,10 @@ import sys import shutil import subprocess from glob import glob - +from logging import getLogger from datetime import datetime from moulinette import m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, chown, chmod from moulinette.utils.process import check_output @@ -38,11 +37,11 @@ from yunohost.service import _run_service_command from yunohost.regenconf import regen_conf from yunohost.log import OperationLogger -logger = getActionLogger("yunohost.certmanager") +logger = getLogger("yunohost.certmanager") CERT_FOLDER = "/etc/yunohost/certs/" -TMP_FOLDER = "/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" @@ -557,6 +556,7 @@ def _fetch_and_enable_new_certificate(domain, no_checks=False): def _prepare_certificate_signing_request(domain, key_file, output_folder): from OpenSSL import crypto # lazy loading this module for performance reasons + from yunohost.hook import hook_callback # Init a request csr = crypto.X509Req() @@ -564,42 +564,34 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Set the domain csr.get_subject().CN = domain - from yunohost.domain import domain_config_get + sanlist = [] + hook_results = hook_callback("cert_alternate_names", env={"domain": domain}) + for hook_name, results in hook_results.items(): + # + # There can be multiple results per hook name, so results look like + # {'/some/path/to/hook1': + # { 'state': 'succeed', + # 'stdreturn': ["foo", "bar"] + # }, + # '/some/path/to/hook2': + # { ... }, + # [...] + # + # Loop over the sub-results + for result in results.values(): + if result.get("stdreturn"): + sanlist += result["stdreturn"] - # 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( - "dnsrecords", item={"domain": domain, "category": "xmpp"} - ).get("data") - or {} - ) - 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, + (", ".join([f"DNS:{sub}.{domain}" for sub in sanlist])).encode("utf-8"), ) - - if sanlist: - csr.add_extensions( - [ - crypto.X509Extension( - b"subjectAltName", - False, - (", ".join(sanlist)).encode("utf-8"), - ) - ] - ) + ] + ) # Set the key with open(key_file, "rt") as f: @@ -733,15 +725,6 @@ def _enable_certificate(domain, new_cert_folder): logger.debug("Restarting services...") - for service in ("dovecot", "metronome"): - # Ugly trick to not restart metronome if it's not installed 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) - 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) @@ -749,6 +732,7 @@ def _enable_certificate(domain, new_cert_folder): regen_conf(names=["nginx", "postfix"]) _run_service_command("reload", "nginx") + _run_service_command("restart", "dovecot") from yunohost.hook import hook_callback @@ -778,7 +762,7 @@ def _check_domain_is_ready_for_ACME(domain): or {} ) - parent_domain = _get_parent_domain_of(domain, return_self=True) + parent_domain = _get_parent_domain_of(domain, return_self=True, topest=True) dnsrecords = ( Diagnoser.get_cached_report( diff --git a/src/diagnosers/00-basesystem.py b/src/diagnosers/00-basesystem.py index 336271bd1..4b4774071 100644 --- a/src/diagnosers/00-basesystem.py +++ b/src/diagnosers/00-basesystem.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -19,9 +19,9 @@ import os import json import subprocess +import logging from typing import List -from moulinette.utils import log from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file, read_json, write_to_json from yunohost.diagnosis import Diagnoser @@ -31,7 +31,7 @@ from yunohost.utils.system import ( system_arch, ) -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): @@ -118,9 +118,11 @@ class MyDiagnoser(Diagnoser): "repo": ynh_packages["yunohost"]["repo"], }, status="INFO" if consistent_versions else "ERROR", - summary="diagnosis_basesystem_ynh_main_version" - if consistent_versions - else "diagnosis_basesystem_ynh_inconsistent_versions", + summary=( + "diagnosis_basesystem_ynh_main_version" + if consistent_versions + else "diagnosis_basesystem_ynh_inconsistent_versions" + ), details=ynh_version_details, ) diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py index 4f9cd9708..09199fb00 100644 --- a/src/diagnosers/10-ip.py +++ b/src/diagnosers/10-ip.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -19,9 +19,9 @@ import re import os import random +import logging from typing import List -from moulinette.utils import log from moulinette.utils.network import download_text from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file @@ -30,7 +30,7 @@ from yunohost.diagnosis import Diagnoser from yunohost.utils.network import get_network_interfaces from yunohost.settings import settings_get -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): @@ -73,9 +73,11 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "dnsresolv"}, status="ERROR", - summary="diagnosis_ip_broken_dnsresolution" - if good_resolvconf - else "diagnosis_ip_broken_resolvconf", + summary=( + "diagnosis_ip_broken_dnsresolution" + if good_resolvconf + else "diagnosis_ip_broken_resolvconf" + ), ) return # Otherwise, if the resolv conf is bad but we were able to resolve domain name, @@ -123,11 +125,9 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv4"}, data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" - if ipv4 - else "ERROR" - if is_ipvx_important(4) - else "WARNING", + status=( + "SUCCESS" if ipv4 else "ERROR" if is_ipvx_important(4) else "WARNING" + ), summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, ) @@ -135,19 +135,27 @@ class MyDiagnoser(Diagnoser): yield dict( meta={"test": "ipv6"}, data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" - if ipv6 - else "ERROR" - if settings_get("misc.network.dns_exposure") == "ipv6" - else "WARNING", + status=( + "SUCCESS" + if ipv6 + else ( + "ERROR" + if 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_important" - if is_ipvx_important(6) - else "diagnosis_ip_no_ipv6_tip" - ], + details=( + ["diagnosis_ip_global", "diagnosis_ip_local"] + if ipv6 + else [ + ( + "diagnosis_ip_no_ipv6_tip_important" + if is_ipvx_important(6) + else "diagnosis_ip_no_ipv6_tip" + ) + ] + ), ) # TODO / FIXME : add some attempt to detect ISP (using whois ?) ? diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py index 2d46f979c..5c0f4ea9e 100644 --- a/src/diagnosers/12-dnsrecords.py +++ b/src/diagnosers/12-dnsrecords.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -18,11 +18,11 @@ # import os import re +import logging from typing import List from datetime import datetime, timedelta from publicsuffix2 import PublicSuffixList -from moulinette.utils import log from moulinette.utils.process import check_output from yunohost.utils.dns import ( @@ -39,7 +39,7 @@ from yunohost.dns import ( _get_relative_name_for_dns_zone, ) -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): @@ -91,7 +91,7 @@ class MyDiagnoser(Diagnoser): domain, include_empty_AAAA_if_no_ipv6=True ) - categories = ["basic", "mail", "xmpp", "extra"] + categories = ["basic", "mail", "extra"] for category in categories: records = expected_configuration[category] @@ -182,6 +182,10 @@ class MyDiagnoser(Diagnoser): if success != "ok": return None else: + if type_ == "TXT" and isinstance(answers, list): + for part in answers: + if part.startswith('"v=spf1'): + return part return answers[0] if len(answers) == 1 else answers def current_record_match_expected(self, r): @@ -211,6 +215,11 @@ class MyDiagnoser(Diagnoser): for part in current if not part.startswith("ip4:") and not part.startswith("ip6:") } + if "v=DMARC1" in r["value"]: + for param in current: + key, value = param.split("=") + if key == "p": + return value in ["none", "quarantine", "reject"] return expected == current elif r["type"] == "MX": # For MX, we want to ignore the priority @@ -282,9 +291,9 @@ class MyDiagnoser(Diagnoser): yield dict( meta=meta, data={}, - status=alert_type.upper() - if alert_type != "not_found" - else "WARNING", + status=( + alert_type.upper() if alert_type != "not_found" else "WARNING" + ), summary="diagnosis_domain_expiration_" + alert_type, details=details[alert_type], ) diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py index 34c512f14..b849273f2 100644 --- a/src/diagnosers/14-ports.py +++ b/src/diagnosers/14-ports.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py index 2050cd658..02b705294 100644 --- a/src/diagnosers/21-web.py +++ b/src/diagnosers/21-web.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -60,9 +60,9 @@ class MyDiagnoser(Diagnoser): domains_to_check.append(domain) self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16)) - rm("/tmp/.well-known/ynh-diagnosis/", recursive=True, force=True) - mkdir("/tmp/.well-known/ynh-diagnosis/", parents=True) - os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce) + rm("/var/www/.well-known/ynh-diagnosis/", recursive=True, force=True) + mkdir("/var/www/.well-known/ynh-diagnosis/", parents=True, mode=0o0775) + os.system("touch /var/www/.well-known/ynh-diagnosis/%s" % self.nonce) if not domains_to_check: return diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py index df14222a5..7ca582147 100644 --- a/src/diagnosers/24-mail.py +++ b/src/diagnosers/24-mail.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -19,11 +19,11 @@ import os import dns.resolver import re +import logging from typing import List from subprocess import CalledProcessError -from moulinette.utils import log from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_yaml @@ -34,7 +34,7 @@ from yunohost.utils.dns import dig DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/dnsbl_list.yml" -logger = log.getActionLogger("yunohost.diagnosis") +logger = logging.getLogger("yunohost.diagnosis") class MyDiagnoser(Diagnoser): @@ -43,7 +43,7 @@ class MyDiagnoser(Diagnoser): dependencies: List[str] = ["ip"] def run(self): - self.ehlo_domain = _get_maindomain() + self.ehlo_domain = _get_maindomain().lower() self.mail_domains = domain_list()["domains"] self.ipversions, self.ips = self.get_ips_checked() @@ -132,7 +132,7 @@ class MyDiagnoser(Diagnoser): summary=summary, details=[summary + "_details"], ) - elif r["helo"] != self.ehlo_domain: + elif r["helo"].lower() != self.ehlo_domain: yield dict( meta={"test": "mail_ehlo", "ipversion": ipversion}, data={"wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain}, @@ -185,7 +185,7 @@ class MyDiagnoser(Diagnoser): rdns_domain = "" if len(value) > 0: rdns_domain = value[0][:-1] if value[0].endswith(".") else value[0] - if rdns_domain != self.ehlo_domain: + if rdns_domain.lower() != self.ehlo_domain: details = [ "diagnosis_mail_fcrdns_different_from_ehlo_domain_details" ] + details @@ -194,7 +194,7 @@ class MyDiagnoser(Diagnoser): data={ "ip": ip, "ehlo_domain": self.ehlo_domain, - "rdns_domain": rdns_domain, + "rdns_domain": rdns_domain.lower(), }, status="ERROR", summary="diagnosis_mail_fcrdns_different_from_ehlo_domain", diff --git a/src/diagnosers/30-services.py b/src/diagnosers/30-services.py index 42ea9d18f..ee63f4559 100644 --- a/src/diagnosers/30-services.py +++ b/src/diagnosers/30-services.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/50-systemresources.py b/src/diagnosers/50-systemresources.py index 096c3483f..536d8fd71 100644 --- a/src/diagnosers/50-systemresources.py +++ b/src/diagnosers/50-systemresources.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/70-regenconf.py b/src/diagnosers/70-regenconf.py index 65195aac5..c6317d4fc 100644 --- a/src/diagnosers/70-regenconf.py +++ b/src/diagnosers/70-regenconf.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/80-apps.py b/src/diagnosers/80-apps.py index 93cefeaaf..3c336eae6 100644 --- a/src/diagnosers/80-apps.py +++ b/src/diagnosers/80-apps.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosers/__init__.py b/src/diagnosers/__init__.py index 7c1e7b0cd..86d229abb 100644 --- a/src/diagnosers/__init__.py +++ b/src/diagnosers/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/diagnosis.py b/src/diagnosis.py index 02047c001..9f542ea47 100644 --- a/src/diagnosis.py +++ b/src/diagnosis.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -21,9 +21,9 @@ import os import time import glob from importlib import import_module +from logging import getLogger from moulinette import m18n, Moulinette -from moulinette.utils import log from moulinette.utils.filesystem import ( read_json, write_to_json, @@ -33,7 +33,7 @@ from moulinette.utils.filesystem import ( from yunohost.utils.error import YunohostError, YunohostValidationError -logger = log.getActionLogger("yunohost.diagnosis") +logger = getLogger("yunohost.diagnosis") DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/" DIAGNOSIS_CONFIG_FILE = "/etc/yunohost/diagnosis.yml" @@ -263,16 +263,12 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): # Sanity checks for the provided arguments if len(filter_) == 0: - raise YunohostValidationError( - "You should provide at least one criteria being the diagnosis category to ignore" - ) + raise YunohostValidationError(m18n.n("diagnosis_ignore_missing_criteria")) category = filter_[0] if category not in all_categories_names: raise YunohostValidationError(f"{category} is not a diagnosis category") if any("=" not in criteria for criteria in filter_[1:]): - raise YunohostValidationError( - "Criterias should be of the form key=value (e.g. domain=yolo.test)" - ) + raise YunohostValidationError(m18n.n("diagnosis_ignore_criteria_error")) # Convert the provided criteria into a nice dict criterias = {c.split("=")[0]: c.split("=")[1] for c in filter_[1:]} @@ -295,7 +291,7 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): issue_matches_criterias(i, criterias) for i in current_issues_for_this_category ): - raise YunohostError("No issues was found matching the given criteria.") + raise YunohostError(m18n.n("diagnosis_ignore_no_issue_found")) # Make sure the subdicts/lists exists if "ignore_filters" not in configuration: @@ -304,12 +300,14 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): configuration["ignore_filters"][category] = [] if criterias in configuration["ignore_filters"][category]: - logger.warning("This filter already exists.") + logger.warning( + m18n.n("diagnosis_ignore_already_filtered", category=category) + ) return configuration["ignore_filters"][category].append(criterias) _diagnosis_write_configuration(configuration) - logger.success("Filter added") + logger.success(m18n.n("diagnosis_ignore_filter_added", category=category)) return if remove_filter: @@ -322,11 +320,14 @@ def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): configuration["ignore_filters"][category] = [] if criterias not in configuration["ignore_filters"][category]: - raise YunohostValidationError("This filter does not exists.") + logger.warning( + m18n.n("diagnosis_ignore_no_filter_found", category=category) + ) + return configuration["ignore_filters"][category].remove(criterias) _diagnosis_write_configuration(configuration) - logger.success("Filter removed") + logger.success(m18n.n("diagnosis_ignore_filter_removed", category=category)) return diff --git a/src/dns.py b/src/dns.py index e3a26044c..c5bd4c69d 100644 --- a/src/dns.py +++ b/src/dns.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -19,12 +19,11 @@ import os import re import time - +from logging import getLogger from difflib import SequenceMatcher from collections import OrderedDict from moulinette import m18n, Moulinette -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, read_toml, mkdir from yunohost.domain import ( @@ -38,11 +37,10 @@ from yunohost.domain import ( from yunohost.utils.dns import dig, is_yunohost_dyndns_domain, is_special_use_tld from yunohost.utils.error import YunohostValidationError, YunohostError from yunohost.utils.network import get_public_ip -from yunohost.settings import settings_get from yunohost.log import is_unit_operation from yunohost.hook import hook_callback -logger = getActionLogger("yunohost.domain") +logger = getLogger("yunohost.domain") DOMAIN_REGISTRAR_LIST_PATH = "/usr/share/yunohost/registrar_list.toml" @@ -77,12 +75,6 @@ def domain_dns_suggest(domain): result += "\n{name} {ttl} IN {type} {value}".format(**record) result += "\n\n" - if dns_conf["xmpp"]: - result += "\n\n" - result += "; XMPP" - for record in dns_conf["xmpp"]: - result += "\n{name} {ttl} IN {type} {value}".format(**record) - if dns_conf["extra"]: result += "\n\n" result += "; Extra" @@ -90,7 +82,7 @@ def domain_dns_suggest(domain): result += "\n{name} {ttl} IN {type} {value}".format(**record) for name, record_list in dns_conf.items(): - if name not in ("basic", "xmpp", "mail", "extra") and record_list: + if name not in ("basic", "mail", "extra") and record_list: result += "\n\n" result += "; " + name for record in record_list: @@ -119,14 +111,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # if ipv6 available {"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600}, ], - "xmpp": [ - {"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600}, - {"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600}, - {"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600}, - {"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600}, - {"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600} - {"type": "CNAME", "name": "xmpp-upload", "value": "@", "ttl": 3600} - ], "mail": [ {"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600}, {"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 }, @@ -146,9 +130,10 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): } """ + from yunohost.settings import settings_get + basic = [] mail = [] - xmpp = [] extra = [] ipv4 = get_public_ip() ipv6 = get_public_ip(6) @@ -211,29 +196,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): [f"_dmarc{suffix}", ttl, "TXT", '"v=DMARC1; p=none"'], ] - ######## - # XMPP # - ######## - if settings["xmpp"]: - xmpp += [ - [ - f"_xmpp-client._tcp{suffix}", - ttl, - "SRV", - f"0 5 5222 {domain}.", - ], - [ - f"_xmpp-server._tcp{suffix}", - ttl, - "SRV", - f"0 5 5269 {domain}.", - ], - [f"muc{suffix}", ttl, "CNAME", f"{domain}."], - [f"pubsub{suffix}", ttl, "CNAME", f"{domain}."], - [f"vjud{suffix}", ttl, "CNAME", f"{domain}."], - [f"xmpp-upload{suffix}", ttl, "CNAME", f"{domain}."], - ] - ######### # Extra # ######### @@ -259,10 +221,6 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): {"name": name, "ttl": ttl_, "type": type_, "value": value} for name, ttl_, type_, value in basic ], - "xmpp": [ - {"name": name, "ttl": ttl_, "type": type_, "value": value} - for name, ttl_, type_, value in xmpp - ], "mail": [ {"name": name, "ttl": ttl_, "type": type_, "value": value} for name, ttl_, type_, value in mail @@ -277,15 +235,8 @@ def _build_dns_conf(base_domain, include_empty_AAAA_if_no_ipv6=False): # Custom records # ################## - # Defined by custom hooks ships in apps for example ... - - # FIXME : this ain't practical for apps that may want to add - # custom dns records for a subdomain ... there's no easy way for - # an app to compare the base domain is the parent of the subdomain ? - # (On the other hand, in sep 2021, it looks like no app is using - # this mechanism...) - - hook_results = hook_callback("custom_dns_rules", args=[base_domain]) + # Defined by custom hooks shipped in apps for example ... + hook_results = hook_callback("custom_dns_rules", env={"base_domain": base_domain, "suffix": suffix}) for hook_name, results in hook_results.items(): # # There can be multiple results per hook name, so results look like @@ -481,9 +432,11 @@ def _get_dns_zone_for_domain(domain): else: zone = parent_list[-1] - logger.warning( - f"Could not identify correctly the dns zone for domain {domain}, returning {zone}" - ) + # Adding this otherwise the CI is flooding about those ... + if domain not in ["example.tld", "sub.example.tld", "domain.tld", "domain_a.dev", "domain_b.dev"]: + logger.warning( + f"Could not identify correctly the dns zone for domain {domain}, returning {zone}" + ) return zone @@ -503,11 +456,26 @@ def _get_relative_name_for_dns_zone(domain, base_dns_zone): def _get_registrar_config_section(domain): from lexicon.providers.auto import _relevant_provider_for_domain - registrar_infos = { - "name": m18n.n( - "registrar_infos" - ), # This is meant to name the config panel section, for proper display in the webadmin - } + registrar_infos = OrderedDict( + { + "name": m18n.n( + "registrar_infos" + ), # This is meant to name the config panel section, for proper display in the webadmin + "registrar": OrderedDict( + { + "readonly": True, + "visible": False, + "default": None, + } + ), + "infos": OrderedDict( + { + "type": "alert", + "style": "info", + } + ), + } + ) dns_zone = _get_dns_zone_for_domain(domain) @@ -520,61 +488,43 @@ def _get_registrar_config_section(domain): else: parent_domain_link = parent_domain - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "info", - "ask": m18n.n( - "domain_dns_registrar_managed_in_parent_domain", - parent_domain=parent_domain, - parent_domain_link=parent_domain_link, - ), - "value": "parent_domain", - } + registrar_infos["registrar"]["default"] = "parent_domain" + registrar_infos["infos"]["ask"] = m18n.n( + "domain_dns_registrar_managed_in_parent_domain", + parent_domain=parent_domain, + parent_domain_link=parent_domain_link, ) - return OrderedDict(registrar_infos) + return registrar_infos # TODO big project, integrate yunohost's dynette as a registrar-like provider # TODO big project, integrate other dyndns providers such as netlib.re, or cf the list of dyndns providers supported by cloudron... if is_yunohost_dyndns_domain(dns_zone): - registrar_infos["registrar"] = OrderedDict( + registrar_infos["registrar"]["default"] = "yunohost" + registrar_infos["infos"]["style"] = "success" + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_yunohost") + registrar_infos["recovery_password"] = OrderedDict( { - "type": "alert", - "style": "success", - "ask": m18n.n("domain_dns_registrar_yunohost"), - "value": "yunohost", + "type": "password", + "ask": m18n.n("ask_dyndns_recovery_password"), + "default": "", } ) - return OrderedDict(registrar_infos) + + return registrar_infos + elif is_special_use_tld(dns_zone): - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "info", - "ask": m18n.n("domain_dns_conf_special_use_tld"), - "value": None, - } - ) + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_conf_special_use_tld") try: registrar = _relevant_provider_for_domain(dns_zone)[0] except ValueError: - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "warning", - "ask": m18n.n("domain_dns_registrar_not_supported"), - "value": None, - } - ) + registrar_infos["registrar"]["default"] = None + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_not_supported") + registrar_infos["infos"]["style"] = "warning" else: - registrar_infos["registrar"] = OrderedDict( - { - "type": "alert", - "style": "info", - "ask": m18n.n("domain_dns_registrar_supported", registrar=registrar), - "value": registrar, - } + registrar_infos["registrar"]["default"] = registrar + registrar_infos["infos"]["ask"] = m18n.n( + "domain_dns_registrar_supported", registrar=registrar ) TESTED_REGISTRARS = ["ovh", "gandi"] @@ -602,7 +552,7 @@ def _get_registrar_config_section(domain): infos["optional"] = infos.get("optional", "False") registrar_infos.update(registrar_credentials) - return OrderedDict(registrar_infos) + return registrar_infos def _get_registar_settings(domain): @@ -640,12 +590,14 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= # FIXME: in the future, properly unify this with yunohost dyndns update if registrar == "yunohost": - logger.info(m18n.n("domain_dns_registrar_yunohost")) + from yunohost.dyndns import dyndns_update + + dyndns_update(domain=domain, force=force) return {} if registrar == "parent_domain": parent_domain = _get_parent_domain_of(domain, topest=True) - registar, registrar_credentials = _get_registar_settings(parent_domain) + registrar, registrar_credentials = _get_registar_settings(parent_domain) if any(registrar_credentials.values()): raise YunohostValidationError( "domain_dns_push_managed_in_parent_domain", @@ -653,9 +605,18 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge= parent_domain=parent_domain, ) else: - raise YunohostValidationError( - "domain_registrar_is_not_configured", domain=parent_domain - ) + new_parent_domain = ".".join(parent_domain.split(".")[-3:]) + registrar, registrar_credentials = _get_registar_settings(new_parent_domain) + if registrar == "yunohost": + raise YunohostValidationError( + "domain_dns_push_managed_in_parent_domain", + domain=domain, + parent_domain=new_parent_domain, + ) + else: + raise YunohostValidationError( + "domain_registrar_is_not_configured", domain=parent_domain + ) if not all(registrar_credentials.values()): raise YunohostValidationError( diff --git a/src/domain.py b/src/domain.py index 4f96d08c4..b6e78ba67 100644 --- a/src/domain.py +++ b/src/domain.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -18,27 +18,33 @@ # import os import time -from typing import List, Optional +from pathlib import Path +from typing import TYPE_CHECKING, Any, List, Optional, Union from collections import OrderedDict +from logging import getLogger from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import write_to_file, read_yaml, write_to_yaml, rm - -from yunohost.app import ( - app_ssowatconf, - _installed_apps, - _get_app_settings, - _get_conflicting_apps, +from moulinette.utils.filesystem import ( + read_json, + read_yaml, + rm, + write_to_file, + write_to_json, + write_to_yaml, ) + from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf -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") +if TYPE_CHECKING: + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + + from yunohost.utils.configpanel import RawConfig + from yunohost.utils.form import FormModel + +logger = getLogger("yunohost.domain") DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" @@ -99,6 +105,30 @@ def _get_domains(exclude_subdomains=False): return domain_list_cache +def _get_domain_portal_dict(): + + domains = _get_domains() + out = OrderedDict() + + for domain in domains: + + parent = None + + # Use the topest parent domain if any + for d in out.keys(): + if domain.endswith(f".{d}"): + parent = d + break + + out[domain] = f'{parent or domain}/yunohost/sso' + + # By default, redirect to $host/yunohost/admin for domains not listed in the dict + # maybe in the future, we can allow to tweak this + out["default"] = "/yunohost/admin" + + return dict(out) + + def domain_list(exclude_subdomains=False, tree=False, features=[]): """ List domains @@ -152,13 +182,14 @@ def domain_info(domain): domain -- Domain to be checked """ - from yunohost.app import app_info + from yunohost.app import app_info, _installed_apps, _get_app_settings from yunohost.dns import _get_registar_settings + from yunohost.certificate import certificate_status _assert_domain_exists(domain) registrar, _ = _get_registar_settings(domain) - certificate = domain_cert_status([domain], full=True)["certificates"][domain] + certificate = certificate_status([domain], full=True)["certificates"][domain] apps = [] for app in _installed_apps(): @@ -210,26 +241,28 @@ def _get_parent_domain_of(domain, return_self=False, topest=False): return domain if return_self else None -@is_unit_operation() -def domain_add(operation_logger, domain, dyndns=False): +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_add( + operation_logger, domain, dyndns_recovery_password=None, ignore_dyndns=False +): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS - + dyndns_recovery_password -- Password used to later unsubscribe from DynDNS + ignore_dyndns -- If we want to just add the DynDNS domain to the list, without subscribing """ from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf from yunohost.utils.ldap import _get_ldap_interface + from yunohost.utils.password import assert_password_is_strong_enough from yunohost.certificate import _certificate_install_selfsigned + from yunohost.utils.dns import is_yunohost_dyndns_domain - if domain.startswith("xmpp-upload."): - raise YunohostValidationError("domain_cannot_add_xmpp_upload") - - if domain.startswith("muc."): - raise YunohostError("domain_cannot_add_muc_upload") + if dyndns_recovery_password: + operation_logger.data_to_redact.append(dyndns_recovery_password) ldap = _get_ldap_interface() @@ -245,27 +278,27 @@ def domain_add(operation_logger, domain, dyndns=False): # Non-latin characters (e.g. café.com => xn--caf-dma.com) domain = domain.encode("idna").decode("utf-8") - # DynDNS domain + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) + dyndns = ( + not ignore_dyndns + and is_yunohost_dyndns_domain(domain) + and len(domain.split(".")) == 3 + ) if dyndns: - from yunohost.utils.dns import is_yunohost_dyndns_domain - from yunohost.dyndns import _guess_current_dyndns_domain + from yunohost.dyndns import is_subscribing_allowed # Do not allow to subscribe to multiple dyndns domains... - if _guess_current_dyndns_domain() != (None, None): + if not is_subscribing_allowed(): raise YunohostValidationError("domain_dyndns_already_subscribed") - - # Check that this domain can effectively be provided by - # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) - if not is_yunohost_dyndns_domain(domain): - raise YunohostValidationError("domain_dyndns_root_unknown") + if dyndns_recovery_password: + assert_password_is_strong_enough("admin", dyndns_recovery_password) operation_logger.start() if dyndns: - from yunohost.dyndns import dyndns_subscribe - - # Actually subscribe - dyndns_subscribe(domain=domain) + domain_dyndns_subscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) _certificate_install_selfsigned([domain], True) @@ -297,7 +330,13 @@ def domain_add(operation_logger, domain, dyndns=False): # should identify the root of this bug... _force_clear_hashes([f"/etc/nginx/conf.d/{domain}.conf"]) regen_conf( - names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"] + names=[ + "nginx", + "dnsmasq", + "postfix", + "mdns", + "dovecot", + ] ) app_ssowatconf() @@ -314,8 +353,15 @@ def domain_add(operation_logger, domain, dyndns=False): logger.success(m18n.n("domain_created")) -@is_unit_operation() -def domain_remove(operation_logger, domain, remove_apps=False, force=False): +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_remove( + operation_logger, + domain, + remove_apps=False, + force=False, + dyndns_recovery_password=None, + ignore_dyndns=False, +): """ Delete domains @@ -324,11 +370,23 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): remove_apps -- Remove applications installed on the domain force -- Force the domain removal and don't not ask confirmation to remove apps if remove_apps is specified - + dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain + ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing """ + import glob from yunohost.hook import hook_callback - from yunohost.app import app_ssowatconf, app_info, app_remove + from yunohost.app import ( + app_ssowatconf, + app_info, + app_remove, + _get_app_settings, + _installed_apps, + ) from yunohost.utils.ldap import _get_ldap_interface + from yunohost.utils.dns import is_yunohost_dyndns_domain + + if dyndns_recovery_password: + operation_logger.data_to_redact.append(dyndns_recovery_password) # the 'force' here is related to the exception happening in domain_add ... # we don't want to check the domain exists because the ldap add may have @@ -362,9 +420,11 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): apps_on_that_domain.append( ( app, - f" - {app} \"{label}\" on https://{domain}{settings['path']}" - if "path" in settings - else app, + ( + f" - {app} \"{label}\" on https://{domain}{settings['path']}" + if "path" in settings + else app + ), ) ) @@ -390,6 +450,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): apps="\n".join([x[1] for x in apps_on_that_domain]), ) + # Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain ) + dyndns = ( + not ignore_dyndns + and is_yunohost_dyndns_domain(domain) + and len(domain.split(".")) == 3 + ) + operation_logger.start() ldap = _get_ldap_interface() @@ -401,14 +468,20 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): global domain_list_cache domain_list_cache = [] - stuff_to_delete = [ - f"/etc/yunohost/certs/{domain}", - f"/etc/yunohost/dyndns/K{domain}.+*", - f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", - ] + # If a password is provided, delete the DynDNS record + if dyndns: + try: + # Actually unsubscribe + domain_dyndns_unsubscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) + except Exception as e: + logger.warning(str(e)) - for stuff in stuff_to_delete: - rm(stuff, force=True, recursive=True) + rm(f"/etc/yunohost/certs/{domain}", force=True, recursive=True) + for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*"): + rm(key_file, force=True) + rm(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", force=True) # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... @@ -431,7 +504,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): f"/etc/nginx/conf.d/{domain}.conf", new_conf=None, save=True ) - regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd", "mdns"]) + regen_conf(names=["nginx", "dnsmasq", "postfix", "mdns"]) app_ssowatconf() hook_callback("post_domain_remove", args=[domain]) @@ -439,6 +512,51 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): logger.success(m18n.n("domain_deleted")) +def domain_dyndns_subscribe(*args, **kwargs): + """ + Subscribe to a DynDNS domain + """ + from yunohost.dyndns import dyndns_subscribe + + dyndns_subscribe(*args, **kwargs) + + +def domain_dyndns_unsubscribe(*args, **kwargs): + """ + Unsubscribe from a DynDNS domain + """ + from yunohost.dyndns import dyndns_unsubscribe + + dyndns_unsubscribe(*args, **kwargs) + + +def domain_dyndns_list(): + """ + Returns all currently subscribed DynDNS domains + """ + from yunohost.dyndns import dyndns_list + + return dyndns_list() + + +def domain_dyndns_update(*args, **kwargs): + """ + Update a DynDNS domain + """ + from yunohost.dyndns import dyndns_update + + dyndns_update(*args, **kwargs) + + +def domain_dyndns_set_recovery_password(*args, **kwargs): + """ + Set a recovery password for an already registered dyndns domain + """ + from yunohost.dyndns import dyndns_set_recovery_password + + dyndns_set_recovery_password(*args, **kwargs) + + @is_unit_operation() def domain_main_domain(operation_logger, new_main_domain=None): """ @@ -472,9 +590,6 @@ def domain_main_domain(operation_logger, new_main_domain=None): logger.warning(str(e), exc_info=1) raise YunohostError("main_domain_change_failed") - # Generate SSOwat configuration file - app_ssowatconf() - # Regen configurations if os.path.exists("/etc/yunohost/installed"): regen_conf() @@ -497,9 +612,25 @@ def domain_url_available(domain, path): path -- The path to check (e.g. /coffee) """ + from yunohost.app import _get_conflicting_apps + return len(_get_conflicting_apps(domain, path)) == 0 +def _get_raw_domain_settings(domain): + """Get domain settings directly from file. + Be carefull, domain settings are saved in `"diff"` mode (i.e. default settings are not saved) + so the file may be completely empty + """ + _assert_domain_exists(domain) + # NB: this corresponds to save_path_tpl in DomainConfigPanel + path = f"{DOMAIN_SETTINGS_DIR}/{domain}.yml" + if os.path.exists(path): + return read_yaml(path) + + return {} + + def domain_config_get(domain, key="", full=False, export=False): """ Display a domain configuration @@ -517,6 +648,7 @@ def domain_config_get(domain, key="", full=False, export=False): else: mode = "classic" + DomainConfigPanel = _get_DomainConfigPanel() config = DomainConfigPanel(domain) return config.get(key, mode) @@ -528,140 +660,175 @@ def domain_config_set( """ Apply a new domain configuration """ + from yunohost.utils.form import BaseOption + + DomainConfigPanel = _get_DomainConfigPanel() BaseOption.operation_logger = operation_logger config = DomainConfigPanel(domain) return config.set(key, value, args, args_file, operation_logger=operation_logger) -class DomainConfigPanel(ConfigPanel): - entity_type = "domain" - save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" - save_mode = "diff" +def _get_DomainConfigPanel(): + from yunohost.utils.configpanel import ConfigPanel - def get(self, key="", mode="classic"): - result = super().get(key=key, mode=mode) + class DomainConfigPanel(ConfigPanel): + entity_type = "domain" + save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" + save_mode = "diff" - 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 + # i18n: domain_config_cert_renew_help + # i18n: domain_config_default_app_help - return result + def _get_raw_config(self) -> "RawConfig": + # TODO add mechanism to share some settings with other domains on the same zone + raw_config = super()._get_raw_config() - def _get_raw_config(self): - toml = super()._get_raw_config() + any_filter = all(self.filter_key) + panel_id, section_id, option_id = self.filter_key - toml["feature"]["xmpp"]["xmpp"]["default"] = ( - 1 if self.entity == _get_maindomain() else 0 - ) + # Portal settings are only available on "topest" domains + if _get_parent_domain_of(self.entity, topest=True) is not None: + del raw_config["feature"]["portal"] - # Optimize wether or not to load the DNS section, - # e.g. we don't want to trigger the whole _get_registary_config_section - # 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 + # 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 + if not any_filter or panel_id == "dns": + from yunohost.dns import _get_registrar_config_section - toml["dns"]["registrar"] = _get_registrar_config_section(self.entity) - - # FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ... - self.registar_id = toml["dns"]["registrar"]["registrar"]["value"] - del toml["dns"]["registrar"]["registrar"]["value"] - - # 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 - and self.future_values["default_app"] != self.values["default_app"] - ): - from yunohost.app import app_ssowatconf, app_map - - if "/" in app_map(raw=True).get(self.entity, {}): - raise YunohostValidationError( - "app_make_default_location_already_used", - app=self.future_values["default_app"], - domain=self.entity, - other_app=app_map(raw=True)[self.entity]["/"]["id"], + raw_config["dns"]["registrar"] = _get_registrar_config_section( + self.entity ) - super()._apply() + # Cert stuff + if not any_filter or panel_id == "cert": + from yunohost.certificate import certificate_status - # Reload ssowat if default app changed - if ( - "default_app" in self.future_values - and self.future_values["default_app"] != self.values["default_app"] - ): - app_ssowatconf() + status = certificate_status([self.entity], full=True)["certificates"][ + self.entity + ] - 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") + raw_config["cert"]["cert"]["cert_summary"]["style"] = status["style"] - 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") + # 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 + raw_config["cert"]["cert"]["cert_summary"]["ask"] = m18n.n( + f"domain_config_cert_summary_{status['summary']}" + ) - if stuff_to_regen_conf: - regen_conf(names=stuff_to_regen_conf) + for option_id, status_key in [ + ("cert_validity", "validity"), + ("cert_issuer", "CA_type"), + ("acme_eligible", "ACME_eligible"), + # FIXME not sure why "summary" was injected in settings values + # ("summary", "summary") + ]: + raw_config["cert"]["cert"][option_id]["default"] = status[ + status_key + ] + + # Other specific strings used in config panels + # i18n: domain_config_cert_renew_help + + return raw_config + + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ) -> None: + next_settings = { + k: v for k, v in form.dict().items() if previous_settings.get(k) != v + } + + if "default_app" in next_settings: + from yunohost.app import app_map + + if "/" in app_map(raw=True).get(self.entity, {}): + raise YunohostValidationError( + "app_make_default_location_already_used", + app=next_settings["default_app"], + domain=self.entity, + other_app=app_map(raw=True)[self.entity]["/"]["id"], + ) + + if next_settings.get("recovery_password", None): + domain_dyndns_set_recovery_password( + self.entity, next_settings["recovery_password"] + ) + + portal_options = [ + "default_app", + "show_other_domains_apps", + "portal_title", + "portal_logo", + "portal_theme", + "search_engine", + "search_engine_name", + "portal_user_intro", + "portal_public_intro", + ] + + if _get_parent_domain_of(self.entity, topest=True) is None and any( + option in next_settings for option in portal_options + ): + from yunohost.portal import PORTAL_SETTINGS_DIR + + # Portal options are also saved in a `domain.portal.yml` file + # that can be read by the portal API. + # FIXME remove those from the config panel saved values? + + portal_values = form.dict(include=set(portal_options)) + # Remove logo from values else filename will replace b64 content + if "portal_logo" in portal_values: + portal_values.pop("portal_logo") + + if "portal_logo" in next_settings: + if previous_settings.get("portal_logo"): + try: + os.remove(previous_settings["portal_logo"]) + except FileNotFoundError: + logger.warning( + f"Coulnd't remove previous logo file, maybe the file was already deleted, path: {previous_settings['portal_logo']}" + ) + finally: + portal_values["portal_logo"] = "" + + if next_settings["portal_logo"]: + portal_values["portal_logo"] = Path(next_settings["portal_logo"]).name + + portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{self.entity}.json") + portal_settings: dict[str, Any] = {"apps": {}} + + if portal_settings_path.exists(): + portal_settings.update(read_json(str(portal_settings_path))) + + # Merge settings since this config file is shared with `app_ssowatconf()` which populate the `apps` key. + portal_settings.update(portal_values) + write_to_json( + str(portal_settings_path), portal_settings, sort_keys=True, indent=4 + ) + + super()._apply(form, previous_settings, exclude={"recovery_password"}) + + # Reload ssowat if default app changed + if "default_app" in next_settings: + from yunohost.app import app_ssowatconf + + app_ssowatconf() + + stuff_to_regen_conf = set() + if "mail_in" in next_settings or "mail_out" in next_settings: + stuff_to_regen_conf.update({"nginx", "postfix", "dovecot"}) + + if stuff_to_regen_conf: + regen_conf(names=list(stuff_to_regen_conf)) + + return DomainConfigPanel def domain_action_run(domain, action, args=None): @@ -718,10 +885,6 @@ def domain_cert_renew(domain_list, force=False, no_checks=False, email=False): return certificate_renew(domain_list, force, no_checks, email) -def domain_dns_conf(domain): - return domain_dns_suggest(domain) - - def domain_dns_suggest(domain): from yunohost.dns import domain_dns_suggest diff --git a/src/dyndns.py b/src/dyndns.py index 2594abe8f..7c538a447 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -17,17 +17,16 @@ # along with this program. If not, see . # import os -import re import json import glob import base64 import subprocess +import hashlib +from logging import getLogger -from moulinette import m18n +from moulinette import Moulinette, m18n from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, rm, chown, chmod -from moulinette.utils.network import download_json from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import _get_maindomain @@ -36,10 +35,21 @@ from yunohost.utils.dns import dig, is_yunohost_dyndns_domain from yunohost.log import is_unit_operation from yunohost.regenconf import regen_conf -logger = getActionLogger("yunohost.dyndns") +logger = getLogger("yunohost.dyndns") DYNDNS_PROVIDER = "dyndns.yunohost.org" DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] +MAX_DYNDNS_DOMAINS = 1 + + +def is_subscribing_allowed(): + """ + Check if the limit of subscribed DynDNS domains has been reached + + Returns: + True if the limit is not reached, False otherwise + """ + return len(dyndns_list()["domains"]) < MAX_DYNDNS_DOMAINS def _dyndns_available(domain): @@ -52,47 +62,85 @@ def _dyndns_available(domain): Returns: True if the domain is available, False otherwise. """ + import requests # lazy loading this module for performance reasons + logger.debug(f"Checking if domain {domain} is available on {DYNDNS_PROVIDER} ...") try: - r = download_json( - f"https://{DYNDNS_PROVIDER}/test/{domain}", expected_status_code=None - ) + r = requests.get(f"https://{DYNDNS_PROVIDER}/test/{domain}", timeout=30) except MoulinetteError as e: logger.error(str(e)) raise YunohostError( "dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER ) - return r == f"Domain {domain} is available" + if r.status_code == 200: + return r.text.strip('"') == f"Domain {domain} is available" + elif r.status_code == 409: + return False + elif r.status_code == 429: + raise YunohostValidationError("dyndns_too_many_requests") + else: + raise YunohostError( + "dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER + ) -@is_unit_operation() -def dyndns_subscribe(operation_logger, domain=None, key=None): +@is_unit_operation(exclude=["recovery_password"]) +def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): """ Subscribe to a DynDNS service Keyword argument: domain -- Full domain to subscribe with - key -- Public DNS key + recovery_password -- Password that will be used to delete the domain """ - if _guess_current_dyndns_domain() != (None, None): - raise YunohostValidationError("domain_dyndns_already_subscribed") - - if domain is None: - domain = _get_maindomain() - operation_logger.related_to.append(("domain", domain)) - # Verify if domain is provided by subscribe_host if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError( "dyndns_domain_not_provided", domain=domain, provider=DYNDNS_PROVIDER ) + # Check adding another dyndns domain is still allowed + if not is_subscribing_allowed(): + raise YunohostValidationError("domain_dyndns_already_subscribed") + # Verify if domain is available if not _dyndns_available(domain): - raise YunohostValidationError("dyndns_unavailable", domain=domain) + # Prompt for a password if running in CLI and no password provided + if not recovery_password and Moulinette.interface.type == "cli": + logger.warning(m18n.n("ask_dyndns_recovery_password_explain_unavailable")) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), is_password=True + ) + + if recovery_password: + # Try to unsubscribe the domain so it can be subscribed again + # If successful, it will be resubscribed with the same recovery password + dyndns_unsubscribe(domain=domain, recovery_password=recovery_password) + else: + raise YunohostValidationError("dyndns_unavailable", domain=domain) + + # Prompt for a password if running in CLI and no password provided + if not recovery_password and Moulinette.interface.type == "cli": + logger.warning(m18n.n("ask_dyndns_recovery_password_explain")) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True + ) + + if not recovery_password: + logger.warning(m18n.n("dyndns_no_recovery_password")) + + if recovery_password: + from yunohost.utils.password import assert_password_is_strong_enough + + assert_password_is_strong_enough("admin", recovery_password) + operation_logger.data_to_redact.append(recovery_password) + + if domain is None: + domain = _get_maindomain() + operation_logger.related_to.append(("domain", domain)) operation_logger.start() @@ -100,30 +148,26 @@ def dyndns_subscribe(operation_logger, domain=None, key=None): # '1234' is idk? doesnt matter, but the old format contained a number here... key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" - if key is None: - if len(glob.glob("/etc/yunohost/dyndns/*.key")) == 0: - if not os.path.exists("/etc/yunohost/dyndns"): - os.makedirs("/etc/yunohost/dyndns") + if not os.path.exists("/etc/yunohost/dyndns"): + os.makedirs("/etc/yunohost/dyndns") - logger.debug(m18n.n("dyndns_key_generating")) + # Here, we emulate the behavior of the old 'dnssec-keygen' utility + # which since bullseye was replaced by ddns-keygen which is now + # in the bind9 package ... but installing bind9 will conflict with dnsmasq + # and is just madness just to have access to a tsig keygen utility -.- - # Here, we emulate the behavior of the old 'dnssec-keygen' utility - # which since bullseye was replaced by ddns-keygen which is now - # in the bind9 package ... but installing bind9 will conflict with dnsmasq - # and is just madness just to have access to a tsig keygen utility -.- + # Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py) + secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii") - # Use 512 // 8 = 64 bytes for hmac-sha512 (c.f. https://git.hactrn.net/sra/tsig-keygen/src/master/tsig-keygen.py) - secret = base64.b64encode(os.urandom(512 // 8)).decode("ascii") + # Idk why but the secret is split in two parts, with the first one + # being 57-long char ... probably some DNS format + secret = f"{secret[:56]} {secret[56:]}" - # Idk why but the secret is split in two parts, with the first one - # being 57-long char ... probably some DNS format - secret = f"{secret[:56]} {secret[56:]}" + key_content = f"{domain}. IN KEY 0 3 165 {secret}" + write_to_file(key_file, key_content) - key_content = f"{domain}. IN KEY 0 3 165 {secret}" - write_to_file(key_file, key_content) - - chmod("/etc/yunohost/dyndns", 0o600, recursive=True) - chown("/etc/yunohost/dyndns", "root", recursive=True) + chmod("/etc/yunohost/dyndns", 0o600, recursive=True) + chown("/etc/yunohost/dyndns", "root", recursive=True) import requests # lazy loading this module for performance reasons @@ -131,21 +175,26 @@ def dyndns_subscribe(operation_logger, domain=None, key=None): try: # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... b64encoded_key = base64.b64encode(secret.encode()).decode() + data = {"subdomain": domain} + if recovery_password: + data["recovery_password"] = hashlib.sha256( + (domain + ":" + recovery_password.strip()).encode("utf-8") + ).hexdigest() r = requests.post( f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", - data={"subdomain": domain}, + data=data, timeout=30, ) except Exception as e: rm(key_file, force=True) - raise YunohostError("dyndns_registration_failed", error=str(e)) + raise YunohostError("dyndns_subscribe_failed", error=str(e)) if r.status_code != 201: rm(key_file, force=True) try: error = json.loads(r.text)["error"] except Exception: error = f'Server error, code: {r.status_code}. (Message: "{r.text}")' - raise YunohostError("dyndns_registration_failed", error=error) + raise YunohostError("dyndns_subscribe_failed", error=error) # Yunohost regen conf will add the dyndns cron job if a key exists # in /etc/yunohost/dyndns @@ -160,7 +209,145 @@ def dyndns_subscribe(operation_logger, domain=None, key=None): subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) - logger.success(m18n.n("dyndns_registered")) + logger.success(m18n.n("dyndns_subscribed")) + + +@is_unit_operation(exclude=["recovery_password"]) +def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): + """ + Unsubscribe from a DynDNS service + + Keyword argument: + domain -- Full domain to unsubscribe with + recovery_password -- Password that is used to delete the domain ( defined when subscribing ) + """ + + import requests # lazy loading this module for performance reasons + + # Unsubscribe the domain using the key if available + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + if keys: + key = keys[0] + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + base64key = base64.b64encode(key.encode()).decode() + credential = {"key": base64key} + # Otherwise, ask for the recovery password + else: + if Moulinette.interface.type == "cli" and not recovery_password: + logger.warning( + m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe") + ) + recovery_password = Moulinette.prompt( + m18n.n("ask_dyndns_recovery_password"), is_password=True + ) + + if not recovery_password: + logger.error( + f"Cannot unsubscribe the domain {domain}: no credential provided" + ) + return + + secret = str(domain) + ":" + str(recovery_password).strip() + credential = { + "recovery_password": hashlib.sha256(secret.encode("utf-8")).hexdigest() + } + + operation_logger.start() + + # Send delete request + try: + r = requests.delete( + f"https://{DYNDNS_PROVIDER}/domains/{domain}", + data=credential, + timeout=30, + ) + except Exception as e: + raise YunohostError("dyndns_unsubscribe_failed", error=str(e)) + + if r.status_code == 200: # Deletion was successful + for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key"): + rm(key_file, force=True) + # Yunohost regen conf will add the dyndns cron job if a key exists + # in /etc/yunohost/dyndns + regen_conf(["yunohost"]) + elif r.status_code == 403: + raise YunohostValidationError("dyndns_unsubscribe_denied") + elif r.status_code == 409: + raise YunohostValidationError("dyndns_unsubscribe_already_unsubscribed") + elif r.status_code == 429: + raise YunohostValidationError("dyndns_too_many_requests") + else: + raise YunohostError( + "dyndns_unsubscribe_failed", + error=f"The server returned code {r.status_code}", + ) + + logger.success(m18n.n("dyndns_unsubscribed")) + + +def dyndns_set_recovery_password(domain, recovery_password): + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + + if not keys: + raise YunohostValidationError("dyndns_key_not_found") + + from yunohost.utils.password import assert_password_is_strong_enough + + assert_password_is_strong_enough("admin", recovery_password) + secret = str(domain) + ":" + str(recovery_password).strip() + + key = keys[0] + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + base64key = base64.b64encode(key.encode()).decode() + + import requests # lazy loading this module for performance reasons + + # Send delete request + try: + r = requests.put( + f"https://{DYNDNS_PROVIDER}/domains/{domain}/recovery_password", + data={ + "key": base64key, + "recovery_password": hashlib.sha256(secret.encode("utf-8")).hexdigest(), + }, + timeout=30, + ) + except Exception as e: + raise YunohostError("dyndns_set_recovery_password_failed", error=str(e)) + + if r.status_code == 200: + logger.success(m18n.n("dyndns_set_recovery_password_success")) + elif r.status_code == 403: + raise YunohostError("dyndns_set_recovery_password_denied") + elif r.status_code == 404: + raise YunohostError("dyndns_set_recovery_password_unknown_domain") + elif r.status_code == 409: + raise YunohostError("dyndns_set_recovery_password_invalid_password") + else: + raise YunohostError( + "dyndns_set_recovery_password_failed", + error=f"The server returned code {r.status_code}", + ) + + +def dyndns_list(): + """ + Returns all currently subscribed DynDNS domains ( deduced from the key files ) + """ + + from yunohost.domain import domain_list + + domains = domain_list(exclude_subdomains=True)["domains"] + dyndns_domains = [ + d + for d in domains + if is_yunohost_dyndns_domain(d) + and glob.glob(f"/etc/yunohost/dyndns/K{d}.+*.key") + ] + + return {"domains": dyndns_domains} @is_unit_operation() @@ -183,22 +370,25 @@ def dyndns_update( import dns.tsigkeyring import dns.update - # If domain is not given, try to guess it from keys available... - key = None + # If domain is not given, update all DynDNS domains if domain is None: - (domain, key) = _guess_current_dyndns_domain() + dyndns_domains = dyndns_list()["domains"] - if domain is None: - raise YunohostValidationError("dyndns_no_domain_registered") + if not dyndns_domains: + raise YunohostValidationError("dyndns_no_domain_registered") + + for domain in dyndns_domains: + dyndns_update(domain, force=force, dry_run=dry_run) + + return # If key is not given, pick the first file we find with the domain given - elif key is None: - keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") - if not keys: - raise YunohostValidationError("dyndns_key_not_found") + if not keys: + raise YunohostValidationError("dyndns_key_not_found") - key = keys[0] + key = keys[0] # Get current IPv4 and IPv6 ipv4 = get_public_ip() @@ -281,7 +471,7 @@ def dyndns_update( # Delete custom DNS records, we don't support them (have to explicitly # authorize them on dynette) for category in dns_conf.keys(): - if category not in ["basic", "mail", "xmpp", "extra"]: + if category not in ["basic", "mail", "extra"]: del dns_conf[category] # Delete the old records for all domain/subdomains @@ -330,34 +520,3 @@ def dyndns_update( print( "Warning: dry run, this is only the generated config, it won't be applied" ) - - -def _guess_current_dyndns_domain(): - """ - This function tries to guess which domain should be updated by - "dyndns_update()" because there's not proper management of the current - dyndns domain :/ (and at the moment the code doesn't support having several - dyndns domain, which is sort of a feature so that people don't abuse the - dynette...) - """ - - DYNDNS_KEY_REGEX = re.compile(r".*/K(?P[^\s\+]+)\.\+165.+\.key$") - - # Retrieve the first registered domain - paths = list(glob.iglob("/etc/yunohost/dyndns/K*.key")) - for path in paths: - match = DYNDNS_KEY_REGEX.match(path) - if not match: - continue - _domain = match.group("domain") - - # Verify if domain is registered (i.e., if it's available, skip - # current domain beause that's not the one we want to update..) - # If there's only 1 such key found, then avoid doing the request - # for nothing (that's very probably the one we want to find ...) - if len(paths) > 1 and _dyndns_available(_domain): - continue - else: - return (_domain, path) - - return (None, None) diff --git a/src/firewall.py b/src/firewall.py index d6e4b5317..4e7337a5d 100644 --- a/src/firewall.py +++ b/src/firewall.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -19,16 +19,16 @@ import os import yaml import miniupnpc +from logging import getLogger from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils import process -from moulinette.utils.log import getActionLogger FIREWALL_FILE = "/etc/yunohost/firewall.yml" UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp" -logger = getActionLogger("yunohost.firewall") +logger = getLogger("yunohost.firewall") def firewall_allow( @@ -256,7 +256,7 @@ def firewall_reload(skip_upnp=False): # IPv4 try: - process.check_output("iptables -w -L") + process.check_output("iptables -n -w -L") except process.CalledProcessError as e: logger.debug( "iptables seems to be not available, it outputs:\n%s", @@ -289,7 +289,7 @@ def firewall_reload(skip_upnp=False): # IPv6 try: - process.check_output("ip6tables -L") + process.check_output("ip6tables -n -L") except process.CalledProcessError as e: logger.debug( "ip6tables seems to be not available, it outputs:\n%s", @@ -331,7 +331,7 @@ def firewall_reload(skip_upnp=False): # Refresh port forwarding with UPnP firewall_upnp(no_refresh=False) - _run_service_command("reload", "fail2ban") + _run_service_command("restart", "fail2ban") if errors: logger.warning(m18n.n("firewall_rules_cmd_failed")) @@ -404,7 +404,7 @@ def firewall_upnp(action="status", no_refresh=False): logger.debug("discovering UPnP devices...") try: nb_dev = upnpc.discover() - except Exception as e: + except Exception: logger.warning("Failed to find any UPnP device on the network") nb_dev = -1 enabled = False diff --git a/src/hook.py b/src/hook.py index 4b07d1c17..acba650be 100644 --- a/src/hook.py +++ b/src/hook.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -23,16 +23,16 @@ import tempfile import mimetypes from glob import iglob from importlib import import_module +from logging import getLogger from moulinette import m18n, Moulinette from yunohost.utils.error import YunohostError, YunohostValidationError -from moulinette.utils import log from moulinette.utils.filesystem import read_yaml, cp HOOK_FOLDER = "/usr/share/yunohost/hooks/" CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/" -logger = log.getActionLogger("yunohost.hook") +logger = getLogger("yunohost.hook") def hook_add(app, file): @@ -359,6 +359,7 @@ def hook_exec( r"Removing obsolete dictionary files", r"Creating new PostgreSQL cluster", r"/usr/lib/postgresql/13/bin/initdb", + r"/usr/lib/postgresql/15/bin/initdb", r"The files belonging to this database system will be owned by user", r"This user must also own the server process.", r"The database cluster will be initialized with locale", @@ -366,6 +367,7 @@ def hook_exec( r"The default text search configuration will be set to", r"Data page checksums are disabled.", r"fixing permissions on existing directory /var/lib/postgresql/13/main ... ok", + r"fixing permissions on existing directory /var/lib/postgresql/15/main ... ok", r"creating subdirectories \.\.\. ok", r"selecting dynamic .* \.\.\. ", r"selecting default .* \.\.\. ", @@ -377,15 +379,21 @@ def hook_exec( 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", + # Java boring messages + r"cannot open '/etc/ssl/certs/java/cacerts'", + # Misc + r"update-binfmts: warning:", ] return all(not re.search(w, msg) for w in irrelevant_warnings) # Define output loggers and call command loggers = ( lambda l: logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) - if is_relevant_warning(l.rstrip()) - else logger.debug(l.rstrip()), + lambda l: ( + logger.warning(l.rstrip()) + if is_relevant_warning(l.rstrip()) + else logger.debug(l.rstrip()) + ), lambda l: logger.info(l.rstrip()), ) @@ -532,6 +540,9 @@ def hook_exec_with_script_debug_if_failure(*args, **kwargs): failed = True if retcode != 0 else False if failed: error = error_message_if_script_failed + # check more specific error message added by ynh_die in $YNH_STDRETURN + if isinstance(retpayload, dict) and "error" in retpayload: + error += " : " + retpayload["error"].strip() logger.error(error_message_if_failed(error)) failure_message_with_debug_instructions = operation_logger.error(error) if Moulinette.interface.type != "api": diff --git a/src/log.py b/src/log.py old mode 100644 new mode 100755 index 753d19839..83b9c38b6 --- a/src/log.py +++ b/src/log.py @@ -1,5 +1,4 @@ -# -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -25,6 +24,7 @@ import glob import psutil import zmq import json +import time from typing import List from datetime import datetime, timedelta @@ -35,25 +35,30 @@ from moulinette import m18n, Moulinette from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.system import get_ynh_package_version -from moulinette.utils.log import getActionLogger, LOG_BROKER_BACKEND_ENDPOINT +from moulinette.utils.log import LOG_BROKER_BACKEND_ENDPOINT from moulinette.utils.filesystem import read_file, read_yaml -logger = getActionLogger("yunohost.log") +logger = getLogger("yunohost.log") -CATEGORIES_PATH = "/var/log/yunohost/categories/" -OPERATIONS_PATH = "/var/log/yunohost/categories/operation/" +OPERATIONS_PATH = "/var/log/yunohost/operations/" METADATA_FILE_EXT = ".yml" LOG_FILE_EXT = ".log" BORING_LOG_LINES = [ r"set [+-]x$", r"set [+-]o xtrace$", + r"\+ set \+o$", + r"\+ grep xtrace$", + r"local 'xtrace_enable=", r"set [+-]o errexit$", r"set [+-]o nounset$", r"trap '' EXIT", r"local \w+$", r"local exit_code=(1|0)$", r"local legacy_args=.*$", + r"local _globalapp=.*$", + r"local checksum_setting_name=.*$", + r"ynh_app_setting ", # (note the trailing space to match the "low level" one called by other setting helpers) r"local -A args_array$", r"args_array=.*$", r"ret_code=1", @@ -65,11 +70,53 @@ BORING_LOG_LINES = [ r"\[?\['? -n '' '?\]\]?$", r"rm -rf /var/cache/yunohost/download/$", r"type -t ynh_clean_setup$", + r"DEBUG - \+ unset \S+$", r"DEBUG - \+ echo '", + r"DEBUG - \+ LC_ALL=C$", + r"DEBUG - \+ DEBIAN_FRONTEND=noninteractive$", r"DEBUG - \+ exit (1|0)$", + r"DEBUG - \+ app=\S+$", + r"DEBUG - \+\+ app=\S+$", + r"DEBUG - \+\+ jq -r .\S+$", + r"DEBUG - \+\+ sed 's/\^null\$//'$", + "DEBUG - \\+ sed --in-place \\$'s\\\\001", + "DEBUG - \\+ sed --in-place 's\u0001.*$", ] +def _update_log_parent_symlinks(): + + one_year_ago = time.time() - 365 * 24 * 3600 + + logs = glob.iglob("*" + METADATA_FILE_EXT, root_dir=OPERATIONS_PATH) + for log_md in logs: + log_md_fullpath = os.path.join(OPERATIONS_PATH, log_md) + if os.path.getctime(log_md_fullpath) < one_year_ago: + # Let's ignore files older than one year because hmpf reading a shitload of yml is not free + continue + + name = log_md[: -len(METADATA_FILE_EXT)] + parent_symlink = os.path.join(OPERATIONS_PATH, f".{name}.parent.yml") + if os.path.islink(parent_symlink): + continue + + try: + metadata = ( + read_yaml(log_md_fullpath) or {} + ) # Making sure this is a dict and not None..? + except Exception as e: + # If we can't read the yaml for some reason, report an error and ignore this entry... + logger.error(m18n.n("log_corrupted_md_file", md_file=log_md, error=e)) + continue + + parent = metadata.get("parent") + parent = parent + METADATA_FILE_EXT if parent else "/dev/null" + try: + os.symlink(parent, parent_symlink) + except Exception as e: + logger.warning(f"Failed to create symlink {parent_symlink} ? {e}") + + def log_list(limit=None, with_details=False, with_suboperations=False): """ List available logs @@ -86,30 +133,43 @@ def log_list(limit=None, with_details=False, with_suboperations=False): operations = {} - logs = [x for x in os.listdir(OPERATIONS_PATH) if x.endswith(METADATA_FILE_EXT)] + _update_log_parent_symlinks() + + one_year_ago = time.time() - 365 * 24 * 3600 + logs = [ + x + for x in os.listdir(OPERATIONS_PATH) + if x.endswith(METADATA_FILE_EXT) and os.path.getctime(os.path.join(OPERATIONS_PATH, x)) > one_year_ago + ] logs = list(reversed(sorted(logs))) + if not with_suboperations: + + def parent_symlink_points_to_dev_null(log): + name = log[: -len(METADATA_FILE_EXT)] + parent_symlink = os.path.join(OPERATIONS_PATH, f".{name}.parent.yml") + return ( + os.path.islink(parent_symlink) + and os.path.realpath(parent_symlink) == "/dev/null" + ) + + logs = [log for log in logs if parent_symlink_points_to_dev_null(log)] + if limit is not None: - if with_suboperations: - logs = logs[:limit] - else: - # If we displaying only parent, we are still gonna load up to limit * 5 logs - # because many of them are suboperations which are not gonna be kept - # Yet we still want to obtain ~limit number of logs - logs = logs[: limit * 5] + logs = logs[:limit] for log in logs: - base_filename = log[: -len(METADATA_FILE_EXT)] + name = log[: -len(METADATA_FILE_EXT)] md_path = os.path.join(OPERATIONS_PATH, log) entry = { - "name": base_filename, + "name": name, "path": md_path, - "description": _get_description_from_name(base_filename), + "description": _get_description_from_name(name), } try: - entry["started_at"] = _get_datetime_from_name(base_filename) + entry["started_at"] = _get_datetime_from_name(name) except ValueError: pass @@ -129,10 +189,8 @@ def log_list(limit=None, with_details=False, with_suboperations=False): if with_suboperations: entry["parent"] = metadata.get("parent") entry["suboperations"] = [] - elif metadata.get("parent") is not None: - continue - operations[base_filename] = entry + operations[name] = entry # When displaying suboperations, we build a tree-like structure where # "suboperations" is a list of suboperations (each of them may also have a list of @@ -176,6 +234,20 @@ def log_show( share """ + # Set up path with correct value if 'last' or 'last-X' magic keywords are used + last = re.match(r"last(?:-(?P[0-9]{1,6}))?$", path) + if last: + position = 1 + if last.group("position") is not None: + position += int(last.group("position")) + + logs = list(log_list()["operation"]) + + if position > len(logs): + raise YunohostValidationError("There isn't that many logs", raw_msg=True) + + path = logs[-position]["path"] + if share: filter_irrelevant = True @@ -218,7 +290,7 @@ def log_show( infos = {} # If it's a unit operation, display the name and the description - if base_path.startswith(CATEGORIES_PATH): + if base_path.startswith(OPERATIONS_PATH): infos["description"] = _get_description_from_name(base_filename) infos["name"] = base_filename @@ -514,7 +586,6 @@ class RedactingFormatter(Formatter): class OperationLogger: - """ Instances of this class represents unit operation done on the ynh instance. @@ -524,7 +595,7 @@ class OperationLogger: This class record logs and metadata like context or start time/end time. """ - _instances: List[object] = [] + _instances: List["OperationLogger"] = [] def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation @@ -820,7 +891,7 @@ class OperationLogger: # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo # And we just want the part starting by "DEBUG - " lines = [line for line in lines if ":" in line.strip()] - lines = [line.strip().split(": ", 1)[1] for line in lines] + lines = [line.strip().split(": ", 1)[-1] for line in lines] # And we ignore boring/irrelevant lines # Annnnnnd we also ignore lines matching [number] + such as # 72971 DEBUG 29739 + ynh_exit_properly @@ -837,7 +908,7 @@ class OperationLogger: # Get the 20 lines before the last 'ynh_exit_properly' rev_lines = list(reversed(lines)) - for i, line in enumerate(rev_lines): + for i, line in enumerate(rev_lines[:50]): if line.endswith("+ ynh_exit_properly"): lines_to_display = reversed(rev_lines[i : i + 20]) break diff --git a/src/migrations/0021_migrate_to_bullseye.py b/src/migrations/0021_migrate_to_bullseye.py deleted file mode 100644 index f320577e1..000000000 --- a/src/migrations/0021_migrate_to_bullseye.py +++ /dev/null @@ -1,578 +0,0 @@ -import glob -import os - -from moulinette import m18n -from yunohost.utils.error import YunohostError -from moulinette.utils.log import getActionLogger -from moulinette.utils.process import check_output, call_async_output -from moulinette.utils.filesystem import read_file, rm, write_to_file - -from yunohost.tools import ( - Migration, - tools_update, - tools_upgrade, - _apt_log_line_is_relevant, -) -from yunohost.app import unstable_apps -from yunohost.regenconf import manually_modified_files, _force_clear_hashes -from yunohost.utils.system import ( - free_space_in_directory, - get_ynh_package_version, - _list_upgradable_apt_packages, -) -from yunohost.service import _get_services, _save_services - -logger = getActionLogger("yunohost.migration") - -N_CURRENT_DEBIAN = 10 -N_CURRENT_YUNOHOST = 4 - -VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" - - -def _get_all_venvs(dir, level=0, maxlevel=3): - """ - Returns the list of all python virtual env directories recursively - - Arguments: - dir - the directory to scan in - maxlevel - the depth of the recursion - level - do not edit this, used as an iterator - """ - if not os.path.exists(dir): - return [] - - result = [] - # Using os functions instead of glob, because glob doesn't support hidden folders, and we need recursion with a fixed depth - for file in os.listdir(dir): - path = os.path.join(dir, file) - if os.path.isdir(path): - activatepath = os.path.join(path, "bin", "activate") - if os.path.isfile(activatepath): - content = read_file(activatepath) - if ("VIRTUAL_ENV" in content) and ("PYTHONHOME" in content): - result.append(path) - continue - if level < maxlevel: - result += _get_all_venvs(path, level=level + 1) - return result - - -def _backup_pip_freeze_for_python_app_venvs(): - """ - Generate a requirements file for all python virtual env located inside /opt/ and /var/www/ - """ - - venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") - for venv in venvs: - # Generate a requirements file from venv - os.system( - f"{venv}/bin/pip freeze > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null" - ) - - -class MyMigration(Migration): - "Upgrade the system to Debian Bullseye and Yunohost 11.x" - - mode = "manual" - - def run(self): - self.check_assertions() - - logger.info(m18n.n("migration_0021_start")) - - # - # Add new apt .deb signing key - # - - new_apt_key = "https://forge.yunohost.org/yunohost_bullseye.asc" - check_output(f"wget -O- {new_apt_key} -q | apt-key add -qq -") - - # - # Patch sources.list - # - logger.info(m18n.n("migration_0021_patching_sources_list")) - self.patch_apt_sources_list() - - # 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 - # hence triggering some dependency conflict (or foobar-ynh-deps uninstall) - # Adding it there shouldnt be a big deal - Yunohost 11.x does add it - # through its regen conf anyway. - if not os.path.exists("/etc/apt/sources.list.d/extra_php_version.list"): - open("/etc/apt/sources.list.d/extra_php_version.list", "w").write( - "deb https://packages.sury.org/php/ bullseye main" - ) - - # 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 - # - - _backup_pip_freeze_for_python_app_venvs() - - # - # Run apt update - # - - tools_update(target="system") - - # Tell libc6 it's okay to restart system stuff during the upgrade - os.system( - "echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections" - ) - - # Do not restart nginx during the upgrade of nginx-common and nginx-extras ... - # c.f. https://manpages.debian.org/bullseye/init-system-helpers/deb-systemd-invoke.1p.en.html - # and zcat /usr/share/doc/init-system-helpers/README.policy-rc.d.gz - # and the code inside /usr/bin/deb-systemd-invoke to see how it calls /usr/sbin/policy-rc.d ... - # and also invoke-rc.d ... - write_to_file( - "/usr/sbin/policy-rc.d", - '#!/bin/bash\n[[ "$1" =~ "nginx" ]] && [[ "$2" == "restart" ]] && exit 101 || exit 0', - ) - os.system("chmod +x /usr/sbin/policy-rc.d") - - # Don't send an email to root about the postgresql migration. It should be handled automatically after. - os.system( - "echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections" - ) - - # - # Patch yunohost conflicts - # - logger.info(m18n.n("migration_0021_patch_yunohost_conflicts")) - - self.patch_yunohost_conflicts() - - # - # Specific tweaking to get rid of custom my.cnf and use debian's default one - # (my.cnf is actually a symlink to mariadb.cnf) - # - - _force_clear_hashes(["/etc/mysql/my.cnf"]) - rm("/etc/mysql/mariadb.cnf", force=True) - rm("/etc/mysql/my.cnf", force=True) - ret = self.apt_install( - "mariadb-common --reinstall -o Dpkg::Options::='--force-confmiss'" - ) - if ret != 0: - raise YunohostError("Failed to reinstall mariadb-common ?", raw_msg=True) - - # - # /usr/share/yunohost/yunohost-config/ssl/yunoCA -> /usr/share/yunohost/ssl - # - if os.path.exists("/usr/share/yunohost/yunohost-config/ssl/yunoCA"): - os.system( - "mv /usr/share/yunohost/yunohost-config/ssl/yunoCA /usr/share/yunohost/ssl" - ) - rm("/usr/share/yunohost/yunohost-config", recursive=True, force=True) - - # - # /home/yunohost.conf -> /var/cache/yunohost/regenconf - # - if os.path.exists("/home/yunohost.conf"): - os.system("mv /home/yunohost.conf /var/cache/yunohost/regenconf") - rm("/home/yunohost.conf", recursive=True, force=True) - - # Remove legacy postgresql service record added by helpers, - # will now be dynamically handled by the core in bullseye - services = _get_services() - if "postgresql" in services: - del services["postgresql"] - _save_services(services) - - # - # 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 - # - logger.info(m18n.n("migration_0021_main_upgrade")) - - apps_packages = self.get_apps_equivs_packages() - self.hold(apps_packages) - tools_upgrade(target="system", allow_yunohost_upgrade=False) - - if self.debian_major_version() == N_CURRENT_DEBIAN: - raise YunohostError("migration_0021_still_on_buster_after_main_upgrade") - - # Force explicit install of php7.4-fpm and other old 'default' dependencies - # that are now only in Recommends - # - # Also, we need to install php7.4 equivalents of other php7.3 dependencies. - # For example, Nextcloud may depend on php7.3-zip, and after the php pool migration - # to autoupgrade Nextcloud to 7.4, it will need the php7.4-zip to work. - # The following list is based on an ad-hoc analysis of php deps found in the - # app ecosystem, with a known equivalent on php7.4. - # - # This is kinda a dirty hack as it doesnt properly update the *-ynh-deps virtual packages - # with the proper list of dependencies, and the dependencies install this way - # will get flagged as 'manually installed'. - # - # We'll probably want to do something during the Bullseye->Bookworm migration to re-flag - # these as 'auto' so they get autoremoved if not needed anymore. - # Also hopefully by then we'll have manifestv2 (maybe) and will be able to use - # the apt resource mecanism to regenerate the *-ynh-deps virtual packages ;) - - php73packages_suffixes = [ - "apcu", - "bcmath", - "bz2", - "dom", - "gmp", - "igbinary", - "imagick", - "imap", - "mbstring", - "memcached", - "mysqli", - "mysqlnd", - "pgsql", - "redis", - "simplexml", - "soap", - "sqlite3", - "ssh2", - "tidy", - "xml", - "xmlrpc", - "xsl", - "zip", - ] - - cmd = ( - "apt show '*-ynh-deps' 2>/dev/null" - " | grep Depends" - f" | grep -o -E \"php7.3-({'|'.join(php73packages_suffixes)})\"" - " | sort | uniq" - " | sed 's/php7.3/php7.4/g'" - " || true" - ) - - basephp74packages_to_install = [ - "php7.4-fpm", - "php7.4-common", - "php7.4-ldap", - "php7.4-intl", - "php7.4-mysql", - "php7.4-gd", - "php7.4-curl", - "php-php-gettext", - ] - - php74packages_to_install = basephp74packages_to_install + [ - f.strip() for f in check_output(cmd).split("\n") if f.strip() - ] - - ret = self.apt_install( - f"{' '.join(php74packages_to_install)} " - "$(dpkg --list | grep ynh-deps | awk '{print $2}') " - "-o Dpkg::Options::='--force-confmiss'" - ) - if ret != 0: - raise YunohostError( - "Failed to force the install of php dependencies ?", raw_msg=True - ) - - # Clean the mess - logger.info(m18n.n("migration_0021_cleaning_up")) - os.system( - "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes" - ) - os.system("apt clean --assume-yes") - - # - # Stupid hack for stupid dnsmasq not picking up its new init.d script then breaking everything ... - # https://forum.yunohost.org/t/20676 - # - if os.path.exists("/etc/init.d/dnsmasq.dpkg-dist"): - logger.info("Copying new version for /etc/init.d/dnsmasq ...") - os.system("cp /etc/init.d/dnsmasq.dpkg-dist /etc/init.d/dnsmasq") - - # - # Yunohost upgrade - # - logger.info(m18n.n("migration_0021_yunohost_upgrade")) - - self.unhold(apps_packages) - - cmd = "LC_ALL=C" - cmd += " DEBIAN_FRONTEND=noninteractive" - cmd += " APT_LISTCHANGES_FRONTEND=none" - cmd += " apt dist-upgrade " - cmd += " --quiet -o=Dpkg::Use-Pty=0 --fix-broken --dry-run" - cmd += " | grep -q 'ynh-deps'" - - logger.info("Simulating upgrade...") - if os.system(cmd) == 0: - raise YunohostError( - "The upgrade cannot be completed, because some app dependencies would need to be removed?", - raw_msg=True, - ) - - postupgradecmds = f"apt-mark auto {' '.join(basephp74packages_to_install)}\n" - postupgradecmds += "rm -f /usr/sbin/policy-rc.d\n" - postupgradecmds += "echo 'Restarting nginx...' >&2\n" - postupgradecmds += "systemctl restart nginx\n" - - tools_upgrade(target="system", postupgradecmds=postupgradecmds) - - def debian_major_version(self): - # The python module "platform" and lsb_release are not reliable because - # on some setup, they may still return Release=9 even after upgrading to - # buster ... (Apparently this is related to OVH overriding some stuff - # with /etc/lsb-release for instance -_-) - # Instead, we rely on /etc/os-release which should be the raw info from - # the distribution... - return int( - check_output( - "grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2" - ) - ) - - def yunohost_major_version(self): - return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) - - def check_assertions(self): - # Be on buster (10.x) and yunohost 4.x - # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be > 9.x but yunohost package - # would still be in 3.x... - if ( - not self.debian_major_version() == N_CURRENT_DEBIAN - and not self.yunohost_major_version() == N_CURRENT_YUNOHOST - ): - 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) - if os.path.exists("/etc/apt/sources.list") and " bullseye " not in read_file( - "/etc/apt/sources.list" - ): - tools_update(target="system") - upgradable_system_packages = list(_list_upgradable_apt_packages()) - upgradable_system_packages = [ - package["name"] for package in upgradable_system_packages - ] - upgradable_system_packages = set(upgradable_system_packages) - # Lime2 have hold packages to avoid ethernet instability - # See https://github.com/YunoHost/arm-images/commit/b4ef8c99554fd1a122a306db7abacc4e2f2942df - lime2_hold_packages = set( - [ - "armbian-firmware", - "armbian-bsp-cli-lime2", - "linux-dtb-current-sunxi", - "linux-image-current-sunxi", - "linux-u-boot-lime2-current", - "linux-image-next-sunxi", - ] - ) - if upgradable_system_packages - lime2_hold_packages: - raise YunohostError("migration_0021_system_not_fully_up_to_date") - - @property - def disclaimer(self): - # Avoid having a super long disclaimer + uncessary check if we ain't - # on buster / yunohost 4.x anymore - # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be >= 10.x but yunohost package - # would still be in 4.x... - if ( - not self.debian_major_version() == N_CURRENT_DEBIAN - and not self.yunohost_major_version() == N_CURRENT_YUNOHOST - ): - return None - - # Get list of problematic apps ? I.e. not official or community+working - problematic_apps = unstable_apps() - problematic_apps = "".join(["\n - " + app for app in problematic_apps]) - - # Manually modified files ? (c.f. yunohost service regen-conf) - modified_files = manually_modified_files() - modified_files = "".join(["\n - " + f for f in modified_files]) - - message = m18n.n("migration_0021_general_warning") - - message = ( - "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/20590\n\n" - + message - ) - - if problematic_apps: - message += "\n\n" + m18n.n( - "migration_0021_problematic_apps_warning", - problematic_apps=problematic_apps, - ) - - if modified_files: - message += "\n\n" + m18n.n( - "migration_0021_modified_files", manually_modified_files=modified_files - ) - - return message - - def patch_apt_sources_list(self): - sources_list = glob.glob("/etc/apt/sources.list.d/*.list") - if os.path.exists("/etc/apt/sources.list"): - sources_list.append("/etc/apt/sources.list") - - # This : - # - replace single 'buster' occurence by 'bulleye' - # - comments lines containing "backports" - # - replace 'buster/updates' by 'bullseye/updates' (or same with -) - # Special note about the security suite: - # https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#security-archive - for f in sources_list: - command = ( - f"sed -i {f} " - "-e 's@ buster @ bullseye @g' " - "-e '/backports/ s@^#*@#@' " - "-e 's@ buster/updates @ bullseye-security @g' " - "-e 's@ buster-@ bullseye-@g' " - ) - os.system(command) - - def get_apps_equivs_packages(self): - command = ( - "dpkg --get-selections" - " | grep -v deinstall" - " | awk '{print $1}'" - " | { grep 'ynh-deps$' || true; }" - ) - - output = check_output(command) - - return output.split("\n") if output else [] - - def hold(self, packages): - for package in packages: - os.system(f"apt-mark hold {package}") - - def unhold(self, packages): - for package in packages: - os.system(f"apt-mark unhold {package}") - - def apt_install(self, cmd): - def is_relevant(line): - return "Reading database ..." not in line.rstrip() - - callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip()), - ) - - cmd = ( - "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " - + cmd - ) - - logger.debug("Running: %s" % cmd) - - return call_async_output(cmd, callbacks, shell=True) - - def patch_yunohost_conflicts(self): - # - # This is a super dirty hack to remove the conflicts from yunohost's debian/control file - # Those conflicts are there to prevent mistakenly upgrading critical packages - # such as dovecot, postfix, nginx, openssl, etc... usually related to mistakenly - # using backports etc. - # - # The hack consists in savagely removing the conflicts directly in /var/lib/dpkg/status - # - - # We only patch the conflict if we're on yunohost 4.x - if self.yunohost_major_version() != N_CURRENT_YUNOHOST: - return - - conflicts = check_output("dpkg-query -s yunohost | grep '^Conflicts:'").strip() - if conflicts: - # We want to keep conflicting with apache/bind9 tho - new_conflicts = "Conflicts: apache2, bind9" - - command = ( - f"sed -i /var/lib/dpkg/status -e 's@{conflicts}@{new_conflicts}@g'" - ) - logger.debug(f"Running: {command}") - os.system(command) diff --git a/src/migrations/0022_php73_to_php74_pools.py b/src/migrations/0022_php73_to_php74_pools.py deleted file mode 100644 index dc428e504..000000000 --- a/src/migrations/0022_php73_to_php74_pools.py +++ /dev/null @@ -1,94 +0,0 @@ -import os -import glob -from shutil import copy2 - -from moulinette.utils.log import getActionLogger - -from yunohost.app import _is_installed -from yunohost.utils.legacy import _patch_legacy_php_versions_in_settings -from yunohost.tools import Migration -from yunohost.service import _run_service_command - -logger = getActionLogger("yunohost.migration") - -OLDPHP_POOLS = "/etc/php/7.3/fpm/pool.d" -NEWPHP_POOLS = "/etc/php/7.4/fpm/pool.d" - -OLDPHP_SOCKETS_PREFIX = "/run/php/php7.3-fpm" -NEWPHP_SOCKETS_PREFIX = "/run/php/php7.4-fpm" - -# Because of synapse é_è -OLDPHP_SOCKETS_PREFIX2 = "/run/php7.3-fpm" -NEWPHP_SOCKETS_PREFIX2 = "/run/php7.4-fpm" - -MIGRATION_COMMENT = ( - "; YunoHost note : this file was automatically moved from {}".format(OLDPHP_POOLS) -) - - -class MyMigration(Migration): - "Migrate php7.3-fpm 'pool' conf files to php7.4" - - dependencies = ["migrate_to_bullseye"] - - def run(self): - # Get list of php7.3 pool files - oldphp_pool_files = glob.glob("{}/*.conf".format(OLDPHP_POOLS)) - - # Keep only basenames - oldphp_pool_files = [os.path.basename(f) for f in oldphp_pool_files] - - # Ignore the "www.conf" (default stuff, probably don't want to touch it ?) - oldphp_pool_files = [f for f in oldphp_pool_files if f != "www.conf"] - - for pf in oldphp_pool_files: - # Copy the files to the php7.3 pool - src = "{}/{}".format(OLDPHP_POOLS, pf) - dest = "{}/{}".format(NEWPHP_POOLS, pf) - copy2(src, dest) - - # Replace the socket prefix if it's found - c = "sed -i -e 's@{}@{}@g' {}".format( - OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, dest - ) - os.system(c) - c = "sed -i -e 's@{}@{}@g' {}".format( - OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, dest - ) - os.system(c) - - # Also add a comment that it was automatically moved from php7.3 - # (for human traceability and backward migration) - c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest) - os.system(c) - - app_id = os.path.basename(pf)[: -len(".conf")] - if _is_installed(app_id): - _patch_legacy_php_versions_in_settings( - "/etc/yunohost/apps/%s/" % app_id - ) - - nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/%s.conf" % app_id) - for nf in nginx_conf_files: - # Replace the socket prefix if it's found - c = "sed -i -e 's@{}@{}@g' {}".format( - OLDPHP_SOCKETS_PREFIX, NEWPHP_SOCKETS_PREFIX, nf - ) - os.system(c) - c = "sed -i -e 's@{}@{}@g' {}".format( - OLDPHP_SOCKETS_PREFIX2, NEWPHP_SOCKETS_PREFIX2, nf - ) - os.system(c) - - os.system( - "rm /etc/logrotate.d/php7.3-fpm" - ) # We remove this otherwise the logrotate cron will be unhappy - - # Reload/restart the php pools - os.system("systemctl stop php7.3-fpm") - os.system("systemctl disable php7.3-fpm") - _run_service_command("restart", "php7.4-fpm") - _run_service_command("enable", "php7.4-fpm") - - # Reload nginx - _run_service_command("reload", "nginx") diff --git a/src/migrations/0025_global_settings_to_configpanel.py b/src/migrations/0025_global_settings_to_configpanel.py deleted file mode 100644 index 3a8818461..000000000 --- a/src/migrations/0025_global_settings_to_configpanel.py +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 43f10a7b6..000000000 --- a/src/migrations/0026_new_admins_group.py +++ /dev/null @@ -1,153 +0,0 @@ -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/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index 85e2235af..a33e1e7ba 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -1,31 +1,41 @@ import glob import os +import subprocess +from time import sleep -from moulinette import m18n +from moulinette import Moulinette, m18n +from moulinette.utils.process import call_async_output from yunohost.utils.error import YunohostError -from moulinette.utils.log import getActionLogger -from moulinette.utils.process import check_output, call_async_output -from moulinette.utils.filesystem import read_file, rm, write_to_file +from yunohost.tools import _write_migration_state +from moulinette.utils.process import check_output +from moulinette.utils.filesystem import read_file, write_to_file from yunohost.tools import ( Migration, tools_update, - tools_upgrade, - _apt_log_line_is_relevant, ) from yunohost.app import unstable_apps -from yunohost.regenconf import manually_modified_files, _force_clear_hashes +from yunohost.regenconf import manually_modified_files, regen_conf from yunohost.utils.system import ( free_space_in_directory, get_ynh_package_version, _list_upgradable_apt_packages, + aptitude_with_progress_bar, ) -from yunohost.service import _get_services, _save_services -logger = getActionLogger("yunohost.migration") +# getActionLogger is not there in bookworm, +# we use this try/except to make it agnostic wether or not we're on 11.x or 12.x +# otherwise this may trigger stupid issues +try: + from moulinette.utils.log import getActionLogger + logger = getActionLogger("yunohost.migration") +except ImportError: + import logging + logger = logging.getLogger("yunohost.migration") -N_CURRENT_DEBIAN = 10 -N_CURRENT_YUNOHOST = 4 + +N_CURRENT_DEBIAN = 11 +N_CURRENT_YUNOHOST = 11 VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bookworm_upgrade.txt" @@ -66,47 +76,28 @@ 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 + # Remove pkg resources from the freeze to avoid an error during the python venv https://stackoverflow.com/a/40167445 os.system( - f"{venv}/bin/pip freeze > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null" + f"{venv}/bin/pip freeze | grep -E -v 'pkg(-|_)resources==' > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null" ) class MyMigration(Migration): - "Upgrade the system to Debian Bookworm and Yunohost 11.x" + "Upgrade the system to Debian Bookworm and Yunohost 12.x" mode = "manual" def run(self): self.check_assertions() - logger.info(m18n.n("migration_0021_start")) + logger.info(m18n.n("migration_0027_start")) # # Add new apt .deb signing key # new_apt_key = "https://forge.yunohost.org/yunohost_bookworm.asc" - check_output(f"wget -O- {new_apt_key} -q | apt-key add -qq -") - - # - # Patch sources.list - # - logger.info(m18n.n("migration_0021_patching_sources_list")) - self.patch_apt_sources_list() - - # Stupid OVH has some repo configured which dont work with bookworm and break apt ... - os.system("sudo rm -f /etc/apt/sources.list.d/ovh-*.list") - - # Force add sury if it's not there yet - # This is to solve some weird issue with php-common breaking php7.3-common, - # hence breaking many php7.3-deps - # hence triggering some dependency conflict (or foobar-ynh-deps uninstall) - # Adding it there shouldnt be a big deal - Yunohost 11.x does add it - # through its regen conf anyway. - if not os.path.exists("/etc/apt/sources.list.d/extra_php_version.list"): - open("/etc/apt/sources.list.d/extra_php_version.list", "w").write( - "deb https://packages.sury.org/php/ bookworm main" - ) + os.system(f'wget --timeout 900 --quiet "{new_apt_key}" --output-document=- | gpg --dearmor >"/usr/share/keyrings/yunohost-bookworm.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 Bookworm @@ -115,9 +106,12 @@ class MyMigration(Migration): '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") + # + # Patch sources.list + # + + logger.info(m18n.n("migration_0027_patching_sources_list")) + self.patch_apt_sources_list() # # Get requirements of the different venvs from python apps @@ -129,7 +123,7 @@ class MyMigration(Migration): # Run apt update # - tools_update(target="system") + aptitude_with_progress_bar("update") # Tell libc6 it's okay to restart system stuff during the upgrade os.system( @@ -137,13 +131,13 @@ class MyMigration(Migration): ) # Do not restart nginx during the upgrade of nginx-common and nginx-extras ... - # c.f. https://manpages.debian.org/bookworm/init-system-helpers/deb-systemd-invoke.1p.en.html + # c.f. https://manpages.debian.org/bullseye/init-system-helpers/deb-systemd-invoke.1p.en.html # and zcat /usr/share/doc/init-system-helpers/README.policy-rc.d.gz # and the code inside /usr/bin/deb-systemd-invoke to see how it calls /usr/sbin/policy-rc.d ... # and also invoke-rc.d ... write_to_file( "/usr/sbin/policy-rc.d", - '#!/bin/bash\n[[ "$1" =~ "nginx" ]] && [[ "$2" == "restart" ]] && exit 101 || exit 0', + '#!/bin/bash\n[[ "$1" =~ "nginx" ]] && exit 101 || exit 0', ) os.system("chmod +x /usr/sbin/policy-rc.d") @@ -155,151 +149,56 @@ class MyMigration(Migration): # # Patch yunohost conflicts # - logger.info(m18n.n("migration_0021_patch_yunohost_conflicts")) + logger.info(m18n.n("migration_0027_patch_yunohost_conflicts")) self.patch_yunohost_conflicts() - # - # Specific tweaking to get rid of custom my.cnf and use debian's default one - # (my.cnf is actually a symlink to mariadb.cnf) - # - - _force_clear_hashes(["/etc/mysql/my.cnf"]) - rm("/etc/mysql/mariadb.cnf", force=True) - rm("/etc/mysql/my.cnf", force=True) - ret = self.apt_install( - "mariadb-common --reinstall -o Dpkg::Options::='--force-confmiss'" - ) - if ret != 0: - raise YunohostError("Failed to reinstall mariadb-common ?", raw_msg=True) - - # - # /usr/share/yunohost/yunohost-config/ssl/yunoCA -> /usr/share/yunohost/ssl - # - if os.path.exists("/usr/share/yunohost/yunohost-config/ssl/yunoCA"): - os.system( - "mv /usr/share/yunohost/yunohost-config/ssl/yunoCA /usr/share/yunohost/ssl" - ) - rm("/usr/share/yunohost/yunohost-config", recursive=True, force=True) - - # - # /home/yunohost.conf -> /var/cache/yunohost/regenconf - # - if os.path.exists("/home/yunohost.conf"): - os.system("mv /home/yunohost.conf /var/cache/yunohost/regenconf") - rm("/home/yunohost.conf", recursive=True, force=True) - - # Remove legacy postgresql service record added by helpers, - # will now be dynamically handled by the core in bookworm - services = _get_services() - if "postgresql" in services: - del services["postgresql"] - _save_services(services) - # # Critical fix for RPI otherwise network is down after rebooting # https://forum.yunohost.org/t/20652 # - if os.system("systemctl | grep -q dhcpcd") == 0: - logger.info("Applying fix for DHCPCD ...") - os.system("mkdir -p /etc/systemd/system/dhcpcd.service.d") - write_to_file( - "/etc/systemd/system/dhcpcd.service.d/wait.conf", - "[Service]\nExecStart=\nExecStart=/usr/sbin/dhcpcd -w", - ) + # FIXME : this is from buster->bullseye, do we still needed it ? + # + #if os.system("systemctl | grep -q dhcpcd") == 0: + # logger.info("Applying fix for DHCPCD ...") + # os.system("mkdir -p /etc/systemd/system/dhcpcd.service.d") + # write_to_file( + # "/etc/systemd/system/dhcpcd.service.d/wait.conf", + # "[Service]\nExecStart=\nExecStart=/usr/sbin/dhcpcd -w", + # ) # # Main upgrade # - logger.info(m18n.n("migration_0021_main_upgrade")) + logger.info(m18n.n("migration_0027_main_upgrade")) + # Mark php, mariadb, metronome and rspamd as "auto" so that they may be uninstalled if they ain't explicitly wanted by app or admins + php_packages = self.get_php_packages() + aptitude_with_progress_bar(f"markauto mariadb-server metronome rspamd {' '.join(php_packages)}") + + # Hold import yunohost packages apps_packages = self.get_apps_equivs_packages() - self.hold(apps_packages) - tools_upgrade(target="system", allow_yunohost_upgrade=False) + aptitude_with_progress_bar(f"hold yunohost moulinette ssowat yunohost-admin {' '.join(apps_packages)}") + + # Dirty hack to be able to remove rspamd because it's causing too many issues due to libluajit ... + command = "sed -i /var/lib/dpkg/status -e 's@rspamd, @@g'" + logger.debug(f"Running: {command}") + os.system(command) + + aptitude_with_progress_bar("upgrade cron rspamd- libluajit-5.1-2- --show-why -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'") + + aptitude_with_progress_bar("full-upgrade --show-why -o Dpkg::Options::='--force-confold'") + + # Force regenconf of nsswitch because for some reason + # /etc/nsswitch.conf is reset despite the --force-confold? It's a + # disaster because then admins cannot "sudo" >_> ... + regen_conf(names=["nsswitch"], force=True) if self.debian_major_version() == N_CURRENT_DEBIAN: - raise YunohostError("migration_0021_still_on_bullseye_after_main_upgrade") - - # Force explicit install of php8.2fpm and other old 'default' dependencies - # that are now only in Recommends - # - # Also, we need to install php8.2 equivalents of other php7.4 dependencies. - # For example, Nextcloud may depend on php7.3-zip, and after the php pool migration - # to autoupgrade Nextcloud to 8.2, it will need the php8.2-zip to work. - # The following list is based on an ad-hoc analysis of php deps found in the - # app ecosystem, with a known equivalent on php8.2. - # - # This is kinda a dirty hack as it doesnt properly update the *-ynh-deps virtual packages - # with the proper list of dependencies, and the dependencies install this way - # will get flagged as 'manually installed'. - # - # We'll probably want to do something during the Bookworm->Bookworm migration to re-flag - # these as 'auto' so they get autoremoved if not needed anymore. - # Also hopefully by then we'll have manifestv2 (maybe) and will be able to use - # the apt resource mecanism to regenerate the *-ynh-deps virtual packages ;) - - php74packages_suffixes = [ - "apcu", - "bcmath", - "bz2", - "dom", - "gmp", - "igbinary", - "imagick", - "imap", - "mbstring", - "memcached", - "mysqli", - "mysqlnd", - "pgsql", - "redis", - "simplexml", - "soap", - "sqlite3", - "ssh2", - "tidy", - "xml", - "xmlrpc", - "xsl", - "zip", - ] - - cmd = ( - "apt show '*-ynh-deps' 2>/dev/null" - " | grep Depends" - f" | grep -o -E \"php7.4-({'|'.join(php74packages_suffixes)})\"" - " | sort | uniq" - " | sed 's/php7.4/php8.2/g'" - " || true" - ) - - basephp82packages_to_install = [ - "php8.2-fpm", - "php8.2-common", - "php8.2-ldap", - "php8.2-intl", - "php8.2-mysql", - "php8.2-gd", - "php8.2-curl", - "php-php-gettext", - ] - - php74packages_to_install = basephp82packages_to_install + [ - f.strip() for f in check_output(cmd).split("\n") if f.strip() - ] - - ret = self.apt_install( - f"{' '.join(php74packages_to_install)} " - "$(dpkg --list | grep ynh-deps | awk '{print $2}') " - "-o Dpkg::Options::='--force-confmiss'" - ) - if ret != 0: - raise YunohostError( - "Failed to force the install of php dependencies ?", raw_msg=True - ) + raise YunohostError("migration_0027_still_on_bullseye_after_main_upgrade") # Clean the mess - logger.info(m18n.n("migration_0021_cleaning_up")) + logger.info(m18n.n("migration_0027_cleaning_up")) os.system( "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes" ) @@ -309,42 +208,56 @@ class MyMigration(Migration): # 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") + # FIXME : this is from buster->bullseye, do we still needed it ? + # + #if os.path.exists("/etc/init.d/dnsmasq.dpkg-dist"): + # logger.info("Copying new version for /etc/init.d/dnsmasq ...") + # os.system("cp /etc/init.d/dnsmasq.dpkg-dist /etc/init.d/dnsmasq") # # Yunohost upgrade # - logger.info(m18n.n("migration_0021_yunohost_upgrade")) + logger.info(m18n.n("migration_0027_yunohost_upgrade")) + aptitude_with_progress_bar("unhold yunohost moulinette ssowat yunohost-admin") - self.unhold(apps_packages) + try: + aptitude_with_progress_bar("full-upgrade --show-why yunohost yunohost-admin yunohost-portal moulinette ssowat python3.9- python3.9-venv- -o Dpkg::Options::='--force-confold'") + except Exception: + # Retry after unholding the app packages, maybe it can unlock the situation idk + if apps_packages: + aptitude_with_progress_bar(f"unhold {' '.join(apps_packages)}") + aptitude_with_progress_bar("full-upgrade --show-why yunohost yunohost-admin yunohost-portal moulinette ssowat python3.9- python3.9-venv- -o Dpkg::Options::='--force-confold'") + else: + # If the upgrade was sucessful, we want to unhold the apps packages + if apps_packages: + aptitude_with_progress_bar(f"unhold {' '.join(apps_packages)}") - cmd = "LC_ALL=C" - cmd += " DEBIAN_FRONTEND=noninteractive" - cmd += " APT_LISTCHANGES_FRONTEND=none" - cmd += " apt dist-upgrade " - cmd += " --quiet -o=Dpkg::Use-Pty=0 --fix-broken --dry-run" - cmd += " | grep -q 'ynh-deps'" + # Mark this migration as completed before triggering the "new" migrations + _write_migration_state(self.id, "done") - logger.info("Simulating upgrade...") - if os.system(cmd) == 0: - raise YunohostError( - "The upgrade cannot be completed, because some app dependencies would need to be removed?", - raw_msg=True, - ) + callbacks = ( + lambda l: logger.debug("+ " + l.rstrip() + "\r"), + lambda l: logger.warning(l.rstrip()), + ) + try: + call_async_output(["yunohost", "tools", "migrations", "run"], callbacks) + except Exception as e: + logger.error(e) - postupgradecmds = f"apt-mark auto {' '.join(basephp74packages_to_install)}\n" - postupgradecmds += "rm -f /usr/sbin/policy-rc.d\n" - postupgradecmds += "echo 'Restarting nginx...' >&2\n" - postupgradecmds += "systemctl restart nginx\n" - - tools_upgrade(target="system", postupgradecmds=postupgradecmds) + # If running from the webadmin, restart the API after a delay + if Moulinette.interface.type == "api": + logger.warning(m18n.n("migration_0027_delayed_api_restart")) + sleep(5) + # 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 + cmd = 'at -M now >/dev/null 2>&1 <<< "sleep 10; systemctl restart nginx yunohost-api"' + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... + subprocess.check_call(["bash", "-c", cmd]) def debian_major_version(self): # The python module "platform" and lsb_release are not reliable because # on some setup, they may still return Release=9 even after upgrading to - # bullseye ... (Apparently this is related to OVH overriding some stuff + # buster ... (Apparently this is related to OVH overriding some stuff # with /etc/lsb-release for instance -_-) # Instead, we rely on /etc/os-release which should be the raw info from # the distribution... @@ -358,10 +271,10 @@ class MyMigration(Migration): return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) def check_assertions(self): - # Be on bullseye (10.x) and yunohost 4.x + # Be on bullseye (11.x) and yunohost 11.x # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be > 9.x but yunohost package - # would still be in 3.x... + # in the middle and debian version could be > 12.x but yunohost package + # would still be in 11.x... if ( not self.debian_major_version() == N_CURRENT_DEBIAN and not self.yunohost_major_version() == N_CURRENT_YUNOHOST @@ -379,14 +292,13 @@ class MyMigration(Migration): # Yeah it's not that important ... it's to simplify support ... pass - raise YunohostError("migration_0021_not_bullseye2") + raise YunohostError("migration_0027_not_bullseye") # 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") + raise YunohostError("migration_0027_not_enough_free_space") # Have > 70 MB free space on /var/ ? - # FIXME: Create a way to ignore this check, on some system 70M is enough... if free_space_in_directory("/boot/") / (1024**2) < 70.0: raise YunohostError( "/boot/ has less than 70MB available. This will probably trigger a crash during the upgrade because a new kernel needs to be installed. Please look for advice on the forum on how to remove old, unused kernels to free up some space in /boot/.", @@ -394,7 +306,7 @@ class MyMigration(Migration): ) # Check system is up to date - # (but we don't if 'bookworm' is already in the sources.list ... + # (but we don't if 'bullseye' is already in the sources.list ... # which means maybe a previous upgrade crashed and we're re-running it) if os.path.exists("/etc/apt/sources.list") and " bookworm " not in read_file( "/etc/apt/sources.list" @@ -418,15 +330,15 @@ class MyMigration(Migration): ] ) if upgradable_system_packages - lime2_hold_packages: - raise YunohostError("migration_0021_system_not_fully_up_to_date") + raise YunohostError("migration_0027_system_not_fully_up_to_date") @property def disclaimer(self): # Avoid having a super long disclaimer + uncessary check if we ain't - # on bullseye / yunohost 4.x anymore + # on bullseye / yunohost 11.x # NB : we do both check to cover situations where the upgrade crashed - # in the middle and debian version could be >= 10.x but yunohost package - # would still be in 4.x... + # in the middle and debian version could be 12.x but yunohost package + # would still be in 11.x... if ( not self.debian_major_version() == N_CURRENT_DEBIAN and not self.yunohost_major_version() == N_CURRENT_YUNOHOST @@ -441,22 +353,24 @@ class MyMigration(Migration): modified_files = manually_modified_files() modified_files = "".join(["\n - " + f for f in modified_files]) - message = m18n.n("migration_0021_general_warning") + message = m18n.n("migration_0027_general_warning") message = ( - "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/20590\n\n" + "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/?? FIXME ?? \n\n" + message + + "\n\n" + + "Packages 'metronome' (xmpp server) and 'rspamd' (mail antispam) are now separate applications available in the catalog, and will be uninstalled during the upgrade. Make sure to explicitly install the corresponding new apps after the upgrade if you care about those!" ) if problematic_apps: message += "\n\n" + m18n.n( - "migration_0021_problematic_apps_warning", + "migration_0027_problematic_apps_warning", problematic_apps=problematic_apps, ) if modified_files: message += "\n\n" + m18n.n( - "migration_0021_modified_files", manually_modified_files=modified_files + "migration_0027_modified_files", manually_modified_files=modified_files ) return message @@ -467,17 +381,28 @@ class MyMigration(Migration): sources_list.append("/etc/apt/sources.list") # This : - # - replace single 'bullseye' occurence by 'bulleye' + # - replace single 'bullseye' occurence by 'bookworm' # - comments lines containing "backports" + # - replace 'bullseye/updates' by 'bookworm/updates' (or same with -) + # - make sure the yunohost line has the "signed-by" thingy + # - replace "non-free" with "non-free non-free-firmware" + # Special note about the security suite: + # https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#security-archive for f in sources_list: command = ( f"sed -i {f} " "-e 's@ bullseye @ bookworm @g' " "-e '/backports/ s@^#*@#@' " + "-e 's@ bullseye/updates @ bookworm-security @g' " "-e 's@ bullseye-@ bookworm-@g' " + "-e 's@ non-free@ non-free non-free-firmware@g' " + "-e 's@deb.*http://forge.yunohost.org@deb [signed-by=/usr/share/keyrings/yunohost-bookworm.gpg] http://forge.yunohost.org@g' " ) os.system(command) + # Stupid OVH has some repo configured which dont work with next debian and break apt ... + os.system("rm -f /etc/apt/sources.list.d/ovh-*.list") + def get_apps_equivs_packages(self): command = ( "dpkg --get-selections" @@ -490,35 +415,17 @@ class MyMigration(Migration): return output.split("\n") if output else [] - def hold(self, packages): - for package in packages: - os.system(f"apt-mark hold {package}") - - def unhold(self, packages): - for package in packages: - os.system(f"apt-mark unhold {package}") - - def apt_install(self, cmd): - def is_relevant(line): - return "Reading database ..." not in line.rstrip() - - callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip()), + def get_php_packages(self): + command = ( + "dpkg --get-selections" + " | grep -v deinstall" + " | awk '{print $1}'" + " | { grep '^php' || true; }" ) - cmd = ( - "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " - + cmd - ) + output = check_output(command) - logger.debug("Running: %s" % cmd) - - return call_async_output(cmd, callbacks, shell=True) + return output.split("\n") if output else [] def patch_yunohost_conflicts(self): # @@ -530,7 +437,7 @@ class MyMigration(Migration): # The hack consists in savagely removing the conflicts directly in /var/lib/dpkg/status # - # We only patch the conflict if we're on yunohost 4.x + # We only patch the conflict if we're on yunohost 11.x if self.yunohost_major_version() != N_CURRENT_YUNOHOST: return diff --git a/src/migrations/0028_delete_legacy_xmpp_permission.py b/src/migrations/0028_delete_legacy_xmpp_permission.py new file mode 100644 index 000000000..de5d2b983 --- /dev/null +++ b/src/migrations/0028_delete_legacy_xmpp_permission.py @@ -0,0 +1,32 @@ +from logging import getLogger + +from yunohost.tools import Migration + +logger = getLogger("yunohost.migration") + +################################################### +# Tools used also for restoration +################################################### + + +class MyMigration(Migration): + """ + Delete legacy XMPP permission + """ + + introduced_in_version = "12.0" + dependencies = [] + + ldap_migration_started = False + + @Migration.ldap_migration + def run(self, *args): + from yunohost.permission import user_permission_list, permission_delete + + self.ldap_migration_started = True + + if "xmpp.main" in user_permission_list()["permissions"]: + permission_delete("xmpp.main", force=True) + + def run_after_system_restore(self): + self.run() diff --git a/src/migrations/0023_postgresql_11_to_13.py b/src/migrations/0029_postgresql_13_to_15.py similarity index 70% rename from src/migrations/0023_postgresql_11_to_13.py rename to src/migrations/0029_postgresql_13_to_15.py index 6d37ffa74..f74d33a76 100644 --- a/src/migrations/0023_postgresql_11_to_13.py +++ b/src/migrations/0029_postgresql_13_to_15.py @@ -1,21 +1,21 @@ import subprocess import time import os +from logging import getLogger from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError -from moulinette.utils.log import getActionLogger from yunohost.tools import Migration from yunohost.utils.system import free_space_in_directory, space_used_by_directory -logger = getActionLogger("yunohost.migration") +logger = getLogger("yunohost.migration") class MyMigration(Migration): - "Migrate DBs from Postgresql 11 to 13 after migrating to Bullseye" + "Migrate DBs from Postgresql 13 to 15 after migrating to Bookworm" - dependencies = ["migrate_to_bullseye"] + dependencies = ["migrate_to_bookworm"] def run(self): if ( @@ -27,37 +27,37 @@ class MyMigration(Migration): 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")) + if not self.package_is_installed("postgresql-13"): + logger.warning(m18n.n("migration_0029_postgresql_13_not_installed")) return - if not self.package_is_installed("postgresql-13"): - raise YunohostValidationError("migration_0023_postgresql_13_not_installed") + if not self.package_is_installed("postgresql-15"): + raise YunohostValidationError("migration_0029_postgresql_15_not_installed") - # Make sure there's a 11 cluster + # Make sure there's a 13 cluster try: - self.runcmd("pg_lsclusters | grep -q '^11 '") + self.runcmd("pg_lsclusters | grep -q '^13 '") except Exception: logger.warning( - "It looks like there's not active 11 cluster, so probably don't need to run this migration" + "It looks like there's not active 13 cluster, so probably don't need to run this migration" ) return if not space_used_by_directory( - "/var/lib/postgresql/11" + "/var/lib/postgresql/13" ) > free_space_in_directory("/var/lib/postgresql"): raise YunohostValidationError( - "migration_0023_not_enough_space", path="/var/lib/postgresql/" + "migration_0029_not_enough_space", path="/var/lib/postgresql/" ) self.runcmd("systemctl stop postgresql") time.sleep(3) self.runcmd( - "LC_ALL=C pg_dropcluster --stop 13 main || true" - ) # We do not trigger an exception if the command fails because that probably means cluster 13 doesn't exists, which is fine because it's created during the pg_upgradecluster) + "LC_ALL=C pg_dropcluster --stop 15 main || true" + ) # We do not trigger an exception if the command fails because that probably means cluster 15 doesn't exists, which is fine because it's created during the pg_upgradecluster) time.sleep(3) - self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 11 main") - self.runcmd("LC_ALL=C pg_dropcluster --stop 11 main") + self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 13 main -v 15") + self.runcmd("LC_ALL=C pg_dropcluster --stop 13 main") self.runcmd("systemctl start postgresql") def package_is_installed(self, package_name): diff --git a/src/migrations/0024_rebuild_python_venv.py b/src/migrations/0030_rebuild_python_venv_in_bookworm.py similarity index 74% rename from src/migrations/0024_rebuild_python_venv.py rename to src/migrations/0030_rebuild_python_venv_in_bookworm.py index 01a229b87..944d72e0f 100644 --- a/src/migrations/0024_rebuild_python_venv.py +++ b/src/migrations/0030_rebuild_python_venv_in_bookworm.py @@ -1,16 +1,16 @@ import os +from logging import getLogger from moulinette import m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.process import call_async_output from yunohost.tools import Migration, tools_migrations_state from moulinette.utils.filesystem import rm -logger = getActionLogger("yunohost.migration") +logger = getLogger("yunohost.migration") -VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bullseye_upgrade.txt" +VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bookworm_upgrade.txt" def extract_app_from_venv_path(venv_path): @@ -56,28 +56,28 @@ class MyMigration(Migration): """ ignored_python_apps = [ - "calibreweb", - "django-for-runners", - "ffsync", - "jupiterlab", - "librephotos", - "mautrix", - "mediadrop", - "mopidy", - "pgadmin", - "tracim", - "synapse", - "matrix-synapse", - "weblate", + "diacamma", # Does an ugly sed in the sites-packages/django_auth_ldap3_ad + "kresus", # uses virtualenv instead of venv, with --system-site-packages (?) + "librephotos", # runs a setup.py ? not sure pip freeze / pip install -r requirements.txt is gonna be equivalent .. + "mautrix", # install stuff from a .tar.gz + "microblogpub", # uses poetry ? x_x + "mopidy", # applies a custom patch? + "motioneye", # install stuff from a .tar.gz + "pgadmin", # bunch of manual patches + "searxng", # uses --system-site-packages ? + "synapse", # specific stuff for ARM to prevent local compiling etc + "matrix-synapse", # synapse is actually installed in /opt/yunohost/matrix-synapse because ... yeah ... + "tracim", # pip install -e . + "weblate", # weblate settings are .. inside the venv T_T ] - dependencies = ["migrate_to_bullseye"] + dependencies = ["migrate_to_bookworm"] state = None def is_pending(self): if not self.state: self.state = tools_migrations_state()["migrations"].get( - "0024_rebuild_python_venv", "pending" + "0030_rebuild_python_venv_in_bookworm", "pending" ) return self.state == "pending" @@ -121,15 +121,15 @@ class MyMigration(Migration): else: rebuild_apps.append(app_corresponding_to_venv) - msg = m18n.n("migration_0024_rebuild_python_venv_disclaimer_base") + msg = m18n.n("migration_0030_rebuild_python_venv_in_bookworm_disclaimer_base") if rebuild_apps: msg += "\n\n" + m18n.n( - "migration_0024_rebuild_python_venv_disclaimer_rebuild", + "migration_0030_rebuild_python_venv_in_bookworm_disclaimer_rebuild", rebuild_apps="\n - " + "\n - ".join(rebuild_apps), ) if ignored_apps: msg += "\n\n" + m18n.n( - "migration_0024_rebuild_python_venv_disclaimer_ignored", + "migration_0030_rebuild_python_venv_in_bookworm_disclaimer_ignored", ignored_apps="\n - " + "\n - ".join(ignored_apps), ) @@ -151,7 +151,7 @@ class MyMigration(Migration): rm(venv + VENV_REQUIREMENTS_SUFFIX) logger.info( m18n.n( - "migration_0024_rebuild_python_venv_broken_app", + "migration_0030_rebuild_python_venv_in_bookworm_broken_app", app=app_corresponding_to_venv, ) ) @@ -159,7 +159,7 @@ class MyMigration(Migration): logger.info( m18n.n( - "migration_0024_rebuild_python_venv_in_progress", + "migration_0030_rebuild_python_venv_in_bookworm_in_progress", app=app_corresponding_to_venv, ) ) @@ -178,7 +178,7 @@ class MyMigration(Migration): if status != 0: logger.error( m18n.n( - "migration_0024_rebuild_python_venv_failed", + "migration_0030_rebuild_python_venv_in_bookworm_failed", app=app_corresponding_to_venv, ) ) diff --git a/src/permission.py b/src/permission.py index 72975561f..8fb7d3499 100644 --- a/src/permission.py +++ b/src/permission.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -20,15 +20,15 @@ import re import copy import grp import random +from logging import getLogger from moulinette import m18n -from moulinette.utils.log import getActionLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation -logger = getActionLogger("yunohost.user") +logger = getLogger("yunohost.user") -SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"] +SYSTEM_PERMS = ["mail", "sftp", "ssh"] # # @@ -170,7 +170,7 @@ def user_permission_update( existing_permission = user_permission_info(permission) - # Refuse to add "visitors" to mail, xmpp ... they require an account to make sense. + # Refuse to add "visitors" to mail ... they require an account to make sense. if add and "visitors" in add and permission.split(".")[0] in SYSTEM_PERMS: raise YunohostValidationError( "permission_require_account", permission=permission diff --git a/src/portal.py b/src/portal.py new file mode 100644 index 000000000..254ae589b --- /dev/null +++ b/src/portal.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2021 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" +import logging +from pathlib import Path +from typing import Any, Union + +import ldap +from moulinette.utils.filesystem import read_json +from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth, user_is_allowed_on_domain +from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract, LDAPInterface +from yunohost.utils.password import ( + assert_password_is_compatible, + assert_password_is_strong_enough, + _hash_user_password, +) + +logger = logging.getLogger("portal") + +PORTAL_SETTINGS_DIR = "/etc/yunohost/portal" +ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] + + +def _get_user_infos( + user_attrs: list[str], +) -> tuple[str, str, dict[str, Any]]: + auth = Auth().get_session_cookie() + username = auth["user"] + result = _get_ldap_interface().search("ou=users", f"uid={username}", user_attrs) + if not result: + raise YunohostValidationError("user_unknown", user=username) + + return username, auth["host"], result[0] + + +def _get_portal_settings( + domain: Union[str, None] = None, username: Union[str, None] = None +): + """ + Returns domain's portal settings which are a combo of domain's portal config panel options + and the list of apps availables on this domain computed by `app.app_ssowatconf()`. + """ + + if not domain: + from bottle import request + + domain = request.get_header("host") + + assert domain and "/" not in domain + + settings: dict[str, Any] = { + "apps": {}, + "public": False, + "portal_logo": "", + "portal_theme": "system", + "portal_title": "YunoHost", + "show_other_domains_apps": False, + "domain": domain, + } + + portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{domain}.json") + + if portal_settings_path.exists(): + settings.update(read_json(str(portal_settings_path))) + # Portal may be public (no login required) + settings["public"] = ( + settings.pop("default_app", None) == "_yunohost_portal_with_public_apps" + ) + + # First clear apps since it may contains private apps + apps: dict[str, Any] = settings.pop("apps", {}) + settings["apps"] = {} + + if settings["show_other_domains_apps"]: + # Enhanced apps with all other domain's apps + import glob + + for path in glob.glob(f"{PORTAL_SETTINGS_DIR}/*.json"): + if path != str(portal_settings_path): + apps.update(read_json(path)["apps"]) + + if username: + # Add user allowed or public apps + settings["apps"] = { + name: app + for name, app in apps.items() + if username in app["users"] or app["public"] + } + elif settings["public"]: + # Add public apps (e.g. with "visitors" in group permission) + settings["apps"] = {name: app for name, app in apps.items() if app["public"]} + + return settings + + +def portal_public(): + """Get public settings + If the portal is set as public, it will include the list of public apps + """ + + portal_settings = _get_portal_settings() + + if "portal_user_intro" in portal_settings: + del portal_settings["portal_user_intro"] + + # Prevent leaking the list of users + for infos in portal_settings["apps"].values(): + del infos["users"] + + return portal_settings + + +def portal_me(): + """ + Get user informations + """ + username, domain, user = _get_user_infos( + ["cn", "mail", "maildrop", "mailuserquota", "memberOf", "permission"] + ) + + groups = [_ldap_path_extract(g, "cn") for g in user["memberOf"]] + groups = [g for g in groups if g not in [username, "all_users"]] + # Get user allowed apps + apps = _get_portal_settings(domain, username)["apps"] + + # Prevent leaking the list of users + for infos in apps.values(): + del infos["users"] + + result_dict = { + "username": username, + "fullname": user["cn"][0], + "mail": user["mail"][0], + "mailalias": user["mail"][1:], + "mailforward": user["maildrop"][1:], + "groups": groups, + "apps": apps, + } + + # FIXME / TODO : add mail quota status ? + # result_dict["mailbox-quota"] = { + # "limit": userquota if is_limited else m18n.n("unlimit"), + # "use": storage_use, + # } + # Could use : doveadm -c /dev/null -f flow quota recalc -u johndoe + # But this requires to be in the mail group ... + + return result_dict + + +def portal_update( + fullname: Union[str, None] = None, + mailforward: Union[list[str], None] = None, + mailalias: Union[list[str], None] = None, + currentpassword: Union[str, None] = None, + newpassword: Union[str, None] = None, +): + from yunohost.domain import domain_list + + domains = domain_list()["domains"] + username, domain, current_user = _get_user_infos( + ["givenName", "sn", "cn", "mail", "maildrop", "memberOf"] + ) + new_attr_dict = {} + + if fullname is not None and fullname != current_user["cn"]: + fullname = fullname.strip() + firstname = fullname.split()[0] + lastname = ( + " ".join(fullname.split()[1:]) or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... + new_attr_dict["givenName"] = [firstname] # TODO: Validate + new_attr_dict["sn"] = [lastname] # TODO: Validate + new_attr_dict["cn"] = new_attr_dict["displayName"] = [ + (firstname + " " + lastname).strip() + ] + + if mailalias is not None: + mailalias = [mail.strip() for mail in mailalias if mail and mail.strip()] + # keep first current mail unaltered + mails = [current_user["mail"][0]] + + for index, mail in enumerate(mailalias): + if mail in current_user["mail"]: + if mail != current_user["mail"][0] and mail not in mails: + mails.append(mail) + continue # already in mails, skip validation + + local_part, domain = mail.split("@") + if local_part in ADMIN_ALIASES: + raise YunohostValidationError( + "mail_unavailable", path=f"mailalias[{index}]" + ) + + try: + _get_ldap_interface().validate_uniqueness({"mail": mail}) + except YunohostError: + raise YunohostValidationError( + "mail_already_exists", mail=mail, path=f"mailalias[{index}]" + ) + + if domain not in domains or not user_is_allowed_on_domain(username, domain): + raise YunohostValidationError( + "mail_alias_unauthorized", domain=domain + ) + + mails.append(mail) + + new_attr_dict["mail"] = mails + + if mailforward is not None: + new_attr_dict["maildrop"] = [current_user["maildrop"][0]] + [ + mail.strip() + for mail in mailforward + if mail and mail.strip() and mail != current_user["maildrop"][0] + ] + + if newpassword: + # Ensure compatibility and sufficiently complex password + try: + assert_password_is_compatible(newpassword) + is_admin = ( + "cn=admins,ou=groups,dc=yunohost,dc=org" in current_user["memberOf"] + ) + assert_password_is_strong_enough( + "admin" if is_admin else "user", newpassword + ) + except YunohostValidationError as e: + raise YunohostValidationError(e.key, path="newpassword") + + new_attr_dict["userPassword"] = [_hash_user_password(newpassword)] + + # Check that current password is valid + # To be able to edit the user info, an authenticated ldap session is needed + if newpassword: + # When setting the password, check the user provided the valid current password + try: + ldap_interface = LDAPInterface(username, currentpassword) + except ldap.INVALID_CREDENTIALS: + raise YunohostValidationError("invalid_password", path="currentpassword") + else: + # Otherwise we use the encrypted password stored in the cookie + ldap_interface = LDAPInterface(username, Auth().get_session_cookie(decrypt_pwd=True)["pwd"]) + + try: + ldap_interface.update(f"uid={username},ou=users", new_attr_dict) + except Exception as e: + raise YunohostError("user_update_failed", user=username, error=e) + finally: + del ldap_interface + + if "userPassword" in new_attr_dict: + Auth.invalidate_all_sessions_for_user(username) + + # FIXME: Here we could want to trigger "post_user_update" hook but hooks has to + # be run as root + if all(field is not None for field in (fullname, mailalias, mailforward)): + return { + "fullname": new_attr_dict["cn"][0], + "mailalias": new_attr_dict["mail"][1:], + "mailforward": new_attr_dict["maildrop"][1:], + } + else: + return {} diff --git a/src/regenconf.py b/src/regenconf.py index 74bbdb17c..5b9f117c6 100644 --- a/src/regenconf.py +++ b/src/regenconf.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -20,12 +20,13 @@ import os import yaml import shutil import hashlib - +import json +from logging import getLogger from difflib import unified_diff from datetime import datetime from moulinette import m18n -from moulinette.utils import log, filesystem +from moulinette.utils.filesystem import mkdir from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError @@ -37,7 +38,7 @@ BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, "backup") PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, "pending") REGEN_CONF_FILE = "/etc/yunohost/regenconf.yml" -logger = log.getActionLogger("yunohost.regenconf") +logger = getLogger("yunohost.regenconf") # FIXME : those ain't just services anymore ... what are we supposed to do with this ... @@ -63,6 +64,8 @@ def regen_conf( """ + from yunohost.settings import settings_get + if names is None: names = [] @@ -102,7 +105,7 @@ def regen_conf( for name in names: shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True) else: - filesystem.mkdir(PENDING_CONF_DIR, 0o755, True) + mkdir(PENDING_CONF_DIR, 0o755, True) # Execute hooks for pre-regen # element 2 and 3 with empty string is because of legacy... @@ -111,7 +114,7 @@ def regen_conf( def _pre_call(name, priority, path, args): # create the pending conf directory for the category category_pending_path = os.path.join(PENDING_CONF_DIR, name) - filesystem.mkdir(category_pending_path, 0o755, True, uid="root") + mkdir(category_pending_path, 0o755, True, uid="root") # return the arguments to pass to the script return pre_args + [ @@ -140,6 +143,10 @@ def regen_conf( domain_list(exclude_subdomains=True)["domains"] ) env["YNH_CONTEXT"] = "regenconf" + # perf: Export all global settings as a environment variable + # so that scripts dont have to call 'yunohost settings get' manually + # which is painful performance-wise + env["YNH_SETTINGS"] = json.dumps(settings_get("", export=True)) pre_result = hook_callback("conf_regen", names, pre_callback=_pre_call, env=env) @@ -622,7 +629,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): backup_dir = os.path.dirname(backup_path) if not os.path.isdir(backup_dir): - filesystem.mkdir(backup_dir, 0o755, True) + mkdir(backup_dir, 0o755, True) shutil.copy2(system_conf, backup_path) logger.debug( @@ -637,7 +644,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): system_dir = os.path.dirname(system_conf) if not os.path.isdir(system_dir): - filesystem.mkdir(system_dir, 0o755, True) + mkdir(system_dir, 0o755, True) shutil.copyfile(new_conf, system_conf) logger.debug(m18n.n("regenconf_file_updated", conf=system_conf)) diff --git a/src/service.py b/src/service.py index 47bc1903a..d53d01108 100644 --- a/src/service.py +++ b/src/service.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -21,14 +21,14 @@ import os import time import yaml import subprocess - +from logging import getLogger from glob import glob from datetime import datetime from moulinette import m18n +from yunohost.diagnosis import diagnosis_ignore, diagnosis_unignore from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.process import check_output -from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import ( read_file, append_to_file, @@ -42,7 +42,7 @@ MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" SERVICES_CONF = "/etc/yunohost/services.yml" SERVICES_CONF_BASE = "/usr/share/yunohost/conf/yunohost/services.yml" -logger = getActionLogger("yunohost.service") +logger = getLogger("yunohost.service") def service_add( @@ -296,6 +296,7 @@ def service_enable(names): names = [names] for name in names: if _run_service_command("enable", name): + diagnosis_unignore(["services", f"service={name}"]) logger.success(m18n.n("service_enabled", service=name)) else: raise YunohostError( @@ -315,6 +316,7 @@ def service_disable(names): names = [names] for name in names: if _run_service_command("disable", name): + diagnosis_ignore(["services", f"service={name}"]) logger.success(m18n.n("service_disabled", service=name)) else: raise YunohostError( @@ -688,13 +690,20 @@ def _get_services(): ] for name in services_with_package_condition: package = services[name]["ignore_if_package_is_not_installed"] - if os.system(f"dpkg --list | grep -q 'ii *{package}'") != 0: + if ( + check_output( + f"dpkg-query --show --showformat='${{db:Status-Status}}' '{package}' 2>/dev/null || true" + ) + != "installed" + ): del services[name] php_fpm_versions = check_output( - r"dpkg --list | grep -P 'ii php\d.\d-fpm' | awk '{print $2}' | grep -o -P '\d.\d' || true" + r"dpkg --list | grep -P 'ii php\d.\d-fpm' | awk '{print $2}' | grep -o -P '\d.\d' || true", + cwd="/tmp", ) php_fpm_versions = [v for v in php_fpm_versions.split("\n") if v.strip()] + for version in php_fpm_versions: # Skip php 7.3 which is most likely dead after buster->bullseye migration # because users get spooked @@ -706,10 +715,6 @@ 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. diff --git a/src/settings.py b/src/settings.py index 6690ab3fd..441b39d93 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -18,18 +18,33 @@ # import os import subprocess +from logging import getLogger +from typing import TYPE_CHECKING, Any, Union from moulinette import m18n from yunohost.utils.error import YunohostError, YunohostValidationError -from yunohost.utils.configpanel import ConfigPanel +from yunohost.utils.configpanel import ConfigPanel, parse_filter_key from yunohost.utils.form import BaseOption -from 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") +if TYPE_CHECKING: + from typing import cast + + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + + from moulinette.utils.log import MoulinetteLogger + from yunohost.log import OperationLogger + from yunohost.utils.configpanel import ( + ConfigPanelGetMode, + RawSettings, + ) + from yunohost.utils.form import FormModel + + logger = cast(MoulinetteLogger, getLogger("yunohost.settings")) +else: + logger = getLogger("yunohost.settings") SETTINGS_PATH = "/etc/yunohost/settings.yml" @@ -55,7 +70,6 @@ def settings_get(key="", full=False, export=False): mode = "classic" settings = SettingsConfigPanel() - key = translate_legacy_settings_to_configpanel_settings(key) return settings.get(key, mode) @@ -84,7 +98,6 @@ def settings_set(operation_logger, key=None, value=None, args=None, args_file=No """ 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) @@ -99,7 +112,6 @@ def settings_reset(operation_logger, key): """ settings = SettingsConfigPanel() - key = translate_legacy_settings_to_configpanel_settings(key) return settings.reset(key, operation_logger=operation_logger) @@ -120,47 +132,49 @@ class SettingsConfigPanel(ConfigPanel): entity_type = "global" save_path_tpl = SETTINGS_PATH save_mode = "diff" - virtual_settings = ["root_password", "root_password_confirm", "passwordless_sudo"] + virtual_settings = {"root_password", "root_password_confirm", "passwordless_sudo"} - def __init__(self, config_path=None, save_path=None, creation=False): + def __init__(self, config_path=None, save_path=None, creation=False) -> None: super().__init__("settings") - def get(self, key="", mode="classic"): + def get( + self, key: Union[str, None] = None, mode: "ConfigPanelGetMode" = "classic" + ) -> Any: result = super().get(key=key, mode=mode) - if mode == "full": - for panel, section, option in self._iterate(): - if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): - option["help"] = m18n.n( - self.config["i18n"] + "_" + option["id"] + "_help" - ) - return self.config - # Dirty hack to let settings_get() to work from a python script if isinstance(result, str) and result in ["True", "False"]: result = bool(result == "True") return result - def reset(self, key="", operation_logger=None): - self.filter_key = key + def reset( + self, + key: Union[str, None] = None, + operation_logger: Union["OperationLogger", None] = None, + ) -> None: + self.filter_key = parse_filter_key(key) # Read config panel toml - self._get_config_panel() + self.config, self.form = self._get_config_panel(prevalidate=True) - if not self.config: - raise YunohostValidationError("config_no_panel") + # FIXME find a better way to exclude previous settings + previous_settings = self.form.dict() - # Replace all values with default values - self.values = self._get_default_values() + for option in self.config.options: + if not option.readonly and ( + option.optional or option.default not in {None, ""} + ): + # FIXME Mypy complains about option.default not being a valid type for normalize but this should be ok + self.form[option.id] = option.normalize(option.default, option) # type: ignore - BaseOption.operation_logger = operation_logger + # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) + # BaseOption.operation_logger = operation_logger if operation_logger: operation_logger.start() - try: - self._apply() + self._apply(self.form, previous_settings) except YunohostError: raise # Script got manually interrupted ... @@ -178,53 +192,40 @@ class SettingsConfigPanel(ConfigPanel): raise logger.success(m18n.n("global_settings_reset_success")) - operation_logger.success() - def _get_raw_config(self): - toml = super()._get_raw_config() + if operation_logger: + operation_logger.success() - # 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 - - return toml - - def _get_raw_settings(self): - super()._get_raw_settings() + def _get_raw_settings(self) -> "RawSettings": + raw_settings = 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"] = "" + raw_settings["root_password"] = "" + raw_settings["root_password_confirm"] = "" # Specific logic for virtual setting "passwordless_sudo" try: from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() - self.values["passwordless_sudo"] = "!authenticate" in ldap.search( + raw_settings["passwordless_sudo"] = "!authenticate" in ldap.search( "ou=sudo", "cn=admins", ["sudoOption"] )[0].get("sudoOption", []) except Exception: - self.values["passwordless_sudo"] = False + raw_settings["passwordless_sudo"] = False - 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) + return raw_settings - self.values = { - k: v for k, v in self.values.items() if k not in self.virtual_settings - } - self.new_values = { - k: v for k, v in self.new_values.items() if k not in self.virtual_settings - } - - assert all(v not in self.future_values for v in self.virtual_settings) + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ) -> None: + root_password = form.get("root_password", None) + root_password_confirm = form.get("root_password_confirm", None) + passwordless_sudo = form.get("passwordless_sudo", None) if root_password and root_password.strip(): if root_password != root_password_confirm: @@ -243,15 +244,20 @@ class SettingsConfigPanel(ConfigPanel): {"sudoOption": ["!authenticate"] if passwordless_sudo else []}, ) - super()._apply() - - settings = { - k: v for k, v in self.future_values.items() if self.values.get(k) != v + # First save settings except virtual + default ones + super()._apply(form, previous_settings, exclude=self.virtual_settings) + next_settings = { + k: v + for k, v in form.dict(exclude=self.virtual_settings).items() + if previous_settings.get(k) != v } - for setting_name, value in settings.items(): + + for setting_name, value in next_settings.items(): try: + # FIXME not sure to understand why we need the previous value if + # updated_settings has already been filtered trigger_post_change_hook( - setting_name, self.values.get(setting_name), value + setting_name, previous_settings.get(setting_name), value ) except Exception as e: logger.error(f"Post-change hook for setting failed : {e}") @@ -292,15 +298,6 @@ def trigger_post_change_hook(setting_name, old_value, new_value): # =========================================== -@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") @@ -343,12 +340,12 @@ def reconfigure_postfix(setting_name, old_value, new_value): @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 is True: + # Depending on how consistent the config panel is, it may spit 1 or True or ..? ... + if new_value: command = [ "apt-get", "-y", @@ -356,7 +353,7 @@ def reconfigure_dovecot(setting_name, old_value, new_value): "-o Dpkg::Options::=--force-confdef", "-o Dpkg::Options::=--force-confold", "install", - dovecot_package, + "dovecot-pop3d", ] subprocess.call(command, env=environment) if old_value != new_value: @@ -364,5 +361,5 @@ def reconfigure_dovecot(setting_name, old_value, new_value): else: if old_value != new_value: regen_conf(names=["dovecot"]) - command = ["apt-get", "-y", "remove", dovecot_package] + command = ["apt-get", "-y", "remove", "dovecot-pop3d"] subprocess.call(command, env=environment) diff --git a/src/ssh.py b/src/ssh.py index 8526e278f..ae09b7117 100644 --- a/src/ssh.py +++ b/src/ssh.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -172,7 +172,7 @@ def _get_user_for_ssh(username, attrs=None): "username": "root", "fullname": "", "mail": "", - "home_path": root_unix.pw_dir, + "homeDirectory": root_unix.pw_dir, } # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html diff --git a/src/tests/test_app_catalog.py b/src/tests/test_app_catalog.py index 40daf5873..f436dd877 100644 --- a/src/tests/test_app_catalog.py +++ b/src/tests/test_app_catalog.py @@ -12,7 +12,6 @@ from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml from yunohost.utils.error import YunohostError from yunohost.app_catalog import ( - _initialize_apps_catalog_system, _read_apps_catalog_list, _update_apps_catalog, _actual_apps_catalog_api_url, @@ -66,44 +65,17 @@ 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 - assert not os.path.exists(APPS_CATALOG_CONF) - - # Initialize ... - mocker.spy(m18n, "n") - _initialize_apps_catalog_system() - m18n.n.assert_any_call("apps_catalog_init_success") - - # And a conf with at least one list - assert os.path.exists(APPS_CATALOG_CONF) - apps_catalog_list = _read_apps_catalog_list() - assert len(apps_catalog_list) - - # Cache is expected to still be empty though - # (if we did update the apps_catalog during init, - # we couldn't differentiate easily exceptions - # related to lack of network connectivity) - assert not glob.glob(APPS_CATALOG_CACHE + "/*") - - def test_apps_catalog_emptylist(): - # Initialize ... - _initialize_apps_catalog_system() # Let's imagine somebody removed the default apps catalog because uh idk they dont want to use our default apps catalog os.system("rm %s" % APPS_CATALOG_CONF) os.system("touch %s" % APPS_CATALOG_CONF) apps_catalog_list = _read_apps_catalog_list() - assert not len(apps_catalog_list) + assert len(apps_catalog_list) == 0 def test_apps_catalog_update_nominal(mocker): - # Initialize ... - _initialize_apps_catalog_system() # Cache is empty assert not glob.glob(APPS_CATALOG_CACHE + "/*") @@ -135,8 +107,6 @@ 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 @@ -149,8 +119,6 @@ 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 @@ -165,8 +133,6 @@ 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 @@ -181,8 +147,6 @@ 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 @@ -197,8 +161,6 @@ def test_apps_catalog_update_corrupted(mocker): def test_apps_catalog_load_with_empty_cache(mocker): - # Initialize ... - _initialize_apps_catalog_system() # Cache is empty assert not glob.glob(APPS_CATALOG_CACHE + "/*") @@ -223,8 +185,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() conf = [ {"id": "default", "url": APPS_CATALOG_DEFAULT_URL}, @@ -261,8 +221,6 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker): def test_apps_catalog_load_with_outdated_api_version(): - # Initialize ... - _initialize_apps_catalog_system() # Update with requests_mock.Mocker() as m: diff --git a/src/tests/test_app_config.py b/src/tests/test_app_config.py index 4a74cbc0d..24abdc5dc 100644 --- a/src/tests/test_app_config.py +++ b/src/tests/test_app_config.py @@ -125,9 +125,9 @@ def test_app_config_get_nonexistentstuff(config_app): with pytest.raises(YunohostValidationError): app_config_get(config_app, "main.components.nonexistent") - app_setting(config_app, "boolean", delete=True) + app_setting(config_app, "number", delete=True) with pytest.raises(YunohostError): - app_config_get(config_app, "main.components.boolean") + app_config_get(config_app, "main.components.number") def test_app_config_regular_setting(config_app): diff --git a/src/tests/test_app_resources.py b/src/tests/test_app_resources.py index d2df647a3..d39c920d7 100644 --- a/src/tests/test_app_resources.py +++ b/src/tests/test_app_resources.py @@ -44,6 +44,7 @@ def setup_function(function): 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") + os.system("echo 'description.en = \"A dummy app to test app resources\"' >> /etc/yunohost/apps/testapp/manifest.toml") def teardown_function(function): @@ -55,7 +56,7 @@ def clean(): 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("apt remove lolcat sl nyancat influxdb2 >/dev/null 2>/dev/null") os.system("userdel testapp 2>/dev/null") for p in user_permission_list()["permissions"]: @@ -294,17 +295,17 @@ def test_resource_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", + "influxdb": { + "repo": "deb https://repos.influxdata.com/debian stable main", + "key": "https://repos.influxdata.com/influxdata-archive_compat.key", + "packages": "influxdb2", } }, } 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 *influxdb2 '") != 0 assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0 @@ -312,7 +313,7 @@ def test_resource_apt(): 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 *influxdb2 '") == 0 assert ( os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 ) # Lolcat shouldnt be installed yet @@ -323,7 +324,7 @@ def test_resource_apt(): 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 *influxdb2 '") == 0 assert os.system("dpkg --list | grep -q 'ii *lolcat '") == 0 assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") == 0 @@ -331,7 +332,7 @@ def test_resource_apt(): 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 *influxdb2 '") != 0 assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0 assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0 @@ -347,7 +348,7 @@ def test_resource_permissions(): conf = { "main": { "url": "/", - "allowed": "visitors" + "allowed": "visitors", # TODO: test protected? }, } diff --git a/src/tests/test_apps.py b/src/tests/test_apps.py index a0c431531..ede986b10 100644 --- a/src/tests/test_apps.py +++ b/src/tests/test_apps.py @@ -112,7 +112,7 @@ def app_expected_files(domain, app): if app.startswith("legacy_app"): yield "/var/www/%s/index.html" % app yield "/etc/yunohost/apps/%s/settings.yml" % app - if "manifestv2" in app: + if "manifestv2" in app or "my_webapp" in app: yield "/etc/yunohost/apps/%s/manifest.toml" % app else: yield "/etc/yunohost/apps/%s/manifest.json" % app @@ -330,7 +330,7 @@ def test_app_from_catalog(): app_install( "my_webapp", - args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0", + args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&init_main_permission=visitors&with_mysql=0&phpversion=none", ) app_map_ = app_map(raw=True) assert main_domain in app_map_ @@ -339,7 +339,9 @@ def test_app_from_catalog(): assert app_map_[main_domain]["/site"]["id"] == "my_webapp" assert app_is_installed(main_domain, "my_webapp") - assert app_is_exposed_on_http(main_domain, "/site", "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") diff --git a/src/tests/test_appurl.py b/src/tests/test_appurl.py index 8e5b14d34..8d1c43f15 100644 --- a/src/tests/test_appurl.py +++ b/src/tests/test_appurl.py @@ -69,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") diff --git a/src/tests/test_backuprestore.py b/src/tests/test_backuprestore.py index a2dcfe8fb..3f30a5492 100644 --- a/src/tests/test_backuprestore.py +++ b/src/tests/test_backuprestore.py @@ -49,13 +49,13 @@ def setup_function(function): for m in function.__dict__.get("pytestmark", []) } - if "with_wordpress_archive_from_4p2" in markers: - add_archive_wordpress_from_4p2() + if "with_wordpress_archive_from_11p2" in markers: + add_archive_wordpress_from_11p2() assert len(backup_list()["archives"]) == 1 if "with_legacy_app_installed" in markers: 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: @@ -72,8 +72,8 @@ def setup_function(function): ) assert app_is_installed("backup_recommended_app") - if "with_system_archive_from_4p2" in markers: - add_archive_system_from_4p2() + if "with_system_archive_from_11p2" in markers: + add_archive_system_from_11p2() assert len(backup_list()["archives"]) == 1 if "with_permission_app_installed" in markers: @@ -148,7 +148,7 @@ def app_is_installed(app): def backup_test_dependencies_are_met(): # Dummy test apps (or backup archives) assert os.path.exists( - os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2") + os.path.join(get_test_apps_dir(), "backup_wordpress_from_11p2") ) assert os.path.exists(os.path.join(get_test_apps_dir(), "legacy_app_ynh")) assert os.path.exists( @@ -211,23 +211,23 @@ def install_app(app, path, additionnal_args=""): ) -def add_archive_wordpress_from_4p2(): +def add_archive_wordpress_from_11p2(): os.system("mkdir -p /home/yunohost.backup/archives") os.system( "cp " - + os.path.join(get_test_apps_dir(), "backup_wordpress_from_4p2/backup.tar") - + " /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar" + + os.path.join(get_test_apps_dir(), "backup_wordpress_from_11p2/backup.tar") + + " /home/yunohost.backup/archives/backup_wordpress_from_11p2.tar" ) -def add_archive_system_from_4p2(): +def add_archive_system_from_11p2(): os.system("mkdir -p /home/yunohost.backup/archives") os.system( "cp " - + os.path.join(get_test_apps_dir(), "backup_system_from_4p2/backup.tar") - + " /home/yunohost.backup/archives/backup_system_from_4p2.tar" + + os.path.join(get_test_apps_dir(), "backup_system_from_11p2/backup.tar") + + " /home/yunohost.backup/archives/backup_system_from_11p2.tar" ) @@ -292,12 +292,12 @@ def test_backup_and_restore_all_sys(): # -# System restore from 3.8 # +# System restore from 11.2 # # -@pytest.mark.with_system_archive_from_4p2 -def test_restore_system_from_Ynh4p2(monkeypatch): +@pytest.mark.with_system_archive_from_11p2 +def test_restore_system_from_Ynh11p2(monkeypatch): name = random_ascii(8) # Backup current system with message("backup_created", name=name): @@ -305,7 +305,7 @@ def test_restore_system_from_Ynh4p2(monkeypatch): archives = backup_list()["archives"] assert len(archives) == 2 - # Restore system archive from 3.8 + # Restore system archive from 11.2 try: with message("restore_complete"): backup_restore( @@ -439,23 +439,24 @@ def test_backup_using_copy_method(): # App restore # # - -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") -def test_restore_app_wordpress_from_Ynh4p2(): +def test_restore_app_wordpress_from_Ynh11p2(): with message("restore_complete"): backup_restore( system=None, name=backup_list()["archives"][0], apps=["wordpress"] ) -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_script_failure_handling(monkeypatch, mocker): def custom_hook_exec(name, *args, **kwargs): if os.path.basename(name).startswith("restore"): monkeypatch.undo() return (1, None) + else: + return (0, {}) monkeypatch.setattr("yunohost.hook.hook_exec", custom_hook_exec) @@ -470,7 +471,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): assert not _is_installed("wordpress") -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 def test_restore_app_not_enough_free_space(monkeypatch, mocker): def custom_free_space_in_directory(dirpath): return 0 @@ -489,7 +490,7 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): assert not _is_installed("wordpress") -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 def test_restore_app_not_in_backup(mocker): assert not _is_installed("wordpress") assert not _is_installed("yoloswag") @@ -504,7 +505,7 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("yoloswag") -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_already_installed(mocker): assert not _is_installed("wordpress") @@ -616,17 +617,17 @@ def test_restore_archive_with_no_json(mocker): backup_restore(name="badbackup", force=True) -@pytest.mark.with_wordpress_archive_from_4p2 +@pytest.mark.with_wordpress_archive_from_11p2 def test_restore_archive_with_bad_archive(mocker): # Break the archive os.system( - "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_4p2.tar > /home/yunohost.backup/archives/backup_wordpress_from_4p2_bad.tar" + "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_11p2.tar > /home/yunohost.backup/archives/backup_wordpress_from_11p2_bad.tar" ) - assert "backup_wordpress_from_4p2_bad" in backup_list()["archives"] + assert "backup_wordpress_from_11p2_bad" in backup_list()["archives"] with raiseYunohostError(mocker, "backup_archive_corrupted"): - backup_restore(name="backup_wordpress_from_4p2_bad", force=True) + backup_restore(name="backup_wordpress_from_11p2_bad", force=True) clean_tmp_backup_directory() diff --git a/src/tests/test_dns.py b/src/tests/test_dns.py index e896d9c9f..744e3e789 100644 --- a/src/tests/test_dns.py +++ b/src/tests/test_dns.py @@ -49,19 +49,19 @@ def test_registrar_list_integrity(): def test_magic_guess_registrar_weird_domain(): - assert _get_registrar_config_section("yolo.tld")["registrar"]["value"] is None + assert _get_registrar_config_section("yolo.tld")["registrar"]["default"] is None def test_magic_guess_registrar_ovh(): assert ( - _get_registrar_config_section("yolo.yunohost.org")["registrar"]["value"] + _get_registrar_config_section("yolo.yunohost.org")["registrar"]["default"] == "ovh" ) def test_magic_guess_registrar_yunodyndns(): assert ( - _get_registrar_config_section("yolo.nohost.me")["registrar"]["value"] + _get_registrar_config_section("yolo.nohost.me")["registrar"]["default"] == "yunohost" ) diff --git a/src/tests/test_domains.py b/src/tests/test_domains.py index b414c21d8..273b8689a 100644 --- a/src/tests/test_domains.py +++ b/src/tests/test_domains.py @@ -1,6 +1,10 @@ import pytest import os +import random +from mock import patch + +from moulinette import Moulinette from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError, YunohostValidationError @@ -16,6 +20,12 @@ from yunohost.domain import ( ) TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"] +TEST_DYNDNS_DOMAIN = ( + "ci-test-" + + "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(12)) + + random.choice([".noho.st", ".ynh.fr", ".nohost.me"]) +) +TEST_DYNDNS_PASSWORD = "astrongandcomplicatedpassphrasethatisverysecure" def setup_function(function): @@ -34,7 +44,9 @@ def setup_function(function): # Clear other domains for domain in domains: - if domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]: + if ( + domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2] + ) and domain != TEST_DYNDNS_DOMAIN: # Clean domains not used for testing domain_remove(domain) elif domain in TEST_DOMAINS: @@ -65,6 +77,46 @@ def test_domain_add(): assert TEST_DOMAINS[2] in domain_list()["domains"] +def test_domain_add_and_remove_dyndns(): + # Devs: if you get `too_many_request` errors, ask the team to add your IP to the rate limit excempt + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + + +def test_domain_dyndns_recovery(): + # Devs: if you get `too_many_request` errors, ask the team to add your IP to the rate limit excempt + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # mocked as API call to avoid CLI prompts + with patch.object(Moulinette.interface, "type", "api"): + # add domain without recovery password + domain_add(TEST_DYNDNS_DOMAIN) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + # set the recovery password with config panel + domain_config_set( + TEST_DYNDNS_DOMAIN, "dns.registrar.recovery_password", TEST_DYNDNS_PASSWORD + ) + # remove domain without unsubscribing + domain_remove(TEST_DYNDNS_DOMAIN, ignore_dyndns=True) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # readding domain with bad password should fail + with pytest.raises(YunohostValidationError): + domain_add( + TEST_DYNDNS_DOMAIN, + dyndns_recovery_password="wrong" + TEST_DYNDNS_PASSWORD, + ) + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + # readding domain with password should work + domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + assert TEST_DYNDNS_DOMAIN in domain_list()["domains"] + # remove the dyndns domain + domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD) + + assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"] + + def test_domain_add_existing_domain(): with pytest.raises(MoulinetteError): assert TEST_DOMAINS[1] in domain_list()["domains"] @@ -95,21 +147,19 @@ def test_change_main_domain(): # Domain settings testing def test_domain_config_get_default(): - assert domain_config_get(TEST_DOMAINS[0], "feature.xmpp.xmpp") == 1 - assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 0 + assert domain_config_get(TEST_DOMAINS[0], "feature.mail.mail_out") == 1 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 + assert domain_config_get(TEST_DOMAINS[0], export=True)["mail_out"] == 1 def test_domain_config_set(): - assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 0 - domain_config_set(TEST_DOMAINS[1], "feature.xmpp.xmpp", "yes") - assert domain_config_get(TEST_DOMAINS[1], "feature.xmpp.xmpp") == 1 + assert domain_config_get(TEST_DOMAINS[1], "feature.mail.mail_out") == 1 + domain_config_set(TEST_DOMAINS[1], "feature.mail.mail_out", "no") + assert domain_config_get(TEST_DOMAINS[1], "feature.mail.mail_out") == 0 def test_domain_configs_unknown(): with pytest.raises(YunohostError): - domain_config_get(TEST_DOMAINS[2], "feature.xmpp.xmpp.xmpp") + domain_config_get(TEST_DOMAINS[2], "feature.foo.bar.baz") diff --git a/src/tests/test_permission.py b/src/tests/test_permission.py index 8620e9611..17a1993ff 100644 --- a/src/tests/test_permission.py +++ b/src/tests/test_permission.py @@ -158,7 +158,7 @@ def setup_function(function): socket.getaddrinfo = new_getaddrinfo - user_create("alice", maindomain, dummy_password, fullname="Alice White") + user_create("alice", maindomain, dummy_password, fullname="Alice White", admin=True) user_create("bob", maindomain, dummy_password, fullname="Bob Snow") _permission_create_with_dummy_app( permission="wiki.main", @@ -338,7 +338,7 @@ def check_LDAP_db_integrity(): def check_permission_for_apps(): # We check that the for each installed apps we have at last the "main" permission # and we don't have any permission linked to no apps. The only exception who is not liked to an app - # is mail, xmpp, and sftp + # is mail, and sftp app_perms = user_permission_list(ignore_system_perms=True)["permissions"].keys() @@ -355,7 +355,7 @@ def check_permission_for_apps(): def can_access_webpage(webpath, logged_as=None): webpath = webpath.rstrip("/") - sso_url = "https://" + maindomain + "/yunohost/sso/" + login_endpoint = f"https://{maindomain}/yunohost/portalapi/login" # Anonymous access if not logged_as: @@ -363,12 +363,11 @@ def can_access_webpage(webpath, logged_as=None): # Login as a user using dummy password else: with requests.Session() as session: - session.post( - sso_url, - data={"user": logged_as, "password": dummy_password}, + r = session.post( + login_endpoint, + data={"credentials": f"{logged_as}:{dummy_password}"}, headers={ - "Referer": sso_url, - "Content-Type": "application/x-www-form-urlencoded", + "X-Requested-With": "", }, verify=False, ) @@ -377,6 +376,15 @@ def can_access_webpage(webpath, logged_as=None): r = session.get(webpath, verify=False) # If we can't access it, we got redirected to the SSO + # with `r=` for anonymous access because they're encouraged to log-in, + # and `msg=access_denied` if we are logged but not allowed for this url + # with `r= + sso_url = f"https://{maindomain}/yunohost/sso/" + if not logged_as: + sso_url += "?r=" + else: + sso_url += "?msg=access_denied" + return not r.url.startswith(sso_url) @@ -389,7 +397,6 @@ def test_permission_list(): res = user_permission_list(full=True)["permissions"] assert "mail.main" in res - assert "xmpp.main" in res assert "wiki.main" in res assert "blog.main" in res @@ -607,7 +614,6 @@ def test_permission_delete_doesnt_existing(mocker): assert "wiki.main" in res assert "blog.main" in res assert "mail.main" in res - assert "xmpp.main" in res def test_permission_delete_main_without_force(mocker): @@ -948,14 +954,8 @@ def test_ssowat_conf(): assert permissions["wiki.main"]["public"] is False assert permissions["blog.main"]["public"] is False - assert permissions["wiki.main"]["auth_header"] is False - assert permissions["blog.main"]["auth_header"] is True - - assert permissions["wiki.main"]["label"] == "Wiki" - assert permissions["blog.main"]["label"] == "Blog" - - assert permissions["wiki.main"]["show_tile"] is True - assert permissions["blog.main"]["show_tile"] is False + assert permissions["wiki.main"]["auth_header"] is None + assert permissions["blog.main"]["auth_header"] == "basic-without-password" def test_show_tile_cant_be_enabled(): @@ -995,12 +995,11 @@ def test_show_tile_cant_be_enabled(): # -@pytest.mark.other_domains(number=1) def test_permission_app_install(): app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), args="domain=%s&domain_2=%s&path=%s&is_public=0&admin=%s" - % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + % (maindomain, maindomain, "/urlpermissionapp", "alice"), force=True, ) @@ -1028,12 +1027,11 @@ def test_permission_app_install(): assert maindomain + "/urlpermissionapp" in app_map(user="bob").keys() -@pytest.mark.other_domains(number=1) def test_permission_app_remove(): app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), args="domain=%s&domain_2=%s&path=%s&is_public=0&admin=%s" - % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + % (maindomain, maindomain, "/urlpermissionapp", "alice"), force=True, ) app_remove("permissions_app") @@ -1043,12 +1041,11 @@ def test_permission_app_remove(): assert not any(p.startswith("permissions_app.") for p in res.keys()) -@pytest.mark.other_domains(number=1) def test_permission_app_change_url(): app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" - % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + % (maindomain, maindomain, "/urlpermissionapp", "alice"), force=True, ) @@ -1066,12 +1063,11 @@ def test_permission_app_change_url(): assert res["permissions_app.dev"]["url"] == "/dev" -@pytest.mark.other_domains(number=1) def test_permission_protection_management_by_helper(): app_install( os.path.join(get_test_apps_dir(), "permissions_app_ynh"), args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" - % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + % (maindomain, maindomain, "/urlpermissionapp", "alice"), force=True, ) @@ -1091,12 +1087,11 @@ def test_permission_protection_management_by_helper(): assert res["permissions_app.dev"]["protected"] is True -@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" - % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + % (maindomain, maindomain, "/urlpermissionapp", "alice"), force=True, ) @@ -1127,24 +1122,23 @@ def test_permission_app_propagation_on_ssowat(): assert not can_access_webpage(app_webroot + "/admin", logged_as="bob") -@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" - % (maindomain, other_domains[0], "/legacy"), + args="domain=%s&domain_2=%s&path=%s&is_public=0" + % (maindomain, maindomain, "/legacy"), force=True, ) # App is configured as public by default using the legacy unprotected_uri mechanics # It should automatically be migrated during the install res = user_permission_list(full=True)["permissions"] - assert "visitors" in res["legacy_app.main"]["allowed"] + assert "visitors" not in res["legacy_app.main"]["allowed"] assert "all_users" in res["legacy_app.main"]["allowed"] app_webroot = "https://%s/legacy" % maindomain - assert can_access_webpage(app_webroot, logged_as=None) + assert not can_access_webpage(app_webroot, logged_as=None) assert can_access_webpage(app_webroot, logged_as="alice") # Try to update the permission and check that permissions are still consistent diff --git a/src/tests/test_questions.py b/src/tests/test_questions.py index 190eb0cba..4828e681c 100644 --- a/src/tests/test_questions.py +++ b/src/tests/test_questions.py @@ -11,27 +11,30 @@ from typing import Any, Literal, Sequence, TypedDict, Union from _pytest.mark.structures import ParameterSet - from moulinette import Moulinette from yunohost import app, domain, user from yunohost.utils.form import ( OPTIONS, + FORBIDDEN_PASSWORD_CHARS, + READONLY_TYPES, ask_questions_and_parse_answers, - DisplayTextOption, - PasswordOption, + BaseChoicesOption, + BaseInputOption, + BaseReadonlyOption, DomainOption, WebPathOption, BooleanOption, FileOption, evaluate_simple_js_expression, ) +from yunohost.utils import form from yunohost.utils.error import YunohostError, YunohostValidationError """ Argument default format: { - "the_name": { + "the_id": { "type": "one_of_the_available_type", // "sting" is not specified "ask": { "en": "the question in english", @@ -48,7 +51,7 @@ Argument default format: } User answers: -{"the_name": "value", ...} +{"the_id": "value", ...} """ @@ -93,6 +96,12 @@ def patch_with_tty(): yield +@pytest.fixture +def patch_cli_retries(): + with patch.object(form, "MAX_RETRIES", 0): + yield + + # ╭───────────────────────────────────────────────────────╮ # │ ╭─╴╭─╴┌─╴╭╮╷╭─┐┌─╮╶┬╴╭─╮╭─╴ │ # │ ╰─╮│ ├─╴│││├─┤├┬╯ │ │ │╰─╮ │ @@ -215,9 +224,11 @@ def generate_test_name(intake, output, raw_option, data): "=".join( [ key, - str(raw_option[key]) - if not isinstance(raw_option[key], str) - else f"'{raw_option[key]}'", + ( + str(raw_option[key]) + if not isinstance(raw_option[key], str) + else f"'{raw_option[key]}'" + ), ] ) for key in raw_option.keys() @@ -254,9 +265,11 @@ def pytest_generate_tests(metafunc): [metafunc.cls.raw_option], metafunc.cls.scenarios ) ids += [ - generate_test_name(*args.values) - if isinstance(args, ParameterSet) - else generate_test_name(*args) + ( + generate_test_name(*args.values) + if isinstance(args, ParameterSet) + else generate_test_name(*args) + ) for args in argvalues ] elif params[1] == "expected_normalized": @@ -376,9 +389,8 @@ def _fill_or_prompt_one_option(raw_option, intake): options = {id_: raw_option} answers = {id_: intake} if intake is not None else {} - option = ask_questions_and_parse_answers(options, answers)[0] - - return (option, option.value) + options, form = ask_questions_and_parse_answers(options, answers) + return (options[0], form[id_] if isinstance(options[0], BaseInputOption) else None) def _test_value_is_expected_output(value, expected_output): @@ -405,6 +417,7 @@ def _test_intake_may_fail(raw_option, intake, expected_output): _test_intake(raw_option, intake, expected_output) +@pytest.mark.usefixtures("patch_cli_retries") # To avoid chain error logging class BaseTest: raw_option: dict[str, Any] = {} prefill: dict[Literal["raw_option", "prefill", "intake"], Any] @@ -435,17 +448,24 @@ class BaseTest: @classmethod def _test_basic_attrs(self): raw_option = self.get_raw_option(optional=True) + + if raw_option["type"] in READONLY_TYPES: + del raw_option["optional"] + + if raw_option["type"] == "select": + raw_option["choices"] = ["one"] + id_ = raw_option["id"] option, value = _fill_or_prompt_one_option(raw_option, None) - is_special_readonly_option = isinstance(option, DisplayTextOption) + is_special_readonly_option = isinstance(option, BaseReadonlyOption) assert isinstance(option, OPTIONS[raw_option["type"]]) assert option.type == raw_option["type"] - assert option.name == id_ - assert option.ask == {"en": id_} + assert option.id == id_ + assert option.ask == id_ assert option.readonly is (True if is_special_readonly_option else False) - assert option.visible is None + assert option.visible is True # assert option.bind is None if is_special_readonly_option: @@ -480,6 +500,7 @@ class BaseTest: base_raw_option = prefill_data["raw_option"] prefill = prefill_data["prefill"] + # FIXME could patch prompt with prefill if we switch to "do not apply default if value is None|''" with patch_prompt("") as prompt: raw_option = self.get_raw_option( raw_option=base_raw_option, @@ -488,15 +509,13 @@ class BaseTest: ) option, value = _fill_or_prompt_one_option(raw_option, None) - expected_message = option.ask["en"] + expected_message = option.ask + choices = [] - if option.choices: - choices = ( - option.choices - if isinstance(option.choices, list) - else option.choices.keys() - ) - expected_message += f" [{' | '.join(choices)}]" + if isinstance(option, BaseChoicesOption): + choices = option.choices + if choices: + expected_message += f" [{' | '.join(choices)}]" if option.type == "boolean": expected_message += " [yes | no]" @@ -506,8 +525,8 @@ class BaseTest: confirm=False, # FIXME no confirm? prefill=prefill, is_multiline=option.type == "text", - autocomplete=option.choices or [], - help=option.help["en"], + autocomplete=choices, + help=option.help, ) def test_scenarios(self, intake, expected_output, raw_option, data): @@ -552,10 +571,10 @@ class TestDisplayText(BaseTest): ask_questions_and_parse_answers({_id: raw_option}, answers) else: with patch.object(sys, "stdout", new_callable=StringIO) as stdout: - options = ask_questions_and_parse_answers( + options, form = ask_questions_and_parse_answers( {_id: raw_option}, answers ) - assert stdout.getvalue() == f"{options[0].ask['en']}\n" + assert stdout.getvalue() == f"{options[0].ask}\n" # ╭───────────────────────────────────────────────────────╮ @@ -584,9 +603,7 @@ class TestAlert(TestDisplayText): (None, None, {"ask": "Some text\na new line"}), (None, None, {"ask": {"en": "Some text\na new line", "fr": "Un peu de texte\nune nouvelle ligne"}}), *[(None, None, {"ask": "question", "style": style}) for style in ("success", "info", "warning", "danger")], - *xpass(scenarios=[ - (None, None, {"ask": "question", "style": "nimp"}), - ], reason="Should fail, wrong style"), + (None, YunohostError, {"ask": "question", "style": "nimp"}), ] # fmt: on @@ -605,10 +622,10 @@ class TestAlert(TestDisplayText): ) else: with patch.object(sys, "stdout", new_callable=StringIO) as stdout: - options = ask_questions_and_parse_answers( + options, form = ask_questions_and_parse_answers( {"display_text_id": raw_option}, answers ) - ask = options[0].ask["en"] + ask = options[0].ask if style in colors: color = colors[style] title = style.title() + (":" if style != "success" else "!") @@ -644,11 +661,15 @@ class TestString(BaseTest): scenarios = [ *nones(None, "", output=""), # basic typed values - *unchanged(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), # FIXME should output as str? + (False, "False"), + (True, "True"), + (0, "0"), + (1, "1"), + (-1, "-1"), + (1337, "1337"), + (13.37, "13.37"), + *all_fails([], ["one"], {}), *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", raw_option={"optional": True}), - *xpass(scenarios=[ - ([], []), - ], reason="Should fail"), # test strip ("value", "value"), ("value\n", "value"), @@ -661,9 +682,7 @@ class TestString(BaseTest): (" ##value \n \tvalue\n ", "##value \n \tvalue"), ], reason=r"should fail or without `\n`?"), # readonly - *xfail(scenarios=[ - ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), - ], reason="Should not be overwritten"), + ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), # FIXME do we want to fail instead? ] # fmt: on @@ -683,13 +702,19 @@ class TestText(BaseTest): scenarios = [ *nones(None, "", output=""), # basic typed values - *unchanged(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), # FIXME should fail or output as str? + (False, "False"), + (True, "True"), + (0, "0"), + (1, "1"), + (-1, "-1"), + (1337, "1337"), + (13.37, "13.37"), + *all_fails([], ["one"], {}), *unchanged("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", raw_option={"optional": True}), - *xpass(scenarios=[ - ([], []) - ], reason="Should fail"), ("value", "value"), ("value\n value", "value\n value"), + ("value", FAIL, {"pattern": {"regexp": r'^[A-F]\d\d$', "error": "Provide a room like F12 : one uppercase and 2 numbers"}}), + ("F12", "F12", {"pattern": {"regexp": r'^[A-F]\d\d$', "error": "Provide a room like F12 : one uppercase and 2 numbers"}}), # test no strip *xpass(scenarios=[ ("value\n", "value"), @@ -700,9 +725,7 @@ class TestText(BaseTest): (r" ##value \n \tvalue\n ", r"##value \n \tvalue\n"), ], reason="Should not be stripped"), # readonly - *xfail(scenarios=[ - ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), - ], reason="Should not be overwritten"), + ("overwrite", "expected value", {"readonly": True, "default": "expected value"}), ] # fmt: on @@ -720,11 +743,11 @@ class TestPassword(BaseTest): } # fmt: off scenarios = [ - *all_fails(False, True, 0, 1, -1, 1337, 13.37, raw_option={"optional": True}, error=TypeError), # FIXME those fails with TypeError + *all_fails(False, True, 0, 1, -1, 1337, 13.37, raw_option={"optional": True}), *all_fails([], ["one"], {}, raw_option={"optional": True}, error=AttributeError), # FIXME those fails with AttributeError *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), - ("s3cr3t!!", FAIL, {"default": "SUPAs3cr3t!!"}), # default is forbidden + ("s3cr3t!!", YunohostError, {"default": "SUPAs3cr3t!!"}), # default is forbidden *xpass(scenarios=[ ("s3cr3t!!", "s3cr3t!!", {"example": "SUPAs3cr3t!!"}), # example is forbidden ], reason="Should fail; example is forbidden"), @@ -734,11 +757,9 @@ class TestPassword(BaseTest): ], reason="Should output exactly the same"), ("s3cr3t!!", "s3cr3t!!"), ("secret", FAIL), - *[("supersecret" + char, FAIL) for char in PasswordOption.forbidden_chars], # FIXME maybe add ` \n` to the list? + *[("supersecret" + char, FAIL) for char in FORBIDDEN_PASSWORD_CHARS], # FIXME maybe add ` \n` to the list? # readonly - *xpass(scenarios=[ - ("s3cr3t!!", "s3cr3t!!", {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + ("s3cr3t!!", YunohostError, {"readonly": True}), # readonly is forbidden ] # fmt: on @@ -751,37 +772,31 @@ class TestPassword(BaseTest): class TestColor(BaseTest): raw_option = {"type": "color", "id": "color_id"} prefill = { - "raw_option": {"default": "#ff0000"}, - "prefill": "#ff0000", - # "intake": "#ff00ff", + "raw_option": {"default": "red"}, + "prefill": "red", } # fmt: off scenarios = [ *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), - *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "0", "1", "-1", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), # custom valid - ("#000000", "#000000"), + (" #fe1 ", "#fe1"), + ("#000000", "#000"), ("#000", "#000"), - ("#fe100", "#fe100"), - (" #fe100 ", "#fe100"), - ("#ABCDEF", "#ABCDEF"), + ("#ABCDEF", "#abcdef"), + ('1337', "#1337"), # rgba=(17, 51, 51, 0.47) + ("000000", "#000"), + ("#feaf", "#fea"), # `#feaf` is `#fea` with alpha at `f|100%` -> equivalent to `#fea` + # named + ("red", "#f00"), + ("yellow", "#ff0"), # custom fail - *xpass(scenarios=[ - ("#feaf", "#feaf"), - ], reason="Should fail; not a legal color value"), - ("000000", FAIL), ("#12", FAIL), ("#gggggg", FAIL), ("#01010101af", FAIL), - *xfail(scenarios=[ - ("red", "#ff0000"), - ("yellow", "#ffff00"), - ], reason="Should work with pydantic"), # readonly - *xfail(scenarios=[ - ("#ffff00", "#fe100", {"readonly": True, "default": "#fe100"}), - ], reason="Should not be overwritten"), + ("#ffff00", "#000", {"readonly": True, "default": "#000"}), ] # fmt: on @@ -805,10 +820,8 @@ class TestNumber(BaseTest): *nones(None, "", output=None), *unchanged(0, 1, -1, 1337), - *xpass(scenarios=[(False, False)], reason="should fail or output as `0`"), - *xpass(scenarios=[(True, True)], reason="should fail or output as `1`"), - *all_as("0", 0, output=0), - *all_as("1", 1, output=1), + *all_as(False, "0", 0, output=0), # FIXME should `False` fail instead? + *all_as(True, "1", 1, output=1), # FIXME should `True` fail instead? *all_as("1337", 1337, output=1337), *xfail(scenarios=[ ("-1", -1) @@ -823,9 +836,7 @@ class TestNumber(BaseTest): (-10, -10, {"default": 10}), (-10, -10, {"default": 10, "optional": True}), # readonly - *xfail(scenarios=[ - (1337, 10000, {"readonly": True, "default": 10000}), - ], reason="Should not be overwritten"), + (1337, 10000, {"readonly": True, "default": "10000"}), ] # fmt: on # FIXME should `step` be some kind of "multiple of"? @@ -850,14 +861,20 @@ class TestBoolean(BaseTest): *all_fails("none", "None"), # FIXME should output as `0` (default) like other none values when required? *all_as(None, "", output=0, raw_option={"optional": True}), # FIXME should output as `None`? *all_as("none", "None", output=None, raw_option={"optional": True}), - # FIXME even if default is explicity `None|""`, it ends up with class_default `0` - *all_as(None, "", output=0, raw_option={"default": None}), # FIXME this should fail, default is `None` - *all_as(None, "", output=0, raw_option={"optional": True, "default": None}), # FIXME even if default is explicity None, it ends up with class_default - *all_as(None, "", output=0, raw_option={"default": ""}), # FIXME this should fail, default is `""` - *all_as(None, "", output=0, raw_option={"optional": True, "default": ""}), # FIXME even if default is explicity None, it ends up with class_default - # With "none" behavior is ok - *all_fails(None, "", raw_option={"default": "none"}), - *all_as(None, "", output=None, raw_option={"optional": True, "default": "none"}), + { + "raw_options": [ + {"default": None}, + {"default": ""}, + {"default": "none"}, + {"default": "None"} + ], + "scenarios": [ + # All none values fails if default is overriden + *all_fails(None, "", "none", "None"), + # All none values ends up as None if default is overriden + *all_as(None, "", "none", "None", output=None, raw_option={"optional": True}), + ] + }, # Unhandled types should fail *all_fails(1337, "1337", "string", [], "[]", ",", "one,two"), *all_fails(1337, "1337", "string", [], "[]", ",", "one,two", {"optional": True}), @@ -890,9 +907,7 @@ class TestBoolean(BaseTest): "scenarios": all_fails("", "y", "n", error=AssertionError), }, # readonly - *xfail(scenarios=[ - (1, 0, {"readonly": True, "default": 0}), - ], reason="Should not be overwritten"), + (1, 0, {"readonly": True, "default": 0}), ] @@ -909,8 +924,12 @@ class TestDate(BaseTest): } # fmt: off scenarios = [ - *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), - *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + # Those passes since False|True are parsed as 0|1 then int|float are considered a timestamp in seconds which ends up as default Unix date + *all_as(False, True, 0, 1, 1337, 13.37, "0", "1", "1337", "13.37", output="1970-01-01"), + # Those are negative one second timestamp ending up as Unix date - 1 sec (so day change) + *all_as(-1, "-1", output="1969-12-31"), + *all_fails([], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), # custom valid ("2070-12-31", "2070-12-31"), @@ -919,20 +938,16 @@ class TestDate(BaseTest): ("2025-06-15T13:45:30", "2025-06-15"), ("2025-06-15 13:45:30", "2025-06-15") ], reason="iso date repr should be valid and extra data striped"), - *xfail(scenarios=[ - (1749938400, "2025-06-15"), - (1749938400.0, "2025-06-15"), - ("1749938400", "2025-06-15"), - ("1749938400.0", "2025-06-15"), - ], reason="timestamp could be an accepted value"), + (1749938400, "2025-06-14"), + (1749938400.0, "2025-06-14"), + ("1749938400", "2025-06-14"), + ("1749938400.0", "2025-06-14"), # custom invalid ("29-12-2070", FAIL), ("12-01-10", FAIL), ("2022-02-29", FAIL), # readonly - *xfail(scenarios=[ - ("2070-12-31", "2024-02-29", {"readonly": True, "default": "2024-02-29"}), - ], reason="Should not be overwritten"), + ("2070-12-31", "2024-02-29", {"readonly": True, "default": "2024-02-29"}), ] # fmt: on @@ -950,24 +965,26 @@ class TestTime(BaseTest): } # fmt: off scenarios = [ - *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), - *all_fails("none", "_none", "False", "True", "0", "1", "-1", "1337", "13.37", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), + # Those passes since False|True are parsed as 0|1 then int|float are considered a timestamp in seconds but we don't take seconds into account so -> 00:00 + *all_as(False, True, 0, 1, 13.37, "0", "1", "13.37", output="00:00"), + # 1337 seconds == 22 minutes + *all_as(1337, "1337", output="00:22"), + # Negative timestamp fails + *all_fails(-1, "-1", error=OverflowError), # FIXME should handle that as a validation error + # *all_fails(False, True, 0, 1, -1, 1337, 13.37, [], ["one"], {}, raw_option={"optional": True}), + *all_fails("none", "_none", "False", "True", "[]", ",", "['one']", "one,two", r"{}", "value", "value\n", raw_option={"optional": True}), *nones(None, "", output=""), # custom valid *unchanged("00:00", "08:00", "12:19", "20:59", "23:59"), - ("3:00", "3:00"), # FIXME should fail or output as `"03:00"`? - *xfail(scenarios=[ - ("22:35:05", "22:35"), - ("22:35:03.514", "22:35"), - ], reason="time as iso format could be valid"), + ("3:00", "03:00"), + ("23:1", "23:01"), + ("22:35:05", "22:35"), + ("22:35:03.514", "22:35"), # custom invalid ("24:00", FAIL), - ("23:1", FAIL), ("23:005", FAIL), # readonly - *xfail(scenarios=[ - ("00:00", "08:00", {"readonly": True, "default": "08:00"}), - ], reason="Should not be overwritten"), + ("00:00", "08:00", {"readonly": True, "default": "08:00"}), ] # fmt: on @@ -990,74 +1007,75 @@ class TestEmail(BaseTest): *nones(None, "", output=""), ("\n Abc@example.tld ", "Abc@example.tld"), + *xfail(scenarios=[("admin@ynh.local", "admin@ynh.local")], reason="Should this pass?"), # readonly - *xfail(scenarios=[ - ("Abc@example.tld", "admin@ynh.local", {"readonly": True, "default": "admin@ynh.local"}), - ], reason="Should not be overwritten"), + ("Abc@example.tld", "admin@ynh.org", {"readonly": True, "default": "admin@ynh.org"}), # Next examples are from https://github.com/JoshData/python-email-validator/blob/main/tests/test_syntax.py # valid email values - ("Abc@example.tld", "Abc@example.tld"), - ("Abc.123@test-example.com", "Abc.123@test-example.com"), - ("user+mailbox/department=shipping@example.tld", "user+mailbox/department=shipping@example.tld"), - ("伊昭傑@郵件.商務", "伊昭傑@郵件.商務"), - ("राम@मोहन.ईन्फो", "राम@मोहन.ईन्फो"), - ("юзер@екзампл.ком", "юзер@екзампл.ком"), - ("θσερ@εχαμπλε.ψομ", "θσερ@εχαμπλε.ψομ"), - ("葉士豪@臺網中心.tw", "葉士豪@臺網中心.tw"), - ("jeff@臺網中心.tw", "jeff@臺網中心.tw"), - ("葉士豪@臺網中心.台灣", "葉士豪@臺網中心.台灣"), - ("jeff葉@臺網中心.tw", "jeff葉@臺網中心.tw"), - ("ñoñó@example.tld", "ñoñó@example.tld"), - ("甲斐黒川日本@example.tld", "甲斐黒川日本@example.tld"), - ("чебурашкаящик-с-апельсинами.рф@example.tld", "чебурашкаящик-с-апельсинами.рф@example.tld"), - ("उदाहरण.परीक्ष@domain.with.idn.tld", "उदाहरण.परीक्ष@domain.with.idn.tld"), - ("ιωάννης@εεττ.gr", "ιωάννης@εεττ.gr"), + *unchanged( + "Abc@example.tld", + "Abc.123@test-example.com", + "user+mailbox/department=shipping@example.tld", + "伊昭傑@郵件.商務", + "राम@मोहन.ईन्फो", + "юзер@екзампл.ком", + "θσερ@εχαμπλε.ψομ", + "葉士豪@臺網中心.tw", + "jeff@臺網中心.tw", + "葉士豪@臺網中心.台灣", + "jeff葉@臺網中心.tw", + "ñoñó@example.tld", + "甲斐黒川日本@example.tld", + "чебурашкаящик-с-апельсинами.рф@example.tld", + "उदाहरण.परीक्ष@domain.with.idn.tld", + "ιωάννης@εεττ.gr", + ), # invalid email (Hiding because our current regex is very permissive) - # ("my@localhost", FAIL), - # ("my@.leadingdot.com", FAIL), - # ("my@.leadingfwdot.com", FAIL), - # ("my@twodots..com", FAIL), - # ("my@twofwdots...com", FAIL), - # ("my@trailingdot.com.", FAIL), - # ("my@trailingfwdot.com.", FAIL), - # ("me@-leadingdash", FAIL), - # ("me@-leadingdashfw", FAIL), - # ("me@trailingdash-", FAIL), - # ("me@trailingdashfw-", FAIL), - # ("my@baddash.-.com", FAIL), - # ("my@baddash.-a.com", FAIL), - # ("my@baddash.b-.com", FAIL), - # ("my@baddashfw.-.com", FAIL), - # ("my@baddashfw.-a.com", FAIL), - # ("my@baddashfw.b-.com", FAIL), - # ("my@example.com\n", FAIL), - # ("my@example\n.com", FAIL), - # ("me@x!", FAIL), - # ("me@x ", FAIL), - # (".leadingdot@domain.com", FAIL), - # ("twodots..here@domain.com", FAIL), - # ("trailingdot.@domain.email", FAIL), - # ("me@⒈wouldbeinvalid.com", FAIL), - ("@example.com", FAIL), - # ("\nmy@example.com", FAIL), - ("m\ny@example.com", FAIL), - ("my\n@example.com", FAIL), - # ("11111111112222222222333333333344444444445555555555666666666677777@example.com", FAIL), - # ("111111111122222222223333333333444444444455555555556666666666777777@example.com", FAIL), - # ("me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444444444455555555556.com", FAIL), - # ("me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", FAIL), - # ("me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", FAIL), - # ("my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info", FAIL), - # ("my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333.info", FAIL), - # ("my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", FAIL), - # ("my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444.info", FAIL), - # ("my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", FAIL), - # ("me@bad-tld-1", FAIL), - # ("me@bad.tld-2", FAIL), - # ("me@xn--0.tld", FAIL), - # ("me@yy--0.tld", FAIL), - # ("me@yy--0.tld", FAIL), + *all_fails( + "my@localhost", + "my@.leadingdot.com", + "my@.leadingfwdot.com", + "my@twodots..com", + "my@twofwdots...com", + "my@trailingdot.com.", + "my@trailingfwdot.com.", + "me@-leadingdash", + "me@-leadingdashfw", + "me@trailingdash-", + "me@trailingdashfw-", + "my@baddash.-.com", + "my@baddash.-a.com", + "my@baddash.b-.com", + "my@baddashfw.-.com", + "my@baddashfw.-a.com", + "my@baddashfw.b-.com", + "my@example\n.com", + "me@x!", + "me@x ", + ".leadingdot@domain.com", + "twodots..here@domain.com", + "trailingdot.@domain.email", + "me@⒈wouldbeinvalid.com", + "@example.com", + "m\ny@example.com", + "my\n@example.com", + "11111111112222222222333333333344444444445555555555666666666677777@example.com", + "111111111122222222223333333333444444444455555555556666666666777777@example.com", + "me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444444444455555555556.com", + "me@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", + "me@中1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555566.com", + "my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info", + "my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333.info", + "my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", + "my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444.info", + "my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info", + "me@bad-tld-1", + "me@bad.tld-2", + "me@xn--0.tld", + "me@yy--0.tld", + "me@yy--0.tld", + ) ] # fmt: on @@ -1106,9 +1124,7 @@ class TestWebPath(BaseTest): ("https://example.com/folder", "/https://example.com/folder") ], reason="Should fail or scheme+domain removed"), # readonly - *xfail(scenarios=[ - ("/overwrite", "/value", {"readonly": True, "default": "/value"}), - ], reason="Should not be overwritten"), + ("/overwrite", "/value", {"readonly": True, "default": "/value"}), # FIXME should path have forbidden_chars? ] # fmt: on @@ -1132,23 +1148,17 @@ class TestUrl(BaseTest): *nones(None, "", output=""), ("http://some.org/folder/file.txt", "http://some.org/folder/file.txt"), + (' https://www.example.com \n', 'https://www.example.com'), # readonly - *xfail(scenarios=[ - ("https://overwrite.org", "https://example.org", {"readonly": True, "default": "https://example.org"}), - ], reason="Should not be overwritten"), + ("https://overwrite.org", "https://example.org", {"readonly": True, "default": "https://example.org"}), # rest is taken from https://github.com/pydantic/pydantic/blob/main/tests/test_networks.py # valid *unchanged( # Those are valid but not sure how they will output with pydantic 'http://example.org', - 'http://test', - 'http://localhost', 'https://example.org/whatever/next/', 'https://example.org', - 'http://localhost', - 'http://localhost/', - 'http://localhost:8000', - 'http://localhost:8000/', + 'https://foo_bar.example.com/', 'http://example.co.jp', 'http://www.example.com/a%C2%B1b', @@ -1172,29 +1182,31 @@ class TestUrl(BaseTest): 'http://twitter.com/@handle/', 'http://11.11.11.11.example.com/action', 'http://abc.11.11.11.11.example.com/action', - 'http://example#', - 'http://example/#', - 'http://example/#fragment', - 'http://example/?#', 'http://example.org/path#', 'http://example.org/path#fragment', 'http://example.org/path?query#', 'http://example.org/path?query#fragment', + 'https://foo_bar.example.com/', + 'https://exam_ple.com/', + 'HTTP://EXAMPLE.ORG', + 'https://example.org', + 'https://example.org?a=1&b=2', + 'https://example.org#a=3;b=3', + 'https://example.xn--p1ai', + 'https://example.xn--vermgensberatung-pwb', + 'https://example.xn--zfr164b', ), - # Pydantic default parsing add a final `/` - ('https://foo_bar.example.com/', 'https://foo_bar.example.com/'), - ('https://exam_ple.com/', 'https://exam_ple.com/'), *xfail(scenarios=[ - (' https://www.example.com \n', 'https://www.example.com/'), - ('HTTP://EXAMPLE.ORG', 'http://example.org/'), - ('https://example.org', 'https://example.org/'), - ('https://example.org?a=1&b=2', 'https://example.org/?a=1&b=2'), - ('https://example.org#a=3;b=3', 'https://example.org/#a=3;b=3'), - ('https://example.xn--p1ai', 'https://example.xn--p1ai/'), - ('https://example.xn--vermgensberatung-pwb', 'https://example.xn--vermgensberatung-pwb/'), - ('https://example.xn--zfr164b', 'https://example.xn--zfr164b/'), - ], reason="pydantic default behavior would append a final `/`"), - + ('http://test', 'http://test'), + ('http://localhost', 'http://localhost'), + ('http://localhost/', 'http://localhost/'), + ('http://localhost:8000', 'http://localhost:8000'), + ('http://localhost:8000/', 'http://localhost:8000/'), + ('http://example#', 'http://example#'), + ('http://example/#', 'http://example/#'), + ('http://example/#fragment', 'http://example/#fragment'), + ('http://example/?#', 'http://example/?#'), + ], reason="Should this be valid?"), # invalid *all_fails( 'ftp://example.com/', @@ -1205,15 +1217,13 @@ class TestUrl(BaseTest): "/", "+http://example.com/", "ht*tp://example.com/", + "http:///", + "http://??", + "https://example.org more", + "http://2001:db8::ff00:42:8329", + "http://[192.168.1.1]:8329", + "http://example.com:99999", ), - *xpass(scenarios=[ - ("http:///", "http:///"), - ("http://??", "http://??"), - ("https://example.org more", "https://example.org more"), - ("http://2001:db8::ff00:42:8329", "http://2001:db8::ff00:42:8329"), - ("http://[192.168.1.1]:8329", "http://[192.168.1.1]:8329"), - ("http://example.com:99999", "http://example.com:99999"), - ], reason="Should fail"), ] # fmt: on @@ -1294,7 +1304,7 @@ class TestFile(BaseTest): def test_basic_attrs(self): raw_option, option, value = self._test_basic_attrs() - accept = raw_option.get("accept", "") # accept default + accept = raw_option.get("accept", None) # accept default assert option.accept == accept def test_options_prompted_with_ask_help(self): @@ -1384,7 +1394,6 @@ class TestSelect(BaseTest): # [-1, 0, 1] "raw_options": [ {"choices": [-1, 0, 1, 10]}, - {"choices": {-1: "verbose -one", 0: "verbose zero", 1: "verbose one", 10: "verbose ten"}}, ], "scenarios": [ *nones(None, "", output=""), @@ -1398,6 +1407,18 @@ class TestSelect(BaseTest): *all_fails("100", 100), ] }, + { + "raw_options": [ + {"choices": {-1: "verbose -one", 0: "verbose zero", 1: "verbose one", 10: "verbose ten"}}, + {"choices": {"-1": "verbose -one", "0": "verbose zero", "1": "verbose one", "10": "verbose ten"}}, + ], + "scenarios": [ + *nones(None, "", output=""), + *all_fails(-1, 0, 1, 10), # Should pass? converted to str? + *unchanged("-1", "0", "1", "10"), + *all_fails("100", 100), + ] + }, # [True, False, None] *unchanged(True, False, raw_option={"choices": [True, False, None]}), # FIXME we should probably forbid None in choices (None, FAIL, {"choices": [True, False, None]}), @@ -1425,9 +1446,7 @@ class TestSelect(BaseTest): ] }, # readonly - *xfail(scenarios=[ - ("one", "two", {"readonly": True, "choices": ["one", "two"], "default": "two"}), - ], reason="Should not be overwritten"), + ("one", "two", {"readonly": True, "choices": ["one", "two"], "default": "two"}), ] # fmt: on @@ -1437,6 +1456,7 @@ class TestSelect(BaseTest): # ╰───────────────────────────────────────────────────────╯ +# [], ["one"], {} class TestTags(BaseTest): raw_option = {"type": "tags", "id": "tags_id"} prefill = { @@ -1445,12 +1465,7 @@ class TestTags(BaseTest): } # fmt: off scenarios = [ - *nones(None, [], "", output=""), - # FIXME `","` could be considered a none value which kinda already is since it fail when required - (",", FAIL), - *xpass(scenarios=[ - (",", ",", {"optional": True}) - ], reason="Should output as `''`? ie: None"), + *nones(None, [], "", ",", output=""), { "raw_options": [ {}, @@ -1470,14 +1485,12 @@ class TestTags(BaseTest): # basic types (not in a list) should fail *all_fails(True, False, -1, 0, 1, 1337, 13.37, {}), # Mixed choices should fail - ([False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}], FAIL, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), - ("False,True,-1,0,1,1337,13.37,[],['one'],{}", FAIL, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), - *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), - *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + ([False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}], YunohostError, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + ("False,True,-1,0,1,1337,13.37,[],['one'],{}", YunohostError, {"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}), + *all_fails(*([t] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}, error=YunohostError), + *all_fails(*([str(t)] for t in [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]), raw_option={"choices": [False, True, -1, 0, 1, 1337, 13.37, [], ["one"], {}]}, error=YunohostError), # readonly - *xfail(scenarios=[ - ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), - ], reason="Should not be overwritten"), + ("one", "one,two", {"readonly": True, "choices": ["one", "two"], "default": "one,two"}), ] # fmt: on @@ -1525,9 +1538,7 @@ class TestDomain(BaseTest): ("doesnt_exist.pouet", FAIL, {}), ("fake.com", FAIL, {"choices": ["fake.com"]}), # readonly - *xpass(scenarios=[ - (domains1[0], domains1[0], {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (domains1[0], YunohostError, {"readonly": True}), # readonly is forbidden ] }, { @@ -1544,6 +1555,10 @@ class TestDomain(BaseTest): ] # fmt: on + def test_options_prompted_with_ask_help(self, prefill_data=None): + with patch_domains(domains=[main_domain], main_domain=main_domain): + super().test_options_prompted_with_ask_help(prefill_data=prefill_data) + def test_scenarios(self, intake, expected_output, raw_option, data): with patch_domains(**data): super().test_scenarios(intake, expected_output, raw_option, data) @@ -1591,8 +1606,7 @@ class TestApp(BaseTest): ], "scenarios": [ # FIXME there are currently 3 different nones (`None`, `""` and `_none`), choose one? - *nones(None, output=None), # FIXME Should return chosen none? - *nones("", output=""), # FIXME Should return chosen none? + *nones(None, "", output=""), # FIXME Should return chosen none? *xpass(scenarios=[ ("_none", "_none"), ("_none", "_none", {"default": "_none"}), @@ -1615,7 +1629,7 @@ class TestApp(BaseTest): (installed_webapp["id"], installed_webapp["id"], {"filter": "is_webapp"}), (installed_webapp["id"], FAIL, {"filter": "is_webapp == false"}), (installed_webapp["id"], FAIL, {"filter": "id != 'my_webapp'"}), - (None, None, {"filter": "id == 'fake_app'", "optional": True}), + (None, "", {"filter": "id == 'fake_app'", "optional": True}), ] }, { @@ -1624,9 +1638,7 @@ class TestApp(BaseTest): (installed_non_webapp["id"], installed_non_webapp["id"]), (installed_non_webapp["id"], FAIL, {"filter": "is_webapp"}), # readonly - *xpass(scenarios=[ - (installed_non_webapp["id"], installed_non_webapp["id"], {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + (installed_non_webapp["id"], YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1673,47 +1685,29 @@ class TestApp(BaseTest): 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": [], + "groups": ["admins"], } regular_username = "normal_user" regular_user = { - "ssh_allowed": False, "username": regular_username, - "mailbox-quota": "0", "mail": "z@ynh.local", "fullname": "john doe", - "group": [], + "groups": [], } @contextmanager -def patch_users( - *, - users, - admin_username, - main_domain, -): +def patch_users(*, users): """ 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): + with patch.object(user, "user_list", return_value={"users": users}): yield @@ -1724,8 +1718,8 @@ class TestUser(BaseTest): # 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}, + {"users": {admin_username: admin_user}}, + {"users": {admin_username: admin_user, regular_username: regular_user}}, ], "scenarios": [ # FIXME User option is not really nullable, even if optional @@ -1736,26 +1730,27 @@ class TestUser(BaseTest): }, { "data": [ - {"users": {admin_username: admin_user, regular_username: regular_user}, "admin_username": admin_username, "main_domain": main_domain}, + {"users": {admin_username: admin_user, regular_username: regular_user}}, ], "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"), + (admin_username, YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] # fmt: on + @pytest.mark.usefixtures("patch_no_tty") + def test_basic_attrs(self): + with patch_users(users={admin_username: admin_user}): + 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, + users={admin_username: admin_user, regular_username: regular_user} ): super().test_options_prompted_with_ask_help( prefill_data={"raw_option": {}, "prefill": admin_username} @@ -1816,13 +1811,9 @@ class TestGroup(BaseTest): "scenarios": [ ("custom_group", "custom_group"), *all_as("", None, output="visitors", raw_option={"default": "visitors"}), - *xpass(scenarios=[ - ("", "custom_group", {"default": "custom_group"}), - ], reason="Should throw 'default must be in (None, 'all_users', 'visitors', 'admins')"), + ("", YunohostError, {"default": "custom_group"}), # Not allowed to set a default which is not a default group # readonly - *xpass(scenarios=[ - ("admins", "admins", {"readonly": True}), - ], reason="Should fail since readonly is forbidden"), + ("admins", YunohostError, {"readonly": True}), # readonly is forbidden ] }, ] @@ -1839,13 +1830,6 @@ class TestGroup(BaseTest): "prefill": "admins", } ) - # FIXME This should fail, not allowed to set a default which is not a default group - super().test_options_prompted_with_ask_help( - prefill_data={ - "raw_option": {"default": "custom_group"}, - "prefill": "custom_group", - } - ) def test_scenarios(self, intake, expected_output, raw_option, data): with patch_groups(**data): @@ -1863,8 +1847,6 @@ def patch_entities(): 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 ): @@ -1902,12 +1884,12 @@ def test_options_query_string(): "string_id": "string", "text_id": "text\ntext", "password_id": "sUpRSCRT", - "color_id": "#ffff00", + "color_id": "#ff0", "number_id": 10, "boolean_id": 1, "date_id": "2030-03-06", "time_id": "20:55", - "email_id": "coucou@ynh.local", + "email_id": "coucou@ynh.org", "path_id": "/ynh-dev", "url_id": "https://yunohost.org", "file_id": file_content1, @@ -1930,7 +1912,7 @@ def test_options_query_string(): "&boolean_id=y" "&date_id=2030-03-06" "&time_id=20:55" - "&email_id=coucou@ynh.local" + "&email_id=coucou@ynh.org" "&path_id=ynh-dev/" "&url_id=https://yunohost.org" f"&file_id={file_repr}" @@ -1947,9 +1929,7 @@ def test_options_query_string(): "&fake_id=fake_value" ) - def _assert_correct_values(options, raw_options): - form = {option.name: option.value for option in options} - + def _assert_correct_values(options, form, raw_options): for k, v in results.items(): if k == "file_id": assert os.path.exists(form["file_id"]) and os.path.isfile( @@ -1965,24 +1945,39 @@ def test_options_query_string(): with patch_interface("api"), patch_file_api(file_content1) as b64content: with patch_query_string(b64content.decode("utf-8")) as query_string: - options = ask_questions_and_parse_answers(raw_options, query_string) - _assert_correct_values(options, raw_options) + options, form = ask_questions_and_parse_answers(raw_options, query_string) + _assert_correct_values(options, form, raw_options) with patch_interface("cli"), patch_file_cli(file_content1) as filepath: with patch_query_string(filepath) as query_string: - options = ask_questions_and_parse_answers(raw_options, query_string) - _assert_correct_values(options, raw_options) + options, form = ask_questions_and_parse_answers(raw_options, query_string) + _assert_correct_values(options, form, raw_options) def test_question_string_default_type(): questions = {"some_string": {}} answers = {"some_string": "some_value"} - out = ask_questions_and_parse_answers(questions, answers)[0] + options, form = ask_questions_and_parse_answers(questions, answers) + option = options[0] + assert option.id == "some_string" + assert option.type == "string" + assert form[option.id] == "some_value" - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "some_value" + +def test_option_default_type_with_choices_is_select(): + questions = { + "some_choices": {"choices": ["a", "b"]}, + # LEGACY (`choices` in option `string` used to be valid) + # make sure this result as a `select` option + "some_legacy": {"type": "string", "choices": ["a", "b"]}, + } + answers = {"some_choices": "a", "some_legacy": "a"} + + options, form = ask_questions_and_parse_answers(questions, answers) + for option in options: + assert option.type == "select" + assert form[option.id] == "a" @pytest.mark.skip # we should do something with this example @@ -2005,75 +2000,6 @@ def test_question_string_input_test_ask_with_example(): assert example_text in prompt.call_args[1]["message"] -def test_question_string_with_choice(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "fr"} - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "fr" - - -def test_question_string_with_choice_prompt(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "fr"} - with patch.object(Moulinette, "prompt", return_value="fr"), patch.object( - os, "isatty", return_value=True - ): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "fr" - - -def test_question_string_with_choice_bad(): - questions = {"some_string": {"type": "string", "choices": ["fr", "en"]}} - answers = {"some_string": "bad"} - - with pytest.raises(YunohostError), patch.object(os, "isatty", return_value=False): - ask_questions_and_parse_answers(questions, answers) - - -def test_question_string_with_choice_ask(): - ask_text = "some question" - choices = ["fr", "en", "es", "it", "ru"] - questions = { - "some_string": { - "ask": ask_text, - "choices": choices, - } - } - answers = {} - - with patch.object(Moulinette, "prompt", return_value="ru") as prompt, patch.object( - os, "isatty", return_value=True - ): - ask_questions_and_parse_answers(questions, answers) - assert ask_text in prompt.call_args[1]["message"] - - for choice in choices: - assert choice in prompt.call_args[1]["message"] - - -def test_question_string_with_choice_default(): - questions = { - "some_string": { - "type": "string", - "choices": ["fr", "en"], - "default": "en", - } - } - answers = {} - with patch.object(os, "isatty", return_value=False): - out = ask_questions_and_parse_answers(questions, answers)[0] - - assert out.name == "some_string" - assert out.type == "string" - assert out.value == "en" - - @pytest.mark.skip # we should do something with this example def test_question_password_input_test_ask_with_example(): ask_text = "some question" diff --git a/src/tests/test_sso_and_portalapi.py b/src/tests/test_sso_and_portalapi.py new file mode 100644 index 000000000..e5ad8992f --- /dev/null +++ b/src/tests/test_sso_and_portalapi.py @@ -0,0 +1,332 @@ +import base64 +import time +import requests +from pathlib import Path +import os + +from .conftest import message, raiseYunohostError, get_test_apps_dir + +from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list +from yunohost.user import user_create, user_list, user_delete +from yunohost.authenticators.ldap_ynhuser import Authenticator, SESSION_FOLDER, short_hash +from yunohost.app import app_install, app_remove, app_setting, app_ssowatconf, app_change_url +from yunohost.permission import user_permission_list, user_permission_update + + +# Get main domain +maindomain = open("/etc/yunohost/current_host").read().strip() +subdomain = f"sub.{maindomain}" +secondarydomain = "secondary.test" +dummy_password = "test123Ynh" + + +def setup_function(function): + Authenticator.invalidate_all_sessions_for_user("alice") + assert number_of_active_session_for_user("alice") == 0 + Authenticator.invalidate_all_sessions_for_user("bob") + assert number_of_active_session_for_user("bob") == 0 + + user_permission_update( + "hellopy.main", add=["visitors", "all_users"], remove=["alice", "bob"] + ) + + app_setting("hellopy", "auth_header", delete=True) + app_setting("hellopy", "protect_against_basic_auth_spoofing", delete=True) + app_ssowatconf() + + +def teardown_function(function): + pass + + +def setup_module(module): + + assert os.system("systemctl is-active yunohost-portal-api >/dev/null") == 0 + + if "alice" not in user_list()["users"]: + user_create("alice", maindomain, dummy_password, fullname="Alice White", admin=True) + if "bob" not in user_list()["users"]: + user_create("bob", maindomain, dummy_password, fullname="Bob Marley") + + app_install( + os.path.join(get_test_apps_dir(), "hellopy_ynh"), + args=f"domain={maindomain}&init_main_permission=visitors", + force=True, + ) + + +def teardown_module(module): + if "alice" in user_list()["users"]: + user_delete("alice") + if "bob" in user_list()["users"]: + user_delete("bob") + + app_remove("hellopy") + + if subdomain in domain_list()["domains"]: + domain_remove(subdomain) + if secondarydomain in domain_list()["domains"]: + domain_remove(secondarydomain) + + +def login(session, logged_as, logged_on=None): + + if not logged_on: + logged_on = maindomain + + login_endpoint = f"https://{logged_on}/yunohost/portalapi/login" + r = session.post( + login_endpoint, + data={"credentials": f"{logged_as}:{dummy_password}"}, + headers={ + "X-Requested-With": "", + }, + verify=False, + ) + + return r + + +def logout(session): + logout_endpoint = f"https://{maindomain}/yunohost/portalapi/logout" + r = session.get( + logout_endpoint, + headers={ + "X-Requested-With": "", + }, + verify=False, + ) + return r + + +def number_of_active_session_for_user(user): + return len(list(Path(SESSION_FOLDER).glob(f"{short_hash(user)}*"))) + + +def request(webpath, logged_as=None, session=None, inject_auth=None, logged_on=None): + webpath = webpath.rstrip("/") + + headers = {} + if inject_auth: + b64loginpassword = base64.b64encode((inject_auth[0] + ":" + inject_auth[1]).encode()).decode() + headers["Authorization"] = f"Basic {b64loginpassword}" + + # Anonymous access + if session: + r = session.get(webpath, verify=False, allow_redirects=False, headers=headers) + elif not logged_as: + r = requests.get(webpath, verify=False, allow_redirects=False, headers=headers) + # Login as a user using dummy password + else: + with requests.Session() as session: + r = login(session, logged_as, logged_on) + # We should have some cookies related to authentication now + assert session.cookies + r = session.get(webpath, verify=False, allow_redirects=False, headers=headers) + + return r + + +def test_api_public_as_anonymous(): + + # FIXME : should list apps only if the domain option is enabled + + r = request(f"https://{maindomain}/yunohost/portalapi/public") + assert r.status_code == 200 and "apps" in r.json() + + +def test_api_me_as_anonymous(): + + r = request(f"https://{maindomain}/yunohost/portalapi/me") + assert r.status_code == 401 + + +def test_api_login_and_logout(): + + with requests.Session() as session: + r = login(session, "alice") + + assert "yunohost.portal" in session.cookies + assert r.status_code == 200 + + assert number_of_active_session_for_user("alice") == 1 + + r = logout(session) + + assert number_of_active_session_for_user("alice") == 0 + + +def test_api_login_nonexistinguser(): + + with requests.Session() as session: + r = login(session, "nonexistent") + + assert r.status_code == 401 + + +def test_api_public_and_me_logged_in(): + + r = request(f"https://{maindomain}/yunohost/portalapi/public", logged_as="alice") + assert r.status_code == 200 and "apps" in r.json() + r = request(f"https://{maindomain}/yunohost/portalapi/me", logged_as="alice") + assert r.status_code == 200 and r.json()["username"] == "alice" + + assert number_of_active_session_for_user("alice") == 2 + + +def test_api_session_expired(): + + with requests.Session() as session: + r = login(session, "alice") + + assert "yunohost.portal" in session.cookies + assert r.status_code == 200 + + r = request(f"https://{maindomain}/yunohost/portalapi/me", session=session) + assert r.status_code == 200 and r.json()["username"] == "alice" + + for file in Path(SESSION_FOLDER).glob(f"{short_hash('alice')}*"): + os.utime(str(file), (0, 0)) + + r = request(f"https://{maindomain}/yunohost/portalapi/me", session=session) + assert number_of_active_session_for_user("alice") == 0 + assert r.status_code == 401 + + +def test_public_routes_not_blocked_by_ssowat(): + + r = request(f"https://{maindomain}/yunohost/api/whatever") + # Getting code 405, Method not allowed, which means the API does answer, + # meaning it's not blocked by ssowat + # Or : on the CI, the yunohost-api is likely to be down (to save resources) + assert r.status_code in [405, 502] + + os.system("mkdir -p /var/www/.well-known/acme-challenge-public") + Path("/var/www/.well-known/acme-challenge-public/toto").touch() + r = request(f"http://{maindomain}/.well-known/acme-challenge/toto") + assert r.status_code == 200 + + r = request(f"http://{maindomain}/.well-known/acme-challenge/nonexistent") + assert r.status_code == 404 + + +def test_permission_propagation_on_ssowat(): + + res = user_permission_list(full=True)["permissions"] + assert "visitors" in res["hellopy.main"]["allowed"] + assert "all_users" in res["hellopy.main"]["allowed"] + + r = request(f"https://{maindomain}/") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + r = request(f"https://{maindomain}/", logged_as="alice") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + r = request(f"https://{maindomain}/", logged_as="bob") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + user_permission_update( + "hellopy.main", remove=["visitors", "all_users"], add="alice" + ) + + r = request(f"https://{maindomain}/") + assert r.status_code == 302 + assert r.headers['Location'].startswith(f"https://{maindomain}/yunohost/sso?r=") + + r = request(f"https://{maindomain}/", logged_as="alice") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + # Bob can't even login because doesnt has access to any app on the domain + # (that's debattable tho) + with requests.Session() as session: + r = login(session, "bob") + assert not session.cookies + + +def test_sso_basic_auth_header(): + + r = request(f"https://{maindomain}/show-auth") + assert r.status_code == 200 and r.content.decode().strip() == "User: None\nPwd: None" + + r = request(f"https://{maindomain}/show-auth", logged_as="alice") + assert r.status_code == 200 and r.content.decode().strip() == "User: alice\nPwd: -" + + app_setting("hellopy", "auth_header", value="basic-with-password") + app_ssowatconf() + + r = request(f"https://{maindomain}/show-auth", logged_as="alice") + assert r.status_code == 200 and r.content.decode().strip() == f"User: alice\nPwd: {dummy_password}" + + +def test_sso_basic_auth_header_spoofing(): + + r = request(f"https://{maindomain}/show-auth") + assert r.status_code == 200 and r.content.decode().strip() == "User: None\nPwd: None" + + r = request(f"https://{maindomain}/show-auth", inject_auth=("foo", "bar")) + assert r.status_code == 200 and r.content.decode().strip() == "User: None\nPwd: None" + + app_setting("hellopy", "protect_against_basic_auth_spoofing", value="false") + app_ssowatconf() + + r = request(f"https://{maindomain}/show-auth", inject_auth=("foo", "bar")) + assert r.status_code == 200 and r.content.decode().strip() == "User: foo\nPwd: bar" + + +def test_sso_on_subdomain(): + + if subdomain not in domain_list()["domains"]: + domain_add(subdomain) + + app_change_url("hellopy", domain=subdomain, path="/") + + r = request(f"https://{subdomain}/") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + r = request(f"https://{subdomain}/", logged_as="alice") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + r = request(f"https://{subdomain}/show-auth", logged_as="alice") + assert r.status_code == 200 and r.content.decode().strip().startswith("User: alice") + + +def test_sso_on_secondary_domain(): + + if secondarydomain not in domain_list()["domains"]: + domain_add(secondarydomain) + + app_change_url("hellopy", domain=secondarydomain, path="/") + + r = request(f"https://{secondarydomain}/") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + r = request(f"https://{secondarydomain}/", logged_as="alice") + assert r.status_code == 200 and r.content.decode().strip() == "Hello world!" + + r = request(f"https://{secondarydomain}/show-auth", logged_as="alice") + # Getting 'User: None despite being logged on the main domain + assert r.status_code == 200 and r.content.decode().strip().startswith("User: None") + + r = request(f"https://{secondarydomain}/show-auth", logged_as="alice", logged_on=secondarydomain) + assert r.status_code == 200 and r.content.decode().strip().startswith("User: alice") + + + + + +# accès à l'api portal + # -> test des routes + # apps publique (seulement si activé ?) + # /me + # /update + + +# accès aux trucs précédent meme avec une app installée sur la racine ? +# ou une app par défaut ? + +# accès à un deuxième "domain principal" + +# accès à un app sur un sous-domaine +# pas loggué -> redirect vers sso sur domaine principal +# se logger sur API sur domain principal, puis utilisation du cookie sur le sous-domaine + diff --git a/src/tests/test_user-group.py b/src/tests/test_user-group.py index 57f9ffa3f..f347fc9bc 100644 --- a/src/tests/test_user-group.py +++ b/src/tests/test_user-group.py @@ -263,12 +263,6 @@ def test_del_group_that_does_not_exist(mocker): def test_update_user(): - with message("user_updated"): - user_update("alice", firstname="NewName", lastname="NewLast") - - info = user_info("alice") - assert info["fullname"] == "NewName NewLast" - with message("user_updated"): user_update("alice", fullname="New2Name New2Last") @@ -315,7 +309,7 @@ def test_update_group_remove_user_not_already_in(): def test_update_user_that_doesnt_exist(mocker): with raiseYunohostError(mocker, "user_unknown"): - user_update("doesnt_exist", firstname="NewName", lastname="NewLast") + user_update("doesnt_exist", fullname="Foo Bar") def test_update_group_that_doesnt_exist(mocker): diff --git a/src/tools.py b/src/tools.py index 740f92c9d..ec90361fc 100644 --- a/src/tools.py +++ b/src/tools.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # +import pwd import re import os import subprocess @@ -23,38 +24,26 @@ import time from importlib import import_module from packaging import version from typing import List +from logging import getLogger from moulinette import Moulinette, m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.process import call_async_output from moulinette.utils.filesystem import read_yaml, write_to_yaml, cp, mkdir, rm, chown -from yunohost.app import ( - app_upgrade, - app_list, - _list_upgradable_apps, -) -from yunohost.app_catalog import ( - _initialize_apps_catalog_system, - _update_apps_catalog, -) -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.system import ( _dump_sources_list, _list_upgradable_apt_packages, ynh_packages_version, dpkg_is_broken, dpkg_lock_available, + _apt_log_line_is_relevant, ) from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation, OperationLogger MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations.yaml" -logger = getActionLogger("yunohost.tools") +logger = getLogger("yunohost.tools") def tools_versions(): @@ -62,10 +51,10 @@ def tools_versions(): def tools_rootpw(new_password, check_strength=True): - from yunohost.user import _hash_user_password from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, + _hash_user_password, ) import spwd @@ -143,25 +132,30 @@ def _set_hostname(hostname, pretty_hostname=None): logger.debug(out) -@is_unit_operation() +@is_unit_operation(exclude=["dyndns_recovery_password", "password"]) def tools_postinstall( operation_logger, domain, username, fullname, password, + dyndns_recovery_password=None, ignore_dyndns=False, force_diskspace=False, overwrite_root_password=True, ): - from yunohost.dyndns import _dyndns_available + from yunohost.service import _run_service_command + from yunohost.dyndns import _dyndns_available, dyndns_unsubscribe from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, ) - from yunohost.domain import domain_main_domain + from yunohost.domain import domain_main_domain, domain_add from yunohost.user import user_create, ADMIN_ALIASES + from yunohost.app_catalog import _update_apps_catalog + from yunohost.firewall import firewall_upnp + import psutil # Do some checks at first @@ -174,6 +168,12 @@ def tools_postinstall( raw_msg=True, ) + # Crash early if the username is already a system user, which is + # a common confusion. We don't want to crash later and end up in an half-configured state. + all_existing_usernames = {x.pw_name for x in pwd.getpwall()} + if username in all_existing_usernames: + raise YunohostValidationError("system_username_exists") + if username in ADMIN_ALIASES: raise YunohostValidationError( f"Unfortunately, {username} cannot be used as a username", raw_msg=True @@ -196,9 +196,8 @@ def tools_postinstall( assert_password_is_strong_enough("admin", password) # If this is a nohost.me/noho.st, actually check for availability - if not ignore_dyndns and is_yunohost_dyndns_domain(domain): - available = None - + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) + if dyndns: # Check if the domain is available... try: available = _dyndns_available(domain) @@ -206,17 +205,19 @@ def tools_postinstall( # connectivity or something. Assume that this domain isn't manageable # and inform the user that we could not contact the dyndns host server. except Exception: - logger.warning( - m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org") + raise YunohostValidationError( + "dyndns_provider_unreachable", provider="dyndns.yunohost.org" ) - - if available: - dyndns = True - # If not, abort the postinstall else: - raise YunohostValidationError("dyndns_unavailable", domain=domain) - else: - dyndns = False + if not available: + if dyndns_recovery_password: + # Try to unsubscribe the domain so it can be subscribed again + # If successful, it will be resubscribed with the same recovery password + dyndns_unsubscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) + else: + raise YunohostValidationError("dyndns_unavailable", domain=domain) if os.system("iptables -V >/dev/null 2>/dev/null") != 0: raise YunohostValidationError( @@ -228,7 +229,11 @@ def tools_postinstall( logger.info(m18n.n("yunohost_installing")) # New domain config - domain_add(domain, dyndns) + domain_add( + domain, + dyndns_recovery_password=dyndns_recovery_password, + ignore_dyndns=ignore_dyndns, + ) domain_main_domain(domain) # First user @@ -240,10 +245,7 @@ def tools_postinstall( # Enable UPnP silently and reload firewall firewall_upnp("enable", no_refresh=True) - # Initialize the apps catalog system - _initialize_apps_catalog_system() - - # Try to update the apps catalog ... + # Try to fetch the apps catalog ... # we don't fail miserably if this fails, # because that could be for example an offline installation... try: @@ -257,10 +259,10 @@ def tools_postinstall( os.system("touch /etc/yunohost/installed") # Enable and start YunoHost firewall at boot time - service_enable("yunohost-firewall") - service_start("yunohost-firewall") + _run_service_command("enable", "yunohost-firewall") + _run_service_command("start", "yunohost-firewall") - regen_conf(names=["ssh"], force=True) + tools_regen_conf(names=["ssh"], force=True) # Restore original ssh conf, as chosen by the # admin during the initial install @@ -275,7 +277,7 @@ def tools_postinstall( if os.path.exists(original_sshd_conf): os.rename(original_sshd_conf, "/etc/ssh/sshd_config") - regen_conf(force=True) + tools_regen_conf(force=True) logger.success(m18n.n("yunohost_configured")) @@ -285,17 +287,8 @@ 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) + from yunohost.regenconf import regen_conf return regen_conf(names, with_diff, force, dry_run, list_pending) @@ -303,6 +296,8 @@ def tools_update(target=None): """ Update apps & system package cache """ + from yunohost.app_catalog import _update_apps_catalog + from yunohost.app import _list_upgradable_apps if not target: target = "all" @@ -338,9 +333,11 @@ def tools_update(target=None): # stdout goes to debug lambda l: logger.debug(l.rstrip()), # stderr goes to warning except for the boring apt messages - lambda l: logger.warning(l.rstrip()) - if is_legit_warning(l) - else logger.debug(l.rstrip()), + lambda l: ( + logger.warning(l.rstrip()) + if is_legit_warning(l) + else logger.debug(l.rstrip()) + ), ) logger.info(m18n.n("updating_apt_cache")) @@ -409,6 +406,8 @@ def tools_upgrade(operation_logger, target=None): system -- True to upgrade system """ + from yunohost.app import app_upgrade, app_list + if dpkg_is_broken(): raise YunohostValidationError("dpkg_is_broken") @@ -477,12 +476,16 @@ def tools_upgrade(operation_logger, target=None): logger.debug("Running apt command :\n{}".format(dist_upgrade)) callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) - if _apt_log_line_is_relevant(l) - else logger.debug(l.rstrip()), + lambda l: ( + logger.info("+ " + l.rstrip() + "\r") + if _apt_log_line_is_relevant(l) + else logger.debug(l.rstrip() + "\r") + ), + lambda l: ( + logger.warning(l.rstrip()) + if _apt_log_line_is_relevant(l) + else logger.debug(l.rstrip()) + ), ) returncode = call_async_output(dist_upgrade, callbacks, shell=True) @@ -511,32 +514,6 @@ def tools_upgrade(operation_logger, target=None): operation_logger.success() -def _apt_log_line_is_relevant(line): - irrelevants = [ - "service sudo-ldap already provided", - "Reading database ...", - "Preparing to unpack", - "Selecting previously unselected package", - "Created symlink /etc/systemd", - "Replacing config file", - "Creating config file", - "Installing new version of config file", - "Installing new config file as you requested", - ", does not exist on system.", - "unable to delete old directory", - "update-alternatives:", - "Configuration file '/etc", - "==> Modified (by you or by a script) since installation.", - "==> Package distributor has shipped an updated version.", - "==> Keeping old config file as default.", - "is a disabled or a static unit", - " update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults", - "insserv: warning: current stop runlevel", - "insserv: warning: current start runlevel", - ] - return line.rstrip() and all(i not in line.rstrip() for i in irrelevants) - - @is_unit_operation() def tools_shutdown(operation_logger, force=False): shutdown = force @@ -611,6 +588,23 @@ def tools_shell(command=None): shell.interact() +def tools_basic_space_cleanup(): + """ + Basic space cleanup. + + apt autoremove + apt autoclean + journalctl vacuum (leaves 50M of logs) + archived logs removal + """ + subprocess.run("apt autoremove && apt autoclean", shell=True) + subprocess.run("journalctl --vacuum-size=50M", shell=True) + subprocess.run("rm /var/log/*.gz", shell=True) + subprocess.run("rm /var/log/*/*.gz", shell=True) + subprocess.run("rm /var/log/*.?", shell=True) + subprocess.run("rm /var/log/*/*.?", shell=True) + + # ############################################ # # # # Migrations management # @@ -945,9 +939,9 @@ class Migration: # Those are to be implemented by daughter classes mode = "auto" - dependencies: List[ - str - ] = [] # List of migration ids required before running this migration + dependencies: List[str] = ( + [] + ) # List of migration ids required before running this migration @property def disclaimer(self): diff --git a/src/user.py b/src/user.py index f17a60942..a60973443 100644 --- a/src/user.py +++ b/src/user.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -20,14 +20,12 @@ import os import re import pwd import grp -import crypt import random -import string import subprocess import copy +from logging import getLogger from moulinette import Moulinette, m18n -from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError, YunohostValidationError @@ -35,10 +33,10 @@ from yunohost.service import service_status from yunohost.log import is_unit_operation from yunohost.utils.system import binary_to_human -logger = getActionLogger("yunohost.user") +logger = getLogger("yunohost.user") FIELDS_FOR_IMPORT = { - "username": r"^[a-z0-9_]+$", + "username": r"^[a-z0-9_.]+$", "firstname": r"^([^\W\d_]{1,30}[ ,.\'-]{0,3})+$", "lastname": r"^([^\W\d_]{1,30}[ ,.\'-]{0,3})+$", "password": r"^|(.{3,})$", @@ -141,39 +139,27 @@ def user_create( domain, password, fullname=None, - firstname=None, - lastname=None, mailbox_quota="0", admin=False, from_import=False, loginShell=None, ): - if firstname or lastname: - logger.warning( - "Options --firstname / --lastname of 'yunohost user create' are deprecated. We recommend using --fullname instead." - ) - if not fullname or not fullname.strip(): - if not firstname.strip(): - raise YunohostValidationError( - "You should specify the fullname of the user using option -F" - ) - lastname = ( - lastname or " " - ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... - fullname = f"{firstname} {lastname}".strip() - else: - fullname = fullname.strip() - firstname = fullname.split()[0] - lastname = ( - " ".join(fullname.split()[1:]) or " " - ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... + raise YunohostValidationError( + "You should specify the fullname of the user using option -F" + ) + fullname = fullname.strip() + firstname = fullname.split()[0] + lastname = ( + " ".join(fullname.split()[1:]) or " " + ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... from yunohost.domain import domain_list, _get_maindomain, _assert_domain_exists from yunohost.hook import hook_callback from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, + _hash_user_password, ) from yunohost.utils.ldap import _get_ldap_interface @@ -181,7 +167,7 @@ def user_create( assert_password_is_compatible(password) assert_password_is_strong_enough("admin" if admin else "user", password) - # Validate domain used for email address/xmpp account + # Validate domain used for email address account if domain is None: if Moulinette.interface.type == "api": raise YunohostValidationError( @@ -319,6 +305,8 @@ def user_create( def user_delete(operation_logger, username, purge=False, from_import=False): from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface + from yunohost.authenticators.ldap_ynhuser import Authenticator as PortalAuth + from yunohost.authenticators.ldap_admin import Authenticator as AdminAuth if username not in user_list()["users"]: raise YunohostValidationError("user_unknown", user=username) @@ -347,6 +335,9 @@ def user_delete(operation_logger, username, purge=False, from_import=False): except Exception as e: raise YunohostError("user_deletion_failed", user=username, error=e) + PortalAuth.invalidate_all_sessions_for_user(username) + AdminAuth.invalidate_all_sessions_for_user(username) + # Invalidate passwd to take user deletion into account subprocess.call(["nscd", "-i", "passwd"]) @@ -364,8 +355,6 @@ def user_delete(operation_logger, username, purge=False, from_import=False): def user_update( operation_logger, username, - firstname=None, - lastname=None, mail=None, change_password=None, add_mailforward=None, @@ -377,23 +366,22 @@ def user_update( fullname=None, loginShell=None, ): - if firstname or lastname: - logger.warning( - "Options --firstname / --lastname of 'yunohost user create' are deprecated. We recommend using --fullname instead." - ) - if fullname and fullname.strip(): fullname = fullname.strip() firstname = fullname.split()[0] lastname = ( " ".join(fullname.split()[1:]) or " " ) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace... + else: + firstname = None + lastname = None from yunohost.domain import domain_list from yunohost.app import app_ssowatconf from yunohost.utils.password import ( assert_password_is_strong_enough, assert_password_is_compatible, + _hash_user_password, ) from yunohost.utils.ldap import _get_ldap_interface from yunohost.hook import hook_callback @@ -549,6 +537,11 @@ def user_update( except Exception as e: raise YunohostError("user_update_failed", user=username, error=e) + if "userPassword" in new_attr_dict: + logger.info("Invalidating sessions") + from yunohost.authenticators.ldap_ynhuser import Authenticator as PortalAuth + PortalAuth.invalidate_all_sessions_for_user(username) + # Invalidate passwd and group to update the loginShell subprocess.call(["nscd", "-i", "passwd"]) subprocess.call(["nscd", "-i", "group"]) @@ -616,7 +609,7 @@ def user_info(username): if service_status("dovecot")["status"] != "running": logger.warning(m18n.n("mailbox_used_space_dovecot_down")) elif username not in user_permission_info("mail.main")["corresponding_users"]: - logger.warning(m18n.n("mailbox_disabled", user=username)) + logger.debug(m18n.n("mailbox_disabled", user=username)) else: try: uid_ = user["uid"][0] @@ -884,8 +877,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False): user_update( new_infos["username"], - firstname=new_infos["firstname"], - lastname=new_infos["lastname"], + fullname=(new_infos["firstname"] + " " + new_infos["lastname"]).strip(), change_password=new_infos["password"], mailbox_quota=new_infos["mailbox-quota"], mail=new_infos["mail"], @@ -930,8 +922,7 @@ def user_import(operation_logger, csvfile, update=False, delete=False): user["password"], user["mailbox-quota"], from_import=True, - firstname=user["firstname"], - lastname=user["lastname"], + fullname=(user["firstname"] + " " + user["lastname"]).strip(), ) update(user) result["created"] += 1 @@ -1189,6 +1180,7 @@ def user_group_update( ) else: operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_add", group=groupname, user=user)) new_group_members += users_to_add @@ -1202,6 +1194,7 @@ def user_group_update( ) else: operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_remove", group=groupname, user=user)) # Remove users_to_remove from new_group_members # Kinda like a new_group_members -= users_to_remove @@ -1237,6 +1230,7 @@ def user_group_update( "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] ) new_group_mail.append(mail) + logger.info(m18n.n("group_mailalias_add", group=groupname, mail=mail)) if remove_mailalias: from yunohost.domain import _get_maindomain @@ -1256,6 +1250,9 @@ def user_group_update( ) if mail in new_group_mail: new_group_mail.remove(mail) + logger.info( + m18n.n("group_mailalias_remove", group=groupname, mail=mail) + ) else: raise YunohostValidationError("mail_alias_remove_failed", mail=mail) @@ -1280,6 +1277,11 @@ def user_group_update( except Exception as e: raise YunohostError("group_update_failed", group=groupname, error=e) + if groupname == "admins" and remove: + from yunohost.authenticators.ldap_admin import Authenticator as AdminAuth + for user in users_to_remove: + AdminAuth.invalidate_all_sessions_for_user(user) + if sync_perm: permission_sync_to_user() @@ -1436,36 +1438,6 @@ def user_ssh_remove_key(username, key): # End SSH subcategory # - -def _hash_user_password(password): - """ - This function computes and return a salted hash for the password in input. - This implementation is inspired from [1]. - - The hash follows SHA-512 scheme from Linux/glibc. - Hence the {CRYPT} and $6$ prefixes - - {CRYPT} means it relies on the OS' crypt lib - - $6$ corresponds to SHA-512, the strongest hash available on the system - - The salt is generated using random.SystemRandom(). It is the crypto-secure - pseudo-random number generator according to the python doc [2] (c.f. the - red square). It internally relies on /dev/urandom - - The salt is made of 16 characters from the set [./a-zA-Z0-9]. This is the - max sized allowed for salts according to [3] - - [1] https://www.redpill-linpro.com/techblog/2016/08/16/ldap-password-hash.html - [2] https://docs.python.org/2/library/random.html - [3] https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html - """ - - char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" - salt = "".join([random.SystemRandom().choice(char_set) for x in range(16)]) - - 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"] diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 7c1e7b0cd..86d229abb 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 2c56eb754..80a156b2a 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -19,33 +19,391 @@ import glob import os import re -import urllib.parse from collections import OrderedDict -from typing import Union +from logging import getLogger +from typing import TYPE_CHECKING, Any, Iterator, Literal, Sequence, Type, Union, cast + +from pydantic import BaseModel, Extra, validator 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, + AnyOption, + BaseInputOption, + BaseReadonlyOption, FileOption, - ask_questions_and_parse_answers, + OptionsModel, + OptionType, + Translation, + build_form, evaluate_simple_js_expression, + parse_prefilled_values, + prompt_or_validate_form, ) from yunohost.utils.i18n import _value_for_locale -logger = getActionLogger("yunohost.configpanel") +if TYPE_CHECKING: + from pydantic.fields import ModelField + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + + from yunohost.utils.form import FormModel, Hooks + from yunohost.log import OperationLogger + +if TYPE_CHECKING: + from moulinette.utils.log import MoulinetteLogger + + logger = cast(MoulinetteLogger, getLogger("yunohost.configpanel")) +else: + logger = getLogger("yunohost.configpanel") + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭╮╮╭─╮┌─╮┌─╴╷ ╭─╴ │ +# │ ││││ ││ │├─╴│ ╰─╮ │ +# │ ╵╵╵╰─╯└─╯╰─╴╰─╴╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + CONFIG_PANEL_VERSION_SUPPORTED = 1.0 +class ContainerModel(BaseModel): + id: str + name: Union[Translation, None] = None + services: list[str] = [] + help: Union[Translation, None] = None + + def translate(self, i18n_key: Union[str, None] = None) -> None: + """ + Translate `ask` and `name` attributes of panels and section. + This is in place mutation. + """ + + for key in ("help", "name"): + value = getattr(self, key) + if value: + setattr(self, key, _value_for_locale(value)) + elif key == "help" and m18n.key_exists(f"{i18n_key}_{self.id}_help"): + setattr(self, key, m18n.n(f"{i18n_key}_{self.id}_help")) + + +class SectionModel(ContainerModel, OptionsModel): + """ + Sections are, basically, options grouped together. Sections are `dict`s defined inside a Panel and require a unique id (in the below example, the id is `customization` prepended by the panel's id `main`). Keep in mind that this combined id will be used in CLI to refer to the section, so choose something short and meaningfull. Also make sure to not make a typo in the panel id, which would implicitly create an other entire panel. + + If at least one `button` is present it then become an action section. + Options in action sections are not considered settings and therefor are not saved, they are more like parameters that exists only during the execution of an action. + FIXME i'm not sure we have this in code. + + #### Examples + ```toml + [main] + + [main.customization] + name.en = "Advanced configuration" + name.fr = "Configuration avancée" + help = "Every form items in this section are not saved." + services = ["__APP__", "nginx"] + + [main.customization.option_id] + type = "string" + # …refer to Options doc + ``` + + #### Properties + - `name` (optional): `Translation` or `str`, displayed as the section's title if any + - `help`: `Translation` or `str`, text to display before the first option + - `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the section changes + - `"__APP__` will refer to the app instance name + - `optional`: `bool` (default: `true`), set the default `optional` prop of all Options in the section + - `visible`: `bool` or `JSExpression` (default: `true`), allow to conditionally display a section depending on user's answers to previous questions. + - Be careful that the `visible` property should only refer to **previous** options's value. Hence, it should not make sense to have a `visible` property on the very first section. + """ + + visible: Union[bool, str] = True + optional: bool = True + is_action_section: bool = False + bind: Union[str, None] = None + + class Config: + @staticmethod + def schema_extra(schema: dict[str, Any]) -> None: + del schema["properties"]["id"] + options = schema["properties"].pop("options") + del schema["required"] + schema["additionalProperties"] = options["items"] + + # Don't forget to pass arguments to super init + def __init__( + self, + id: str, + name: Union[Translation, None] = None, + services: list[str] = [], + help: Union[Translation, None] = None, + visible: Union[bool, str] = True, + optional: bool = True, + bind: Union[str, None] = None, + **kwargs, + ) -> None: + options = self.options_dict_to_list(kwargs, optional=optional) + is_action_section = any( + [option["type"] == OptionType.button for option in options] + ) + ContainerModel.__init__( # type: ignore + self, + id=id, + name=name, + services=services, + help=help, + visible=visible, + bind=bind, + options=options, + is_action_section=is_action_section, + ) + + def is_visible(self, context: dict[str, Any]) -> bool: + if isinstance(self.visible, bool): + return self.visible + + return evaluate_simple_js_expression(self.visible, context=context) + + def translate(self, i18n_key: Union[str, None] = None) -> None: + """ + Call to `Container`'s `translate` for self translation + + Call to `OptionsContainer`'s `translate_options` for options translation + """ + super().translate(i18n_key) + self.translate_options(i18n_key) + + +class PanelModel(ContainerModel): + """ + Panels are, basically, sections grouped together. Panels are `dict`s defined inside a ConfigPanel file and require a unique id (in the below example, the id is `main`). Keep in mind that this id will be used in CLI to refer to the panel, so choose something short and meaningfull. + + #### Examples + ```toml + [main] + name.en = "Main configuration" + name.fr = "Configuration principale" + help = "" + services = ["__APP__", "nginx"] + + [main.customization] + # …refer to Sections doc + ``` + #### Properties + - `name`: `Translation` or `str`, displayed as the panel title + - `help` (optional): `Translation` or `str`, text to display before the first section + - `services` (optional): `list` of services names to `reload-or-restart` when any option's value contained in the panel changes + - `"__APP__` will refer to the app instance name + - `actions`: FIXME not sure what this does + """ + + # FIXME what to do with `actions? + actions: dict[str, Translation] = {"apply": {"en": "Apply"}} + bind: Union[str, None] = None + sections: list[SectionModel] + + class Config: + extra = Extra.allow + + @staticmethod + def schema_extra(schema: dict[str, Any]) -> None: + del schema["properties"]["id"] + del schema["properties"]["sections"] + del schema["required"] + schema["additionalProperties"] = {"$ref": "#/definitions/SectionModel"} + + # Don't forget to pass arguments to super init + def __init__( + self, + id: str, + name: Union[Translation, None] = None, + services: list[str] = [], + help: Union[Translation, None] = None, + bind: Union[str, None] = None, + **kwargs, + ) -> None: + sections = [data | {"id": name} for name, data in kwargs.items()] + super().__init__( # type: ignore + id=id, name=name, services=services, help=help, bind=bind, sections=sections + ) + + def translate(self, i18n_key: Union[str, None] = None) -> None: + """ + Recursivly mutate translatable attributes to their translation + """ + super().translate(i18n_key) + + for section in self.sections: + section.translate(i18n_key) + + +class ConfigPanelModel(BaseModel): + """ + This is the 'root' level of the config panel toml file + + #### Examples + + ```toml + version = 1.0 + + [config] + # …refer to Panels doc + ``` + + #### Properties + + - `version`: `float` (default: `1.0`), version that the config panel supports in terms of features. + - `i18n` (optional): `str`, an i18n property that let you internationalize options text. + - However this feature is only available in core configuration panel (like `yunohost domain config`), prefer the use `Translation` in `name`, `help`, etc. + + """ + + version: float = CONFIG_PANEL_VERSION_SUPPORTED + i18n: Union[str, None] = None + panels: list[PanelModel] + + class Config: + arbitrary_types_allowed = True + extra = Extra.allow + + @staticmethod + def schema_extra(schema: dict[str, Any]) -> None: + """Update the schema to the expected input + In actual TOML definition, schema is like: + ```toml + [panel_1] + [panel_1.section_1] + [panel_1.section_1.option_1] + ``` + Which is equivalent to `{"panel_1": {"section_1": {"option_1": {}}}}` + so `section_id` (and `option_id`) are additional property of `panel_id`, + which is convinient to write but not ideal to iterate. + In ConfigPanelModel we gather additional properties of panels, sections + and options as lists so that structure looks like: + `{"panels`: [{"id": "panel_1", "sections": [{"id": "section_1", "options": [{"id": "option_1"}]}]}] + """ + del schema["properties"]["panels"] + del schema["required"] + schema["additionalProperties"] = {"$ref": "#/definitions/PanelModel"} + + # Don't forget to pass arguments to super init + def __init__( + self, + version: float, + i18n: Union[str, None] = None, + **kwargs, + ) -> None: + panels = [data | {"id": name} for name, data in kwargs.items()] + super().__init__(version=version, i18n=i18n, panels=panels) + + @property + def sections(self) -> Iterator[SectionModel]: + """Convinient prop to iter on all sections""" + for panel in self.panels: + for section in panel.sections: + yield section + + @property + def options(self) -> Iterator[AnyOption]: + """Convinient prop to iter on all options""" + for section in self.sections: + for option in section.options: + yield option + + def get_option(self, option_id) -> Union[AnyOption, None]: + for option in self.options: + if option.id == option_id: + return option + # FIXME raise error? + return None + + @property + def services(self) -> list[str]: + services = set() + for panel in self.panels: + services |= set(panel.services) + for section in panel.sections: + services |= set(section.services) + + services_ = list(services) + services_.sort(key="nginx".__eq__) + return services_ + + def iter_children( + self, + trigger: list[Literal["panel", "section", "option", "action"]] = ["option"], + ): + for panel in self.panels: + if "panel" in trigger: + yield (panel, None, None) + for section in panel.sections: + if "section" in trigger: + yield (panel, section, None) + if "action" in trigger: + for option in section.options: + if option.type is OptionType.button: + yield (panel, section, option) + if "option" in trigger: + for option in section.options: + yield (panel, section, option) + + def translate(self) -> None: + """ + Recursivly mutate translatable attributes to their translation + """ + for panel in self.panels: + panel.translate(self.i18n) + + @validator("version", always=True) + def check_version(cls, value: float, field: "ModelField") -> float: + if value < CONFIG_PANEL_VERSION_SUPPORTED: + raise ValueError( + f"Config panels version '{value}' are no longer supported." + ) + + return value + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭─╴╭─╮╭╮╷┌─╴╶┬╴╭─╮ ╶┬╴╭╮╮┌─╮╷ │ +# │ │ │ ││││├─╴ │ │╶╮ │ │││├─╯│ │ +# │ ╰─╴╰─╯╵╰╯╵ ╶┴╴╰─╯ ╶┴╴╵╵╵╵ ╰─╴ │ +# ╰───────────────────────────────────────────────────────╯ + +if TYPE_CHECKING: + FilterKey = Sequence[Union[str, None]] + RawConfig = OrderedDict[str, Any] + RawSettings = dict[str, Any] + ConfigPanelGetMode = Literal["classic", "full", "export"] + + +def parse_filter_key(key: Union[str, None] = None) -> "FilterKey": + if key and key.count(".") > 2: + raise YunohostError( + f"The filter key {key} has too many sub-levels, the max is 3.", + raw_msg=True, + ) + + if not key: + return (None, None, None) + keys = key.split(".") + return tuple(keys[i] if len(keys) > i else None for i in range(3)) + + class ConfigPanel: entity_type = "config" save_path_tpl: Union[str, None] = None config_path_tpl = "/usr/share/yunohost/config_{entity_type}.toml" save_mode = "full" + settings_must_be_defined: bool = False + filter_key: "FilterKey" = (None, None, None) + config: Union[ConfigPanelModel, None] = None + form: Union["FormModel", None] = None + raw_settings: "RawSettings" = {} + hooks: "Hooks" = {} @classmethod def list(cls): @@ -64,7 +422,9 @@ class ConfigPanel: entities = [] return entities - def __init__(self, entity, config_path=None, save_path=None, creation=False): + def __init__( + self, entity, config_path=None, save_path=None, creation=False + ) -> None: self.entity = entity self.config_path = config_path if not config_path: @@ -74,9 +434,6 @@ class ConfigPanel: self.save_path = save_path if not save_path and self.save_path_tpl: self.save_path = self.save_path_tpl.format(entity=entity) - self.config = {} - self.values = {} - self.new_values = {} if ( self.save_path @@ -100,85 +457,87 @@ class ConfigPanel: and re.match("^(validate|post_ask)__", func) } - def get(self, key="", mode="classic"): - self.filter_key = key or "" + def get( + self, key: Union[str, None] = None, mode: "ConfigPanelGetMode" = "classic" + ) -> Any: + self.filter_key = parse_filter_key(key) + self.config, self.form = self._get_config_panel(prevalidate=False) - # Read config panel toml - self._get_config_panel() - - if not self.config: - raise YunohostValidationError("config_no_panel") - - # Read or get values and hydrate the config - self._get_raw_settings() - self._hydrate() + panel_id, section_id, option_id = self.filter_key # In 'classic' mode, we display the current value if key refer to an option - if self.filter_key.count(".") == 2 and mode == "classic": - option = self.filter_key.split(".")[-1] - value = self.values.get(option, None) + if option_id and mode == "classic": + option = self.config.get_option(option_id) - option_type = None - for _, _, option_ in self._iterate(): - if option_["id"] == option: - option_type = OPTIONS[option_["type"]] - break + if option is None: + # FIXME i18n + raise YunohostValidationError( + f"Couldn't find any option with id {option_id}", raw_msg=True + ) - return option_type.normalize(value) if option_type else value + if isinstance(option, BaseReadonlyOption): + return None + + return option.normalize(self.form[option_id], option) # Format result in 'classic' or 'export' mode + self.config.translate() logger.debug(f"Formating result in '{mode}' mode") - result = {} - for panel, section, option in self._iterate(): - if section["is_action_section"] and mode != "full": - continue - - 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: + result = self.config.dict(exclude_none=True) + + for panel in result["panels"]: + for section in panel["sections"]: + for opt in section["options"]: + instance = self.config.get_option(opt["id"]) + if isinstance(instance, BaseInputOption): + opt["value"] = instance.normalize( + self.form[opt["id"]], instance + ) return result + result = OrderedDict() + + for panel in self.config.panels: + for section in panel.sections: + if section.is_action_section and mode != "full": + continue + + for option in section.options: + # FIXME not sure why option resolves as possibly `None` + option = cast(AnyOption, option) + + if mode == "export": + if isinstance(option, BaseInputOption): + result[option.id] = self.form[option.id] + continue + + if mode == "classic": + key = f"{panel.id}.{section.id}.{option.id}" + result[key] = {"ask": option.ask} + + if isinstance(option, BaseInputOption): + result[key]["value"] = option.humanize( + self.form[option.id], option + ) + if option.type is OptionType.password: + result[key][ + "value" + ] = "**************" # Prevent displaying password in `config get` + + 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") + self, + key: Union[str, None] = None, + value: Any = None, + args: Union[str, None] = None, + args_file: Union[str, None] = None, + operation_logger: Union["OperationLogger", None] = None, + ) -> None: + self.filter_key = parse_filter_key(key) + panel_id, section_id, option_id = self.filter_key if (args is not None or args_file is not None) and value is not None: raise YunohostValidationError( @@ -186,24 +545,35 @@ class ConfigPanel: raw_msg=True, ) - if self.filter_key.count(".") != 2 and value is not None: + if not option_id and value is not None: raise YunohostValidationError("config_cant_set_value_on_section") # Import and parse pre-answered options logger.debug("Import and parse pre-answered options") - self._parse_pre_answered(args, value, args_file) + if option_id and value is not None: + prefilled_answers = {option_id: value} + else: + prefilled_answers = parse_prefilled_values(args, args_file) - # Read or get values and hydrate the config - self._get_raw_settings() - self._hydrate() - BaseOption.operation_logger = operation_logger - self._ask() + self.config, self.form = self._get_config_panel() + # FIXME find a better way to exclude previous settings + previous_settings = self.form.dict() + + # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) + # BaseOption.operation_logger = operation_logger + + self.form = self._ask( + self.config, + self.form, + prefilled_answers=prefilled_answers, + hooks=self.hooks, + ) if operation_logger: operation_logger.start() try: - self._apply() + self._apply(self.form, previous_settings) except YunohostError: raise # Script got manually interrupted ... @@ -229,46 +599,59 @@ class ConfigPanel: self._reload_services() logger.success("Config updated as expected") - operation_logger.success() - def list_actions(self): + if operation_logger: + operation_logger.success() + + def list_actions(self) -> dict[str, str]: 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"]) + self.config, self.form = self._get_config_panel() + + for panel, section, option in self.config.iter_children(): + if option.type == OptionType.button: + key = f"{panel.id}.{section.id}.{option.id}" + actions[key] = _value_for_locale(option.ask) return actions - def run_action(self, action=None, args=None, args_file=None, operation_logger=None): + def run_action( + self, + key: Union[str, None] = None, + args: Union[str, None] = None, + args_file: Union[str, None] = None, + operation_logger: Union["OperationLogger", None] = None, + ) -> None: # # FIXME : this stuff looks a lot like set() ... # + panel_id, section_id, action_id = parse_filter_key(key) + # since an action may require some options from its section, + # remove the action_id from the filter + self.filter_key = (panel_id, section_id, None) - self.filter_key = ".".join(action.split(".")[:2]) - action_id = action.split(".")[2] - - # Read config panel toml - self._get_config_panel() + self.config, self.form = self._get_config_panel() # FIXME: should also check that there's indeed a key called action - if not self.config: - raise YunohostValidationError(f"No action named {action}", raw_msg=True) + if not action_id or not self.config.get_option(action_id): + raise YunohostValidationError(f"No action named {action_id}", raw_msg=True) # Import and parse pre-answered options logger.debug("Import and parse pre-answered options") - self._parse_pre_answered(args, None, args_file) + prefilled_answers = parse_prefilled_values(args, args_file) - # Read or get values and hydrate the config - self._get_raw_settings() - self._hydrate() - BaseOption.operation_logger = operation_logger - self._ask(action=action_id) + self.form = self._ask( + self.config, + self.form, + prefilled_answers=prefilled_answers, + action_id=action_id, + hooks=self.hooks, + ) + + # FIXME Not sure if this is need (redact call to operation logger does it on all the instances) + # BaseOption.operation_logger = operation_logger # FIXME: here, we could want to check constrains on # the action's visibility / requirements wrt to the answer to questions ... @@ -277,21 +660,21 @@ class ConfigPanel: operation_logger.start() try: - self._run_action(action_id) + self._run_action(self.form, action_id) except YunohostError: raise # Script got manually interrupted ... # N.B. : KeyboardInterrupt does not inherit from Exception except (KeyboardInterrupt, EOFError): error = m18n.n("operation_interrupted") - logger.error(m18n.n("config_action_failed", action=action, error=error)) + logger.error(m18n.n("config_action_failed", action=key, error=error)) raise # Something wrong happened in Yunohost's code (most probably hook_exec) except Exception: import traceback error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) - logger.error(m18n.n("config_action_failed", action=action, error=error)) + logger.error(m18n.n("config_action_failed", action=key, error=error)) raise finally: # Delete files uploaded from API @@ -302,385 +685,240 @@ class ConfigPanel: # FIXME: i18n logger.success(f"Action {action_id} successful") - operation_logger.success() - def _get_raw_config(self): + if operation_logger: + operation_logger.success() + + def _get_raw_config(self) -> "RawConfig": + if not os.path.exists(self.config_path): + raise YunohostValidationError("config_no_panel") + return read_toml(self.config_path) - def _get_config_panel(self): - # Split filter_key - filter_key = self.filter_key.split(".") if self.filter_key != "" else [] - if len(filter_key) > 3: - raise YunohostError( - f"The filter key {filter_key} has too many sub-levels, the max is 3.", - raw_msg=True, + def _get_raw_settings(self) -> "RawSettings": + if not self.save_path or not os.path.exists(self.save_path): + return {} + + return read_yaml(self.save_path) or {} + + def _get_partial_raw_config(self) -> "RawConfig": + def filter_keys( + data: "RawConfig", + key: str, + model: Union[Type[ConfigPanelModel], Type[PanelModel], Type[SectionModel]], + ) -> "RawConfig": + # filter in keys defined in model, filter out panels/sections/options that aren't `key` + return OrderedDict( + {k: v for k, v in data.items() if k in model.__fields__ or k == key} ) - if not os.path.exists(self.config_path): - logger.debug(f"Config panel {self.config_path} doesn't exists") - return None + raw_config = self._get_raw_config() - toml_config_panel = self._get_raw_config() - - # 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") + panel_id, section_id, option_id = self.filter_key try: - self.config["panels"][0]["sections"][0]["options"][0] + if panel_id: + raw_config = filter_keys(raw_config, panel_id, ConfigPanelModel) + + if section_id: + raw_config[panel_id] = filter_keys( + raw_config[panel_id], section_id, PanelModel + ) + + if option_id: + raw_config[panel_id][section_id] = filter_keys( + raw_config[panel_id][section_id], option_id, SectionModel + ) + except KeyError: + raise YunohostValidationError( + "config_unknown_filter_key", + filter_key=".".join([k for k in self.filter_key if k]), + ) + + return raw_config + + def _get_partial_raw_settings_and_mutate_config( + self, config: ConfigPanelModel + ) -> tuple[ConfigPanelModel, "RawSettings"]: + raw_settings = self._get_raw_settings() + # Save `raw_settings` for diff at `_apply` + self.raw_settings = raw_settings + values = {} + + for _, section, option in config.iter_children(): + value = data = raw_settings.get(option.id, getattr(option, "default", None)) + + if isinstance(option, BaseInputOption) and option.id not in raw_settings: + if option.default is not None: + value = option.default + elif option.type is OptionType.file or option.bind == "null": + continue + elif self.settings_must_be_defined: + raise YunohostError( + f"Config panel question '{option.id}' should be initialized with a value during install or upgrade.", + raw_msg=True, + ) + + if isinstance(data, dict): + # Settings data if gathered from bash "ynh_app_config_show" + # may be a custom getter that returns a dict with `value` or `current_value` + # and other attributes meant to override those of the option. + + if "value" in data: + value = data.pop("value") + + # Allow to use value instead of current_value in app config script. + # e.g. apps may write `echo 'value: "foobar"'` in the config file (which is more intuitive that `echo 'current_value: "foobar"'` + # For example hotspot used it... + # See https://github.com/YunoHost/yunohost/pull/1546 + # FIXME do we still need the `current_value`? + if "current_value" in data: + value = data.pop("current_value") + + # Mutate other possible option attributes + for k, v in data.items(): + setattr(option, k, v) + + if isinstance(option, BaseInputOption): # or option.bind == "null": + values[option.id] = value + + return (config, values) + + def _get_config_panel( + self, prevalidate: bool = False + ) -> tuple[ConfigPanelModel, "FormModel"]: + raw_config = self._get_partial_raw_config() + config = ConfigPanelModel(**raw_config) + config, raw_settings = self._get_partial_raw_settings_and_mutate_config(config) + config.translate() + Settings = build_form(config.options) + settings = ( + Settings(**raw_settings) + if prevalidate + else Settings.construct(**raw_settings) + ) + + try: + 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"] + return (config, settings) - 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): + def _ask( + self, + config: ConfigPanelModel, + form: "FormModel", + prefilled_answers: dict[str, Any] = {}, + action_id: Union[str, None] = None, + hooks: "Hooks" = {}, + ) -> "FormModel": + # FIXME could be turned into a staticmethod logger.debug("Ask unanswered question and prevalidate data") - if "i18n" in self.config: - for panel, section, option in self._iterate(): - if "ask" not in option: - option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"]) - # auto add i18n help text if present in locales - if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"): - option["help"] = m18n.n( - self.config["i18n"] + "_" + option["id"] + "_help" - ) + interactive = Moulinette.interface.type == "cli" and os.isatty(1) + verbose = action_id is None or len(list(config.options)) > 1 - def display_header(message): - """CLI panel/section header display""" - if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2: - Moulinette.display(colorize(message, "purple")) + if interactive: + config.translate() - for panel, section, obj in self._iterate(["panel", "section"]): - if ( - section - and section.get("visible") - and not evaluate_simple_js_expression( - section["visible"], context=self.future_values + for panel in config.panels: + if interactive and verbose: + Moulinette.display( + colorize(f"\n{'='*40}\n>>>> {panel.name}\n{'='*40}", "purple") ) - ): - continue - # Ugly hack to skip action section ... except when when explicitly running actions - if not action: - if section and section["is_action_section"]: + # A section or option may only evaluate its conditions (`visible` + # and `enabled`) with its panel's local context that is built + # prompt after prompt. + # That means that a condition can only reference options of its + # own panel and options that are previously defined. + context: dict[str, Any] = {} + + for section in panel.sections: + if ( + action_id is None and section.is_action_section + ) or not section.is_visible(context): continue - if panel == obj: - name = _value_for_locale(panel["name"]) - display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}") - else: - name = _value_for_locale(section["name"]) - if name: - display_header(f"\n# {name}") - elif section: + if interactive and verbose and section.name: + Moulinette.display(colorize(f"\n# {section.name}", "purple")) + # filter action section options in case of multiple buttons - section["options"] = [ + options = [ option - for option in section["options"] - if option.get("type", "string") != "button" - or option["id"] == action + for option in section.options + if option.type is not OptionType.button or option.id == action_id ] - if panel == obj: - continue + form = prompt_or_validate_form( + options, + form, + prefilled_answers=prefilled_answers, + context=context, + hooks=hooks, + ) - # Check and ask unanswered questions - prefilled_answers = self.args.copy() - prefilled_answers.update(self.new_values) + return form - 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): + def _apply( + self, + form: "FormModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ) -> None: + """ + Save settings in yaml file. + If `save_mode` is `"diff"` (which is the default), only values that are + different from their default value will be saved. + """ logger.info("Saving the new configuration...") + dir_path = os.path.dirname(os.path.realpath(self.save_path)) if not os.path.exists(dir_path): mkdir(dir_path, mode=0o700) - values_to_save = self.future_values - if self.save_mode == "diff": - defaults = self._get_default_values() - values_to_save = { - k: v for k, v in values_to_save.items() if defaults.get(k) != v + exclude_defaults = self.save_mode == "diff" + # get settings keys filtered by filter_key + partial_settings_keys = form.__fields__.keys() + # get filtered settings + partial_settings = form.dict(exclude_defaults=exclude_defaults, exclude=exclude) # type: ignore + # get previous settings that we will updated with new settings + current_settings = self.raw_settings.copy() + + if exclude: + current_settings = { + key: value + for key, value in current_settings.items() + if key not in exclude } - # Save the settings to the .yaml file - write_to_yaml(self.save_path, values_to_save) + for key in partial_settings_keys: + if ( + exclude_defaults + and key not in partial_settings + and key in current_settings + ): + del current_settings[key] + elif key in partial_settings: + current_settings[key] = partial_settings[key] - def _reload_services(self): + # Save the settings to the .yaml file + write_to_yaml(self.save_path, current_settings) + + def _run_action(self, form: "FormModel", action_id: str) -> None: + raise NotImplementedError() + + def _reload_services(self) -> None: from yunohost.service import service_reload_or_restart - services_to_reload = set() - for panel, section, obj in self._iterate(["panel", "section", "option"]): - services_to_reload |= set(obj.get("services", [])) + services_to_reload = self.config.services if self.config else [] - 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 b3ca4b564..b48aa9136 100644 --- a/src/utils/dns.py +++ b/src/utils/dns.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -21,7 +21,7 @@ from typing import List from moulinette.utils.filesystem import read_file -SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] +SPECIAL_USE_TLDS = ["home.arpa", "local", "localhost", "onion", "test"] YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] diff --git a/src/utils/error.py b/src/utils/error.py index 9be48c5df..3ed62d24a 100644 --- a/src/utils/error.py +++ b/src/utils/error.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # diff --git a/src/utils/form.py b/src/utils/form.py index 12c3249c3..21f4b2015 100644 --- a/src/utils/form.py +++ b/src/utils/form.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 YunoHost Contributors +# Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # @@ -17,23 +17,53 @@ # along with this program. If not, see . # import ast +import datetime import operator as op import os import re import shutil import tempfile import urllib.parse -from typing import Any, Callable, Dict, List, Mapping, Optional, Union +from enum import Enum +from logging import getLogger +from typing import ( + TYPE_CHECKING, + cast, + overload, + Annotated, + Any, + Callable, + Iterable, + Literal, + Mapping, + Type, + Union, +) + +from pydantic import ( + BaseModel, + Extra, + ValidationError, + create_model, + validator, + root_validator, +) +from pydantic.color import Color +from pydantic.fields import Field +from pydantic.networks import EmailStr, HttpUrl +from pydantic.types import constr from moulinette import Moulinette, m18n from moulinette.interfaces.cli import colorize -from moulinette.utils.filesystem import read_file, write_to_file -from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_yaml, write_to_file from yunohost.log import OperationLogger from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.i18n import _value_for_locale -logger = getActionLogger("yunohost.form") +if TYPE_CHECKING: + from pydantic.fields import ModelField, FieldInfo + +logger = getLogger("yunohost.form") # ╭───────────────────────────────────────────────────────╮ @@ -91,11 +121,11 @@ def evaluate_simple_ast(node, context=None): ): # left = evaluate_simple_ast(node.left, context) right = evaluate_simple_ast(node.right, context) - if type(node.op) == ast.Add: + if type(node.op) is ast.Add: if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 left = str(left) right = str(right) - elif type(left) != type(right): # support "111" - "1" -> 110 + elif type(left) is type(right): # support "111" - "1" -> 110 left = float(left) right = float(right) @@ -115,7 +145,7 @@ def evaluate_simple_ast(node, context=None): left = float(left) right = float(right) except ValueError: - return type(operator) == ast.NotEq + return type(operator) is ast.NotEq try: return operators[type(operator)](left, right) except TypeError: # support "e" > 1 -> False like in JS @@ -179,7 +209,7 @@ def js_to_python(expr): return py_expr -def evaluate_simple_js_expression(expr, context={}): +def evaluate_simple_js_expression(expr: str, context: dict[str, Any] = {}) -> bool: if not expr.strip(): return False node = ast.parse(js_to_python(expr), mode="eval").body @@ -193,46 +223,408 @@ def evaluate_simple_js_expression(expr, context={}): # ╰───────────────────────────────────────────────────────╯ -class BaseOption: - hide_user_input_in_prompt = False - pattern: Optional[Dict] = None +class OptionType(str, Enum): + # display + display_text = "display_text" + markdown = "markdown" + alert = "alert" + # action + button = "button" + # text + string = "string" + text = "text" + password = "password" + color = "color" + # numeric + number = "number" + range = "range" + # boolean + boolean = "boolean" + # time + date = "date" + time = "time" + # location + email = "email" + path = "path" + url = "url" + # file + file = "file" + # choice + select = "select" + tags = "tags" + # entity + domain = "domain" + app = "app" + user = "user" + group = "group" - def __init__( - self, - question: Dict[str, Any], - context: Mapping[str, Any] = {}, - hooks: Dict[str, Callable] = {}, - ): - self.name = question["name"] - self.context = context - self.hooks = hooks - self.type = question.get("type", "string") - self.default = question.get("default", None) - self.optional = question.get("optional", False) - self.visible = question.get("visible", None) - self.readonly = question.get("readonly", False) - # Don't restrict choices if there's none specified - self.choices = question.get("choices", None) - self.pattern = question.get("pattern", self.pattern) - self.ask = question.get("ask", self.name) - if not isinstance(self.ask, dict): - self.ask = {"en": self.ask} - self.help = question.get("help") - self.redact = question.get("redact", False) - self.filter = question.get("filter", None) - # .current_value is the currently stored value - self.current_value = question.get("current_value") - # .value is the "proposed" value which we got from the user - self.value = question.get("value") - # Use to return several values in case answer is in mutipart - self.values: Dict[str, Any] = {} - # Empty value is parsed as empty string - if self.default == "": - self.default = None +READONLY_TYPES = { + OptionType.display_text, + OptionType.markdown, + OptionType.alert, + OptionType.button, +} +FORBIDDEN_READONLY_TYPES = { + OptionType.password, + OptionType.app, + OptionType.domain, + OptionType.user, + OptionType.group, +} + +# To simplify AppConfigPanel bash scripts, we've chosen to use question +# short_ids as global variables. The consequence is that there is a risk +# of collision with other variables, notably different global variables +# used to expose old values or the type of a question... +# In addition to conflicts with bash variables, there is a direct +# conflict with the TOML properties of sections, so the keywords `name`, +# `visible`, `services`, `optional` and `help` cannot be used either. +FORBIDDEN_KEYWORDS = { + "old", + "app", + "changed", + "file_hash", + "binds", + "types", + "formats", + "getter", + "setter", + "short_setting", + "type", + "bind", + "nothing_changed", + "changes_validated", + "result", + "max_progression", + "name", + "visible", + "services", + "optional", + "help", +} + +Context = dict[str, Any] +Translation = Union[dict[str, str], str] +JSExpression = str +Values = dict[str, Any] +Mode = Literal["python", "bash"] + + +class Pattern(BaseModel): + regexp: str + error: Translation = "pydantic.value_error.str.regex" # FIXME add generic i18n key + + +class BaseOption(BaseModel): + """ + Options are fields declaration that renders as form items, button, alert or text in the web-admin and printed or prompted in CLI. + They are used in app manifests to declare the before installation form and in config panels. + + [Have a look at the app config panel doc](/packaging_apps_config_panels) for details about Panels and Sections. + + ! IMPORTANT: as for Panels and Sections you have to choose an id, but this one should be unique in all this document, even if the question is in an other panel. + + #### Example + + ```toml + [section.my_option_id] + type = "string" + # ask as `str` + ask = "The text in english" + # ask as `dict` + ask.en = "The text in english" + ask.fr = "Le texte en français" + # advanced props + visible = "my_other_option_id != 'success'" + readonly = true + # much advanced: config panel only? + bind = "null" + ``` + + #### Properties + + - `type`: the actual type of the option, such as 'markdown', 'password', 'number', 'email', ... + - `ask`: `Translation` (default to the option's `id` if not defined): + - text to display as the option's label for inputs or text to display for readonly options + - in config panels, questions are displayed on the left side and therefore have not much space to be rendered. Therefore, it is better to use a short question, and use the `help` property to provide additional details if necessary. + - `visible` (optional): `bool` or `JSExpression` (default: `true`) + - define if the option is diplayed/asked + - if `false` and used alongside `readonly = true`, you get a context only value that can still be used in `JSExpression`s + - `readonly` (optional): `bool` (default: `false`, forced to `true` for readonly types): + - If `true` for input types: forbid mutation of its value + - `bind` (optional): `Binding`, config panels only! A powerful feature that let you configure how and where the setting will be read, validated and written + - if not specified, the value will be read/written in the app `settings.yml` + - if `"null"`: + - the value will not be stored at all (can still be used in context evaluations) + - if in `scripts/config` there's a function named: + - `get__my_option_id`: the value will be gathered from this custom getter + - `set__my_option_id`: the value will be passed to this custom setter where you can do whatever you want with the value + - `validate__my_option_id`: the value will be passed to this custom validator before any custom setter + - if `bind` is a file path: + - if the path starts with `:`, the value be saved as its id's variable/property counterpart + - this only works for first level variables/properties and simple types (no array) + - else the value will be stored as the whole content of the file + - you can use `__FINALPATH__` or `__INSTALL_DIR__` in your path to point to dynamic install paths + - FIXME are other global variables accessible? + - [refer to `bind` doc for explaination and examples](#read-and-write-values-the) + """ + + type: OptionType + id: str + mode: Mode = "bash" # TODO use "python" as default mode with AppConfigPanel setuping it to "bash" + ask: Union[Translation, None] + readonly: bool = False + visible: Union[JSExpression, bool] = True + bind: Union[str, None] = None + name: Union[str, None] = None # LEGACY (replaced by `id`) + + class Config: + arbitrary_types_allowed = True + use_enum_values = True + validate_assignment = True + extra = Extra.forbid + + @staticmethod + def schema_extra(schema: dict[str, Any]) -> None: + del schema["properties"]["id"] + del schema["properties"]["name"] + schema["required"] = [ + required for required in schema.get("required", []) if required != "id" + ] + if not schema["required"]: + del schema["required"] + + @validator("id", pre=True) + def check_id_is_not_forbidden(cls, value: str) -> str: + if value in FORBIDDEN_KEYWORDS: + raise ValueError(m18n.n("config_forbidden_keyword", keyword=value)) + return value + + # FIXME Legacy, is `name` still needed? + @validator("name") + def apply_legacy_name(cls, value: Union[str, None], values: Values) -> str: + if value is None: + return values["id"] + return value + + @validator("readonly", pre=True) + def can_be_readonly(cls, value: bool, values: Values) -> bool: + if value is True and values["type"] in FORBIDDEN_READONLY_TYPES: + raise ValueError( + m18n.n( + "config_forbidden_readonly_type", + type=values["type"], + id=values["id"], + ) + ) + return value + + def is_visible(self, context: Context) -> bool: + if isinstance(self.visible, bool): + return self.visible + + return evaluate_simple_js_expression(self.visible, context=context) + + def _get_prompt_message(self, value: None) -> str: + # force type to str + # `OptionsModel.translate_options()` should have been called before calling this method + return cast(str, self.ask) + + +# ╭───────────────────────────────────────────────────────╮ +# │ DISPLAY OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +class BaseReadonlyOption(BaseOption): + readonly: Literal[True] = True + + +class DisplayTextOption(BaseReadonlyOption): + """ + Display simple multi-line content. + + #### Example + + ```toml + [section.my_option_id] + type = "display_text" + ask = "Simple text rendered as is." + ``` + """ + + type: Literal[OptionType.display_text] = OptionType.display_text + + +class MarkdownOption(BaseReadonlyOption): + """ + Display markdown multi-line content. + Markdown is currently only rendered in the web-admin + + #### Example + ```toml + [section.my_option_id] + type = "display_text" + ask = "Text **rendered** in markdown." + ``` + """ + + type: Literal[OptionType.markdown] = OptionType.markdown + + +class State(str, Enum): + success = "success" + info = "info" + warning = "warning" + danger = "danger" + + +class AlertOption(BaseReadonlyOption): + """ + Alerts displays a important message with a level of severity. + You can use markdown in `ask` but will only be rendered in the web-admin. + + #### Example + + ```toml + [section.my_option_id] + type = "alert" + ask = "The configuration seems to be manually modified..." + style = "warning" + icon = "warning" + ``` + #### Properties + + - [common properties](#common-properties) + - `style`: any of `"success|info|warning|danger"` (default: `"info"`) + - `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/) + - Currently only displayed in the web-admin + """ + + type: Literal[OptionType.alert] = OptionType.alert + style: State = State.info + icon: Union[str, None] = None + + def _get_prompt_message(self, value: None) -> str: + colors = { + State.success: "green", + State.info: "cyan", + State.warning: "yellow", + State.danger: "red", + } + message = m18n.g(self.style) if self.style != State.danger else m18n.n("danger") + return f"{colorize(message, colors[self.style])} {self.ask}" + + +class ButtonOption(BaseReadonlyOption): + """ + Triggers actions. + Available only in config panels. + Renders as a `button` in the web-admin and can be called with `yunohost [app|domain|settings] action run ` in CLI. + + Every options defined in an action section (a config panel section with at least one `button`) is guaranted to be shown/asked to the user and available in `scripts/config`'s scope. + [check examples in advanced use cases](#actions). + + #### Example + + ```toml + [section.my_option_id] + type = "button" + ask = "Break the system" + style = "danger" + icon = "bug" + # enabled only if another option's value (a `boolean` for example) is positive + enabled = "aknowledged" + ``` + + To be able to trigger an action we have to add a bash function starting with `run__` in your `scripts/config` + + ```bash + run__my_action_id() { + ynh_print_info "Running 'my_action_id' action" + } + ``` + + #### Properties + + - [common properties](#common-properties) + - `bind`: forced to `"null"` + - `style`: any of `"success|info|warning|danger"` (default: `"success"`) + - `enabled`: `JSExpression` or `bool` (default: `true`) + - when used with `JSExpression` you can enable/disable the button depending on context + - `icon` (optional): any icon name from [Fork Awesome](https://forkaweso.me/Fork-Awesome/icons/) + - Currently only displayed in the web-admin + """ + + type: Literal[OptionType.button] = OptionType.button + bind: Literal["null"] = "null" + help: Union[Translation, None] = None + style: State = State.success + icon: Union[str, None] = None + enabled: Union[JSExpression, bool] = True + + def is_enabled(self, context: Context) -> bool: + if isinstance(self.enabled, bool): + return self.enabled + + return evaluate_simple_js_expression(self.enabled, context=context) + + +# ╭───────────────────────────────────────────────────────╮ +# │ INPUT OPTIONS │ +# ╰───────────────────────────────────────────────────────╯ + + +class BaseInputOption(BaseOption): + """ + Rest of the option types available are considered `inputs`. + + #### Example + + ```toml + [section.my_option_id] + type = "string" + # …any common props… + + optional = false + redact = false + default = "some default string" + help = "You can enter almost anything!" + example = "an example string" + placeholder = "write something…" + ``` + + #### Properties + + - [common properties](#common-properties) + - `optional`: `bool` (default: `false`, but `true` in config panels) + - `redact`: `bool` (default: `false`), to redact the value in the logs when the value contain private information + - `default`: depends on `type`, the default value to assign to the option + - in case of readonly values, you can use this `default` to assign a value (or return a dynamic `default` from a custom getter) + - `help` (optional): `Translation`, to display a short help message in cli and web-admin + - `example` (optional): `str`, to display an example value in web-admin only + - `placeholder` (optional): `str`, shown in the web-admin fields only + """ + + help: Union[Translation, None] = None + example: Union[str, None] = None + placeholder: Union[str, None] = None + redact: bool = False + optional: bool = False # FIXME keep required as default? + default: Any = None + _annotation = Any + _none_as_empty_str: bool = True + + @validator("default", pre=True) + def check_empty_default(value: Any) -> Any: + if value == "": + return None + return value @staticmethod - def humanize(value, option={}): + def humanize(value: Any, option={}) -> str: + if value is None: + return "" return str(value) @staticmethod @@ -241,140 +633,97 @@ class BaseOption: value = value.strip() return value - def ask_if_needed(self): - if self.visible and not evaluate_simple_js_expression( - self.visible, context=self.context - ): - # FIXME There could be several use case if the question is not displayed: - # - we doesn't want to give a specific value - # - we want to keep the previous value - # - we want the default value - self.value = self.values[self.name] = None - return self.values + @property + def _dynamic_annotation(self) -> Any: + """ + Returns the expected type of an Option's value. + This may be dynamic based on constraints. + """ + return self._annotation - 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) + @property + def _validators(self) -> dict[str, Callable]: + return { + "pre": self._value_pre_validator, + "post": self._value_post_validator, + } - # Apply default value - class_default = getattr(self, "default_value", None) - if self.value in [None, ""] and ( - self.default is not None or class_default is not None - ): - self.value = class_default if self.default is None else self.default - - try: - # Normalize and validate - self.value = self.normalize(self.value, self) - self._value_pre_validator() - except YunohostValidationError as e: - # If in interactive cli, re-ask the current question - if i < 4 and Moulinette.interface.type == "cli" and os.isatty(1): - logger.error(str(e)) - self.value = None - continue - - # Otherwise raise the ValidationError - raise - - break - - self.value = self.values[self.name] = self._value_post_validator() - - # Search for post actions in hooks - post_hook = f"post_ask__{self.name}" - if post_hook in self.hooks: - self.values.update(self.hooks[post_hook](self)) - - return self.values - - def _prompt(self, text): - prefill = "" - if self.current_value is not None: - prefill = self.humanize(self.current_value, self) - elif self.default is not None: - prefill = self.humanize(self.default, self) - self.value = Moulinette.prompt( - message=text, - is_password=self.hide_user_input_in_prompt, - confirm=False, - prefill=prefill, - is_multiline=(self.type == "text"), - autocomplete=self.choices or [], - help=_value_for_locale(self.help), - ) - - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = _value_for_locale(self.ask) + def _get_field_attrs(self) -> dict[str, Any]: + """ + Returns attributes to build a `pydantic.Field`. + This may contains non `Field` attrs that will end up in `Field.extra`. + Those extra can be used as constraints in custom validators and ends up + in the JSON Schema. + """ + # TODO + # - help + # - placeholder + attrs: dict[str, Any] = { + "redact": self.redact, # extra + "none_as_empty_str": self._none_as_empty_str, + } if self.readonly: - text_for_user_input_in_cli = colorize(text_for_user_input_in_cli, "purple") - if self.choices: - return ( - text_for_user_input_in_cli + f" {self.choices[self.current_value]}" - ) - return text_for_user_input_in_cli + f" {self.humanize(self.current_value)}" - elif self.choices: - # Prevent displaying a shitload of choices - # (e.g. 100+ available users when choosing an app admin...) - choices = ( - list(self.choices.keys()) - if isinstance(self.choices, dict) - else self.choices - ) - choices_to_display = choices[:20] - remaining_choices = len(choices[20:]) + attrs["allow_mutation"] = False - if remaining_choices > 0: - choices_to_display += [ - m18n.n("other_available_options", n=remaining_choices) - ] + if self.example: + attrs["examples"] = [self.example] - choices_to_display = " | ".join(choices_to_display) + if self.default is not None: + attrs["default"] = self.default + else: + attrs["default"] = ... if not self.optional else None - text_for_user_input_in_cli += f" [{choices_to_display}]" + return attrs - return text_for_user_input_in_cli + def _as_dynamic_model_field(self) -> tuple[Any, "FieldInfo"]: + """ + Return a tuple of a type and a Field instance to be injected in a + custom form declaration. + """ + attrs = self._get_field_attrs() + anno = ( + self._dynamic_annotation + if not self.optional + else Union[self._dynamic_annotation, None] + ) + field = Field(default=attrs.pop("default", None), **attrs) - def _value_pre_validator(self): - if self.value in [None, ""] and not self.optional: - raise YunohostValidationError("app_argument_required", name=self.name) + return (anno, field) - # 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 _get_prompt_message(self, value: Any) -> str: + message = super()._get_prompt_message(value) - def _value_post_validator(self): - if not self.redact: - return self.value + if self.readonly: + message = colorize(message, "purple") + return f"{message} {self.humanize(value, self)}" + + return message + + @classmethod + def _value_pre_validator(cls, value: Any, field: "ModelField") -> Any: + if value == "": + return None + + return value + + @classmethod + def _value_post_validator(cls, value: Any, field: "ModelField") -> Any: + extras = field.field_info.extra + + if value is None and extras["none_as_empty_str"]: + value = "" + + if not extras.get("redact"): + return value # Tell the operation_logger to redact all password-type / secret args # Also redact the % escaped version of the password that might appear in # the 'args' section of metadata (relevant for password with non-alphanumeric char) data_to_redact = [] - if self.value and isinstance(self.value, str): - data_to_redact.append(self.value) - if self.current_value and isinstance(self.current_value, str): - data_to_redact.append(self.current_value) + if value and isinstance(value, str): + data_to_redact.append(value) + data_to_redact += [ urllib.parse.quote(data) for data in data_to_redact @@ -384,123 +733,209 @@ class BaseOption: for operation_logger in OperationLogger._instances: operation_logger.data_to_redact.extend(data_to_redact) - return self.value - - -# ╭───────────────────────────────────────────────────────╮ -# │ 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 │ -# ╰───────────────────────────────────────────────────────╯ + return value # ─ STRINGS ─────────────────────────────────────────────── -class StringOption(BaseOption): - argument_type = "string" - default_value = "" +class BaseStringOption(BaseInputOption): + default: Union[str, None] + pattern: Union[Pattern, None] = None + _annotation = str + + @property + def _dynamic_annotation(self) -> Type[str]: + if self.pattern: + return constr(regex=self.pattern.regexp) + + return self._annotation + + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + + if self.pattern: + attrs["regex_error"] = self.pattern.error # extra + + return attrs -class PasswordOption(BaseOption): - hide_user_input_in_prompt = True - argument_type = "password" - default_value = "" - forbidden_chars = "{}" +class StringOption(BaseStringOption): + r""" + Ask for a simple string. - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) - self.redact = True - if self.default is not None: - raise YunohostValidationError( - "app_argument_password_no_default", name=self.name - ) + #### Example + ```toml + [section.my_option_id] + type = "string" + default = "E10" + pattern.regexp = '^[A-F]\d\d$' + pattern.error = "Provide a room like F12 : one uppercase and 2 numbers" + ``` - def _value_pre_validator(self): - super()._value_pre_validator() + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ - if self.value not in [None, ""]: - if any(char in self.value for char in self.forbidden_chars): + type: Literal[OptionType.string] = OptionType.string + + +class TextOption(BaseStringOption): + """ + Ask for a multiline string. + Renders as a `textarea` in the web-admin and by opening a text editor on the CLI. + + #### Example + ```toml + [section.my_option_id] + type = "text" + default = "multi\\nline\\ncontent" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + + type: Literal[OptionType.text] = OptionType.text + + +FORBIDDEN_PASSWORD_CHARS = r"{}" + + +class PasswordOption(BaseInputOption): + """ + Ask for a password. + The password is tested as a regular user password (at least 8 chars) + + #### Example + ```toml + [section.my_option_id] + type = "password" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: forced to `""` + - `redact`: forced to `true` + - `example`: forbidden + """ + + type: Literal[OptionType.password] = OptionType.password + example: Literal[None] = None + default: Literal[None] = None + redact: Literal[True] = True + _annotation = str + _forbidden_chars: str = FORBIDDEN_PASSWORD_CHARS + + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + + attrs["forbidden_chars"] = self._forbidden_chars # extra + + return attrs + + @classmethod + def _value_pre_validator( + cls, value: Union[str, None], field: "ModelField" + ) -> Union[str, None]: + value = super()._value_pre_validator(value, field) + + if value is not None and value != "": + forbidden_chars: str = field.field_info.extra["forbidden_chars"] + if any(char in value for char in forbidden_chars): raise YunohostValidationError( - "pattern_password_app", forbidden_chars=self.forbidden_chars + "pattern_password_app", forbidden_chars=forbidden_chars ) # If it's an optional argument the value should be empty or strong enough from yunohost.utils.password import assert_password_is_strong_enough - assert_password_is_strong_enough("user", self.value) + assert_password_is_strong_enough("user", value) + + return value -class ColorOption(StringOption): - pattern = { - "regexp": r"^#[ABCDEFabcdef\d]{3,6}$", - "error": "config_validate_color", # i18n: config_validate_color - } +class ColorOption(BaseInputOption): + """ + Ask for a color represented as a hex value (with possibly an alpha channel). + Renders as color picker in the web-admin and as a prompt that accept named color like `yellow` in CLI. + + #### Example + ```toml + [section.my_option_id] + type = "color" + default = "#ff0" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + + type: Literal[OptionType.color] = OptionType.color + default: Union[str, None] + _annotation = Color + + @staticmethod + def humanize(value: Union[Color, str, None], option={}) -> str: + if isinstance(value, Color): + value.as_named(fallback=True) + + return super(ColorOption, ColorOption).humanize(value, option) + + @staticmethod + def normalize(value: Union[Color, str, None], option={}) -> str: + if isinstance(value, Color): + return value.as_hex() + + return super(ColorOption, ColorOption).normalize(value, option) + + @classmethod + def _value_post_validator( + cls, value: Union[Color, None], field: "ModelField" + ) -> Union[str, None]: + if isinstance(value, Color): + return value.as_hex() + + return super()._value_post_validator(value, field) # ─ NUMERIC ─────────────────────────────────────────────── -class NumberOption(BaseOption): - argument_type = "number" - default_value = None +class NumberOption(BaseInputOption): + """ + Ask for a number (an integer). - 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) + #### Example + ```toml + [section.my_option_id] + type = "number" + default = 100 + min = 50 + max = 200 + step = 5 + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `type`: `number` or `range` (input or slider in the web-admin) + - `min` (optional): minimal int value inclusive + - `max` (optional): maximal int value inclusive + - `step` (optional): currently only used in the webadmin as the `` step jump + """ + + # `number` and `range` are exactly the same, but `range` does render as a slider in web-admin + type: Literal[OptionType.number, OptionType.range] = OptionType.number + default: Union[int, None] + min: Union[int, None] = None + max: Union[int, None] = None + step: Union[int, None] = None + _annotation = int + _none_as_empty_str = False @staticmethod - def normalize(value, option={}): + def normalize(value, option={}) -> Union[int, None]: if isinstance(value, int): return value @@ -513,54 +948,70 @@ class NumberOption(BaseOption): if value in [None, ""]: return None - option = option.__dict__ if isinstance(option, BaseOption) else option + option = option.dict() if isinstance(option, BaseOption) else option raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error=m18n.n("invalid_number"), ) - def _value_pre_validator(self): - super()._value_pre_validator() - if self.value in [None, ""]: - return + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + attrs["ge"] = self.min + attrs["le"] = self.max + attrs["step"] = self.step # extra - 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), - ) + return attrs - 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), - ) + @classmethod + def _value_pre_validator( + cls, value: Union[int, None], field: "ModelField" + ) -> Union[int, None]: + value = super()._value_pre_validator(value, field) + + if value is None: + return None + + return value # ─ 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"] +class BooleanOption(BaseInputOption): + """ + Ask for a boolean. + Renders as a switch in the web-admin and a yes/no prompt in CLI. - 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 + #### Example + ```toml + [section.my_option_id] + type = "boolean" + default = 1 + yes = "agree" + no = "disagree" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `0` + - `yes` (optional): (default: `1`) define as what the thruthy value should output + - can be `true`, `True`, `"yes"`, etc. + - `no` (optional): (default: `0`) define as what the thruthy value should output + - can be `0`, `"false"`, `"n"`, etc. + """ + + type: Literal[OptionType.boolean] = OptionType.boolean + yes: Any = 1 + no: Any = 0 + default: Union[bool, int, str, None] = 0 + _annotation = Union[bool, int, str] + _yes_answers: set[str] = {"1", "yes", "y", "true", "t", "on"} + _no_answers: set[str] = {"0", "no", "n", "false", "f", "off"} + _none_as_empty_str = False @staticmethod - def humanize(value, option={}): - option = option.__dict__ if isinstance(option, BaseOption) else option + def humanize(value, option={}) -> str: + option = option.dict() if isinstance(option, BaseOption) else option yes = option.get("yes", 1) no = option.get("no", 0) @@ -576,14 +1027,14 @@ class BooleanOption(BaseOption): raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name"), + name=option.get("id"), value=value, choices="yes/no", ) @staticmethod - def normalize(value, option={}): - option = option.__dict__ if isinstance(option, BaseOption) else option + def normalize(value, option={}) -> Any: + option = option.dict() if isinstance(option, BaseOption) else option if isinstance(value, str): value = value.strip() @@ -591,8 +1042,8 @@ class BooleanOption(BaseOption): technical_yes = option.get("yes", 1) technical_no = option.get("no", 0) - no_answers = BooleanOption.no_answers - yes_answers = BooleanOption.yes_answers + no_answers = BooleanOption._no_answers + yes_answers = BooleanOption._yes_answers assert ( str(technical_yes).lower() not in no_answers @@ -601,8 +1052,8 @@ class BooleanOption(BaseOption): str(technical_no).lower() not in yes_answers ), f"'no' value can't be in {yes_answers}" - no_answers += [str(technical_no).lower()] - yes_answers += [str(technical_yes).lower()] + no_answers.add(str(technical_no).lower()) + yes_answers.add(str(technical_yes).lower()) strvalue = str(value).lower() @@ -616,7 +1067,7 @@ class BooleanOption(BaseOption): raise YunohostValidationError( "app_argument_choice_invalid", - name=option.get("name"), + name=option.get("id"), value=strvalue, choices="yes/no", ) @@ -624,65 +1075,150 @@ class BooleanOption(BaseOption): def get(self, key, default=None): return getattr(self, key, default) - def _format_text_for_user_input_in_cli(self): - text_for_user_input_in_cli = super()._format_text_for_user_input_in_cli() + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + attrs["parse"] = { # extra + True: self.yes, + False: self.no, + } + return attrs + + def _get_prompt_message(self, value: Union[bool, None]) -> str: + message = super()._get_prompt_message(value) if not self.readonly: - text_for_user_input_in_cli += " [yes | no]" + message += " [yes | no]" - return text_for_user_input_in_cli + return message + + @classmethod + def _value_post_validator( + cls, value: Union[bool, None], field: "ModelField" + ) -> Any: + if isinstance(value, bool): + return field.field_info.extra["parse"][value] + + return super()._value_post_validator(value, field) # ─ TIME ────────────────────────────────────────────────── -class DateOption(StringOption): - pattern = { - "regexp": r"^\d{4}-\d\d-\d\d$", - "error": "config_validate_date", # i18n: config_validate_date - } +class DateOption(BaseInputOption): + """ + Ask for a date in the form `"2025-06-14"`. + Renders as a date-picker in the web-admin and a regular prompt in CLI. - def _value_pre_validator(self): - from datetime import datetime + Can also take a timestamp as value that will output as an ISO date string. - super()._value_pre_validator() + #### Example + ```toml + [section.my_option_id] + type = "date" + default = "2070-12-31" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ - if self.value not in [None, ""]: - try: - datetime.strptime(self.value, "%Y-%m-%d") - except ValueError: - raise YunohostValidationError("config_validate_date") + type: Literal[OptionType.date] = OptionType.date + default: Union[str, None] + _annotation = datetime.date + + @classmethod + def _value_post_validator( + cls, value: Union[datetime.date, None], field: "ModelField" + ) -> Union[str, None]: + if isinstance(value, datetime.date): + return value.isoformat() + + return super()._value_post_validator(value, field) -class TimeOption(StringOption): - pattern = { - "regexp": r"^(?:\d|[01]\d|2[0-3]):[0-5]\d$", - "error": "config_validate_time", # i18n: config_validate_time - } +class TimeOption(BaseInputOption): + """ + Ask for an hour in the form `"22:35"`. + Renders as a date-picker in the web-admin and a regular prompt in CLI. + + #### Example + ```toml + [section.my_option_id] + type = "time" + default = "12:26" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + + type: Literal[OptionType.time] = OptionType.time + default: Union[str, int, None] + _annotation = datetime.time + + @classmethod + def _value_post_validator( + cls, value: Union[datetime.date, None], field: "ModelField" + ) -> Union[str, None]: + if isinstance(value, datetime.time): + # FIXME could use `value.isoformat()` to get `%H:%M:%S` + return value.strftime("%H:%M") + + return super()._value_post_validator(value, field) # ─ LOCATIONS ───────────────────────────────────────────── -class EmailOption(StringOption): - pattern = { - "regexp": r"^.+@.+", - "error": "config_validate_email", # i18n: config_validate_email - } +class EmailOption(BaseInputOption): + """ + Ask for an email. Validation made with [python-email-validator](https://github.com/JoshData/python-email-validator) + + #### Example + ```toml + [section.my_option_id] + type = "email" + default = "Abc.123@test-example.com" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + + type: Literal[OptionType.email] = OptionType.email + default: Union[EmailStr, None] + _annotation = EmailStr -class WebPathOption(BaseOption): - argument_type = "path" - default_value = "" +class WebPathOption(BaseStringOption): + """ + Ask for an web path (the part of an url after the domain). Used by default in app install to define from where the app will be accessible. + + #### Example + ```toml + [section.my_option_id] + type = "path" + default = "/" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + + type: Literal[OptionType.path] = OptionType.path @staticmethod - def normalize(value, option={}): - option = option.__dict__ if isinstance(option, BaseOption) else option + def normalize(value, option={}) -> str: + option = option.dict() if isinstance(option, BaseOption) else option + + if value is None: + value = "" if not isinstance(value, str): raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error="Argument for path should be a string.", ) @@ -695,161 +1231,417 @@ class WebPathOption(BaseOption): elif option.get("optional") is False: raise YunohostValidationError( "app_argument_invalid", - name=option.get("name"), + name=option.get("id"), error="Option is mandatory", ) return "/" + value.strip().strip(" /") -class URLOption(StringOption): - pattern = { - "regexp": r"^https?://.*$", - "error": "config_validate_url", # i18n: config_validate_url - } +class URLOption(BaseStringOption): + """ + Ask for any url. + #### Example + ```toml + [section.my_option_id] + type = "url" + default = "https://example.xn--zfr164b/@handle/" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + + type: Literal[OptionType.url] = OptionType.url + _annotation = HttpUrl + + @classmethod + def _value_post_validator( + cls, value: Union[HttpUrl, None], field: "ModelField" + ) -> Union[str, None]: + if isinstance(value, HttpUrl): + return str(value) + + return super()._value_post_validator(value, field) # ─ FILE ────────────────────────────────────────────────── -class FileOption(BaseOption): - argument_type = "file" - upload_dirs: List[str] = [] +class FileOption(BaseInputOption): + r""" + Ask for file. + Renders a file prompt in the web-admin and ask for a path in CLI. - def __init__( - self, question, context: Mapping[str, Any] = {}, hooks: Dict[str, Callable] = {} - ): - super().__init__(question, context, hooks) - self.accept = question.get("accept", "") + #### Example + ```toml + [section.my_option_id] + type = "file" + accept = ".json" + # bind the file to a location to save the file there + bind = "/tmp/my_file.json" + ``` + #### Properties + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `accept`: a comma separated list of extension to accept like `".conf, .ini` + - /!\ currently only work on the web-admin + """ + + type: Literal[OptionType.file] = OptionType.file + # `FilePath` for CLI (path must exists and must be a file) + # `bytes` for API (a base64 encoded file actually) + accept: Union[list[str], None] = None # currently only used by the web-admin + default: Union[str, None] + _annotation = str # TODO could be Path at some point + _upload_dirs: set[str] = set() + + @property + def _validators(self) -> dict[str, Callable]: + return { + "pre": self._value_pre_validator, + "post": ( + self._bash_value_post_validator + if self.mode == "bash" + else self._python_value_post_validator + ), + } + + def _get_field_attrs(self) -> dict[str, Any]: + attrs = super()._get_field_attrs() + + if self.accept: + attrs["accept"] = self.accept # extra + + attrs["bind"] = self.bind + + return attrs @classmethod - def clean_upload_dirs(cls): + def clean_upload_dirs(cls) -> None: # Delete files uploaded from API - for upload_dir in cls.upload_dirs: + for upload_dir in cls._upload_dirs: if os.path.exists(upload_dir): shutil.rmtree(upload_dir) - 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): + @classmethod + def _base_value_post_validator( + cls, value: Any, field: "ModelField" + ) -> tuple[bytes, str | None]: + import mimetypes + from pathlib import Path + from magic import Magic from base64 import b64decode - if not self.value: + if Moulinette.interface.type != "api": + path = Path(value) + if not (path.exists() and path.is_absolute() and path.is_file()): + raise YunohostValidationError("File doesn't exists", raw_msg=True) + content = path.read_bytes() + else: + content = b64decode(value) + + accept_list = field.field_info.extra.get("accept") + mimetype = Magic(mime=True).from_buffer(content) + + if accept_list and mimetype not in accept_list: + raise YunohostValidationError( + f"Unsupported image type : {mimetype}", raw=True + ) + + ext = mimetypes.guess_extension(mimetype) + + return content, ext + + @classmethod + def _bash_value_post_validator(cls, value: Any, field: "ModelField") -> str: + """File handling for "bash" config panels (app)""" + if not value: return "" + content, _ = cls._base_value_post_validator(value, field) + upload_dir = tempfile.mkdtemp(prefix="ynh_filequestion_") _, file_path = tempfile.mkstemp(dir=upload_dir) - FileOption.upload_dirs += [upload_dir] + FileOption._upload_dirs.add(upload_dir) - logger.debug(f"Saving file {self.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) + logger.debug(f"Saving file {field.name} for file question into {file_path}") write_to_file(file_path, content, file_mode="wb") - self.value = file_path + return file_path - return self.value + @classmethod + def _python_value_post_validator(cls, value: Any, field: "ModelField") -> str: + """File handling for "python" config panels""" + + from pathlib import Path + import hashlib + + if not value: + return "" + + bind = field.field_info.extra["bind"] + + # to avoid "filename too long" with b64 content + if len(value.encode("utf-8")) < 255: + # Check if value is an already hashed and saved filepath + path = Path(value) + if path.exists() and value == bind.format( + filename=path.stem, ext=path.suffix + ): + return value + + content, ext = cls._base_value_post_validator(value, field) + + m = hashlib.sha256() + m.update(content) + sha256sum = m.hexdigest() + filename = Path(bind.format(filename=sha256sum, ext=ext)) + filename.write_bytes(content) + + return str(filename) # ─ CHOICES ─────────────────────────────────────────────── -class TagsOption(BaseOption): - argument_type = "tags" - default_value = "" +class BaseChoicesOption(BaseInputOption): + # FIXME probably forbid choices to be None? + filter: Union[JSExpression, None] = None # filter before choices + # We do not declare `choices` here to be able to declare other fields before `choices` and acces their values in `choices` validators + # choices: Union[dict[str, Any], list[Any], None] + + @validator("choices", pre=True, check_fields=False) + def parse_comalist_choices(value: Any) -> Union[dict[str, Any], list[Any], None]: + if isinstance(value, str): + values = [value.strip() for value in value.split(",")] + return [value for value in values if value] + return value + + @property + def _dynamic_annotation(self) -> Union[object, Type[str]]: + if self.choices is not None: + choices = ( + self.choices if isinstance(self.choices, list) else self.choices.keys() + ) + return Literal[tuple(choices)] + + return self._annotation + + def _get_prompt_message(self, value: Any) -> str: + message = super()._get_prompt_message(value) + + if self.readonly: + if isinstance(self.choices, dict) and value is not None: + value = self.choices[value] + + return f"{colorize(message, 'purple')} {value}" + + if self.choices: + # Prevent displaying a shitload of choices + # (e.g. 100+ available users when choosing an app admin...) + choices = ( + list(self.choices.keys()) + if isinstance(self.choices, dict) + else self.choices + ) + splitted_choices = choices[:20] + remaining_choices = len(choices[20:]) + + if remaining_choices > 0: + splitted_choices += [ + m18n.n("other_available_options", n=remaining_choices) + ] + + choices_to_display = " | ".join(str(choice) for choice in splitted_choices) + + return f"{message} [{choices_to_display}]" + + return message + + +class SelectOption(BaseChoicesOption): + """ + Ask for value from a limited set of values. + Renders as a regular `