commit edb4c397dfd0e4902b2f5c8ecf084315c5f24b9d Author: machine-a-tsoins Date: Fri Jul 3 17:20:06 2026 +0000 🍽 Fork YunoHost — snapshot mangé par la machine à tsoins Upstream: https://github.com/YunoHost/yunohost @ 3a5f8bac8301c450897b96cbd43a4c7d3ba750fb But (José) : transformer tout le code en bions + ploxions du xerboxion. La carte de digestion vit au labo : /yunohost-digest.json diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..453396f --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,24 @@ +--- +version: "2" +plugins: + duplication: + enabled: true + config: + languages: + python: + python_version: 3 + shellcheck: + enabled: true + pep8: + enabled: true + fixme: + enabled: true + sonar-python: + enabled: true + config: + tests_patterns: + - bin/* + - data/** + - doc/* + - src/** + - tests/** \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c3b4600 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +custom: https://donate.yunohost.org +liberapay: YunoHost diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2e76bd1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,35 @@ +## The problem + +... + +**Related issues:** +**Related PRs:** + +## Solution + +... + +**AI transparency:** *If AI has been used, explain how. See https://doc.yunohost.org/en/dev/?persistLocale=true#%EF%B8%8F-develop* + +## PR Status +*Work in progress / Untested / Tested partially / Fully tested / Wait for other PR to be merged / Ready* + +### Code TODOs + +*Here are frequently forgotten tasks:* + - [ ] Discuss about this change with others contributors (on chat, contributors meeting, forum, issues or pr) + - [ ] Update related repo (like yunohost-admin, yunohost-portal, package-linter, etc.) + - [ ] Write data or settings migration + - [ ] Pass Continuous Integration checks + - [ ] Add some missing translations keys + - [ ] Update/write unit tests + - [ ] Update/write documentation + +### Tests TODOs +*Indicate here tests you have already done, and tests you think should be done* + - [ ] Run the code in cli by calling command by hand + - [ ] Test change on configuration on an instance + +## How to test +*Please add here commands to install dependencies, manual change to do in ynh-dev container, commands to make some basic tests, etc.* +... diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml new file mode 100644 index 0000000..cc4bcc4 --- /dev/null +++ b/.github/workflows/auto-format.yml @@ -0,0 +1,41 @@ +name: Format Python code with Ruff, and Bash code with Shfmt + +on: + push: + branches: [ "dev" ] + +jobs: + format: + runs-on: ubuntu-latest + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository. + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Run Shfmt on Bash files + uses: collin-miller/shfmt-action@v1 + with: + # -i=4 # indent + # -kp # keep column alignment paddings + # -sr # redirect operators will be followed by a space + # -bn # binary ops like && and | may start a line + # -ci # switch cases will be indented + # -w # write to file instead of stdout + args: -i=4 -kp -sr -bn -ci -w hooks/ helpers/helpers helpers/helpers.v1.d/ helpers/helpers.v2.1.d/ + continue-on-error: true + + - name: Run Ruff on Python files for import sorting + # See https://docs.astral.sh/ruff/formatter/#sorting-imports + uses: astral-sh/ruff-action@v3 + with: + args: "check --select I --fix" + + - name: Run Ruff on Python files + uses: astral-sh/ruff-action@v3 + with: + args: "format" + + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: ":art: ${{ github.workflow }}" diff --git a/.github/workflows/auto-lint.yml b/.github/workflows/auto-lint.yml new file mode 100644 index 0000000..cf16451 --- /dev/null +++ b/.github/workflows/auto-lint.yml @@ -0,0 +1,58 @@ +name: Run code lint + +on: + push: + branches: [ "dev" ] + pull_request: + +jobs: + python-lint: + name: Python lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install tox and any other packages + run: pip install tox toml pyyaml + + - if: success() || failure() + run: tox -e py311-lint + + - if: success() || failure() + run: tox -e py311-invalidcode + + - if: success() || failure() + run: tox -e py311-mypy + + - name: Check i18n keys + if: success() || failure() + run: python3 maintenance/missing_i18n_keys.py check + + + bash-lint: + name: Bash lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run ShellCheck + uses: salamandar/action-shellcheck@master + with: + additional_files: | + bin/yunoprompt + debian/postinst + debian/postrm + debian/prerm + hooks/*/* + tests + helpers/helpers.v2.1.d/* + + ignore_paths: + tests/test_helpers.v2.d + helpers/vendor + src/vendor diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..c0eaff2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: [ "dev" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "dev" ] + paths-ignore: + - 'tests/**' + schedule: + - cron: '43 12 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/n_updater.yml b/.github/workflows/n_updater.yml new file mode 100644 index 0000000..340a589 --- /dev/null +++ b/.github/workflows/n_updater.yml @@ -0,0 +1,39 @@ +# This workflow allows GitHub Actions to automagically update YunoHost NodeJS helper whenever a new release of n is detected. +name: Check for new n releases +on: + # Allow to manually trigger the workflow + workflow_dispatch: + # Run it every day at 5:00 UTC + schedule: + - cron: '0 5 * * *' +jobs: + updater: + runs-on: ubuntu-latest + steps: + - name: Fetch the source code + uses: actions/checkout@v4 + + - name: Run the updater script + id: run_updater + run: | + # Download n + wget https://raw.githubusercontent.com/tj/n/master/bin/n --output-document=helpers/vendor/n/n + + 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 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: Update n to ${{ env.VERSION }} + committer: 'yunohost-bot ' + author: 'yunohost-bot ' + signoff: false + base: dev + branch: ci-auto-update-n-${{ env.VERSION }} + delete-branch: true + title: 'Upgrade n to ${{ env.VERSION }}' + body: | + Upgrade `n` to ${{ env.VERSION }} + draft: false diff --git a/.github/workflows/releases_from_tags.yaml b/.github/workflows/releases_from_tags.yaml new file mode 100644 index 0000000..b589d00 --- /dev/null +++ b/.github/workflows/releases_from_tags.yaml @@ -0,0 +1,35 @@ +name: Automatically write releases when tags appear + +on: + push: + tags: [ "debian/*" ] + + +jobs: + pre-release: + name: Generate releases + runs-on: ubuntu-latest + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository. + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@master + with: + fetch-depth: 0 # Fetch all tags + + - name: Patch tag name + id: patch-tag-name + run: echo "version=$(echo ${{ github.ref }} | sed 's|refs/tags/debian/||')" >> $GITHUB_OUTPUT + + - name: Create Release for Tag + id: release_tag + uses: epreston/release-tag@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + release_name: YunoHost ${{ steps.patch-tag-name.outputs.version }} + tag_name: ${{ github.ref }} + body: | + Refer to [the debian changelog](${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref }}/debian/changelog) for details. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..430a90a --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +*.swp +*.swo +*~ +dist +build +eggs +parts +cache +var +sdist +develop-eggs +.installed.cfg +log +uv.lock + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg + +# moulinette lib +src/locales + +# Test +tests/apps + +# Tmp/local doc stuff +doc/bash-completion.sh +doc/bash_completion.d +doc/openapi.js +doc/openapi.json +doc/swagger diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..4849d36 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,52 @@ +--- +stages: + - lint + - build + - install + - test + - bot + +default: + tags: + - yunohost-ci + # All jobs are interruptible by default + interruptible: true + +code_quality: + rules: + - if: $CI_COMMIT_TAG # Only for tags + + +code_quality_html: + extends: code_quality + variables: + REPORT_FORMAT: html + artifacts: + paths: [gl-code-quality-report.html] + rules: + - if: $CI_COMMIT_TAG # Only for tags + + +# see: https://docs.gitlab.com/ee/ci/yaml/#switch-between-branch-pipelines-and-merge-request-pipelines +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" # If we move to gitlab one day + - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" # For github PR + - if: $CI_COMMIT_TAG # For tags + - if: $CI_COMMIT_REF_NAME == "ci-format-$CI_DEFAULT_BRANCH" # Ignore black formatting branch created by the CI + when: never + - if: $CI_COMMIT_REF_NAME == "actions/black" # Ignore black formatting branch created by the CI + when: never + - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" # If it's not the default branch and if it's a push, then do not trigger a build + when: never + - when: always + +variables: + GIT_CLONE_PATH: '$CI_BUILDS_DIR/$CI_COMMIT_SHA/$CI_JOB_ID' + YNH_SOURCE: "https://github.com/yunohost" + YNH_DEBIAN: "bookworm" + YNH_SKIP_DIAGNOSIS_DURING_UPGRADE: "true" + +include: + - template: Code-Quality.gitlab-ci.yml + - local: .gitlab/ci/*.gitlab-ci.yml diff --git a/.gitlab/ci/bot.gitlab-ci.yml b/.gitlab/ci/bot.gitlab-ci.yml new file mode 100644 index 0000000..7ab8204 --- /dev/null +++ b/.gitlab/ci/bot.gitlab-ci.yml @@ -0,0 +1,42 @@ +generate-helpers-doc: + stage: bot + image: "build-and-lint" + needs: [] + before_script: + - git config --global user.email "yunohost@yunohost.org" + - git config --global user.name "$GITHUB_USER" + script: + - hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo + - doc_repo/scripts/generate_docs.sh "$PWD" + + - 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 + only: + - tags + +autofix-translated-strings: + stage: bot + image: "build-and-lint" + 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 new file mode 100644 index 0000000..aedef5a --- /dev/null +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -0,0 +1,58 @@ +.build-stage: + stage: build + needs: + - job: actionsmap + - job: invalidcode311 + image: "build-and-lint" + variables: + YNH_BUILD_DIR: "$GIT_CLONE_PATH/build" + before_script: + - echo $PWD + - echo $CI_PROJECT_DIR + - mkdir -p $YNH_BUILD_DIR + artifacts: + paths: + - ./*.deb + +.build_script: &build_script + - cd $YNH_BUILD_DIR/$PACKAGE + - VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null) + - VERSION_TIMESTAMPED="${VERSION}+$(date +%Y%m%d%H%M)" + - dch --package "${PACKAGE}" --force-bad-version -v "${VERSION_TIMESTAMPED}" -D "unstable" --force-distribution "CI build." + - debuild --no-lintian -us -uc + - cp $YNH_BUILD_DIR/*.deb ${CI_PROJECT_DIR}/ + - cd ${CI_PROJECT_DIR} + +######################################## +# BUILD DEB +######################################## + +build-yunohost: + extends: .build-stage + variables: + PACKAGE: "yunohost" + script: + - git ls-files | xargs tar -czf archive.tar.gz + - mkdir -p $YNH_BUILD_DIR/$PACKAGE + - cat archive.tar.gz | tar -xz -C $YNH_BUILD_DIR/$PACKAGE + - rm archive.tar.gz + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE || { apt-get update && DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE; } + - *build_script + +build-ssowat: + extends: .build-stage + variables: + PACKAGE: "ssowat" + script: + - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $YNH_DEBIAN $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE || { apt-get update && DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE; } + - *build_script + +build-moulinette: + extends: .build-stage + variables: + PACKAGE: "moulinette" + script: + - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $YNH_DEBIAN $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 + - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE || { apt-get update && DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $YNH_BUILD_DIR/$PACKAGE; } + - *build_script diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml new file mode 100644 index 0000000..e725b8a --- /dev/null +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -0,0 +1,29 @@ +.install-stage: + stage: install + needs: + - job: build-yunohost + artifacts: true + - job: build-ssowat + artifacts: true + - job: build-moulinette + artifacts: true + +######################################## +# INSTALL DEB +######################################## + +upgrade: + extends: .install-stage + image: "core-tests" + script: + - apt update + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + + +install-postinstall: + extends: .install-stage + image: "before-install" + script: + - apt update + - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb + - yunohost tools postinstall -d domain.tld -u syssa -F 'Syssa Mine' -p the_password --ignore-dyndns --force-diskspace diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml new file mode 100644 index 0000000..871bbf3 --- /dev/null +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -0,0 +1,41 @@ +######################################## +# LINTER +######################################## +# later we must fix lint and format-check jobs and remove "allow_failure" + +actionsmap: + stage: lint + image: "build-and-lint" + needs: [] + script: + - python3 -c 'import yaml; yaml.safe_load(open("share/actionsmap.yml"))' + - python3 -c 'import yaml; yaml.safe_load(open("share/actionsmap-portal.yml"))' + +lint311: + stage: lint + image: "build-and-lint" + needs: [] + allow_failure: true + script: + - tox -e py311-lint + +invalidcode311: + stage: lint + image: "build-and-lint" + needs: [] + script: + - tox -e py311-invalidcode + +mypy: + stage: lint + image: "build-and-lint" + needs: [] + script: + - tox -e py311-mypy + +i18n-keys: + stage: lint + image: "build-and-lint" + needs: [] + script: + - python3 maintenance/missing_i18n_keys.py check diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml new file mode 100644 index 0000000..80154e9 --- /dev/null +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -0,0 +1,187 @@ +.install_debs: &install_debs + # Temporary hack for the CI to install python3-zmq not yet in the image + - DEBIAN_FRONTEND=noninteractive apt update + - DEBIAN_FRONTEND=noninteractive apt --assume-yes install python3-zmq + - 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 + image: "core-tests" + variables: + #PYTEST_ADDOPTS: "--color=yes --cov=src" + PYTEST_ADDOPTS: "--color=yes" + COVERAGE_FILE: ".coverage_$CI_JOB_NAME" + before_script: + - *install_debs + - ln -s src yunohost + needs: + - job: build-yunohost + artifacts: true + - job: build-ssowat + artifacts: true + - job: build-moulinette + artifacts: true + - job: upgrade + artifacts: + paths: + - ./.coverage_* + +######################################## +# TESTS +######################################## + +.test-pytest: + extends: .test-stage + script: + - python3 -m pytest $TEST_FILE --durations=50 + +test-helpers: + extends: .test-pytest + variables: + TEST_FILE: tests/test_helpers.py + +test-domains: + extends: .test-pytest + variables: + TEST_FILE: tests/test_domains.py + +test-dns: + extends: .test-pytest + variables: + TEST_FILE: tests/test_dns.py + +test-apps: + extends: .test-pytest + variables: + TEST_FILE: tests/test_apps.py + +test-appscatalog: + extends: .test-pytest + variables: + TEST_FILE: tests/test_app_catalog.py + +test-appurl: + extends: .test-pytest + variables: + TEST_FILE: tests/test_appurl.py + +test-questions: + extends: .test-pytest + variables: + TEST_FILE: tests/test_questions.py + +test-app-config: + extends: .test-pytest + variables: + TEST_FILE: tests/test_app_config.py + +test-app-resources: + extends: .test-pytest + variables: + TEST_FILE: tests/test_app_resources.py + +test-changeurl: + extends: .test-pytest + variables: + TEST_FILE: tests/test_changeurl.py + +test-backuprestore: + extends: .test-pytest + variables: + TEST_FILE: tests/test_backuprestore.py + +test-permission: + extends: .test-pytest + variables: + TEST_FILE: tests/test_permission.py + +test-settings: + extends: .test-pytest + variables: + TEST_FILE: tests/test_settings.py + +test-user-group: + extends: .test-pytest + variables: + TEST_FILE: tests/test_user-group.py + +test-regenconf: + extends: .test-pytest + variables: + TEST_FILE: tests/test_regenconf.py + +test-service: + extends: .test-pytest + variables: + TEST_FILE: tests/test_service.py + +test-ldapauth: + extends: .test-pytest + variables: + TEST_FILE: tests/test_ldapauth.py + +test-sso-and-portalapi: + extends: .test-pytest + variables: + TEST_FILE: tests/test_sso_and_portalapi.py + +test-process: + extends: .test-pytest + variables: + TEST_FILE: tests/test_process.py + +test-file-utils: + extends: .test-pytest + variables: + TEST_FILE: tests/test_file_utils.py + +######################################## +# COVERAGE REPORT +######################################## + +#coverage: +# stage: test +# image: "core-tests" +# needs: +# # Yeah ... gotta list all of those individually ... https://gitlab.com/gitlab-org/gitlab/-/issues/332326 +# - job: test-domains +# artifacts: true +# - job: test-dns +# artifacts: true +# - job: test-apps +# artifacts: true +# - job: test-appscatalog +# artifacts: true +# - job: test-appurl +# artifacts: true +# - job: test-questions +# artifacts: true +# - job: test-app-config +# artifacts: true +# - job: test-app-resources +# artifacts: true +# - job: test-changeurl +# artifacts: true +# - job: test-backuprestore +# artifacts: true +# - job: test-permission +# artifacts: true +# - job: test-settings +# artifacts: true +# - job: test-user-group +# artifacts: true +# - job: test-regenconf +# artifacts: true +# - job: test-service +# artifacts: true +# - job: test-ldapauth +# artifacts: true +# - job: test-sso-and-portalapi +# artifacts: true +# - job: test-process +# artifacts: true +# - job: test-file-utils +# artifacts: true +# script: +# - coverage combine ./.coverage_* +# - coverage report diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..0a7f0d2 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,14 @@ +external-sources=true +source-path=SCRIPTDIR + +# Declare and assign separately +disable=SC2155 + +# In case cd / pushd / popd fails +disable=SC2164 + +# Useless cat when sed/grep +disable=SC2002 + +# Those are errors that we haven't fixed yet +disable=SC2155,SC2012,SC2013,SC2038,SC2076,SC2034,SC2154,SC2001 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dbbe355 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..673c3cb --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +

+ YunoHost +

+ +

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) +[![Project license](https://img.shields.io/gitlab/license/yunohost/yunohost)](https://github.com/YunoHost/yunohost/blob/dev/LICENSE) +[![CodeQL](https://github.com/yunohost/yunohost/workflows/CodeQL/badge.svg)](https://github.com/YunoHost/yunohost/security/code-scanning) +[![Mastodon Follow](https://img.shields.io/mastodon/follow/28084)](https://mastodon.social/@yunohost) + +
+ +YunoHost is an operating system aiming to simplify as much as possible the administration of a server. + +This repository corresponds to the core code of YunoHost, mainly written in Python and Bash. + +- [Project features](https://doc.yunohost.org/admin/what_is_yunohost/) +- [Project website](https://yunohost.org) +- [Install documentation](https://doc.yunohost.org/admin/get_started/install_on/) +- [Issue tracker](https://github.com/YunoHost/issues) + +## Screenshots + +Webadmin ([Yunohost-Admin](https://github.com/YunoHost/yunohost-admin)) | Single sign-on user portal ([Yunohost-portal](https://github.com/YunoHost/yunohost-portal) + [SSOwat](https://github.com/YunoHost/ssowat)) +--- | --- +Web admin insterface screenshot | User portal screenshot + + + + +## Contributing + +- You can learn how to get started with developing on YunoHost by reading [this piece of documentation](https://doc.yunohost.org/dev). +- Come chat with us on the [dev chatroom](https://doc.yunohost.org/community/chat_rooms/)! +- You can help translate YunoHost on our [translation platform](https://translate.yunohost.org/engage/yunohost/?utm_source=widget). + +

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

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

+NLnet Foundation +Next Generation Internet +Code Lutin +

+

+Globenet +Gitoyen +tetaneutral.net +Octopuce +

+
+ +This project was funded successively through the [NGI0 PET Fund](https://nlnet.nl/PET) and the [NGI0 Commons Fund](https://nlnet.nl/commonsfund), two funds established by NLnet with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under respective grant agreements No 825310 and No 101135429. If you're interested, [check out how to apply in this video](https://media.ccc.de/v/36c3-10795-ngi_zero_a_treasure_trove_of_it_innovation)! diff --git a/bin/yunohost b/bin/yunohost new file mode 100755 index 0000000..0f92b65 --- /dev/null +++ b/bin/yunohost @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import argparse +import os +import sys + +import yunohost + + +def _parse_cli_args() -> tuple[argparse.ArgumentParser, argparse.Namespace, list[str]]: + """Parse additional arguments for the cli""" + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument( + "--output-as", + choices=["json", "plain", "none"], + default=None, + help="Output result in another format", + ) + parser.add_argument( + "--debug", + action="store_true", + default=False, + help="Log and print debug messages", + ) + parser.add_argument( + "--quiet", action="store_true", default=False, help="Don't produce any output" + ) + parser.add_argument( + "--version", + action="store_true", + default=False, + help="Display YunoHost packages versions (alias to 'yunohost tools versions')", + ) + parser.add_argument( + "--timeout", + type=int, + default=None, + help="Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock", + ) + # deprecated arguments + parser.add_argument( + "--plain", action="store_true", default=False, help=argparse.SUPPRESS + ) + parser.add_argument( + "--json", action="store_true", default=False, help=argparse.SUPPRESS + ) + + opts, args = parser.parse_known_args() + + # output compatibility + if opts.plain: + opts.output_as = "plain" + elif opts.json: + opts.output_as = "json" + + return parser, opts, args + + +# Stupid PATH management because sometimes (e.g. some cron job) PATH is only /usr/bin:/bin ... + +default_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +if os.environ["PATH"] != default_path: + os.environ["PATH"] = default_path + ":" + os.environ["PATH"] + +# Main action ---------------------------------------------------------- + + +def main() -> None: + if os.geteuid() != 0: + print("\033[1;31mError:\033[0m yunohost command must be run as root or with sudo.", file=sys.stderr) + sys.exit(1) + + parser, opts, args = _parse_cli_args() + + if opts.version: + args = ["tools", "versions"] + + # Execute the action + yunohost.cli( + debug=opts.debug, + quiet=opts.quiet, + output_as=opts.output_as, + timeout=opts.timeout, + args=args, + parser=parser, + ) + + +if __name__ == "__main__": + main() diff --git a/bin/yunohost-api b/bin/yunohost-api new file mode 100755 index 0000000..c0d9d85 --- /dev/null +++ b/bin/yunohost-api @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import argparse + +import yunohost + +# Default server configuration +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 6787 + + +def _parse_api_args() -> argparse.Namespace: + """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, + ) + srv_group.add_argument( + "--actionsmap", + action="store", + default=None, + type=str, + help="Alternate actionsmap to use for moulinette; useful for development", + ) + 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.api(debug=opts.debug, host=opts.host, port=opts.port, actionsmap=opts.actionsmap) diff --git a/bin/yunohost-portal-api b/bin/yunohost-portal-api new file mode 100755 index 0000000..d01aed8 --- /dev/null +++ b/bin/yunohost-portal-api @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import argparse + +import yunohost + +# Default server configuration +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 6788 + + +def _parse_api_args() -> argparse.Namespace: + """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/yunomdns b/bin/yunomdns new file mode 100755 index 0000000..c3cfb7c --- /dev/null +++ b/bin/yunomdns @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Pythonic declaration of mDNS .local domains for YunoHost +""" + +import sys +from ipaddress import ip_address +from time import sleep +from typing import Dict, List + +import ifaddr +import yaml +from zeroconf import ServiceBrowser, ServiceInfo, Zeroconf + + +def get_network_local_interfaces() -> Dict[str, Dict[str, List[str]]]: + """ + Returns interfaces with their associated local IPs + """ + + interfaces = { + adapter.name: { + "ipv4": [ + ip.ip + for ip in adapter.ips + if ip.is_IPv4 + and ip_address(ip.ip).is_private + and not ip_address(ip.ip).is_link_local + ], + "ipv6": [ + ip.ip[0] + for ip in adapter.ips + if ip.is_IPv6 + and ip_address(ip.ip[0]).is_private + and not ip_address(ip.ip[0]).is_link_local + ], + } + for adapter in ifaddr.get_adapters() + if adapter.name != "lo" + } + return interfaces + + +# Listener class, to detect duplicates on the network +# Stores the list of servers in its list property +class Listener: + def __init__(self) -> None: + self.list = [] + + def remove_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: + info = zeroconf.get_service_info(type, name) + self.list.remove(info.server) + + def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: + pass + + def add_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: + info = zeroconf.get_service_info(type, name) + self.list.append(info.server[:-1]) + + +def main() -> bool: + ### + # CONFIG + ### + + with open("/etc/yunohost/mdns.yml", "r") as f: + config = yaml.safe_load(f) or {} + + required_fields = ["domains"] + missing_fields = [field for field in required_fields if field not in config] + interfaces = get_network_local_interfaces() + + if missing_fields: + print(f"The fields {missing_fields} are required in mdns.yml") + return False + + if "interfaces" not in config: + config["interfaces"] = [ + interface + for interface, local_ips in interfaces.items() + if local_ips["ipv4"] + ] + + if "ban_interfaces" in config: + config["interfaces"] = [ + interface + for interface in config["interfaces"] + if interface not in config["ban_interfaces"] + ] + + # Let's discover currently published .local domains accross the network + zc = Zeroconf() + listener = Listener() + browser = ServiceBrowser(zc, "_device-info._tcp.local.", listener) + sleep(2) + browser.cancel() + zc.close() + + # Always attempt to publish yunohost.local + if "yunohost.local" not in config["domains"]: + config["domains"].append("yunohost.local") + + def find_domain_not_already_published(domain): + + # Try domain.local ... but if it's already published by another entity, + # try domain-2.local, domain-3.local, ... + + i = 1 + domain_i = domain + + while domain_i in listener.list: + print(f"Uh oh, {domain_i} already exists on the network...") + + i += 1 + domain_i = domain.replace(".local", f"-{i}.local") + + return domain_i + + config["domains"] = [ + find_domain_not_already_published(domain) for domain in config["domains"] + ] + + zcs: Dict[Zeroconf, List[ServiceInfo]] = {} + + for interface in config["interfaces"]: + + if interface not in interfaces: + print( + f"Interface {interface} listed in config file is not present on system." + ) + continue + + # Broadcast IPv4 and IPv6 + ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"] + + # If at least one IP is listed + if not ips: + continue + + # Create a Zeroconf object, and store the ServiceInfos + zc = Zeroconf(interfaces=ips) # type: ignore + zcs[zc] = [] + + for d in config["domains"]: + d_domain = d.replace(".local", "") + if "." in d_domain: + print(f"{d_domain}.local: subdomains are not supported.") + continue + # Create a ServiceInfo object for each .local domain + zcs[zc].append( + ServiceInfo( + type_="_device-info._tcp.local.", + name=f"{interface}: {d_domain}._device-info._tcp.local.", + parsed_addresses=ips, + port=80, + server=f"{d}.", + ) + ) + print(f"Adding {d} with addresses {ips} on interface {interface}") + + # Run registration + print("Registering...") + for zc, infos in zcs.items(): + for info in infos: + zc.register_service( + info, allow_name_change=True, cooperating_responders=True + ) + + try: + print("Registered. Press Ctrl+C or stop service to stop.") + while True: + sleep(1) + except KeyboardInterrupt: + pass + finally: + print("Unregistering...") + for zc, infos in zcs.items(): + zc.unregister_all_services() + zc.close() + + return True + + +if __name__ == "__main__": + sys.exit(0 if main() else 1) diff --git a/bin/yunopaste b/bin/yunopaste new file mode 100755 index 0000000..5d2a587 --- /dev/null +++ b/bin/yunopaste @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import json +import sys + +import requests + +SERVER_URL = "https://paste.yunohost.org" +TIMEOUT = 3 + + +def create_snippet(data: str) -> str: + try: + url = f"{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 f"{SERVER_URL}/raw/{dockey}" + except requests.exceptions.RequestException as e: + print(f"\033[31mError: {e}\033[0m", file=sys.stderr) + sys.exit(1) + + +def main() -> None: + output = sys.stdin.read() + + if not output: + print("\033[31mError: No input received from stdin.\033[0m", file=sys.stderr) + sys.exit(1) + + url = create_snippet(output) + + print("\033[32mURL: {}\033[0m".format(url)) + + +if __name__ == "__main__": + main() diff --git a/bin/yunoprompt b/bin/yunoprompt new file mode 100755 index 0000000..7b4cd51 --- /dev/null +++ b/bin/yunoprompt @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Fetch x509 fingerprint +x509_fingerprint=$(openssl x509 -in /etc/yunohost/certs/yunohost.org/crt.pem -noout -fingerprint -sha256 | cut -d= -f2) + + +# Fetch SSH fingerprints +i=0 +for keyfile in /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub; do + if [ ! -f "$keyfile" ]; then + continue + fi + output=$(ssh-keygen -l -f "$keyfile") + fingerprint[i]=" - $(echo "$output" | cut -d' ' -f2) $(echo "$output" | cut -d' ' -f4)" + i=$((i + 1)) +done + +# +# Build the logo +# + +LOGO=$(cat << 'EOF' + __ __ __ __ __ _ _______ __ __ _______ _______ _______ + | | | || | | || | | || || | | || || || | + | |_| || | | || |_| || _ || |_| || _ || _____||_ _| + | || |_| || || | | || || | | || |_____ | | + |_ _|| || _ || |_| || _ || |_| ||_____ | | | + | | | || | | || || | | || | _____| | | | + |___| |_______||_| |__||_______||__| |__||_______||_______| |___| +EOF +) + +# ' Put a quote in comment to make vim happy about syntax highlighting :s + +# Reminder for default login / password when postinstall is not done yet +if [ ! -f /etc/yunohost/installed ] +then + default_credentials_reminder="To log in in this console or via SSH, the default login and password are 'root' and 'yunohost' (until post-install is done)." +else + default_credentials_reminder="" +fi + +# +# Build the actual message +# + +sleep 5 +# Get local IP +# (we do this after the sleep 5 to have +# better chances that the network is up) +local_ip=$(hostname --all-ip-address | awk '{print $1}') + +LOGO_AND_FINGERPRINTS=$(cat << EOF + +$LOGO + + Local IP: ${local_ip:-(no ip detected?)} + Local SSL CA X509 fingerprint: + ${x509_fingerprint} + SSH fingerprints: + ${fingerprint[0]} + ${fingerprint[1]} + ${fingerprint[2]} + + ${default_credentials_reminder} + +EOF +) + +echo "$LOGO_AND_FINGERPRINTS" > /etc/issue + +if ! groups | grep -q all_users && [[ ! -f /etc/yunohost/installed ]] +then + chvt 2 + + # Formatting + [[ -n "$local_ip" ]] && local_ip=$(echo -e "https://$local_ip/") || local_ip="(no ip detected?)" + + echo "$LOGO_AND_FINGERPRINTS" + cat << EOF +=============================================================================== +You should now proceed with YunoHost post-installation. This is where you will +be asked for: + - the main domain of your server; + - the username and password for the first admin + +You can perform this step: + - from your web browser, by accessing: https://yunohost.local/ or ${local_ip} + - or in this terminal by answering 'yes' to the following question + +If this is your first time with YunoHost, it is strongly recommended to take +time to read the administrator documentation and in particular the sections +'Finalizing your setup' and 'Getting to know YunoHost'. It is available at +the following URL: https://doc.yunohost.org/admin/ +=============================================================================== +${default_credentials_reminder} + +EOF + + read -rp "Press any key to continue " -n 1 + + chvt 3 + + exit 0 +fi diff --git a/conf/dnsmasq/dnsmasq.conf.tpl b/conf/dnsmasq/dnsmasq.conf.tpl new file mode 100644 index 0000000..eece530 --- /dev/null +++ b/conf/dnsmasq/dnsmasq.conf.tpl @@ -0,0 +1,10 @@ +domain-needed +expand-hosts +localise-queries + +{% set interfaces = wireless_interfaces.strip().split(' ') %} +{% for interface in interfaces %} +interface={{ interface }} +{% endfor %} +resolv-file=/etc/resolv.dnsmasq.conf +cache-size=256 diff --git a/conf/dnsmasq/domain.tpl b/conf/dnsmasq/domain.tpl new file mode 100644 index 0000000..64d6a1e --- /dev/null +++ b/conf/dnsmasq/domain.tpl @@ -0,0 +1,11 @@ +{% set interfaces_list = interfaces.split(' ') %} +{% for interface in interfaces_list %} +interface-name={{ domain }},{{ interface }} +{% endfor %} +{% if ipv6 %} +host-record={{ domain }},{{ ipv6 }} +{% endif %} +txt-record={{ domain }},"v=spf1 mx a -all" +{% if mail_in == "True" %} +mx-host={{ domain }},{{ domain }},5 +{% endif %} diff --git a/conf/dnsmasq/plain/etcdefault b/conf/dnsmasq/plain/etcdefault new file mode 100644 index 0000000..e62dbbf --- /dev/null +++ b/conf/dnsmasq/plain/etcdefault @@ -0,0 +1,33 @@ +# This file has five functions: +# 1) to completely disable starting dnsmasq, +# 2) to set DOMAIN_SUFFIX by running `dnsdomainname` +# 3) to select an alternative config file +# by setting DNSMASQ_OPTS to --conf-file= +# 4) to tell dnsmasq to read the files in /etc/dnsmasq.d for +# more configuration variables. +# 5) to stop the resolvconf package from controlling dnsmasq's +# idea of which upstream nameservers to use. +# For upgraders from very old versions, all the shell variables set +# here in previous versions are still honored by the init script +# so if you just keep your old version of this file nothing will break. + +#DOMAIN_SUFFIX=`dnsdomainname` +#DNSMASQ_OPTS="--conf-file=/etc/dnsmasq.alt" + +# Whether or not to run the dnsmasq daemon; set to 0 to disable. +ENABLED=1 + +# By default search this drop directory for configuration options. +# Libvirt leaves a file here to make the system dnsmasq play nice. +# Comment out this line if you don't want this. The dpkg-* are file +# endings which cause dnsmasq to skip that file. This avoids pulling +# in backups made by dpkg. +CONFIG_DIR=/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new + +# If the resolvconf package is installed, dnsmasq will use its output +# rather than the contents of /etc/resolv.conf to find upstream +# nameservers. Uncommenting this line inhibits this behaviour. +# Note that including a "resolv-file=" line in +# /etc/dnsmasq.conf is not enough to override resolvconf if it is +# installed: the line below must be uncommented. +IGNORE_RESOLVCONF=yes diff --git a/conf/dnsmasq/plain/resolv.dnsmasq.conf b/conf/dnsmasq/plain/resolv.dnsmasq.conf new file mode 100644 index 0000000..3f2c589 --- /dev/null +++ b/conf/dnsmasq/plain/resolv.dnsmasq.conf @@ -0,0 +1,30 @@ +# This file will be used to generate /etc/resolv.dnsmasq.conf +# if no custom resolvers are set. +# Custom resolvers can be defined in YunoHost settings. +# To avoid that every instance rely on the first server as primary +# server, this list is *shuffled* during every regen-conf of dnsmasq +# In the possibility where the first nameserver is down, dnsmasq +# will automatically switch to the next as primary server. + +# List taken from +# http://diyisp.org/dokuwiki/doku.php?id=technical:dnsresolver +# and some others from https://sebsauvage.net/wiki/doku.php?id=dns-alternatifs + +# (FR) ARN +nameserver 89.234.141.66 +nameserver 2a00:5881:8100:1000::3 +# (FR) Aquilenet +nameserver 45.67.81.23 +nameserver 2a0c:e300::1337 +nameserver 185.233.100.100 +nameserver 2a0c:e300::100 +nameserver 185.233.100.101 +nameserver 2a0c:e300::101 +# (DE) AS250 +nameserver 194.150.168.168 +# (DE) Ideal-Hosting +nameserver 2001:1608:10:25::1c04:b12f +nameserver 2001:1608:10:25::9249:d69b +# DNS4all +nameserver 194.0.5.3 +nameserver 2001:678:8::3 diff --git a/conf/dovecot/dovecot-ldap.conf b/conf/dovecot/dovecot-ldap.conf new file mode 100644 index 0000000..3a80ba4 --- /dev/null +++ b/conf/dovecot/dovecot-ldap.conf @@ -0,0 +1,9 @@ +hosts = 127.0.0.1 +auth_bind = yes +ldap_version = 3 +base = ou=users,dc=yunohost,dc=org +user_attrs = uidNumber=500,gidNumber=8,mailuserquota=quota_rule=*:bytes=%$ +user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) +pass_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) +default_pass_scheme = SSHA + diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf new file mode 100644 index 0000000..82d72a3 --- /dev/null +++ b/conf/dovecot/dovecot.conf @@ -0,0 +1,154 @@ +!include yunohost.d/pre-ext.conf + +listen = *, :: +auth_mechanisms = plain login + +mail_gid = 8 +mail_home = /var/mail/%n +mail_location = maildir:/var/mail/%n +mail_uid = 500 + +protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %} + +mail_plugins = $mail_plugins quota notify push_notification + +############################################################################### +# 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 + +ssl_cert = /path/to/dhparam +ssl_dh = , +# and return true if the IP is to be ignored. False otherwise. +# +# ignorecommand = /path/to/command +ignorecommand = + +# "bantime" is the number of seconds that a host is banned. +bantime = 10m + +# A host is banned if it has generated "maxretry" during the last "findtime" +# seconds. +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. +# +# pyinotify: requires pyinotify (a file alteration monitor) to be installed. +# If pyinotify is not installed, Fail2ban will use auto. +# gamin: requires Gamin (a file alteration monitor) to be installed. +# If Gamin is not installed, Fail2ban will use auto. +# polling: uses a polling algorithm which does not require external libraries. +# systemd: uses systemd python library to access the systemd journal. +# Specifying "logpath" is not valid for this backend. +# See "journalmatch" in the jails associated filter config +# auto: will try to use the following backends, in order: +# pyinotify, gamin, polling. +# +# Note: if systemd backend is chosen as the default but you enable a jail +# for which logs are present only in its own log files, specify some other +# backend for that jail (e.g. polling) and provide empty value for +# journalmatch. See https://github.com/fail2ban/fail2ban/issues/959#issuecomment-74901200 +backend = auto + +# "usedns" specifies if jails should trust hostnames in logs, +# warn when DNS lookups are performed, or ignore all hostnames in logs +# +# yes: if a hostname is encountered, a DNS lookup will be performed. +# warn: if a hostname is encountered, a DNS lookup will be performed, +# but it will be logged as a warning. +# no: if a hostname is encountered, will not be used for banning, +# but it will be logged as info. +# raw: use raw value (no hostname), allow use it for no-host filters/actions (example user) +usedns = warn + +# "logencoding" specifies the encoding of the log files handled by the jail +# This is used to decode the lines from the log file. +# Typical examples: "ascii", "utf-8" +# +# auto: will use the system locale setting +logencoding = auto + +# "enabled" enables the jails. +# By default all jails are disabled, and it should stay this way. +# Enable only relevant to your setup jails in your .local or jail.d/*.conf +# +# true: jail will be enabled and log files will get monitored for changes +# false: jail is not enabled +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[mode=%(mode)s] + + +# +# ACTIONS +# + +# Some options used for actions + +# Destination email address used solely for the interpolations in +# jail.{conf,local,d/*} configuration files. +destemail = root@localhost + +# Sender email address used solely for some actions +sender = root@localhost + +# E-mail action. Since 0.8.1 Fail2Ban uses sendmail MTA for the +# mailing. Change mta configuration parameter to mail if you want to +# revert to conventional 'mail'. +mta = sendmail + +# Default protocol +protocol = tcp + +# Specify chain where jumps would need to be added in ban-actions expecting parameter chain +chain = INPUT + +# Ports to be banned +# Usually should be overridden in a particular jail +port = 0:65535 + +# Format of user-agent https://tools.ietf.org/html/rfc7231#section-5.5.3 +fail2ban_agent = Fail2Ban/%(fail2ban_version)s + +# +# Action shortcuts. To be used to define action parameter + +# Default banning action (e.g. iptables, iptables-new, +# iptables-multiport, shorewall, etc) It is used to define +# action_* variables. Can be overridden globally or per +# section within jail.local file +banaction = nftables-multiport +banaction_allports = nftables-allports + +# The simplest action to take: ban only +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 = %(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 = %(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 = %(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[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. +# 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="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] + +# Report ban via abuseipdb.com. +# +# See action.d/abuseipdb.conf for usage example and details. +# +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 +# globally (section [DEFAULT]) or per specific section +action = %(action_)s + + +# +# JAILS +# + +# +# SSH servers +# + +[sshd] + +# 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 + + +[dropbear] + +port = ssh +logpath = %(dropbear_log)s +backend = %(dropbear_backend)s + + +[selinux-ssh] + +port = ssh +logpath = %(auditd_log)s + + +# +# HTTP servers +# + +[apache-auth] + +port = http,https +logpath = %(apache_error_log)s + + +[apache-badbots] +# Ban hosts which agent identifies spammer robots crawling the web +# for email addresses. The mail outputs are buffered. +port = http,https +logpath = %(apache_access_log)s +bantime = 48h +maxretry = 1 + + +[apache-noscript] + +port = http,https +logpath = %(apache_error_log)s + + +[apache-overflows] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 2 + + +[apache-nohome] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 2 + + +[apache-botsearch] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 2 + + +[apache-fakegooglebot] + +port = http,https +logpath = %(apache_access_log)s +maxretry = 1 +ignorecommand = %(fail2ban_confpath)s/filter.d/ignorecommands/apache-fakegooglebot + + +[apache-modsecurity] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 2 + + +[apache-shellshock] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 1 + + +[openhab-auth] + +filter = openhab +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 + +# To use 'nginx-limit-req' jail you should have `ngx_http_limit_req_module` +# and define `limit_req` and `limit_req_zone` as described in nginx documentation +# http://nginx.org/en/docs/http/ngx_http_limit_req_module.html +# or for example see in 'config/filter.d/nginx-limit-req.conf' +[nginx-limit-req] +port = http,https +logpath = %(nginx_error_log)s + +[nginx-botsearch] + +port = http,https +logpath = %(nginx_error_log)s + +[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 +# of usage in production environments. + +[php-url-fopen] + +port = http,https +logpath = %(nginx_access_log)s + %(apache_access_log)s + + +[suhosin] + +port = http,https +logpath = %(suhosin_log)s + + +[lighttpd-auth] +# Same as above for Apache's mod_auth +# It catches wrong authentifications +port = http,https +logpath = %(lighttpd_error_log)s + + +# +# Webmail and groupware servers +# + +[roundcube-auth] + +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] + +port = http,https +logpath = /var/log/openwebmail.log + + +[horde] + +port = http,https +logpath = /var/log/horde/horde.log + + +[groupoffice] + +port = http,https +logpath = /home/groupoffice/log/info.log + + +[sogo-auth] +# Monitor SOGo groupware server +# without proxy this would be: +# port = 20000 +port = http,https +logpath = /var/log/sogo/sogo.log + + +[tine20] + +logpath = /var/log/tine20/tine20.log +port = http,https + + +# +# Web Applications +# +# + +[drupal-auth] + +port = http,https +logpath = %(syslog_daemon)s +backend = %(syslog_backend)s + +[guacamole] + +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] + +port = 10000 +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +[froxlor-auth] + +port = http,https +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +# +# HTTP Proxy servers +# +# + +[squid] + +port = 80,443,3128,8080 +logpath = /var/log/squid/access.log + + +[3proxy] + +port = 3128 +logpath = /var/log/3proxy.log + + +# +# FTP servers +# + + +[proftpd] + +port = ftp,ftp-data,ftps,ftps-data +logpath = %(proftpd_log)s +backend = %(proftpd_backend)s + + +[pure-ftpd] + +port = ftp,ftp-data,ftps,ftps-data +logpath = %(pureftpd_log)s +backend = %(pureftpd_backend)s + + +[gssftpd] + +port = ftp,ftp-data,ftps,ftps-data +logpath = %(syslog_daemon)s +backend = %(syslog_backend)s + + +[wuftpd] + +port = ftp,ftp-data,ftps,ftps-data +logpath = %(wuftpd_log)s +backend = %(wuftpd_backend)s + + +[vsftpd] +# or overwrite it in jails.local to be +# logpath = %(syslog_authpriv)s +# if you want to rely on PAM failed login attempts +# vsftpd's failregex should match both of those formats +port = ftp,ftp-data,ftps,ftps-data +logpath = %(vsftpd_log)s + + +# +# Mail servers +# + +# ASSP SMTP Proxy Jail +[assp] + +port = smtp,465,submission +logpath = /root/path/to/assp/logs/maillog.txt + + +[courier-smtp] + +port = smtp,465,submission +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[postfix] +# 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] + +filter = postfix[mode=rbl] +port = smtp,465,submission +logpath = %(postfix_log)s +backend = %(postfix_backend)s +maxretry = 1 + + +[sendmail-auth] + +port = submission,465,smtp +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[sendmail-reject] +# 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 + + +[qmail-rbl] + +filter = qmail +port = smtp,465,submission +logpath = /service/qmail/log/main/current + + +# dovecot defaults to logging to the mail syslog facility +# but can be set by syslog_facility in the dovecot configuration. +[dovecot] + +port = pop3,pop3s,imap,imaps,submission,465,sieve +logpath = %(dovecot_log)s +backend = %(dovecot_backend)s + + +[sieve] + +port = smtp,465,submission +logpath = %(dovecot_log)s +backend = %(dovecot_backend)s + + +[solid-pop3d] + +port = pop3,pop3s +logpath = %(solidpop3d_log)s + + +[exim] +# 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,465,submission +logpath = %(exim_main_log)s + + +[kerio] + +port = imap,smtp,imaps,465 +logpath = /opt/kerio/mailserver/store/logs/security.log + + +# +# Mail servers authenticators: might be used for smtp,ftp,imap servers, so +# all relevant ports get banned +# + +[courier-auth] + +port = smtp,465,submission,imap,imaps,pop3,pop3s +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[postfix-sasl] + +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. +logpath = %(postfix_log)s +backend = %(postfix_backend)s + + +[perdition] + +port = imap,imaps,pop3,pop3s +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[squirrelmail] + +port = smtp,465,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks +logpath = /var/lib/squirrelmail/prefs/squirrelmail_access_log + + +[cyrus-imap] + +port = imap,imaps +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[uwimap-auth] + +port = imap,imaps +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +# +# +# DNS servers +# + + +# !!! WARNING !!! +# Since UDP is connection-less protocol, spoofing of IP and imitation +# of illegal actions is way too simple. Thus enabling of this filter +# might provide an easy way for implementing a DoS against a chosen +# victim. See +# http://nion.modprobe.de/blog/archives/690-fail2ban-+-dns-fail.html +# Please DO NOT USE this jail unless you know what you are doing. +# +# IMPORTANT: see filter.d/named-refused for instructions to enable logging +# This jail blocks UDP traffic for DNS requests. +# [named-refused-udp] +# +# filter = named-refused +# port = domain,953 +# protocol = udp +# logpath = /var/log/named/security.log + +# IMPORTANT: see filter.d/named-refused for instructions to enable logging +# This jail blocks TCP traffic for DNS requests. + +[named-refused] + +port = domain,953 +logpath = /var/log/named/security.log + + +[nsd] + +port = 53 +action_ = %(default/action_)s[name=%(__name__)s-tcp, protocol="tcp"] + %(default/action_)s[name=%(__name__)s-udp, protocol="udp"] +logpath = /var/log/nsd.log + + +# +# Miscellaneous +# + +[asterisk] + +port = 5060,5061 +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 + + +[freeswitch] + +port = 5060,5061 +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-warnings = 2 +# +# for syslog (daemon facility) +# [mysqld_safe] +# syslog +# +# for own logfile +# [mysqld] +# log-error=/var/log/mysqld.log +[mysqld-auth] + +port = 3306 +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 +port = 27017 +logpath = /var/log/mongodb/mongodb.log + + +# Jail for more extended banning of persistent abusers +# !!! WARNINGS !!! +# 1. Make sure that your loglevel specified in fail2ban.conf/.local +# is not at DEBUG level -- which might then cause fail2ban to fall into +# an infinite loop constantly feeding itself with non-informative lines +# 2. Increase dbpurgeage defined in fail2ban.conf to e.g. 648000 (7.5 days) +# to maintain entries for failed logins for sufficient amount of time +[recidive] + +logpath = /var/log/fail2ban.log +banaction = %(banaction_allports)s +bantime = 1w +findtime = 1d + + +# Generic filter for PAM. Has to be used with action which bans all +# ports such as iptables-allports, shorewall + +[pam-generic] +# pam-generic filter can be customized to monitor specific subset of 'tty's +banaction = %(banaction_allports)s +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +[xinetd-fail] + +banaction = nftables-multiport-log +logpath = %(syslog_daemon)s +backend = %(syslog_backend)s +maxretry = 2 + + +# stunnel - need to set port for this +[stunnel] + +logpath = /var/log/stunnel4/stunnel.log + + +[ejabberd-auth] + +port = 5222 +logpath = /var/log/ejabberd/ejabberd.log + + +[counter-strike] + +logpath = /opt/cstrike/logs/L[0-9]*.log +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_ = %(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 +[nagios] + +logpath = %(syslog_daemon)s ; nrpe.cfg may define a different log_facility +backend = %(syslog_backend)s +maxretry = 1 + + +[oracleims] +# see "oracleims" filter file for configuration requirement for Oracle IMS v6 and above +logpath = /opt/sun/comms/messaging64/log/mail.log_current +banaction = %(banaction_allports)s + +[directadmin] +logpath = /var/log/directadmin/login.log +port = 2222 + +[portsentry] +logpath = /var/lib/portsentry/portsentry.history +maxretry = 1 + +[pass2allow-ftp] +# this pass2allow example allows FTP traffic after successful HTTP authentication +port = ftp,ftp-data,ftps,ftps-data +# knocking_url variable must be overridden to some secret value in jail.local +knocking_url = /knocking/ +filter = apache-pass[knocking_url="%(knocking_url)s"] +# access log of the website with HTTP auth +logpath = %(apache_access_log)s +blocktype = RETURN +returntype = DROP +action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s, + actionstart_on_demand=false, actionrepair_on_unban=true] +bantime = 1h +maxretry = 1 +findtime = 1 + + +[murmur] +# AKA mumble-server +port = 64738 +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 + + +[screensharingd] +# For Mac OS Screen Sharing Service (VNC) +logpath = /var/log/system.log +logencoding = utf-8 + +[haproxy-http-auth] +# HAProxy by default doesn't log to file you'll need to set it up to forward +# logs to a syslog server which would then write them to disk. +# See "haproxy-http-auth" filter for a brief cautionary note when setting +# maxretry and findtime. +logpath = /var/log/haproxy.log + +[slapd] +port = ldap,ldaps +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/postfix-sasl.conf b/conf/fail2ban/postfix-sasl.conf new file mode 100644 index 0000000..a9f4707 --- /dev/null +++ b/conf/fail2ban/postfix-sasl.conf @@ -0,0 +1,6 @@ +# Fail2Ban filter for postfix authentication failures +[INCLUDES] +before = common.conf +[Definition] +_daemon = postfix/smtpd +failregex = ^%(__prefix_line)swarning: [-._\w]+\[\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/]*={0,2})?\s*$ diff --git a/conf/fail2ban/systemd-override-bind-nftables.conf b/conf/fail2ban/systemd-override-bind-nftables.conf new file mode 100644 index 0000000..96b9492 --- /dev/null +++ b/conf/fail2ban/systemd-override-bind-nftables.conf @@ -0,0 +1,5 @@ +# This override config restarts and reloads fail2ban when nftables is started/reloaded + +[Unit] +PartOf=nftables.service +ReloadPropagatedFrom=nftables.service diff --git a/conf/fail2ban/yunohost-jails.conf b/conf/fail2ban/yunohost-jails.conf new file mode 100644 index 0000000..d04ea41 --- /dev/null +++ b/conf/fail2ban/yunohost-jails.conf @@ -0,0 +1,42 @@ +[sshd] +port = {{ssh_port}} +enabled = true + +[nginx-http-auth] +enabled = true + +[postfix] +enabled = true + +[sasl] +enabled = true +port = smtp +filter = postfix-sasl +logpath = /var/log/mail.log +maxretry = 5 + +[dovecot] +enabled = true + +[recidive] +enabled = true + +[pam-generic] +enabled = true + +[yunohost] +enabled = true +port = http,https +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 0000000..c4a1657 --- /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 new file mode 100644 index 0000000..be20e23 --- /dev/null +++ b/conf/fail2ban/yunohost.conf @@ -0,0 +1,3 @@ +[Definition] +failregex = ^ -.*\"POST /yunohost/api/login HTTP/\d.\d\" 401 +ignoreregex = diff --git a/conf/mdns/yunomdns.service b/conf/mdns/yunomdns.service new file mode 100644 index 0000000..5fc98af --- /dev/null +++ b/conf/mdns/yunomdns.service @@ -0,0 +1,15 @@ +[Unit] +Description=YunoHost mDNS service +Wants=network-online.target +After=network-online.target + +[Service] +User=mdns +Group=mdns +Type=simple +Environment=PYTHONUNBUFFERED=1 +ExecStart=/usr/bin/yunomdns +StandardOutput=journal + +[Install] +WantedBy=default.target diff --git a/conf/nftables/nftables.conf b/conf/nftables/nftables.conf new file mode 100644 index 0000000..5483be1 --- /dev/null +++ b/conf/nftables/nftables.conf @@ -0,0 +1,20 @@ +#!/usr/sbin/nft -f + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority filter; + } + chain forward { + type filter hook forward priority filter; + } + chain output { + type filter hook output priority filter; + } +} + +## Above is the standard nftables.conf +## Below is to include YunoHost configuration + +include "/etc/nftables.d/*.conf" diff --git a/conf/nftables/nftables.d/yunohost-firewall.tpl.conf b/conf/nftables/nftables.d/yunohost-firewall.tpl.conf new file mode 100644 index 0000000..1ec0aa9 --- /dev/null +++ b/conf/nftables/nftables.d/yunohost-firewall.tpl.conf @@ -0,0 +1,25 @@ +#!/usr/sbin/nft -f + +define tcp_ports = { {{ tcp_ports.strip().split(' ') | join(', ') }} } +{% if udp_ports.strip() %} +define udp_ports = { {{ udp_ports.strip().split(' ') | join(', ') }} } +{% endif %} + +table inet filter { + chain input { + policy drop; + ct state related,established counter accept; + + tcp dport $tcp_ports counter accept; + {% if udp_ports.strip() %} + udp dport $udp_ports counter accept; + {% endif %} + + udp sport 1900 udp dport >= 1024 ip6 saddr { fd00::/8, fe80::/10 } meta pkttype unicast limit rate 4/second burst 20 packets accept comment "Accept UPnP IGD port mapping reply" + udp sport 1900 udp dport >= 1024 ip saddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 } meta pkttype unicast limit rate 4/second burst 20 packets accept comment "Accept UPnP IGD port mapping reply" + + iifname "lo" counter accept; + ip protocol icmp counter accept; + ip6 nexthdr icmpv6 counter accept; + } +} diff --git a/conf/nginx/acme-challenge.conf.inc b/conf/nginx/acme-challenge.conf.inc new file mode 100644 index 0000000..859aa68 --- /dev/null +++ b/conf/nginx/acme-challenge.conf.inc @@ -0,0 +1,6 @@ +location ^~ '/.well-known/acme-challenge/' +{ + default_type "text/plain"; + alias /var/www/.well-known/acme-challenge-public/; + gzip off; +} diff --git a/conf/nginx/autoconfig.tpl.xml b/conf/nginx/autoconfig.tpl.xml new file mode 100644 index 0000000..a426431 --- /dev/null +++ b/conf/nginx/autoconfig.tpl.xml @@ -0,0 +1,19 @@ + + + {{ domain }} + + {{ domain }} + 993 + SSL + password-cleartext + %EMAILLOCALPART% + + + {{ domain }} + 587 + STARTTLS + password-cleartext + %EMAILLOCALPART% + + + diff --git a/conf/nginx/fastcgi_params_no_auth b/conf/nginx/fastcgi_params_no_auth new file mode 100644 index 0000000..7366262 --- /dev/null +++ b/conf/nginx/fastcgi_params_no_auth @@ -0,0 +1,53 @@ +fastcgi_index index.php; +fastcgi_split_path_info ^(.+?\.php)(/.*)$; + +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param PATH_INFO $fastcgi_path_info; +fastcgi_param SCRIPT_FILENAME $request_filename; +fastcgi_param SCRIPT_NAME $fastcgi_script_name; + +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +# (no REMOTE_USER compared to the version "with auth") +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; + +# Hotfix CVE-2026-42945 +# In such a case, passing "$http_host" upstream exposes the raw client-supplied +# Host value ("malformedhost") to the backend application, even though it does +# not match the effective request target. Applications often use HTTP_HOST for +# redirects, absolute URL generation, virtual host routing, or security checks; +# forwarding the raw Host header can therefore lead to incorrect or unsafe +# behaviour. +# +# Newer nginx versions (since 1.30.0) introduce variables "$is_request_port" and +# "$request_port", allowing HTTP_HOST to be constructed as: +# $host$is_request_port$request_port +# +# In stable/oldstable packages we use "$host" as a security workaround. +# It avoids forwarding an untrusted raw Host header to the backend. +# +# Note: this changes behaviour compared to previous versions, because "$host" +# does not preserve the client-supplied port, while "$http_host" typically +# does. Existing deployments that rely on "$http_host" containing a port number +# may therefore break or behave differently after this change. + +fastcgi_param HTTP_HOST $host; diff --git a/conf/nginx/fastcgi_params_with_auth b/conf/nginx/fastcgi_params_with_auth new file mode 100644 index 0000000..8026830 --- /dev/null +++ b/conf/nginx/fastcgi_params_with_auth @@ -0,0 +1,53 @@ +fastcgi_index index.php; +fastcgi_split_path_info ^(.+?\.php)(/.*)$; + +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param PATH_INFO $fastcgi_path_info; +fastcgi_param SCRIPT_FILENAME $request_filename; +fastcgi_param SCRIPT_NAME $fastcgi_script_name; + +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param REMOTE_USER $http_ynh_user if_not_empty; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +# PHP only, required if PHP was built with --enable-force-cgi-redirect +fastcgi_param REDIRECT_STATUS 200; + +# Hotfix CVE-2026-42945 +# In such a case, passing "$http_host" upstream exposes the raw client-supplied +# Host value ("malformedhost") to the backend application, even though it does +# not match the effective request target. Applications often use HTTP_HOST for +# redirects, absolute URL generation, virtual host routing, or security checks; +# forwarding the raw Host header can therefore lead to incorrect or unsafe +# behaviour. +# +# Newer nginx versions (since 1.30.0) introduce variables "$is_request_port" and +# "$request_port", allowing HTTP_HOST to be constructed as: +# $host$is_request_port$request_port +# +# In stable/oldstable packages we use "$host" as a security workaround. +# It avoids forwarding an untrusted raw Host header to the backend. +# +# Note: this changes behaviour compared to previous versions, because "$host" +# does not preserve the client-supplied port, while "$http_host" typically +# does. Existing deployments that rely on "$http_host" containing a port number +# may therefore break or behave differently after this change. + +fastcgi_param HTTP_HOST $host; diff --git a/conf/nginx/global.conf b/conf/nginx/global.conf new file mode 100644 index 0000000..b3a5f35 --- /dev/null +++ b/conf/nginx/global.conf @@ -0,0 +1 @@ +server_tokens off; diff --git a/conf/nginx/proxy_params_no_auth b/conf/nginx/proxy_params_no_auth new file mode 100644 index 0000000..7237ea5 --- /dev/null +++ b/conf/nginx/proxy_params_no_auth @@ -0,0 +1,23 @@ +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Scheme $scheme; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $server_name; +proxy_set_header X-Forwarded-Scheme $scheme; +proxy_set_header X-Forwarded-Ssl $https; +proxy_set_header X-Forwarded-Server $host; + +# Mainly for websocket support but shouldn't hurt to set it globally +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $connection_upgrade; + +# Clean auth headers to ensure that the client can't inject any header for authentication +# The Authorization header cannot be force-cleared here, because some apps do have auth mechanism that depend on other things than the YunoHost SSO (cf other basic-auth based stuff like Webdav(?) or "Bearer"-type auth +# proxy_set_header Authorization ""; +proxy_set_header Ynh-User ""; +proxy_set_header Ynh-User-Email ""; +proxy_set_header Ynh-User-Fullname ""; +proxy_set_header REMOTE_USER ""; +proxy_set_header X-Forwarded-User ""; diff --git a/conf/nginx/proxy_params_with_auth b/conf/nginx/proxy_params_with_auth new file mode 100644 index 0000000..91692f6 --- /dev/null +++ b/conf/nginx/proxy_params_with_auth @@ -0,0 +1,22 @@ +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Scheme $scheme; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $server_name; +proxy_set_header X-Forwarded-Scheme $scheme; +proxy_set_header X-Forwarded-Ssl $https; +proxy_set_header X-Forwarded-Server $host; + +# Mainly for websocket support but shouldn't hurt to set it globally +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $connection_upgrade; + +# Set Authentication header, in addition to the header by ssowat. Note ssowat inject YNH_USER and here we inject Ynh-User which is different. +proxy_set_header Authorization $http_authorization; +proxy_set_header Ynh-User $http_ynh_user; +proxy_set_header Ynh-User-Email $http_ynh_user_email; +proxy_set_header Ynh-User-Fullname $http_ynh_user_fullname; +proxy_set_header REMOTE_USER $http_ynh_user; +proxy_set_header X-Forwarded-User $http_ynh_user; diff --git a/conf/nginx/redirect_to_admin.conf b/conf/nginx/redirect_to_admin.conf new file mode 100644 index 0000000..1d7933c --- /dev/null +++ b/conf/nginx/redirect_to_admin.conf @@ -0,0 +1,3 @@ +location / { + return 302 https://$host/yunohost/admin; +} diff --git a/conf/nginx/security.conf.inc b/conf/nginx/security.conf.inc new file mode 100644 index 0000000..2c9eb65 --- /dev/null +++ b/conf/nginx/security.conf.inc @@ -0,0 +1,51 @@ +ssl_session_timeout 1d; +ssl_session_cache shared:SSL:50m; # about 200000 sessions +ssl_session_tickets off; + +{% if compatibility == "modern" %} +# 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 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:DHE-RSA-CHACHA20-POLY1305; +ssl_prefer_server_ciphers off; + +# Pre-defined FFDHE group (RFC 7919) +# From https://ssl-config.mozilla.org/ffdhe2048.txt +# https://security.stackexchange.com/a/149818 +ssl_dhparam /usr/share/yunohost/ffdhe2048.pem; +{% endif %} + + +# Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners +# 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'; worker-src 'self' blob:;"; +{% else %} +more_set_headers "Content-Security-Policy : upgrade-insecure-requests"; +{% endif %} +more_set_headers "X-Content-Type-Options : nosniff"; +more_set_headers "X-XSS-Protection : 1; mode=block"; +more_set_headers "X-Download-Options : noopen"; +more_set_headers "X-Permitted-Cross-Domain-Policies : none"; +more_set_headers "X-Frame-Options : SAMEORIGIN"; + +# Disable the disaster privacy thing that is FLoC +{% if experimental == "True" %} +more_set_headers "Permissions-Policy : fullscreen=(), geolocation=(), payment=(), accelerometer=(), battery=(), magnetometer=(), usb=(), interest-cohort=()"; +# Force HTTPOnly and Secure for all cookies +# Disabled because incompatible with the new cookie management system +# proxy_cookie_path ~$ "; HTTPOnly; Secure;"; +{% else %} +more_set_headers "Permissions-Policy : interest-cohort=()"; +{% endif %} + +# Disable gzip to protect against BREACH +# Read https://trac.nginx.org/nginx/ticket/1720 (text/html cannot be disabled!) +gzip off; diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf new file mode 100644 index 0000000..effb475 --- /dev/null +++ b/conf/nginx/server.tpl.conf @@ -0,0 +1,81 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + listen [::]:80; + 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 /var/www/.well-known/ynh-diagnosis/; + } + {%- if mail_enabled == "True" %} + + location ^~ '/.well-known/autoconfig/mail/' { + alias /var/www/.well-known/{{ domain }}/autoconfig/mail/; + } + {%- endif %} + + {#- Note that this != "False" is meant to be failure-safe, in the case the redrect_to_https would happen to contain empty string or whatever value. We absolutely don't want to disable the HTTPS redirect *except* when it's explicitly being asked to be disabled. #} + {%- if redirect_to_https != "False" %} + + location / { + return 301 https://$host$request_uri; + } + {#- The app config snippets are not included in the HTTP conf unless HTTPS redirect is disabled, because app's location may blocks will conflict or bypass/ignore the HTTPS redirection. #} + {%- else %} + + include /etc/nginx/conf.d/{{ domain }}.d/*.conf; + {%- endif %} + + include /etc/nginx/conf.d/yunohost_http_errors.conf.inc; + + access_log /var/log/nginx/{{ domain }}-access.log; + error_log /var/log/nginx/{{ domain }}-error.log; +} + +server { + {%- if tls_passthrough_enabled != "True" %} + listen 443 ssl http2; + listen [::]:443 ssl http2; + {%- else %} + listen 127.0.0.1:444 ssl proxy_protocol; + port_in_redirect off; + set_real_ip_from 127.0.0.1/32; + real_ip_header proxy_protocol; + {%- endif %} + server_name {{ domain }}; + + 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 mail_enabled == "True" %} + + location ^~ '/.well-known/autoconfig/mail/' { + alias /var/www/.well-known/{{ domain }}/autoconfig/mail/; + } + {%- endif %} + + access_by_lua_file /usr/share/ssowat/access.lua; + + include /etc/nginx/conf.d/{{ domain }}.d/*.conf; + + include /etc/nginx/conf.d/yunohost_sso.conf.inc; + include /etc/nginx/conf.d/yunohost_admin.conf.inc; + include /etc/nginx/conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/yunohost_http_errors.conf.inc; + + access_log /var/log/nginx/{{ domain }}-access.log; + error_log /var/log/nginx/{{ domain }}-error.log; +} diff --git a/conf/nginx/ssowat.conf b/conf/nginx/ssowat.conf new file mode 100644 index 0000000..bd8d5a7 --- /dev/null +++ b/conf/nginx/ssowat.conf @@ -0,0 +1,3 @@ +lua_shared_dict cache 10m; +init_by_lua_file /usr/share/ssowat/init.lua; +server_names_hash_bucket_size 128; diff --git a/conf/nginx/tls_passthrough.conf b/conf/nginx/tls_passthrough.conf new file mode 100644 index 0000000..7a2560e --- /dev/null +++ b/conf/nginx/tls_passthrough.conf @@ -0,0 +1,28 @@ +{% set domain_ip_map = tls_passthrough_list.split(',') %} +stream { + + map $ssl_preread_server_name $name { + {% for domain_ip in domain_ip_map %} + {{ domain_ip.split(";")[0] }} {{ domain_ip.split(";")[0].replace('.', '_') }}; + {%- endfor %} + default https_default_backend; + } + {%- for domain_ip in domain_ip_map %} + + upstream {{ domain_ip.split(";")[0].replace('.', '_') }} { + server {{ domain_ip.split(";")[1] }}:{{ domain_ip.split(";")[2] }}; + } + {%- endfor %} + + upstream https_default_backend { + server 127.0.0.1:444; + } + + server { + listen 443; + listen [::]:443; + proxy_pass $name; + proxy_protocol on; + ssl_preread on; + } +} diff --git a/conf/nginx/tls_passthrough_server.conf b/conf/nginx/tls_passthrough_server.conf new file mode 100644 index 0000000..8abc9f8 --- /dev/null +++ b/conf/nginx/tls_passthrough_server.conf @@ -0,0 +1,38 @@ +# This snippet is only here to redirect traffic to another domain on port 80, +# which is also forwarded for port 443 based on the SNI (which is handled +# differently because of the whole SNI story) + +# We don't explicitly redirect to HTTPS by default and let the forwarded server +# handle the redirection (or not depending on what's configured on the other +# server) + +server { + listen 80; + listen [::]:80; + server_name {{ tls_passthrough_domain }}; + + location / { + proxy_pass http://{{ tls_passthrough_ip }}; + + proxy_set_header Host $host; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Uri $request_uri; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Connection ""; + + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + send_timeout 5m; + proxy_read_timeout 360; + proxy_send_timeout 360; + proxy_connect_timeout 360; + } + + access_log /var/log/nginx/{{ tls_passthrough_domain }}-access.log; + error_log /var/log/nginx/{{ tls_passthrough_domain }}-error.log; +} diff --git a/conf/nginx/yunohost_admin.conf b/conf/nginx/yunohost_admin.conf new file mode 100644 index 0000000..03c5ac4 --- /dev/null +++ b/conf/nginx/yunohost_admin.conf @@ -0,0 +1,35 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + include /etc/nginx/conf.d/default.d/*.conf; +} + +server { + + {% if tls_passthrough_enabled != "True" %} + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; + {% else %} + listen 127.0.0.1:444 ssl http2 default_server; + # Prevent 301/302 rewrite/redirect from including the 444 port + port_in_redirect off; + {% endif %} + + include /etc/nginx/conf.d/security.conf.inc; + + ssl_certificate /etc/yunohost/certs/yunohost.org/crt.pem; + ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; + + more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; + more_set_headers "Referrer-Policy : 'same-origin'"; + + location /yunohost { + # Redirect most of 404 to maindomain.tld/yunohost/sso + access_by_lua_file /usr/share/ssowat/access.lua; + } + + include /etc/nginx/conf.d/yunohost_admin.conf.inc; + include /etc/nginx/conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/default.d/*.conf; +} diff --git a/conf/nginx/yunohost_admin.conf.inc b/conf/nginx/yunohost_admin.conf.inc new file mode 100644 index 0000000..0c4a96f --- /dev/null +++ b/conf/nginx/yunohost_admin.conf.inc @@ -0,0 +1,30 @@ +# Avoid the nginx path/alias traversal weakness ( #1037 ) +rewrite ^/yunohost/admin$ /yunohost/admin/ permanent; + +location /yunohost/admin/ { + alias /usr/share/yunohost/admin/; + default_type text/html; + index index.html; + + {% if webadmin_allowlist_enabled == "True" %} + {% if webadmin_allowlist.strip() -%} + {% for ip in webadmin_allowlist.strip().split(',') -%} + allow {{ ip.strip() }}; + {% endfor -%} + {% endif -%} + deny all; + {% endif %} + + location = /yunohost/admin/index.html { + etag off; + expires off; + more_set_headers "Cache-Control: no-store, no-cache, must-revalidate"; + } + + location /yunohost/admin/applogos/ { + alias /usr/share/yunohost/applogos/; + } + + more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; connect-src 'self' https://paste.yunohost.org wss://$host; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; object-src 'none'; img-src 'self' data:;"; + more_set_headers "Content-Security-Policy-Report-Only:"; +} diff --git a/conf/nginx/yunohost_api.conf.inc b/conf/nginx/yunohost_api.conf.inc new file mode 100644 index 0000000..53a790b --- /dev/null +++ b/conf/nginx/yunohost_api.conf.inc @@ -0,0 +1,46 @@ +location /yunohost/api/ { + proxy_read_timeout 3600s; + proxy_pass http://127.0.0.1:6787/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + + {% if webadmin_allowlist_enabled == "True" %} + {% for ip in webadmin_allowlist.split(',') %} + allow {{ ip }}; + {% endfor %} + deny all; + {% endif %} + + # Custom 502 error page + error_page 502 /yunohost/api/error/502; +} + +# Yunohost admin output complete 502 error page, so use only plain text. +location = /yunohost/api/error/502 { + return 502 '502 - Bad Gateway'; + add_header Content-Type text/plain; + internal; +} + +location /yunohost/portalapi/ { + + proxy_read_timeout 30s; + 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/yunohost_http_errors.conf.inc b/conf/nginx/yunohost_http_errors.conf.inc new file mode 100644 index 0000000..76f1015 --- /dev/null +++ b/conf/nginx/yunohost_http_errors.conf.inc @@ -0,0 +1,7 @@ +error_page 502 /502.html; + +location = /502.html { + + root /usr/share/yunohost/html/; + +} diff --git a/conf/nginx/yunohost_sso.conf.inc b/conf/nginx/yunohost_sso.conf.inc new file mode 100644 index 0000000..839b50c --- /dev/null +++ b/conf/nginx/yunohost_sso.conf.inc @@ -0,0 +1,28 @@ +# 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/; + } + + location = /yunohost/sso/customassets/custom.css { + alias /usr/share/yunohost/portal/customassets/$host.custom.css; + etag off; + expires off; + more_set_headers "Cache-Control: no-store, no-cache, must-revalidate"; + } + + 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/nslcd/nslcd.conf b/conf/nslcd/nslcd.conf new file mode 100644 index 0000000..7cfe73e --- /dev/null +++ b/conf/nslcd/nslcd.conf @@ -0,0 +1,37 @@ +# /etc/nslcd.conf +# nslcd configuration file. See nslcd.conf(5) +# for details. + +# The user and group nslcd should run as. +uid nslcd +gid nslcd + +# The location at which the LDAP server(s) should be reachable. +uri ldap://localhost/ + +# The search base that will be used for all queries. +base dc=yunohost,dc=org + +# The LDAP protocol version to use. +#ldap_version 3 + +# The DN to bind with for normal lookups. +#binddn cn=annonymous,dc=example,dc=net +#bindpw secret + +# The DN used for password modifications by root. +#rootpwmoddn cn=admin,dc=example,dc=com + +# SSL options +#ssl off +#tls_reqcert never +tls_cacertfile /etc/ssl/certs/ca-certificates.crt + +# The search scope. +#scope sub + +# Build a full list of non-LDAP users on startup. +nss_initgroups_ignoreusers ALLLOCAL + +# The minimum numeric user id to lookup. +nss_min_uid 1000 diff --git a/conf/nsswitch/nsswitch.conf b/conf/nsswitch/nsswitch.conf new file mode 100644 index 0000000..8f46e4f --- /dev/null +++ b/conf/nsswitch/nsswitch.conf @@ -0,0 +1,17 @@ +# /etc/nsswitch.conf + +passwd: files systemd ldap +group: files systemd ldap +shadow: files ldap +gshadow: files + +hosts: files myhostname mdns4_minimal [NOTFOUND=return] dns +networks: files + +protocols: db files +services: db files +ethers: db files +rpc: db files + +netgroup: nis +sudoers: files ldap diff --git a/conf/opendkim/opendkim.conf b/conf/opendkim/opendkim.conf new file mode 100644 index 0000000..303e504 --- /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 new file mode 100644 index 0000000..2c9e6c3 --- /dev/null +++ b/conf/postfix/main.cf @@ -0,0 +1,223 @@ +# See /usr/share/postfix/main.cf.dist for a commented, more complete version + + +# Debian specific: Specifying a file name will cause the first +# line of that file to be used as the name. The Debian default +# is /etc/mailname. +#myorigin = /etc/mailname + +smtpd_banner = $myhostname Service ready +biff = no + +# appending .domain is the MUA's job. +append_dot_mydomain = no + +# Uncomment the next line to generate "delayed mail" warnings +#delay_warning_time = 4h + +readme_directory = no + +# -- TLS for incoming connections +############################################################################### +smtpd_use_tls = yes + +smtpd_tls_security_level = may +smtpd_tls_auth_only = yes +smtpd_tls_chain_files = + /etc/yunohost/certs/{{ main_domain }}/key.pem, + /etc/yunohost/certs/{{ main_domain }}/crt.pem + +tls_server_sni_maps = hash:/etc/postfix/sni + +{% if compatibility == "intermediate" %} +# 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 +smtpd_tls_mandatory_ciphers = medium + +# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam.pem +# 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:DHE-RSA-CHACHA20-POLY1305 +{% else %} +# 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 +{% endif %} + +tls_preempt_cipherlist = no +############################################################################### +smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache +smtpd_tls_loglevel=1 + +# -- TLS for outgoing connections +# Use TLS if this is supported by the remote SMTP server, otherwise use plaintext. +{% if relay_port == "465" %} +smtp_tls_wrappermode = yes +smtp_tls_security_level = encrypt +{% else %} +smtp_tls_security_level = may +{% endif %} +smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache +smtp_tls_exclude_ciphers = aNULL, MD5, DES, ADH, RC4, 3DES +smtp_tls_mandatory_ciphers= high +smtp_tls_loglevel=1 + +# Configure Root CA certificates +# (for example, avoids getting "Untrusted TLS connection established to" messages in logs) +smtpd_tls_CAfile = /etc/ssl/certs/ca-certificates.crt +smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt + +# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for +# information on enabling SSL in the smtp client. + +myhostname = {{ main_domain }} +alias_maps = hash:/etc/aliases +alias_database = hash:/etc/aliases +mydomain = {{ main_domain }} +mydestination = localhost +{% if relay_enabled != "True" %} +relayhost = +{% else %} +relayhost = [{{ relay_host }}]:{{ relay_port }} +{% endif %} +mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 +mailbox_command = procmail -a "$EXTENSION" +mailbox_size_limit = 0 +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. +# 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 = /etc/postfix/virtual-mailbox-domains +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 = 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 +dovecot_destination_recipient_limit = 1 + +# Enable SASL authentication for the smtpd daemon +smtpd_sasl_auth_enable = yes +smtpd_sasl_type = dovecot +smtpd_sasl_path = private/auth +# Fix some outlook's bugs +broken_sasl_auth_clients = yes +# Reject anonymous connections +smtpd_sasl_security_options = noanonymous +smtpd_sasl_local_domain = + + +# Wait until the RCPT TO command before evaluating restrictions +smtpd_delay_reject = yes + +# Basics Restrictions +smtpd_helo_required = yes +strict_rfc821_envelopes = yes + +# Requirements for the connecting server +smtpd_client_restrictions = + permit_mynetworks, + permit_sasl_authenticated, + {%- if enable_blocklists == 'True' %} + reject_rbl_client bl.spamcop.net, + reject_rbl_client zen.spamhaus.org, + {%- endif %} + permit + +# Requirements for the HELO statement +smtpd_helo_restrictions = + permit_mynetworks, + permit_sasl_authenticated, + reject_non_fqdn_hostname, + reject_invalid_hostname, + permit + +# Requirements for the sender address +smtpd_sender_restrictions = + reject_sender_login_mismatch, + permit_mynetworks, + permit_sasl_authenticated, + reject_non_fqdn_sender, + reject_unknown_sender_domain, + permit + +# Requirement for the recipient address +smtpd_recipient_restrictions = + permit_mynetworks, + permit_sasl_authenticated, + reject_non_fqdn_recipient, + reject_unknown_recipient_domain, + reject_unauth_destination, + permit + +# SRS +sender_canonical_maps = tcp:localhost:10001 +sender_canonical_classes = envelope_sender +recipient_canonical_maps = tcp:localhost:10002 +recipient_canonical_classes= envelope_recipient,header_recipient + +# Ignore some headers +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} {auth_type} +milter_protocol = 6 +smtpd_milters = inet:localhost:8891 +non_smtpd_milters = inet:localhost:8891 + +# Skip email without checking if milter has died +milter_default_action = accept + +# Avoid to send simultaneously too many emails +smtp_destination_concurrency_limit = 2 +default_destination_rate_delay = 5s + +# Avoid to be blacklisted due to too many recipient +smtpd_client_recipient_rate_limit=150 + +# Avoid email adress scanning +# By default it's possible to detect if the email adress exist +# So it's easly possible to scan a server to know which email adress is valid +# and after to send spam +disable_vrfy_command = yes + +{% if relay_enabled == "True" %} +# Relay email through an other smtp account +# enable SASL authentication +smtp_sasl_auth_enable = yes +# disallow methods that allow anonymous authentication. +smtp_sasl_security_options = noanonymous +# where to find sasl_passwd +smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd +{% endif %} + +{% if backup_mx_domains != "" %} +# Backup MX (secondary MX) +relay_domains = $mydestination {{backup_mx_domains}} +relay_recipient_maps = hash:/etc/postfix/relay_recipients +maximal_queue_lifetime = 20d +{% endif %} + diff --git a/conf/postfix/plain/header_checks b/conf/postfix/plain/header_checks new file mode 100644 index 0000000..bf5c335 --- /dev/null +++ b/conf/postfix/plain/header_checks @@ -0,0 +1,4 @@ +/^X-Originating-IP:/ IGNORE +/^Received:/ IGNORE +/^User-Agent:/ IGNORE +/^X-Mailer:/ IGNORE diff --git a/conf/postfix/plain/ldap-accounts.cf b/conf/postfix/plain/ldap-accounts.cf new file mode 100644 index 0000000..75f38cf --- /dev/null +++ b/conf/postfix/plain/ldap-accounts.cf @@ -0,0 +1,5 @@ +server_host = localhost +server_port = 389 +search_base = dc=yunohost,dc=org +query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) +result_attribute = uid diff --git a/conf/postfix/plain/ldap-aliases.cf b/conf/postfix/plain/ldap-aliases.cf new file mode 100644 index 0000000..46563ae --- /dev/null +++ b/conf/postfix/plain/ldap-aliases.cf @@ -0,0 +1,5 @@ +server_host = localhost +server_port = 389 +search_base = dc=yunohost,dc=org +query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)) +result_attribute = maildrop diff --git a/conf/postfix/plain/ldap-domains.cf b/conf/postfix/plain/ldap-domains.cf new file mode 100644 index 0000000..e69de29 diff --git a/conf/postfix/plain/ldap-groups.cf b/conf/postfix/plain/ldap-groups.cf new file mode 100644 index 0000000..215081f --- /dev/null +++ b/conf/postfix/plain/ldap-groups.cf @@ -0,0 +1,7 @@ +server_host = localhost +server_port = 389 +search_base = dc=yunohost,dc=org +query_filter = (&(objectClass=groupOfNamesYnh)(mail=%s)) +scope = sub +result_attribute = memberUid, mail +terminal_result_attribute = memberUid diff --git a/conf/postfix/plain/master.cf b/conf/postfix/plain/master.cf new file mode 100644 index 0000000..377a909 --- /dev/null +++ b/conf/postfix/plain/master.cf @@ -0,0 +1,127 @@ +# +# Postfix master process configuration file. For details on the format +# of the file, see the master(5) manual page (command: "man 5 master" or +# on-line: http://www.postfix.org/master.5.html). +# +# Do not forget to execute "postfix reload" after editing this file. +# +# ========================================================================== +# service type private unpriv chroot wakeup maxproc command + args +# (yes) (yes) (no) (never) (100) +# ========================================================================== +smtp inet n - y - - smtpd +#smtp inet n - y - 1 postscreen +#smtpd pass - - y - - smtpd +#dnsblog unix - - y - 0 dnsblog +#tlsproxy unix - - y - 0 tlsproxy +submission inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes +# -o smtpd_reject_unlisted_recipient=no +# -o smtpd_client_restrictions=$mua_client_restrictions +# -o smtpd_helo_restrictions=$mua_helo_restrictions +# -o smtpd_sender_restrictions=$mua_sender_restrictions +# -o smtpd_recipient_restrictions= +# -o smtpd_relay_restrictions=permit_sasl_authenticated,reject +# -o milter_macro_daemon_name=ORIGINATING +#smtps inet n - y - - smtpd +# -o syslog_name=postfix/smtps +# -o smtpd_tls_wrappermode=yes +# -o smtpd_sasl_auth_enable=yes +# -o smtpd_reject_unlisted_recipient=no +# -o smtpd_client_restrictions=$mua_client_restrictions +# -o smtpd_helo_restrictions=$mua_helo_restrictions +# -o smtpd_sender_restrictions=$mua_sender_restrictions +# -o smtpd_recipient_restrictions= +# -o smtpd_relay_restrictions=permit_sasl_authenticated,reject +# -o milter_macro_daemon_name=ORIGINATING +#628 inet n - y - - qmqpd +pickup unix n - y 60 1 pickup +cleanup unix n - y - 0 cleanup +qmgr unix n - n 300 1 qmgr +#qmgr unix n - n 300 1 oqmgr +tlsmgr unix - - y 1000? 1 tlsmgr +rewrite unix - - y - - trivial-rewrite +bounce unix - - y - 0 bounce +defer unix - - y - 0 bounce +trace unix - - y - 0 bounce +verify unix - - y - 1 verify +flush unix n - y 1000? 0 flush +proxymap unix - - n - - proxymap +proxywrite unix - - n - 1 proxymap +smtp unix - - y - - smtp +relay unix - - y - - smtp +# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5 +showq unix n - y - - showq +error unix - - y - - error +retry unix - - y - - error +discard unix - - y - - discard +local unix - n n - - local +virtual unix - n n - - virtual +lmtp unix - - y - - lmtp +anvil unix - - y - 1 anvil +scache unix - - y - 1 scache +# +# ==================================================================== +# Interfaces to non-Postfix software. Be sure to examine the manual +# pages of the non-Postfix software to find out what options it wants. +# +# Many of the following services use the Postfix pipe(8) delivery +# agent. See the pipe(8) man page for information about ${recipient} +# and other message envelope options. +# ==================================================================== +# +# maildrop. See the Postfix MAILDROP_README file for details. +# Also specify in main.cf: maildrop_destination_recipient_limit=1 +# +maildrop unix - n n - - pipe + flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient} +# +# ==================================================================== +# +# Recent Cyrus versions can use the existing "lmtp" master.cf entry. +# +# Specify in cyrus.conf: +# lmtp cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4 +# +# Specify in main.cf one or more of the following: +# mailbox_transport = lmtp:inet:localhost +# virtual_transport = lmtp:inet:localhost +# +# ==================================================================== +# +# Cyrus 2.1.5 (Amos Gouaux) +# Also specify in main.cf: cyrus_destination_recipient_limit=1 +# +#cyrus unix - n n - - pipe +# user=cyrus argv=/cyrus/bin/deliver -e -r ${sender} -m ${extension} ${user} +# +# ==================================================================== +# Old example of delivery via Cyrus. +# +#old-cyrus unix - n n - - pipe +# flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user} +# +# ==================================================================== +# +# See the Postfix UUCP_README file for configuration details. +# +uucp unix - n n - - pipe + flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient) +# +# Other external delivery methods. +# +ifmail unix - n n - - pipe + flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient) +bsmtp unix - n n - - pipe + flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient +scalemail-backend unix - n n - 2 pipe + flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension} +mailman unix - n n - - pipe + flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py + ${nexthop} ${user} + +# Dovecot LDA +dovecot unix - n n - - pipe + flags=DRhu user=vmail:mail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -m ${extension} -a ${recipient} diff --git a/conf/postfix/plain/sender_canonical b/conf/postfix/plain/sender_canonical new file mode 100644 index 0000000..caf093d --- /dev/null +++ b/conf/postfix/plain/sender_canonical @@ -0,0 +1 @@ +/^(.*)@(.*)$/ ${1} diff --git a/conf/postfix/plain/smtp_reply_filter b/conf/postfix/plain/smtp_reply_filter new file mode 100644 index 0000000..5f15648 --- /dev/null +++ b/conf/postfix/plain/smtp_reply_filter @@ -0,0 +1,8 @@ +# Google Mail bounces email sent via IPv6, while this works ok with IPv4. +# +# Convert Google Mail IPv6 complaint permanent error into a temporary error. +# Turn 550 error containing gsmtp in the message into 450 error. +# This way Postfix will attempt to deliver this e-mail using another MX +# (via IPv4). +# +/^5(\d\d )5(.*information. \S+ - gsmtp.*)/ 4${1}4$2 diff --git a/conf/postfix/postsrsd b/conf/postfix/postsrsd new file mode 100644 index 0000000..a0451fa --- /dev/null +++ b/conf/postfix/postsrsd @@ -0,0 +1,43 @@ +# Default settings for postsrsd + +# Local domain name. +# Addresses are rewritten to originate from this domain. The default value +# is taken from `postconf -h mydomain` and probably okay. +# +SRS_DOMAIN={{ main_domain }} + +# Exclude additional domains. +# You may list domains which shall not be subjected to address rewriting. +# If a domain name starts with a dot, it matches all subdomains, but not +# the domain itself. Separate multiple domains by space or comma. +# We have to put some "dummy" stuff at start and end... see this comment : +# https://github.com/roehling/postsrsd/issues/64#issuecomment-284003762 +SRS_EXCLUDE_DOMAINS="dummy {{ domain_list }} dummy" + +# First separator character after SRS0 or SRS1. +# Can be one of: -+= +SRS_SEPARATOR== + +# Secret key to sign rewritten addresses. +# When postsrsd is installed for the first time, a random secret is generated +# and stored in /etc/postsrsd.secret. For most installations, that's just fine. +# +SRS_SECRET=/etc/postsrsd.secret + +# Local ports for TCP list. +# These ports are used to bind the TCP list for postfix. If you change +# these, you have to modify the postfix settings accordingly. The ports +# are bound to the loopback interface, and should never be exposed on +# the internet. +# +SRS_FORWARD_PORT=10001 +SRS_REVERSE_PORT=10002 + +# Drop root privileges and run as another user after initialization. +# This is highly recommended as postsrsd handles untrusted input. +# +RUN_AS=postsrsd + +# Jail daemon in chroot environment +CHROOT=/var/lib/postsrsd + diff --git a/conf/postfix/sni b/conf/postfix/sni new file mode 100644 index 0000000..b57b7e0 --- /dev/null +++ b/conf/postfix/sni @@ -0,0 +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 %} diff --git a/conf/slapd/config.ldif b/conf/slapd/config.ldif new file mode 100644 index 0000000..9325df6 --- /dev/null +++ b/conf/slapd/config.ldif @@ -0,0 +1,233 @@ +# OpenLDAP server configuration for YunoHost +# ------------------------------------------ +# +# Because of the YunoHost's regen-conf mechanism, it is NOT POSSIBLE to +# edit the config database using an LDAP request. +# +# If you wish to edit the config database, you should edit THIS file +# and update the config database based on this file. +# +# Config database customization: +# 1. Edit this file as you want. +# 2. Apply your modifications. For this just run this following command in a shell: +# $ /usr/share/yunohost/hooks/conf_regen/06-slapd post true +# +# Note that if you customize this file, YunoHost's regen-conf will NOT +# overwrite this file. But that also means that you should be careful about +# upgrades, because they may ship important/necessary changes to this +# configuration that you will have to propagate yourself. + +# +# Main configuration +# +dn: cn=config +objectClass: olcGlobal +cn: config +olcConfigFile: /etc/ldap/slapd.conf +olcConfigDir: /etc/ldap/slapd.d/ +# List of arguments that were passed to the server +olcArgsFile: /var/run/slapd/slapd.args +# +olcAttributeOptions: lang- +olcAuthzPolicy: none +olcConcurrency: 0 +olcConnMaxPending: 100 +olcConnMaxPendingAuth: 1000 +olcSizeLimit: 50000 +olcIdleTimeout: 0 +olcIndexSubstrIfMaxLen: 4 +olcIndexSubstrIfMinLen: 2 +olcIndexSubstrAnyLen: 4 +olcIndexSubstrAnyStep: 2 +olcIndexIntLen: 4 +olcListenerThreads: 1 +olcLocalSSF: 71 +# Read slapd.conf(5) for possible values +olcLogLevel: None +# Where the pid file is put. The init.d script +# will not stop the server if you change this. +olcPidFile: /var/run/slapd/slapd.pid +olcReverseLookup: FALSE +olcThreads: 16 +# TLS Support +olcTLSCertificateFile: /etc/yunohost/certs/yunohost.org/crt.pem +olcTLSCertificateKeyFile: /etc/yunohost/certs/yunohost.org/key.pem +olcTLSVerifyClient: never +olcTLSProtocolMin: 0.0 +# The tool-threads parameter sets the actual amount of cpu's that is used +# for indexing. +olcToolThreads: 1 +structuralObjectClass: olcGlobal + +# +# Schema and objectClass definitions +# +dn: cn=schema,cn=config +objectClass: olcSchemaConfig +cn: schema + +include: file:///etc/ldap/schema/core.ldif +include: file:///etc/ldap/schema/cosine.ldif +include: file:///etc/ldap/schema/nis.ldif +include: file:///etc/ldap/schema/inetorgperson.ldif +include: file:///etc/ldap/schema/mailserver.ldif +include: file:///etc/ldap/schema/sudo.ldif +include: file:///etc/ldap/schema/permission.ldif + +# +# Module management +# +dn: cn=module{0},cn=config +objectClass: olcModuleList +cn: module{0} +# Where the dynamically loaded modules are stored +olcModulePath: /usr/lib/ldap +olcModuleLoad: {0}back_mdb +olcModuleLoad: {1}memberof +structuralObjectClass: olcModuleList + +# +# Frontend database +# +dn: olcDatabase={-1}frontend,cn=config +objectClass: olcDatabaseConfig +objectClass: olcFrontendConfig +olcDatabase: {-1}frontend +olcAddContentAcl: FALSE +olcLastMod: TRUE +olcSchemaDN: cn=Subschema +# Hashes to be used in generation of user passwords +olcPasswordHash: {SSHA} +structuralObjectClass: olcDatabaseConfig + +# +# Config database Configuration (#0) +# +dn: olcDatabase={0}config,cn=config +objectClass: olcDatabaseConfig +olcDatabase: {0}config +# Give access to root user. +# This give the possiblity to the admin to customize the LDAP configuration +olcAccess: {0}to * by * none +olcAddContentAcl: TRUE +olcLastMod: TRUE +olcRootDN: cn=config +structuralObjectClass: olcDatabaseConfig + +# +# Main database Configuration (#1) +# +dn: olcDatabase={1}mdb,cn=config +objectClass: olcDatabaseConfig +objectClass: olcMdbConfig +olcDatabase: {1}mdb +# The base of your directory in database #1 +olcSuffix: dc=yunohost,dc=org +# +# The userPassword by default can be changed +# by the entry owning it if they are authenticated. +# Others should not be able to see it, except the +# admin entry below +# These access lines apply to database #1 only +olcAccess: {0}to attrs=userPassword,shadowLastChange + by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write + by anonymous auth + by self write + by * none +# +# Personnal information can be changed by the entry +# owning it if they are authenticated. +# Others should be able to see it. +olcAccess: {1}to attrs=cn,gecos,givenName,mail,maildrop,displayName,sn + by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write + by self write + by * read +# +# Ensure read access to the base for things like +# supportedSASLMechanisms. Without this you may +# have problems with SASL not knowing what +# mechanisms are available and the like. +# Note that this is covered by the 'access to *' +# ACL below too but if you change that as people +# are wont to do you'll still need this if you +# want SASL (and possible other things) to work +# happily. +olcAccess: {2}to dn.base="" + by * read +# +# The admin dn has full write access, everyone else +# can read everything. +olcAccess: {3}to * + by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write + by group/groupOfNamesYnh/member.exact="cn=admins,ou=groups,dc=yunohost,dc=org" write + by * read +# +olcAddContentAcl: FALSE +# Save the time that the entry gets modified, for database #1 +olcLastMod: TRUE +# Where the database file are physically stored for database #1 +olcDbDirectory: /var/lib/ldap +# Checkpoint the BerkeleyDB database periodically in case of system +# failure and to speed slapd shutdown. +olcDbCheckpoint: 512 30 +olcDbNoSync: FALSE +# Indexing options for database #1 +olcDbIndex: objectClass eq +olcDbIndex: entryUUID eq +olcDbIndex: entryCSN eq +olcDbIndex: cn eq +olcDbIndex: uid eq,sub +olcDbIndex: uidNumber eq +olcDbIndex: gidNumber eq +olcDbIndex: sudoUser eq,sub +olcDbIndex: member eq +olcDbIndex: mail eq +olcDbIndex: memberUid eq +olcDbIndex: uniqueMember eq +olcDbIndex: virtualdomain eq +olcDbIndex: permission eq +olcDbMaxSize: 104857600 +structuralObjectClass: olcMdbConfig + +# +# Configure Memberof Overlay (used for YunoHost permission) +# + +# Link user <-> group +dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config +objectClass: olcOverlayConfig +objectClass: olcMemberOf +olcOverlay: {0}memberof +olcMemberOfDangling: error +olcMemberOfDanglingError: constraintViolation +olcMemberOfRefInt: TRUE +olcMemberOfGroupOC: groupOfNamesYnh +olcMemberOfMemberAD: member +olcMemberOfMemberOfAD: memberOf +structuralObjectClass: olcMemberOf + +# Link permission <-> groupes (OBSOLETE) +#dn: olcOverlay={1}memberof,olcDatabase={1}mdb,cn=config +#objectClass: olcOverlayConfig +#objectClass: olcMemberOf +#olcOverlay: {1}memberof +#olcMemberOfDangling: error +#olcMemberOfDanglingError: constraintViolation +#olcMemberOfRefInt: TRUE +#olcMemberOfGroupOC: permissionYnh +#olcMemberOfMemberAD: groupPermission +#olcMemberOfMemberOfAD: permission +#structuralObjectClass: olcMemberOf + +# Link permission <-> user +dn: olcOverlay={2}memberof,olcDatabase={1}mdb,cn=config +objectClass: olcOverlayConfig +objectClass: olcMemberOf +olcOverlay: {2}memberof +olcMemberOfDangling: error +olcMemberOfDanglingError: constraintViolation +olcMemberOfRefInt: TRUE +olcMemberOfGroupOC: permissionYnh +olcMemberOfMemberAD: inheritPermission +olcMemberOfMemberOfAD: permission +structuralObjectClass: olcMemberOf diff --git a/conf/slapd/db_init.ldif b/conf/slapd/db_init.ldif new file mode 100644 index 0000000..eab9214 --- /dev/null +++ b/conf/slapd/db_init.ldif @@ -0,0 +1,81 @@ +dn: dc=yunohost,dc=org +objectClass: top +objectClass: dcObject +objectClass: organization +o: yunohost.org +dc: yunohost + +dn: ou=users,dc=yunohost,dc=org +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: ou=domains,dc=yunohost,dc=org +objectClass: organizationalUnit +objectClass: top +ou: domains + +dn: ou=apps,dc=yunohost,dc=org +objectClass: organizationalUnit +objectClass: top +ou: apps + +dn: ou=permission,dc=yunohost,dc=org +objectClass: organizationalUnit +objectClass: top +ou: permission + +dn: ou=groups,dc=yunohost,dc=org +objectClass: organizationalUnit +objectClass: top +ou: groups + +dn: cn=admins,ou=sudo,dc=yunohost,dc=org +cn: admins +objectClass: sudoRole +objectClass: top +sudoCommand: ALL +sudoUser: %admins +sudoHost: ALL + +dn: ou=sudo,dc=yunohost,dc=org +objectClass: organizationalUnit +objectClass: top +ou: sudo + +dn: cn=admins,ou=groups,dc=yunohost,dc=org +objectClass: posixGroup +objectClass: top +objectClass: groupOfNamesYnh +gidNumber: 4001 +cn: admins + +dn: cn=all_users,ou=groups,dc=yunohost,dc=org +objectClass: posixGroup +objectClass: groupOfNamesYnh +gidNumber: 4002 +cn: all_users + +dn: cn=visitors,ou=groups,dc=yunohost,dc=org +objectClass: posixGroup +objectClass: groupOfNamesYnh +gidNumber: 4003 +cn: visitors + +dn: cn=mail.main,ou=permission,dc=yunohost,dc=org +cn: mail.main +objectClass: posixGroup +objectClass: permissionYnh +gidNumber: 5001 + +dn: cn=ssh.main,ou=permission,dc=yunohost,dc=org +cn: ssh.main +objectClass: posixGroup +objectClass: permissionYnh +gidNumber: 5003 + +dn: cn=sftp.main,ou=permission,dc=yunohost,dc=org +cn: sftp.main +objectClass: posixGroup +objectClass: permissionYnh +gidNumber: 5004 diff --git a/conf/slapd/ldap.conf b/conf/slapd/ldap.conf new file mode 100644 index 0000000..dfcb17e --- /dev/null +++ b/conf/slapd/ldap.conf @@ -0,0 +1,18 @@ +# +# LDAP Defaults +# + +# See ldap.conf(5) for details +# This file should be world readable but not world writable. + +BASE dc=yunohost,dc=org +URI ldap://localhost:389 + +SIZELIMIT 10000 +#TIMELIMIT 15 +#DEREF never + +# TLS certificates (needed for GnuTLS) +TLS_CACERT /etc/ssl/certs/ca-certificates.crt + +sudoers_base ou=sudo,dc=yunohost,dc=org diff --git a/conf/slapd/mailserver.ldif b/conf/slapd/mailserver.ldif new file mode 100644 index 0000000..09f5c64 --- /dev/null +++ b/conf/slapd/mailserver.ldif @@ -0,0 +1,95 @@ +## LDAP Schema Yunohost EMAIL +## Version 0.1 +## Adrien Beudin + +dn: cn=mailserver,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: mailserver +# +# Attributes +olcAttributeTypes: ( 1.3.6.1.4.1.40328.1.20.2.1 + NAME 'maildrop' + DESC 'Mail addresses where mails are forwarded -- ie forwards' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512}) +# +olcAttributeTypes: ( 1.3.6.1.4.1.40328.1.20.2.2 + NAME 'mailalias' + DESC 'Mail addresses accepted by this account -- ie aliases' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512}) +# +olcAttributeTypes: ( 1.3.6.1.4.1.40328.1.20.2.3 + NAME 'mailenable' + DESC 'Mail Account validity' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{8}) +# +olcAttributeTypes: ( 1.3.6.1.4.1.40328.1.20.2.4 + NAME 'mailbox' + DESC 'Mailbox path where mails are delivered' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512}) +# +olcAttributeTypes: ( 1.3.6.1.4.1.40328.1.20.2.5 + NAME 'virtualdomain' + DESC 'A mail domain name' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512}) +# +olcAttributeTypes: ( 1.3.6.1.4.1.40328.1.20.2.6 + NAME 'virtualdomaindescription' + DESC 'Virtual domain description' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512}) +# +olcAttributeTypes: ( 1.3.6.1.4.1.40328.1.20.2.7 + NAME 'mailuserquota' + DESC 'Mailbox quota for a user' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{16} SINGLE-VALUE ) +# +# Mail Account Objectclass +olcObjectClasses: ( 1.3.6.1.4.1.40328.1.1.2.1 + NAME 'mailAccount' + DESC 'Mail Account' + SUP top + AUXILIARY + MUST ( + mail + ) + MAY ( + mailalias $ maildrop $ mailenable $ mailbox $ mailuserquota + ) + ) +# +# Mail Domain Objectclass +olcObjectClasses: ( 1.3.6.1.4.1.40328.1.1.2.2 + NAME 'mailDomain' + DESC 'Domain mail entry' + SUP top + STRUCTURAL + MUST ( + virtualdomain + ) + MAY ( + virtualdomaindescription $ mailuserquota + ) + ) +# +# Mail Group Objectclass +olcObjectClasses: ( 1.3.6.1.4.1.40328.1.1.2.3 + NAME 'mailGroup' SUP top AUXILIARY + DESC 'Mail Group' + MUST ( mail ) + MAY ( + mailalias $ maildrop + ) + ) diff --git a/conf/slapd/permission.ldif b/conf/slapd/permission.ldif new file mode 100644 index 0000000..5e42efa --- /dev/null +++ b/conf/slapd/permission.ldif @@ -0,0 +1,56 @@ +# YunoHost schema for group and permission support + +dn: cn=yunohost,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: yunohost +# ATTRIBUTES +# For Permission +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.1 NAME 'permission' + DESC 'YunoHost permission on user and group side' + SUP distinguishedName ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.2 NAME 'groupPermission' + DESC 'YunoHost permission for a group on permission side' OBSOLETE + SUP distinguishedName ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.3 NAME 'inheritPermission' + DESC 'YunoHost permission for user on permission side' + SUP distinguishedName ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.4 NAME 'URL' + DESC 'YunoHost permission main URL' OBSOLETE + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.5 NAME 'additionalUrls' + DESC 'YunoHost permission additionnal URL' OBSOLETE + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.6 NAME 'authHeader' + DESC 'YunoHost application, enable authentication header' OBSOLETE + EQUALITY booleanMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.7 NAME 'label' + DESC 'YunoHost permission label, also used for the tile name in the SSO' OBSOLETE + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.8 NAME 'showTile' + DESC 'YunoHost application, show/hide the tile in the SSO for this permission' OBSOLETE + EQUALITY booleanMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE ) +olcAttributeTypes: ( 1.3.6.1.4.1.17953.9.1.9 NAME 'isProtected' + DESC 'YunoHost application permission protection' OBSOLETE + EQUALITY booleanMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE ) +# OBJECTCLASS +# For Applications +olcObjectClasses: ( 1.3.6.1.4.1.17953.9.2.1 NAME 'groupOfNamesYnh' + DESC 'YunoHost user group' + SUP top AUXILIARY + MAY ( member $ businessCategory $ seeAlso $ owner $ ou $ o $ permission ) ) +olcObjectClasses: ( 1.3.6.1.4.1.17953.9.2.2 NAME 'permissionYnh' + DESC 'a YunoHost permission' + SUP top AUXILIARY + MUST ( cn ) + MAY ( groupPermission $ inheritPermission $ URL $ additionalUrls $ authHeader $ label $ showTile $ isProtected ) ) +# For User +olcObjectClasses: ( 1.3.6.1.4.1.17953.9.2.3 NAME 'userPermissionYnh' + DESC 'a YunoHost user with permission attributes' + SUP top AUXILIARY + MAY ( permission ) ) diff --git a/conf/slapd/slapd.default b/conf/slapd/slapd.default new file mode 100644 index 0000000..6baca1e --- /dev/null +++ b/conf/slapd/slapd.default @@ -0,0 +1,45 @@ +# Default location of the slapd.conf file or slapd.d cn=config directory. If +# empty, use the compiled-in default (/etc/ldap/slapd.d with a fallback to +# /etc/ldap/slapd.conf). +SLAPD_CONF= + +# System account to run the slapd server under. If empty the server +# will run as root. +SLAPD_USER="openldap" + +# System group to run the slapd server under. If empty the server will +# run in the primary group of its user. +SLAPD_GROUP="openldap" + +# Path to the pid file of the slapd server. If not set the init.d script +# will try to figure it out from $SLAPD_CONF (/etc/ldap/slapd.conf by +# default) +SLAPD_PIDFILE= + +# slapd normally serves ldap only on all TCP-ports 389. slapd can also +# service requests on TCP-port 636 (ldaps) and requests via unix +# sockets. +# Example usage: +# SLAPD_SERVICES="ldap://127.0.0.1:389/ ldaps:/// ldapi:///" +SLAPD_SERVICES="ldap://localhost:389/ ldaps:/// ldapi:///" + +# If SLAPD_NO_START is set, the init script will not start or restart +# slapd (but stop will still work). Uncomment this if you are +# starting slapd via some other means or if you don't want slapd normally +# started at boot. +#SLAPD_NO_START=1 + +# If SLAPD_SENTINEL_FILE is set to path to a file and that file exists, +# the init script will not start or restart slapd (but stop will still +# work). Use this for temporarily disabling startup of slapd (when doing +# maintenance, for example, or through a configuration management system) +# when you don't want to edit a configuration file. +SLAPD_SENTINEL_FILE=/etc/ldap/noslapd + +# For Kerberos authentication (via SASL), slapd by default uses the system +# keytab file (/etc/krb5.keytab). To use a different keytab file, +# uncomment this line and change the path. +#export KRB5_KTNAME=/etc/krb5.keytab + +# Additional options to pass to slapd +SLAPD_OPTIONS="" diff --git a/conf/slapd/sudo.ldif b/conf/slapd/sudo.ldif new file mode 100644 index 0000000..a7088c8 --- /dev/null +++ b/conf/slapd/sudo.ldif @@ -0,0 +1,78 @@ +# +# OpenLDAP schema file for Sudo +# Save as /etc/openldap/schema/sudo.ldif +# + +dn: cn=sudo,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: sudo +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.1 + NAME 'sudoUser' + DESC 'User(s) who may run sudo' + EQUALITY caseExactIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.2 + NAME 'sudoHost' + DESC 'Host(s) who may run sudo' + EQUALITY caseExactIA5Match + SUBSTR caseExactIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.3 + NAME 'sudoCommand' + DESC 'Command(s) to be executed by sudo' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.4 + NAME 'sudoRunAs' + DESC 'User(s) impersonated by sudo (deprecated)' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.5 + NAME 'sudoOption' + DESC 'Options(s) followed by sudo' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.6 + NAME 'sudoRunAsUser' + DESC 'User(s) impersonated by sudo' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.7 + NAME 'sudoRunAsGroup' + DESC 'Group(s) impersonated by sudo' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.8 + NAME 'sudoNotBefore' + DESC 'Start of time interval for which the entry is valid' + EQUALITY generalizedTimeMatch + ORDERING generalizedTimeOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.9 + NAME 'sudoNotAfter' + DESC 'End of time interval for which the entry is valid' + EQUALITY generalizedTimeMatch + ORDERING generalizedTimeOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) +# +olcAttributeTypes: ( 1.3.6.1.4.1.15953.9.1.10 + NAME 'sudoOrder' + DESC 'an integer to order the sudoRole entries' + EQUALITY integerMatch + ORDERING integerOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) +# +olcObjectClasses: ( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL + DESC 'Sudoer Entries' + MUST ( cn ) + MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoOrder $ sudoNotBefore $ sudoNotAfter $ description ) + ) diff --git a/conf/slapd/systemd-override.conf b/conf/slapd/systemd-override.conf new file mode 100644 index 0000000..afa821b --- /dev/null +++ b/conf/slapd/systemd-override.conf @@ -0,0 +1,9 @@ +[Service] +# Prevent slapd from getting killed by oom reaper as much as possible +OOMScoreAdjust=-1000 +# If slapd exited (for instance if got killed) the service should not be +# considered as active anymore... +RemainAfterExit=no +# Automatically restart the service if the service gets down +Restart=always +RestartSec=3 diff --git a/conf/ssh/sshd_config b/conf/ssh/sshd_config new file mode 100644 index 0000000..c340e45 --- /dev/null +++ b/conf/ssh/sshd_config @@ -0,0 +1,114 @@ +# This configuration has been automatically generated +# by YunoHost + +Protocol 2 +# PLEASE: if you wish to change the ssh port properly in YunoHost, use this command: +# yunohost settings set security.ssh.ssh_port -v +Port {{ port }} + +{% if ipv6_enabled == "true" %}ListenAddress ::{% endif %} +ListenAddress 0.0.0.0 + +{% for key in ssh_keys.split() %} +HostKey {{ key }}{% endfor %} + +# ############################################## +# Stuff recommended by Mozilla "modern" compat' +# https://infosec.mozilla.org/guidelines/openssh +# ############################################## + +{% if compatibility == "intermediate" %} + KexAlgorithms diffie-hellman-group-exchange-sha256 + Ciphers aes256-ctr,aes192-ctr,aes128-ctr + MACs hmac-sha2-512,hmac-sha2-256 +{% else %} + # By default use "modern" Mozilla configuration + # Keys, ciphers and MACS + KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256 + Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr + MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com +{% endif %} + +# LogLevel VERBOSE logs user's key fingerprint on login. +# Needed to have a clear audit track of which key was using to log in. +SyslogFacility AUTH +LogLevel VERBOSE + +# ####################### +# Authentication settings +# ####################### + +# Comment from Mozilla about the motivation behind disabling root login +# +# Root login is not allowed for auditing reasons. This is because it's difficult to track which process belongs to which root user: +# +# On Linux, user sessions are tracking using a kernel-side session id, however, this session id is not recorded by OpenSSH. +# Additionally, only tools such as systemd and auditd record the process session id. +# On other OSes, the user session id is not necessarily recorded at all kernel-side. +# Using regular users in combination with /bin/su or /usr/bin/sudo ensure a clear audit track. + +LoginGraceTime 120 +PermitRootLogin no +StrictModes yes +PubkeyAuthentication yes +PermitEmptyPasswords no +ChallengeResponseAuthentication no +UsePAM yes + +# PLEASE: if you wish to force everybody to authenticate using ssh keys, run this command: +# yunohost settings set security.ssh.ssh_password_authentication -v no +{% if password_authentication == "False" %} +PasswordAuthentication no +{% else %} +#PasswordAuthentication yes +{% endif %} + +# Post-login stuff +# Banner none +PrintMotd no +PrintLastLog yes +ClientAliveInterval 60 +AcceptEnv LANG LC_* + +# Disallow user without ssh or sftp permissions +AllowGroups ssh.main sftp.main ssh.app sftp.app admins root + +# Allow users to create tunnels or forwarding +AllowTcpForwarding yes +AllowStreamLocalForwarding yes +PermitTunnel yes +PermitUserRC yes + +# SFTP stuff +Subsystem sftp internal-sftp + +# Apply following instructions to user with sftp perm only +Match Group sftp.main,!ssh.main + ForceCommand internal-sftp -u 0002 + # We can't restrict to /home/%u because the chroot base must be owned by root + # So we chroot only on /home + # See https://serverfault.com/questions/584986/bad-ownership-or-modes-for-chroot-directory-component + ChrootDirectory /home + # Forbid SFTP users from using their account SSH as a VPN (even if SSH login is disabled) + AllowTcpForwarding no + AllowStreamLocalForwarding no + PermitTunnel no + # Disable .ssh/rc, which could be edited (e.g. from Nextcloud or whatever) by users to execute arbitrary commands even if SSH login is disabled + PermitUserRC no + +Match Group sftp.app,!ssh.app + ForceCommand internal-sftp -u 0002 + ChrootDirectory %h + AllowTcpForwarding no + AllowStreamLocalForwarding no + PermitTunnel no + PermitUserRC no + PasswordAuthentication yes + +# root login is allowed on local networks +# It's meant to be a backup solution in case LDAP is down and +# user admin can't be used... +# If the server is a VPS, it's expected that the owner of the +# server has access to a web console through which to log in. +Match Address 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,169.254.0.0/16,fe80::/10,fd00::/8 + PermitRootLogin yes diff --git a/conf/ssl/openssl.cnf b/conf/ssl/openssl.cnf new file mode 100644 index 0000000..84da1b9 --- /dev/null +++ b/conf/ssl/openssl.cnf @@ -0,0 +1,293 @@ +# +# OpenSSL example configuration file. +# This is mostly being used for generation of certificate requests. +# + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = /usr/share/yunohost/ssl +RANDFILE = $ENV::HOME/.rnd + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] + +# We can add new OIDs in here for use by 'ca' and 'req'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +#################################################################### +[ ca ] +default_ca = Yunohost # The default ca section + +#################################################################### +[ Yunohost ] + +dir = /usr/share/yunohost/ssl # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +unique_subject = no # Set to 'no' to allow creation of + # several ctificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/ca/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +#crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = $dir/ca/cakey.pem # The private key +RANDFILE = $dir/ca/.rand # private random number file + +x509_extensions = usr_cert # The extentions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 3650 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = sha256 # which md to use. +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = optional +stateOrProvinceName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extentions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString. +# utf8only: only UTF8Strings. +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings +# so use this option with caution! +string_mask = nombstr + +req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +commonName = Common Name (eg, YOUR name) +commonName_max = 64 +commonName_default = yunohost.org + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +subjectAltName=DNS:yunohost.org,DNS:www.yunohost.org,DNS:ns.yunohost.org + +[ v3_ca ] + + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer:always + +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Some might want this also +# nsCertType = sslCA, emailCA + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always,issuer:always + +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer:always + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo diff --git a/conf/yunohost/dpkg-origins b/conf/yunohost/dpkg-origins new file mode 100644 index 0000000..b3079a5 --- /dev/null +++ b/conf/yunohost/dpkg-origins @@ -0,0 +1,4 @@ +Vendor: YunoHost +Vendor-URL: https://yunohost.org/ +Bugs: https://github.com/YunoHost/issues/ +Parent: Debian diff --git a/conf/yunohost/firewall.yml b/conf/yunohost/firewall.yml new file mode 100644 index 0000000..7a4de86 --- /dev/null +++ b/conf/yunohost/firewall.yml @@ -0,0 +1,45 @@ +router_forwarding_upnp: false + +tcp: + 22: + open: true + upnp: true + comment: Default SSH port + 25: + open: true + upnp: true + comment: SMTP email server (postfix) + 80: + open: true + upnp: true + comment: HTTP server (nginx) + 443: + open: true + upnp: true + comment: HTTPS server (nginx) + 587: + open: true + upnp: true + comment: SMTP MSA email server (postfix) + 993: + open: true + upnp: true + comment: IMAP email server (dovecot) + +udp: + 53: + open: true + upnp: false + comment: DNS server (dnsmasq) + 1900: + open: true + upnp: false + comment: UPnP services + 5353: + open: true + upnp: false + comment: mDNS (yunomdns) + 55354: + open: true + upnp: false + comment: YunoHost UPnP firewall configurator diff --git a/conf/yunohost/proc-hidepid.service b/conf/yunohost/proc-hidepid.service new file mode 100644 index 0000000..ec6fabe --- /dev/null +++ b/conf/yunohost/proc-hidepid.service @@ -0,0 +1,14 @@ +[Unit] +Description=Mounts /proc with hidepid=2 +DefaultDependencies=no +Before=sysinit.target +Requires=local-fs.target +After=local-fs.target + +[Service] +Type=oneshot +ExecStart=/bin/mount -o remount,nosuid,nodev,noexec,hidepid=2 /proc +RemainAfterExit=yes + +[Install] +WantedBy=sysinit.target diff --git a/conf/yunohost/services.yml b/conf/yunohost/services.yml new file mode 100644 index 0000000..51abacd --- /dev/null +++ b/conf/yunohost/services.yml @@ -0,0 +1,63 @@ +dnsmasq: + test_conf: dnsmasq --test +dovecot: + log: [/var/log/mail.log,/var/log/mail.err] + needs_exposed_ports: [993] + category: email +fail2ban: + log: /var/log/fail2ban.log + category: security + test_conf: fail2ban-server --test +mysql: + log: [/var/log/mysql.log,/var/log/mysql.err,/var/log/mysql/error.log] + actual_systemd_service: mariadb + category: database + ignore_if_package_is_not_installed: mariadb-server +nginx: + log: /var/log/nginx + test_conf: nginx -t + needs_exposed_ports: [80, 443] + category: web +# Yunohost will dynamically add installed php-fpm services (7.3, 7.4, 8.0, ...) in services.py +#php7.4-fpm: +# log: /var/log/php7.4-fpm.log +# test_conf: php-fpm7.4 --test +# category: web +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@15-main' + category: database + ignore_if_package_is_not_installed: postgresql-15 +redis-server: + log: /var/log/redis/redis-server.log + category: database + ignore_if_package_is_not_installed: redis-server +slapd: + category: database + test_conf: slapd -Tt +ssh: + log: /var/log/auth.log + 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 +nftables: + test_status: nft list chain inet filter input | grep "dport" | grep -q "accept" + category: security +yunomdns: + category: mdns +php5-fpm: null +php7.0-fpm: null +php7.3-fpm: null diff --git a/conf/yunohost/yunohost-api.service b/conf/yunohost/yunohost-api.service new file mode 100644 index 0000000..aa429ec --- /dev/null +++ b/conf/yunohost/yunohost-api.service @@ -0,0 +1,13 @@ +[Unit] +Description=YunoHost API Server +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/yunohost-api +Restart=always +RestartSec=5 +TimeoutStopSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/conf/yunohost/yunohost-nftables-hooks-override.conf b/conf/yunohost/yunohost-nftables-hooks-override.conf new file mode 100644 index 0000000..1b3b29a --- /dev/null +++ b/conf/yunohost/yunohost-nftables-hooks-override.conf @@ -0,0 +1,5 @@ +# This override config calls yunohost hooks when nftables is started/reloaded + +[Service] +ExecStartPre=/usr/share/yunohost/yunohost-nftables-hooks pre +ExecStartPost=/usr/share/yunohost/yunohost-nftables-hooks post diff --git a/conf/yunohost/yunohost-portal-api.service b/conf/yunohost/yunohost-portal-api.service new file mode 100644 index 0000000..006af00 --- /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/conf/yunohost/yunoprompt.service b/conf/yunohost/yunoprompt.service new file mode 100644 index 0000000..effb695 --- /dev/null +++ b/conf/yunohost/yunoprompt.service @@ -0,0 +1,15 @@ +[Unit] +Description=YunoHost boot prompt +After=getty@tty2.service +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/yunoprompt +StandardInput=tty +TTYPath=/dev/tty2 +TTYReset=yes +TTYVHangup=yes + +[Install] +WantedBy=default.target diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..a26b2ef --- /dev/null +++ b/debian/changelog @@ -0,0 +1,6331 @@ +yunohost (12.1.40.1) stable; urgency=low + + - diagnosis: fix fetching of the kernel version info, we want the mainline kernel version and not the debian kernel version (f5f28ba4a) + - diagnosis: fix confusing 'fixed in version' diagnosis message when it's in fact a dict (f2ec3cf48) + + -- Alexandre Aubin Wed, 20 May 2026 17:05:00 +0200 + +yunohost (12.1.40) stable; urgency=low + + - settings: be able to personalize DNS resolvers ([#2246](http://github.com/YunoHost/yunohost/pull/2246)) + - helpers/systemd: add a --mount option similar to --service for systemd helpers ([#2266](http://github.com/YunoHost/yunohost/pull/2266), [#2275](http://github.com/YunoHost/yunohost/pull/2275)) + - services: display timestamp 0 as unknown instead of actual date ([#2257](http://github.com/YunoHost/yunohost/pull/2257)) + - diagnosis: fix inconsistent date arithmetic ([#2261](http://github.com/YunoHost/yunohost/pull/2261)) + - security: handle the kernel special case when checking for security issues via the security.toml index ([#2300](http://github.com/YunoHost/yunohost/pull/2300)) + - migrations: add generic mechanism for postgreql and python migrations which are recurrent to every major debian upgrades (6aa42d104, e501f7107, 4e8f7fb31, c050319d3, ef72dc6e2) + - migrations: trixie: remove legacy apt list files if they still exist after apt upgrade (c936c0b60) + - regenconf: yunohost: fix file touched in /etc instead of pending_dir preventing to get rid of legacy node_update cron (ea56d9a82) + - regenconf: fix dnsmasq do_init_regen, YNH_SETTINGS should not be empty (49339ced0) + - helpers: fixnconsistent permissions on /var/log/$app between when provisioned via ynh_add_logrotate vs ynh_restore ([#2263](http://github.com/YunoHost/yunohost/pull/2263)) + - misc: missing doc for ynh_del_swap helper (69291d571) + - misc: refactoring to use subprocess call ([#2264](http://github.com/YunoHost/yunohost/pull/2264)) + - misc: fix typos, update doc URL ([#2259](http://github.com/YunoHost/yunohost/pull/2259), 81d68c336, bf7f6a9f5, 739c3ecec, 6ab8123be) + - misc: when calling apt update, use --error-on=any to have a valid returncode (edb84a2f2) + - i18n: Translations updated for Basque, Catalan, Czech, Dutch, French, Galician, Polish, Portuguese (Brazil), Russian, Ukrainian + + Thanks to all contributors <3 ! (1Poireau, Alekseï, Ale Muñoz, bruno van den bosch, CodeShakingSheep, Dhanush G, Eduardo Mozart de Oliveira, Félix Piédallu, Francescc, Gredin67, Jaroslav Nezbeda, José M, orhtej2, ppr, Rin, SveDec, tituspijean, xabirequejo, zamentur) + + -- Alexandre Aubin Tue, 19 May 2026 23:11:21 +0200 + +yunohost (12.1.39) stable; urgency=low + + - nginx: in proxy_params_no_auth snippet, do not clear the Authorization header ([#2255](http://github.com/YunoHost/yunohost/pull/2255)) + - helpers/systemd: add support for .mount in ynh_config_add_systemd ([#2230](http://github.com/YunoHost/yunohost/pull/2230)) + - trixie migration: Install deb GPG keys in /usr/share/keyrings (d1c69c1a9) + - doc/helpers: document escape capture group parentheses for back-references to work in `ynh_replace` ([#2250](http://github.com/YunoHost/yunohost/pull/2250)) + - doc/helpers: in `ynh_setup_source`, the default filename is not what you may think it is ([#2254](http://github.com/YunoHost/yunohost/pull/2254)) + - doc: Fix links in autogenerated configpanel doc (b269cfbc0) + - i18n: Translations updated for Kabyle + + Thanks to all contributors <3 ! (ButterflyOfFire, Félix Piédallu, Gredin67, oleole39, Thomas, tituspijean) + + -- Alexandre Aubin Fri, 09 Jan 2026 19:01:16 +0100 + +yunohost (12.1.38) stable; urgency=low + + - certificates: add timeout for acme_tiny.py, to prevent requests from hanging on forever ... cf https://github.com/diafygi/acme-tiny/pull/293 (c6d91b893) + - nginx/proxy_params: add proxy_http_version 1.1 + Upgrade and Connection headers, explicitly set (or unset) the Authorization header (d089395ba, 4bb1ab797) + - nginx/fastcgi_params: add 'fastcgi_index index.php' and the classic fastcgi_split_path_info statement (9ccf76455) + - app/helpers: in ynh_local_curl, only print response to HTTP request ([#2247](http://github.com/YunoHost/yunohost/pull/2247)) + - mail/dianogis: fix non-blocklist return code from Hostkarma ([#2245](http://github.com/YunoHost/yunohost/pull/2245)) + - i18n: Translations updated for Basque, Catalan, French, Galician, Kabyle, Portuguese (Brazil), Russian, Ukrainian + + Thanks to all contributors <3 ! (Alekseï, ButterflyOfFire, Eduardo Mozart de Oliveira, Florent, Francescc, José M, otm33, ppr, tituspijean, xabirequejo) + + -- Alexandre Aubin Wed, 31 Dec 2025 14:35:36 +0100 + +yunohost (12.1.37) stable; urgency=low + + - security: introduce a new security issue list that is fetched alongside the catalog, such that the diagnosis reports a warning/error for older versions of apps (or system packagers) vulnerable to known security issues ([#2077](http://github.com/YunoHost/yunohost/pull/2077)) + - regenconf: properly check that categories to regen exists, to remove confusion when people try to regenconf stuff that dont exist like 'ssowat' or whatever... (1285fb7ff) + - apps: improve app restore debugging with a new no-remove-on-failure option, similar to the one for install ([#2184](http://github.com/YunoHost/yunohost/pull/2184)) + - apps/nginx: add include snippets for fastcgi params and proxy param with and without authentication headers ([#2163](http://github.com/YunoHost/yunohost/pull/2163)) + + Thanks to all contributors <3 ! (Josue-T) + + -- Alexandre Aubin Thu, 18 Dec 2025 17:39:45 +0100 + +yunohost (12.1.36) stable; urgency=low + + - bookworm->trixie: fix some issues encountered during migration ([#2227](http://github.com/YunoHost/yunohost/pull/2227)) + - sse: Improve heartbeat handling and error logging in sse_stream ([#2238](http://github.com/YunoHost/yunohost/pull/2238)) + - logs: fix redacting mechanism not redacting secrets in some cases because of the way set -x display if comparisons in some cases ([#2234](http://github.com/YunoHost/yunohost/pull/2234)) + - logs: improve yunopaste to also redact email-like patterns ([#2232](http://github.com/YunoHost/yunohost/pull/2232)) + - logs/helpers: remove debug logs in some part of configpanel's ynh_app_config_validate to reduce noise, and as an additional layer to prevent from leaking secrets ([#2233](http://github.com/YunoHost/yunohost/pull/2233)) + - backup: add interactive terminal check for backup confirmation prompt ([#2229](http://github.com/YunoHost/yunohost/pull/2229)) + - postinstall: improve resilience and logging when no internet connectivity is available during postinstall (14248db34) + - i18n: Translations updated for Basque, Catalan, French, Galician, Occitan, Polish, Portuguese (Brazil), Russian + + Thanks to all contributors <3 ! (Alekseï, Eduardo Mozart de Oliveira, Eryk Michalak, Francescc, HgO, José M, Kayou, ljf, orhtej2, ppr, Quentí, xabirequejo) + + -- Alexandre Aubin Thu, 27 Nov 2025 01:29:22 +0100 + +yunohost (12.1.35) stable; urgency=low + + - i18n/doc: update yunohost.org urls such that they point directly to the new doc on doc.yunohost.org etc (4ca736130) + - debian/mail: Apparently using email relay requires libsasl2-modules in some context (69a2b95b8) + - helpers: split ynh_local_curl to have a low-level version to cover elaborate usecases where packagers want to pass arbitrary curl options and headers, not just post data (7898db59a) + - trixie: Fix pip freeze call of subprocess (9d2627d54) + - trixie: Reorganize the steps * First run the pip, debconf-set-selections, chattr, apt hold, etc * Then run the apt/dpkg patching, at the last possible moment before apt update/uprgade (7b37187bf) + - helpers: utils: add quotes for shellcheck (c796a7eef) + - i18n: Translations updated for Basque, Catalan, Galician + + Thanks to all contributors <3 ! (Félix Piédallu, Francescc, José M, xabirequejo) + + -- Alexandre Aubin Fri, 07 Nov 2025 15:29:25 +0100 + +yunohost (12.1.34) stable; urgency=low + + - migrations: Add draft, "hidden" migration to Trixie ([#2216](http://github.com/YunoHost/yunohost/pull/2216), 97c9c2ca2, c90e7264a, ca7d9dc1d) + - upgrades: properly handle .deb category 'non-free / misc' in package classification (3722281b7) + - diagnosis: Fixup kern.log parsing (c9c31acc6) + + Thanks to all contributors <3 ! (Félix Piédallu, orhtej2) + + -- Alexandre Aubin Mon, 03 Nov 2025 17:39:08 +0100 + +yunohost (12.1.33) stable; urgency=low + + - upgrades: when parsing package<->section lines, try to gracefully handle edgecase were there's more than 2 items to split (17a11b12a) + - i18n: Translations updated for Catalan, French, Galician + + Thanks to all contributors <3 ! (Francescc, José M, ppr) + + -- Alexandre Aubin Fri, 31 Oct 2025 13:38:22 +0100 + +yunohost (12.1.32.1) stable; urgency=low + + - helpers/templating: fixes boolean detection (d3dbdfa45) + + Thanks to all contributors <3 ! (ljf (zamentur)) + + -- OniriCorpe Tue, 28 Oct 2025 12:35:00 +0100 + +yunohost (12.1.32) stable; urgency=low + + - spamhaushell: try another way to address the issue by generating a dnsmasq snippet during regenconf (61d2f9fe8) + - dnsmasq: stabilize the resolver pool list shuffling by using a seed computed from the machine id and current month (0858ea37f) + + -- Alexandre Aubin Thu, 23 Oct 2025 14:28:38 +0200 + +yunohost (12.1.31) stable; urgency=low + + - Revert "diagnosis/mail/blocklists: use spamhaus' own ns to avoid 'open resolver' errors" (bf75e1940) + + -- tituspijean Thu, 23 Oct 2025 02:41:48 +0200 + +yunohost (12.1.30) stable; urgency=low + + - dns/mail: in DNSmasq conf, route queries about spamhaus to spamhaus's own nameservers to avoid 'open resolver' errors (b45b9d4f4) + - mail/postfix: remove reject_rbl_client abuseat.org because it's in fact spamshaus.org since a few years (42f0b91bf) + - diagnosis/mail/blocklists: revert prefix fix for diagnosis for spamhaus, which is obsolete now that dns queries for spamhaus are now route at dnsmasq level (51c468735) + - diagnosis/mail/blocklists: remove abuseat.org which is in fact spamhaus.org since a few years (6af034820) + - diagnosis/mail/blocklists: when obtaining an 'open resolver' reason, advise admins to check their /etc/resolv.conf (#2201) (d525bfbc6) + - dns: remove obsolete DNS resolver from pool because it times out (#2203) + - system updates: improve system packages categorization (89c27e3aa) + - helpers/nodejs: disable npm/node checking if there's a new version available and triggering a boring warning ... we use n which has legit versions etc (059e876af) + - helpers/templating: insist about case sensitivity in documentation (#2207) + - helpers/configpanel: fix boolean values handling in ynh_write_var_in_file (#2209) + - i18n: Translations updated for French + + Thanks to all contributors <3 ! (Félix Piédallu, ljf, oleole39, tituspijean) + + -- Alexandre Aubin Wed, 22 Oct 2025 23:40:23 +0200 + +yunohost (12.1.29) stable; urgency=low + + - diagnosis/email/dnsbl: fix infamous issue with spamhaus returning 'openresolver' (396e5d327) + - firewall: Fix using an ssh port not listed in firewall.yml leading to partial error (#2202) + - i18n: Translations updated for Persian) + + Thanks to all contributors <3 ! (Félix Piédallu, otm33) + + -- Alexandre Aubin Sat, 18 Oct 2025 19:33:21 +0200 + +yunohost (12.1.28) stable; urgency=low + + - dns: Fix lexicon integration (#2194) + - mail: Include spam and trash in user quota (#2193) + - app upgrade: When upgrading a single app and that upgrade fails, make sure to raise an exception (an therefore a returncode != 0) (#2196) + - app shell: Inherit TERM when opening an app shell (#2195) + - app shell: Make sure to list all systemd units including dead/empty ones when looking for services related to the app (#2197) + - app helpers: fix files not templated correctly when values contains ampersands (&) (#2199) + + Thanks to all contributors <3 ! (Florent, ljf, orhtej2) + + -- Alexandre Aubin Sun, 12 Oct 2025 16:46:10 +0200 + +yunohost (12.1.27) stable; urgency=low + + - postfix: make sure regen conf forgets about relay_recipients.db when not applicable (1184b6703) + - certificate: Allow IPv6-only domains to run ACME (10f666fc3) + - helpers: in 2.1, YNH_DEFAULT_PHP_VERSION was set to 7.4 instead of 8.2 like in the v2.0 helpers x_x (704cf0872) + - helpers: fix edge case in apt helpers for apps with 'extras' dependency *and* php version, it would automatically reset the php_version to the default one during the 'extra' install intead of keeping the correct value (8ece3163d) + + Thanks to all contributors <3 ! (Félix Piédallu) + + -- Alexandre Aubin Sun, 05 Oct 2025 04:15:45 +0200 + +yunohost (12.1.26) stable; urgency=low + + - helpers:: YNH_APP_BASEDIR missing for app shell (#2183) + - app resources: fix a typo in ruby helper (#2187) + - doc: Add ZSH completion script (de06819ed, e359fe841) + - postfix: 'smtp_backup_mx_domains' was passed as 'null' to the jinja template instead of empty string, resulting in the corresponding snippet in main.cf being enabled which may cause a variety of issues (0fdf3e5d7) + + Thanks to all contributors <3 ! (Félix Piédallu, getzze, Tagada, tituspijean) + + -- Alexandre Aubin Sun, 28 Sep 2025 16:46:46 +0200 + +yunohost (12.1.25) stable; urgency=low + + - dns: Update registrars list for the Lexicon integration (#2161) + - helpers/apt: When failing to install dependencies, suggest that it might be due to a conflict with other apps (#2170) + - tools: Make basic-space-cleanup delete YunoHost logs too (#2116) + + Thanks to all contributors <3 ! (Félix Piédallu, ljf, Thomas, tituspijean) + + -- Alexandre Aubin Thu, 18 Sep 2025 17:37:24 +0200 + +yunohost (12.1.24) stable; urgency=low + + - firewall: only call open_port(ssh) when it's closed to prevent overriding comment (f6d08477c) + - permissions: fix edge case where there's some unknown key in system perm conf, previous code was triggering a 'dictionary changed size during iteration' exception (8f9cccbff) + - helpers(string): ensure that ynh_string_random returns a result (#2181) (c524acfc8) + - helpers(apt): fix shellcheck (a999219f7) + - helpers(systemd): typo in log (#2178) (100659581) + + Thanks to all contributors <3 ! (Alexandre Aubin, DeMiro5001, Félix Piédallu, Florent) + + -- tituspijean Tue, 16 Sep 2025 21:49:12 +0200 + +yunohost (12.1.23) stable; urgency=low + + - helpers: fix ynh_add_swap helper / some filesystem do not support chattr +C (8708946f2) + - helpers/systemd: 60s timeout is too harsh for low-end hardware, let's go back to 300s as it was in helpers v2, but keep it to 120s on CI (eae5fa98c) + - helpers/apt: fix extra apt dependencies issue where apt update is not re-ran (2aa2aed9f) + - helpers: fix tests incorrectly passing (#2174) + - firewall: fix/add upnp rules in the nftables conf (#2175) (a122b73fa) + + Thanks to all contributors <3 ! (Josue-T, Kayou, Sylvain) + + -- Alexandre Aubin Sat, 13 Sep 2025 00:12:26 +0200 + +yunohost (12.1.22) stable; urgency=low + + - postfix/mailusers: check and display an error if postmap fails (a1aaa1e3f) + - 'set -Eeuo pipefail' is a fucking pitfall (1dece38a3) + + -- Alexandre Aubin Wed, 10 Sep 2025 18:39:32 +0200 + +yunohost (12.1.21) stable; urgency=low + + - helpers/apt: tweak optimization to not run apt update : actually let's use 'find /etc/apt' when checking for changes, because that will also cover source files that got added or removed (because that will change the mtime of the folders such as sources.list.d) (490b2fc2c) + - helpers: fix YNH_J2_FILTERS_FILE_PATH definition (#2172, #2173) + + Thanks to all contributors <3 ! (Florian, Josue-T) + + -- Alexandre Aubin Wed, 10 Sep 2025 12:55:52 +0200 + +yunohost (12.1.20) stable; urgency=low + + - perf/postfix regenconf: use $YNH_SETTINGS rather than calling yunohost settings get (b6545c318) + - perf/log show: stoopid typo leading to unecessarily opening a gazillion yamls (c59b43b42) + + -- Alexandre Aubin Tue, 09 Sep 2025 01:46:07 +0200 + +yunohost (12.1.19) stable; urgency=low + + - perf: further prevent regenconf from taking an absurd amount of time because calls to 'yunohost domain list --features' take, for example, 10ish secs on RPi2 ... To do so, the list of mail_in/mail_out domain is computed beforehand and passed as a global vars to the regen conf scripts (fbb99348a) + + -- Alexandre Aubin Tue, 09 Sep 2025 00:26:41 +0200 + +yunohost (12.1.18) stable; urgency=low + + - perf: prevent domain list --features from taking ages because it fetches DNS infos and app upgradability even though it doesnt matter in that context (53ef24cad) + - perf: implement cache for load_apps_catalog because parsing 50 times the same 700kBish json in a single command is probably not a very efficient way to use CPU and IO bruh (c80e917e2) + - perf/helpers2.1: skip apt update when cache was already updated less than 30 minutes ago (#2147) + - helpers/configs: add jinja filter to load and dump json, yaml and toml (#2150) + - helpers/multimedia: disable logging when running all the mkdir and ln -s for each user to prevent leaking the users in logs, and also it's long and boring af (1fdaad770) + - helpers/systemd: when starting a service reaches timeout, don't try to check wether it's active or not to handle it as a failure (3268781dd) + - regenconf: fix postgresql issue (and possibly others) because sometimes, for god knows whatever reason, using dpkg --list | grep 'ii foobar' doesn't work, gotta use \s ... (17ea71641) + - logging: moar boring warnings to ignore (55d7eee0d) + - i18n: Translations updated for Basque, Chinese (Simplified) + + Thanks to all contributors <3 ! (Josué Tille, xabirequejo, zamentur) + + -- Alexandre Aubin Mon, 08 Sep 2025 19:30:06 +0200 + +yunohost (12.1.17.1) stable; urgency=low + + - fix: portal domain conf not properly updated with the portal_allow_mail_edit settings (#2167) + + Thanks to all contributors <3 ! (tituspijean) + + -- Alexandre Aubin Sun, 31 Aug 2025 23:37:15 +0200 + +yunohost (12.1.17) stable; urgency=low + + - tools_update: remove passed requirements because it's hella confusing in CLI output (c3946a57c) + - app upgrade infos: fix display values when current_version == new_version (78d6b83e1) + - user: when updating admins group mail aliases, don't try to add aliases that are already there because LDAP is dumdum. (In particular for migration 0034) (249881336) + - portal api: fix allow_edit_email vs portal_allow_edit_email confusion (c465e40be) + - portal email settings: add post_change_hook to propagate settings when it's changed (a69553cec) + + Thanks to all contributors <3 ! (tituspijean) + + -- Alexandre Aubin Sun, 31 Aug 2025 23:12:38 +0200 + +yunohost (12.1.16) stable; urgency=medium + + - fix(nftables): drop policy and allow icmpv6 ([#2165](http://github.com/YunoHost/yunohost/pull/2165)) + - fix(nginx): remove deprecated OCSP ([#2166](http://github.com/YunoHost/yunohost/pull/2166)) + - fix(sse): logstreamcache files location ([#2164](http://github.com/YunoHost/yunohost/pull/2164)) + - helpers2.1: fix attempting to load go/ruby/nodejs when it's not provisioned yet in some context (7a2b6820b) + - helpers2.1: remove --message ([#2162](http://github.com/YunoHost/yunohost/pull/2162)) + + Thanks to all contributors <3 ! (Alexandre Aubin, Tagada, Thomas) + + -- tituspijean Sat, 30 Aug 2025 22:04:30 +0200 + +yunohost (12.1.15.1) stable; urgency=low + + - network: fixes IP reuse in cache in all cases (30ae06809) + - i18n: Translations updated for French, Spanish + + Thanks to all contributors <3 ! (ako, OniriCorpe) + + -- OniriCorpe Sat, 23 Aug 2025 22:00:00 +0200 + +yunohost (12.1.15) stable; urgency=low + + - 0033_rework_permission_infos: actually skip apps that do not exist (e0b6d5496) + - 0033_rework_permission_infos: make permission migration more robust against LDAP being a fucking idiot (3da214d95) + - dyndns: fix 'dictionary changed size during iteration' (0d5828657) + - dyndns: fixes 2 typos in log sentences (eac758595) + - codequality: Add typing and TypedDict in ssh.py (19831a5b1) + - codequality: add typing to Migration class (783e5175b) + + Thanks to all contributors <3 ! (Félix Piédallu, OniriCorpe) + + -- Alexandre Aubin Sat, 23 Aug 2025 15:54:02 +0200 + +yunohost (12.1.14) stable; urgency=low + + - apps: fix tmp workdir for upgrade being rm'ed during the safety backup, because mtime / ctime confusion, inheriting metadata from the original source or whatev (95f148d2d) + + -- Alexandre Aubin Wed, 20 Aug 2025 22:51:49 +0200 + +yunohost (12.1.13.1) stable; urgency=low + + - Typo T_T (c3e8084b2) + + -- Alexandre Aubin Wed, 20 Aug 2025 19:50:37 +0200 + +yunohost (12.1.13) stable; urgency=low + + - nodejs migration: fix list of file just containing empty string [''] (ce93b8674) + - ldap: prevent stupid issue where trailing space make LDAP explode because ???? (8f9c24d93) + - firewall: fix case where no udp port is open, nftable doesnt like empty list/sets (cb2a7f976) + + -- Alexandre Aubin Wed, 20 Aug 2025 17:24:16 +0200 + +yunohost (12.1.12.1) stable; urgency=low + + - apps: fix manifest cache not properly invalidating (2dd0e3d24) + + -- Alexandre Aubin Wed, 20 Aug 2025 02:49:27 +0200 + +yunohost (12.1.12) stable; urgency=low + + - apps: add tmp debug info/exception to hunt bug with workdir disappearing ? (bd3730ad7) + - Release as stable! + + -- Alexandre Aubin Wed, 20 Aug 2025 01:34:59 +0200 + +yunohost (12.1.11) testing; urgency=low + + - app: in the app permission config panel, absolute url containing '//' in some cases (73624d4c0) + - user: user create/delete should regen the ssowat conf (4823d5a80) + - permissions: automatically remove user/group that dont exist from app perms (b8ae908c5) + - i18n: fix all value -> content in dns-related diagnosis strings (e90c8212a) + + -- Alexandre Aubin Tue, 19 Aug 2025 15:48:45 +0200 + +yunohost (12.1.10.2) testing; urgency=low + + - nodejs: in version migration, skip app for which no matching version is found (c899ed858) + - tools_update: don't list apps that are up to date (e2b3bdcae) + + -- Alexandre Aubin Tue, 19 Aug 2025 02:15:18 +0200 + +yunohost (12.1.10.1) testing; urgency=low + + - dns: value -> content in dns-diagnosis-related strings (308202836) + - nodejs: fix version migration: some nodejs app have legit no service file (a4a658fcf) + - i18n: Translations updated for Basque, French, Galician + + Thanks to all contributors <3 ! (José M, ppr, xabirequejo) + + -- Alexandre Aubin Mon, 18 Aug 2025 23:57:54 +0200 + +yunohost (12.1.10) testing; urgency=low + + - firewall: only open UPnP when open and upnp are enabled in conf (#2112) + - firewall: Fix typo, doc and typing (16d33404c, a5e2bec2f, f080cbd4e, 08edd38ab) + - dns: fix another value -> content renaming (0acc047a0) + - certificate: bump key size from 3072 to 4096 (9f5f38883) + - apps: fix bug when computing app upgrade infos (0f3b19a2c) + - nodejs: Add a migration to patch app using nodejs such that the nodejs_version setting and the systemd file point to the full X.Y.Z version instead of the major version (023678b14) + + Thanks to all contributors <3 ! (eric_G, Félix Piédallu, Kayou) + + -- Alexandre Aubin Mon, 18 Aug 2025 21:42:39 +0200 + +yunohost (12.1.9) testing; urgency=low + + - Add support for (plain) XZ and ZST source archives (#2118) + + Thanks to all contributors <3 ! (orhtej2) + + -- Alexandre Aubin Sun, 10 Aug 2025 22:15:43 +0200 + +yunohost (12.1.8) testing; urgency=low + + - dnsrecords: fix, values might be None according to types (b23568ec7) + - dns/diagnosis: dns record content is in 'content', not 'value' (c7dc0f021) + - error handling: Use YunohostErrors in log to properly display error modal in webadmin (42c1477b0) + - sse: do not set log ref on error when 'sse_only' since there is no log (60ed72b70) + - sse: fix moulinette lock path leading to current_operation being always None (1faf53f47) + - sse: when asking CLI confirmation, emit a special info message such as people looking at the webadmin will know that the CLI is hanging and waiting for confirmation (68e623e76) + - permission migration: display a warning + skip permissions for apps that are not on the system, cf report from ljf (6a4acad78) + - email: yoloimplement migration to fix missing admin mail aliases (348bd2ea5) + - helpers/nodejs: Fix nodejs version handling in helpers 2.0, major vs minor (f397eb16f) + - firewall: Prevent disaster when we can't find any TCP port to open in nftables regenconf (324da4969) + - Misc typing (386216b73, a687098a7, 7c4d89eea, eafdb3a3d, a46faa210, cd1222fd6) + - i18n: Translations updated for French, Galician + + Thanks to all contributors <3 ! (axolotle, Félix Piédallu, José M, ppr) + + -- Alexandre Aubin Sun, 10 Aug 2025 22:01:57 +0200 + +yunohost (12.1.7.1) testing; urgency=low + + - Fix edge case in _group_packages_per_categories for system packages with no category (95a92b665) + - Fix remaining typing issues + + -- Alexandre Aubin Tue, 05 Aug 2025 19:28:57 +0200 + +yunohost (12.1.7) testing; urgency=low + + - upgrades: refactor the upgrade flows, check requirements beforehand, allow to have a different upgrade source (eg testing branch), group system package by categories to improve UI/UX (#2115) + - settings: fix comma-seperated list of TLS-passthrough configurations (#2119) + - misc: IP servers are now ipv4.yunohost.org, ipv6.yunohost.org (dd8dc914f) + - helpers: fix POST arg syntax in ynh_local_curl (f629078d1) + - helpers/doc: fix documentation for ynh_redis_remove_db (#2135) + - helpers/doc: document supported CPU archs (#2127) + - refactor: replace absolute yunohost.xx imports with relative .xx imports (972da6d38, a903a52f9) + - refactor: use standard functools.cache for some data (d1448d51d, a65bf0bfc, 5348fe2e0, e4d7463a7) + - refactor: split low-level app utils from app.py to a new utils/app_utils.py (4fb81ad11) + - refactor: integrate utils/ from Moulinette directly in yunohost core (#2142) + - codequality: plenty of typing improvements (...too many commits to be listed ;)) + - codequality: replace tox.ini, pytest.ini, .coveragerc with pyproject.toml (...too many commits to be listed ;)) + - codequality: update .shellcheckrc with disabled checks (36967edd6) + - codequality: replace deprecated logger.warn -> logger.warning (#2114) + - i18n: Translations updated for Basque, Dutch, French, Galician, German, Kabyle, Spanish + + Thanks to all contributors <3 ! (ButterflyOfFire, Emmanuel Ferdman, Félix Piédallu, Florent, Johannes Ebeling, José M, Lex Leenders, ljf, oleole39, ppr, rosbeef andino, Thomas Weiss, tituspijean, xabirequejo) + + -- Alexandre Aubin Mon, 04 Aug 2025 17:00:00 +0200 + +yunohost (12.1.6.1) testing; urgency=low + + - Typo T.T + + -- Alexandre Aubin Sun, 18 May 2025 19:00:37 +0200 + +yunohost (12.1.6) testing; urgency=low + + - portal: change default value of show_other_domains_apps to True (407eae5e0) + - portal: add setting to allow/disallow users to edit their main email, alias and forward from portal ([#1997](https://github.com/YunoHost/yunohost/pull/1997)) + - mail: add setting to enable/disable blocklists ([#2096](https://github.com/YunoHost/yunohost/pull/2096)) + - apps: Add pre_app_upgrade hook ([#2095](http://github.com/YunoHost/yunohost/pull/2095)) + - settings: Replace spwd with direct call to passwd when changing root password ([#2081](http://github.com/YunoHost/yunohost/pull/2081)) + - storage: fix patch import ([#2100](http://github.com/YunoHost/yunohost/pull/2100)) + - configpanel: fix docstrings for proper doc generation (82e804f5c) + - configpanel: fix app_action_run operation logger instanciation ([#2107](http://github.com/YunoHost/yunohost/pull/2107)) + - i18n: Translations updated for Dutch, German + + Thanks to all contributors <3 ! (Boudewijn, bruno van den bosch, Christophe Henry, Félix Piédallu, Florent, Josue, Matthias Roy) + + -- Alexandre Aubin Sun, 18 May 2025 18:36:09 +0200 + +yunohost (12.1.5.1) testing; urgency=low + + - disks: Fix a lambda typo + MyPy error in src/utils/udisks2_interfaces.py ([#2094](http://github.com/YunoHost/yunohost/pull/2094)) + - actionsmap: remove weird yaml magic that seems to make build expose (2f569679d) + + Thanks to all contributors <3 ! (Christophe Henry) + + -- Alexandre Aubin Sun, 20 Apr 2025 19:58:58 +0200 + +yunohost (12.1.5) testing; urgency=low + + - disks: Add SMART status to disk infos ([#2046](http://github.com/YunoHost/yunohost/pull/2046)) + - helpers: fix n_install_dir againix nodejs helper.v1 (93d696bbe) + - helpers: document keywords in settings' keys that trigger value masking in the logs ([#2093](http://github.com/YunoHost/yunohost/pull/2093)) + - helpers2.1: fix edge case in ynh_apt_install_dependencies_from_extra_repository (6b18ca98c) + - helpers2.1: fix apt helper debug info when install fails (e0459dcaa) + - helpers2.1: fix `yunohost app shell` when `systemctl show` outputs ` (ignore_error=no)` ([#2086](http://github.com/YunoHost/yunohost/pull/2086)) + - helpers2.1: fix unbound BACKUP_CORE_ONLY in ynh_backup ([#2092](http://github.com/YunoHost/yunohost/pull/2092)) + - configpanels: fix form prompt when option is a "choice" but with no options ([#2087](http://github.com/YunoHost/yunohost/pull/2087)) + - configpanels: fix helper `ynh_app_config_get_one` for $type = "text" and $bind = "settings" ([#2091](http://github.com/YunoHost/yunohost/pull/2091)) + - nginx: fix template for TLS passthrough ([#2089](http://github.com/YunoHost/yunohost/pull/2089)) + - sse: fix/refactor _guess_who_started_process when process doesn't have a parent (668e60e19, [#2083](http://github.com/YunoHost/yunohost/pull/2083)) + - migrations: in 0032_firewall_config, explicitly enable nftables service (01277caa0) + - i18n: Translations updated for Czech, French, Galician, German + + Thanks to all contributors <3 ! (Christophe Henry, demodé, Félix Piédallu, Florent, José M, Kay0u, Krakinou, oleole39, OniriCorpe, tituspijean) + + -- Alexandre Aubin Sat, 19 Apr 2025 17:19:21 +0200 + +yunohost (12.1.4) testing; urgency=low + + - helpers2.1: Make sure utils helpers are loaded first and fix jq usage ([#2073](http://github.com/YunoHost/yunohost/pull/2073)) + - helpers2.1: More fixes for go/ruby/nodejs fixes ([#2074](http://github.com/YunoHost/yunohost/pull/2074)) + - mail: Support sending mail to external mailbox of a managed domain ([#2075](http://github.com/YunoHost/yunohost/pull/2075)) + - regenconf/dnsmasq: optimize the number of time we call 'yunohost domain' for perf + use True/False instead of 1/0 (f53f805c4) + - i18n: Translations updated for Basque, French, German, Italian, Persian, Polish, Portuguese, Russian, Slovak + + Thanks to all contributors <3 ! (harc, ljf, OniriCorpe, ppr, tituspijean, xabirequejo) + + -- Alexandre Aubin Fri, 28 Mar 2025 10:40:45 +0100 + +yunohost (12.1.3) testing; urgency=low + + - dev: Add autoreleases from tags github actions ([dc02a1395](http://github.com/YunoHost/yunohost/commit/dc02a1395)) + - dev: Allow using an alternate actionsmap for development ([#2052](http://github.com/YunoHost/yunohost/pull/2052)) + - dev: Shellcheck improvements ([#2017](http://github.com/YunoHost/yunohost/pull/2017), [#2056](http://github.com/YunoHost/yunohost/pull/2056)) + - diagnosis: Complain about apps only requiring yunohost 4.x ([b244d7981](http://github.com/YunoHost/yunohost/commit/b244d7981)), and apps still using packaging v1 ([fa8cce212](http://github.com/YunoHost/yunohost/commit/fa8cce212)) + - dns: Remove now restriced FDN resolvers, add Mullvad and DNS4all ([#2066](http://github.com/YunoHost/yunohost/pull/2066)) + - dns: Add DNS server from Aquilenet ([#2068](http://github.com/YunoHost/yunohost/pull/2068)) + - helpers2.1: Cleanup label parameters ([#2064](http://github.com/YunoHost/yunohost/pull/2064)) + - helpers2.1/apps: Simplify helpers and implement new resources for nodejs/ruby/go/composer ([#2042](http://github.com/YunoHost/yunohost/pull/2042)) + - helpers2.1: Make sure to keep composer.phar when using ynh_setup_source, otherwise that defeat the purpose of the composer resource (d1e93e3fb) + - firewall: Fix "Both" protocol for firewall allow/disallow ([#2067](http://github.com/YunoHost/yunohost/pull/2067)) + - domains: Domain actions should trigger a regenconf for opendkim ([535ce3be0](http://github.com/YunoHost/yunohost/commit/535ce3be0)) + - upnp: Various fixes and improvements ([#2055](http://github.com/YunoHost/yunohost/pull/2055)) + - documentation: Scripts improvements ([#2061](http://github.com/YunoHost/yunohost/pull/2061), [#2062](http://github.com/YunoHost/yunohost/pull/2062), [#2045](http://github.com/YunoHost/yunohost/pull/2045), 29bbc1fd0, 3bf811193, 9b67ed825) + - documentation: Prepare migration to Docusaurus (([dc2b22541](http://github.com/YunoHost/yunohost/commit/dc2b22541)), ([77f7135f4](http://github.com/YunoHost/yunohost/commit/77f7135f4))) + - i18n: Translations updated for Basque, Catalan, Chinese (Simplified), Esperanto, French, Galician, German, Indonesian, Italian, Japanese, Occitan, Persian, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Alexandre Aubin, Christophe Henry, Félix Piédallu, Josué Tille, Kayou, OniriCorpe, Quiwy, ljf (zamentur), ppr, sachaz, Éric Gaspar) + + -- tituspijean Sun, 23 Mar 2025 09:32:14 +0100 + +yunohost (12.1.2) testing; urgency=low + + - storage: Fix YAML indent for storage.disk.list.arguments in actionsmap.yml ([#2058](http://github.com/YunoHost/yunohost/pull/2058)) + - yunohost app shell: fix env loading by remove 'ignore_errors=yes' from systemctl show output in some cases ([#2057](http://github.com/YunoHost/yunohost/pull/2057)) + - app.py: fix path requirement validation for non-web apps and full-domain apps (75f5af054, 597aa0c6c) + - i18n: fix consistency for oc.json, the {logs} stuff got removed (37a3547c5) + - i18n: Reformat / remove stale translated strings ([#2059](http://github.com/YunoHost/yunohost/pull/2059)) + - i18n: Translations updated for Basque, French, Galician + + Thanks to all contributors <3 ! (Christophe Henry, Félix Piédallu, José M, ppr, tituspijean, xabirequejo) + + -- Alexandre Aubin Wed, 05 Mar 2025 18:09:55 +0100 + +yunohost (12.1.1) testing; urgency=low + + - firewall: let's not try to refresh upnp after disabling it because it's just gonna fail (5b127c7a5) + - firewall: do not actually enable upnp if no upnp device is available on the network (207de4228) + - permission migration: missing app_ssowatconf at the end (c2da2d3ab) + - i18n: Translations updated for Chinese (Simplified) + + Thanks to all contributors <3 ! (Félix Piédallu, Poesty Li) + + -- Alexandre Aubin Sun, 02 Mar 2025 19:20:00 +0100 + +yunohost (12.1.0) testing; urgency=low + + - general: New "SSE" API to stream/broadcast logs of the current action, using zero-mq ([#1663](http://github.com/YunoHost/yunohost/pull/1663)) + - apps/permissions: rework the way permission info are stored, introduce a new 'app core' config panel allowing to change the app logo, label, etc. ([#1917](http://github.com/YunoHost/yunohost/pull/1917)) + - perf: tweaks to improve LDAP performances when updating records, especially when many user entries exist ([#1975](http://github.com/YunoHost/yunohost/pull/1975)) + - firewall: complete rework of the code. Disable UPnP by default on new installs ([#2011](http://github.com/YunoHost/yunohost/pull/2011)) + - storage/disk: add the beginning of a new section for storage management, for now retrieving disk list and info ([#1953](https://github.com/YunoHost/yunohost/pull/1953)) + - apps: add a new --ignore-yunohost-version option to 'yunohost app install' to bypass ynh version requirement ([#2036](https://github.com/YunoHost/yunohost/pull/2036)) + - portal: allow to login using the email address ([#2041](http://github.com/YunoHost/yunohost/pull/2041)) + - portal: use samesite 'lax' for portal cookies ([#2018](https://github.com/YunoHost/yunohost/pull/2018)) + - wording: simplify the "user portal" naming to just "portal" (642e1afba) + - i18n: Translations updated for Basque, Catalan, French, Galician, Italian, Spanish + + Thanks to all contributors <3 ! (axolotle, Christophe Henry, Félix Piédallu, Joan, José M, Josué Tille, ljf, OniriCorpe, orhtej2, ppr, rosbeef andino, Tommi, xabirequejo) + + -- Alexandre Aubin Sun, 23 Feb 2025 21:22:02 +0100 + +yunohost (12.0.17) stable; urgency=low + + - regenconf: force YNH_HELPERS_VERSION to 2 when running regenconf script, because this var env may be inherited from app scripts running in 2.1 (6c92e77a6) + - helpers.v2.1: PSQL_VERSION should be 15 not 13 x_x (7db6422b7) + + -- Alexandre Aubin Sun, 18 May 2025 18:36:09 +0200 + +yunohost (12.0.16) stable; urgency=low + + - yunoprompt: in fact if we dont chvt to another term it stays on the current one forever ~_~ (d36d2d542) + - auth: fix postinstall exploding just because we have no message queue / no session cookie ... (to be removed in 12.1) (5b06aaadc) + + -- Alexandre Aubin Mon, 05 May 2025 00:05:41 +0200 + +yunohost (12.0.15) stable; urgency=low + + - yunoprompt: remove postinstall directly in tty after boot (for some reason it's not interactive and loops forever) (59e7f3b99) + - yunoprompt: add a reminder for the default login/password (3f376bf61) + + -- Alexandre Aubin Sun, 04 May 2025 21:04:54 +0200 + +yunohost (12.0.14) stable; urgency=low + + - dnsmasq: remove DNS64 version of DNS4all resolver which is only intended to be used with NAT64 and causing a bunch of issues in the context of yunohost etc (606705274) + + -- Alexandre Aubin Wed, 09 Apr 2025 12:07:46 +0200 + +yunohost (12.0.13) stable; urgency=low + + - dnsmasq: properly initialize dnsmasq conf upon installing the yunohost package to prevent dns resolution not working during postinstall and making everything explode (1bd63fe0c) + - nginx: increase timeout for portal API, on slow hardware for some reason the login takes more than 5~10ish sec (that should be profiled and improved meh) but at least increasing the timeout prevents the 504 (bd7b79e98) + + -- Alexandre Aubin Fri, 04 Apr 2025 22:04:32 +0200 + +yunohost (12.0.12) stable; urgency=low + + - [fix] FDN dns resolver are not available for all AS ([#2066](http://github.com/YunoHost/yunohost/pull/2066)) + - Update resolv.dnsmasq.conf + 1 DNS server Aquilenet ([#2068](http://github.com/YunoHost/yunohost/pull/2068)) + + Thanks to all contributors <3 ! (Alexandre Aubin, OniriCorpe, ljf (zamentur), Sacha) + + -- tituspijean Sun, 16 Mar 2025 12:41:55 +0100 + +yunohost (12.0.11) stable; urgency=low + + - users: Prevent to remove the last user from admins ([#2029](http://github.com/YunoHost/yunohost/pull/2029), [#2030](http://github.com/YunoHost/yunohost/pull/2030), [#2031](http://github.com/YunoHost/yunohost/pull/2031), a18928475, d7b57a87a, fc2596d04, 8d6fd9c32) + - apps: incorrect warning about amount of RAM during upgrade ([#1971](http://github.com/YunoHost/yunohost/pull/1971)) + - doc/helpers: document the possibility of using regex in .url property ([#2028](http://github.com/YunoHost/yunohost/pull/2028)) + - logs: fix boring hack causing boring issue on the webadmin preventing the 'show more' button to be displayed sometimes (6117aa800) + - i18n: Translations updated for French, German + + Thanks to all contributors <3 ! (Félix Piédallu, JocelynDelalande, Kay0u, ljf (zamentur), ppr, Tagadda, Vinzenz Vietzke) + + -- Alexandre Aubin Mon, 20 Jan 2025 15:46:36 +0100 + +yunohost (12.0.10) stable; urgency=low + + - helpers2.1: force COREPACK_ENABLE_DOWNLOAD_PROMPT=0 when using ynh_exec_as_app (4a07a8301) + - helpers2.1: fix fail2ban helper for non-/var/log/$app paths ([#2024](http://github.com/YunoHost/yunohost/pull/2024)) + + Thanks to all contributors <3 ! (Félix Piédallu) + + -- Alexandre Aubin Fri, 10 Jan 2025 23:52:09 +0100 + +yunohost (12.0.9.2) stable; urgency=low + + - helpers: Fix go installation by passing HOME to goenv init + - doc: Fix support of shebangs + - i18n: Translations updated for Spanish + + Thanks to all contributors <3 ! (Félix Piédallu) + + -- Félix Piédallu Sun, 5 Jan 2025 20:40:00 +0100 + +yunohost (12.0.9.1) stable; urgency=low + + - yunohost app shell: helpers2.0 need PATH to be defined (62f43b410) + - i18n: Translations updated for Occitan, Polish + + Thanks to all contributors <3 ! (harc, Quentí) + + -- Alexandre Aubin Sun, 22 Dec 2024 16:03:35 +0100 + +yunohost (12.0.9) stable; urgency=low + + - apps: when opening an 'app shell', fix PHP version for helpers 2.1 + prevent bash flooding debug infos ([#2009](http://github.com/YunoHost/yunohost/pull/2009)) + - helpers2.1: fix patches handling when there's several sources to setup (c4ba7a9a8) + - actionsmap: fix hooks callback/exec, they don't take --no-trace anymore (d1a4ab782) + - quality: add shellcheck workflow, fix shellcheck issues ([#2015](http://github.com/YunoHost/yunohost/pull/2015)) + - i18n: Translations updated for Basque + + Thanks to all contributors <3 ! (Félix Piédallu, Mateusz, Salamandar, xabirequejo) + + -- Alexandre Aubin Tue, 17 Dec 2024 17:55:57 +0100 + +yunohost (12.0.8.2) stable; urgency=low + + - fix unset variable in helper 2.1 mysql ([#2010](http://github.com/YunoHost/yunohost/pull/2010)) + + Thanks to all contributors <3 ! (Kay0u) + + -- Alexandre Aubin Mon, 09 Dec 2024 14:38:40 +0100 + +yunohost (12.0.8.1) stable; urgency=low + + - helpers: typo T_T (75c81bbef) + + -- Alexandre Aubin Mon, 09 Dec 2024 14:27:00 +0100 + +yunohost (12.0.8) stable; urgency=low + + - dns: support '.internal' TLD as a reserved TLD for private networks ([#1999](http://github.com/YunoHost/yunohost/pull/1999)) + - typing: pre pydantic v2 ([#2000](http://github.com/YunoHost/yunohost/pull/2000)) + - apps: add my_webdav to the list of apps that need basic auth header to go through (8c620a3c6) + - misc: fix postinstall in offline context (27611864d) + - backup/restore: typo preventing portal settings to be restored eh ? (300723974) + - regenconf: don't miserably crash regenconf when setfacl can't be applied due to the FS type (92164edaa) + - helpers2.1: automatically add --default-character-set for mysql stuff (in particular for nextcloud) (64a39a123) + - apps: allow apps to be installed on / if another app is using /.well-known/ ([#2007](http://github.com/YunoHost/yunohost/pull/2007)) + - settings: having a visible on an entire section doesn't work and prevent changing any global setting ? (dce043a66) + - i18n: Translations updated for Catalan + + Thanks to all contributors <3 ! (axolotle, Félix Piédallu, Francescc, Kesefon, tituspijean) + + -- Alexandre Aubin Sun, 08 Dec 2024 21:59:47 +0100 + +yunohost (12.0.7) stable; urgency=low + + - bullseye->bookworm: migration shouldnt rebuild venv for homeassistant and immich ([#1996](http://github.com/YunoHost/yunohost/pull/1996)) + - regenconf: disable the experimental proxy_cookie_path setting in nginx security.conf ([#1998](http://github.com/YunoHost/yunohost/pull/1998)) + - certificates: Support domains in cert_alternate_names hook ([#1994](http://github.com/YunoHost/yunohost/pull/1994)) + - helpers2.1: fix is_data definition in ynh_restore (a6414dd84) + - helpers/nodejs: upgrade n to v10.1.0 ([#1995](http://github.com/YunoHost/yunohost/pull/1995)) + - misc: lower logging about failure to retrieve app label ([#1993](http://github.com/YunoHost/yunohost/pull/1993)) + - i18n: Translations updated for Basque, French, Galician + + Thanks to all contributors <3 ! (Éric Gaspar, ewilly, Félix Piédallu, José M, tituspijean, xabirequejo) + + -- Alexandre Aubin Wed, 13 Nov 2024 14:38:40 +0100 + +yunohost (12.0.6) stable; urgency=low + + - Sync with bullseye branch + + -- Alexandre Aubin Thu, 31 Oct 2024 18:04:21 +0100 + +yunohost (12.0.5.3) stable; urgency=low + + - typo in generate-helpers-doc (ab7bbefe5) + - Merge branch 'dev' into bookworm (9d24a071d) + + Thanks to all contributors <3 ! (Alexandre Aubin, Kayou) + + -- tituspijean Wed, 30 Oct 2024 21:46:48 +0100 + +yunohost (12.0.5.2) testing; urgency=low + + - typo² x_x + + -- Alexandre Aubin Wed, 30 Oct 2024 01:49:37 +0100 + +yunohost (12.0.5.1) testing; urgency=low + + - typo x_x + + -- Alexandre Aubin Wed, 30 Oct 2024 01:21:37 +0100 + +yunohost (12.0.5) testing; urgency=low + + - sso: ihatemoney and owncloud needs the basic auth workaround ([#1984](http://github.com/YunoHost/yunohost/pull/1984)) + - sso: pass email, username and fullname to ssowat via the JWT such that SSOwat can define custom HTTP headers ([#1981](http://github.com/YunoHost/yunohost/pull/1981)) + - portal: fix backup and restore portal settings (d0005e66a) + - portal: fix cookie validity issues by defining 'maxage' + re-setting the cookie every time the validity is extended, such that the client always know wether the cookie is expired or not (deeabfabb) + - portal: support different style/theme for the app tiles via a portal setting ([#1986](http://github.com/YunoHost/yunohost/pull/1986)) + - portal: custom css setting couldnt be emptied (f64cc3f46) + - postinstall/tos: Allow bypassing the terms of services at postinstall time ([#1966](http://github.com/YunoHost/yunohost/pull/1966)) + - helpersv2.1/fail2ban: Fix reload-or-restart match line (4105df9d8) + - helpers/nginx: yoloremove the REMOTE_USER lines from app's conf because it's already provided by /etc/nginx/fastcgi_params + make sure it is mapped to the YNH_USER from SSOwat rather than $remote_user (cd367575f) + - configpanel: better error message when uploading a file from unsupported mimetype (d9f59f3c1) + - i18n: Translations updated for Basque, Chinese (Simplified), French, Kabyle, Turkish + + Thanks to all contributors <3 ! (Aksel Azwaw, axolotle, Éric Gaspar, Félix Piédallu, Furkan Samed Acet, Jinx, Josué Tille, Kay0u, ljf, Mateusz, oleole39, OniriCorpe, Tagada, Thatoo, tituspijean, xabirequejo) + + -- Alexandre Aubin Wed, 30 Oct 2024 00:12:27 +0100 + +yunohost (12.0.4.1) testing; urgency=low + + - ssowat: allow secondaries instances of apps_that_need_external_auth_maybe to be recognized ([#1954](http://github.com/YunoHost/yunohost/pull/1954)) + - domains: fix missing translation param in domain add (cebd8864c) + + Thanks to all contributors <3 ! (axolotle, Nathanaël H.) + + -- Alexandre Aubin Wed, 25 Sep 2024 21:04:38 +0200 + +yunohost (12.0.4) testing; urgency=low + + - general: add TOS acknowledgement during postinstall, dyndns domain creation, and migration for existing installs ([#1949](http://github.com/YunoHost/yunohost/pull/1949)) + - domains: fix `custom_css` may not be in the the form when changing other domain panel values (d5d6fb87a) + - domains: add option to install a lets encrypt certificate when adding a subdomain of an already added domain ([#1936](http://github.com/YunoHost/yunohost/pull/1936)) + - configpanels: fix boolean option in context evaluation for custom yes/no values (f19b6f84a) + - dns: make auto dns feature optional ([#1951](http://github.com/YunoHost/yunohost/pull/1951)) + - cli: Add yunohost-portal to the --version output ([#1946](http://github.com/YunoHost/yunohost/pull/1946)) + + Thanks to all contributors <3 ! (axolotle, Kay0u, Mer, selfhoster1312) + + -- Alexandre Aubin Mon, 16 Sep 2024 18:18:14 +0200 + +yunohost (12.0.3) testing; urgency=low + + - (sync bullseye changes since 12.0.2) + - apps: magically handle yarn as a regular package instead of an 'extra' repo now that yarn's repo is in the core ([#1888](http://github.com/YunoHost/yunohost/pull/1888)) + - portalapi: we don't need absolute URLs for app logos ? (This ain't working when enabling the 'show other domains apps' because of CSP) (bc93a2e07) + - portalapi: fix portal_user_intro not being sent when authenticated, hence not displayed at all (cdf443c86) + - portal/domain settings: Improve explanation about search engine (24fb87725) + - portal/domain settings: Reduce theme list because there were too many (cf change in yunohost-portal) (9973cc703) + - portal/domain settings: add proper i18n string + help for new settings (831131476, ff0388556) + - domain settings: add a title to the Email section to have a separation w.r.t. the portal settings (279f33288) + - portal: fix extra app tiles not being displayed, gotta use the perm id as key, not just the app id (credit rodinux) (603c64e34) + - portal/sso: with the public app page, fix the root of the domain not redirecting to /yunohost/sso (a6b7ba843) + - portal: allow to configure custom CSS from the domain config panel (8f636561d) + - portal: change the way the new 'public apps' page in the portal is configured: add a simple bool toggle instead of having the 'public apps page' as a default app option, which allows to still configure a default app while the portal has the public apps page (748a20d86) + - ci: fix test_permission_propagation_on_ssowat, auth header tests (656e5c75d, 9e9313067, 44920d891) + - ci: optimizations, cleanups, partial refactor because of new CI image build process (fe9a4fba5, 059818254, a9e71e88d, 7f2da0af7, 94594e5a3, d639e1c42, 4f3b9df3f, 5a6a915af, 55e7e798f, 2fe24424f, 4fc929005, fd040b864, 0bbc14f54, 2976e7bf6) + - quality: add type hints to user.py (1ba75df0e, d4f39da20, 611846aa1, efce7f9f0, fe1c04fb2) + - i18n: Translations updated for Basque, French, Galician, Greek, Indonesian, Russian, Turkish + + Thanks to all contributors <3 ! (Ali Çırçır, cjdw, craftrac, Emmanuel Averty, Félix Piédallu, Ivan Davydov, José M, Josué Tille, ljf, OniriCorpe, ppr, selfhoster1312, Tagada, tituspijean, xabirequejo) + + -- Alexandre Aubin Sat, 31 Aug 2024 19:45:00 +0200 + +yunohost (12.0.2) testing; urgency=low + + - Cleanup redis regen conf since redis ain't installed by default anymore (7b50c4eb6) + - bullseye->bookworm: add a trick to flag the migration as done if it's still marked as pending (0503a38a7) + - Sync with main branch + + Thanks to all contributors <3 ! (Kayou) + + -- Alexandre Aubin Thu, 01 Aug 2024 18:08:33 +0200 + +yunohost (12.0.1) testing; urgency=low + + - The user portal and SSO system have been reworked and split into three distinct pieces + - SSOwat only handling only the SSO/ACL logic (nginx lua middleware) + - A new “portal API” (yunohost-portal-api) service delivering authentication cookies and allowing users to retrieve/update infos + - A new portal front end (yunohost-portal) + - More information on the release note on the forum + - The base system does not install Mysql/Mariadb and PHP anymore + - Rspamd (antispam system) and Metronome (XMPP server) are not part of the core anymore. Instead, they are now separate applications : rspamd_ynh and metronome_ynh + - webadmin: rework cookie/session expiration mechanism. Cookies are now still valid after restarting the API (preventing clumsy disconnect during self-upgrades) and the cookie validity is automatically extended every time an API request is performed. + - mail: DKIM email signing is now done using opendkim instead of rspamd + - various compatibility tweakings for Bookworm + - regenconf: update nginx and dovecot ciphers according to Mozilla recommendation + - regenconf: update fail2ban config + - configpanels: refactor to use pydantic for more typing and consistency, add proper autogenerated doc + - apps: Yarn third-party repo is now available by default in apt config just like Sury, no need for an extra apt resource thingy + - various legacy cleanups (more info on the release note on the forum) + - perf: minimize regen-conf calls to yunohost settings get, and other misc lazy-loading optimizations + - quality: simplify the logging mess + - quality: rework ci tests workflow + + -- Alexandre Aubin Fri, 26 Jul 2024 22:40:16 +0200 + +yunohost (12.0.0) unstable; urgency=low + + - Tmp changelog to prepare Bookworm + + -- Alexandre Aubin Thu, 04 May 2023 20:30:19 +0200 + +yunohost (11.3.0.2) stable; urgency=low + + - Fix migration by early-importing _ldap ([#1987](http://github.com/YunoHost/yunohost/pull/1987)) + + Thanks to all contributors <3 ! (Félix Piédallu) + + -- Alexandre Aubin Thu, 31 Oct 2024 18:01:44 +0100 + +yunohost (11.3.0.1) stable; urgency=low + + - (Forgot the commit that actually enabled the migration @_@) (00243961c) + + Thanks to all contributors <3 ! (Félix Piédallu) + + -- Alexandre Aubin Thu, 31 Oct 2024 16:49:51 +0100 + +yunohost (11.3.0) stable; urgency=low + + - Enable migration to Bookworm + - i18n: Translations updated for Basque, French, Turkish + + Thanks to all contributors <3 ! (Atalay Daban, ppr, xabirequejo) + + -- Alexandre Aubin Thu, 31 Oct 2024 13:51:33 +0100 + +yunohost (11.2.32) stable; urgency=low + + - configpanel: fix error message to hint if the bind key wasn't found ([#1972](http://github.com/YunoHost/yunohost/pull/1972)) + - tools_shell: Bad catch IPython missing ([#1974](http://github.com/YunoHost/yunohost/pull/1974)) + - nginx: new global setting options to enable TLS passthrough and reverse proxy to external domains ([#1697](http://github.com/YunoHost/yunohost/pull/1697)) + - backups: app hooks were not recreated when restoring backups ([#1962](http://github.com/YunoHost/yunohost/pull/1962)) + - migrate-to-bookworm: improve disclaimer (e755401ea) + - i18n: Translations updated for Kabyle, Turkish + + Thanks to all contributors <3 ! (Aksel Azwaw, Félix Piédallu, Furkan Samed Acet, ljf, oleole39, OniriCorpe) + + -- Alexandre Aubin Tue, 29 Oct 2024 23:48:07 +0100 + +yunohost (11.2.31) stable; urgency=low + + - apps/helpers2.1: Properly detect 'is_data' (data dir or log folder) when calling ynh_backup ([#1957](http://github.com/YunoHost/yunohost/pull/1957)) + - users/mail: Allow to force-adding aliases for special groups ([#1850](http://github.com/YunoHost/yunohost/pull/1850)) + - helpers/nodejs: Upgrade n to v10.0.0 ([#1948](http://github.com/YunoHost/yunohost/pull/1948)) + - backups: fix messages for custom methods ([#1959](http://github.com/YunoHost/yunohost/pull/1959), [#1963](http://github.com/YunoHost/yunohost/pull/1963)) + - doc: fix escaping in generate helpers doc template & v2.1 helpers comments ([#1952](http://github.com/YunoHost/yunohost/pull/1952)) + - doc: fix link to autoupdate_app_sources.py in resources documentation ([#1956](http://github.com/YunoHost/yunohost/pull/1956)) + - doc: document the 'systemd' option for 'ynh_systemctl --line_match' ([#1958](http://github.com/YunoHost/yunohost/pull/1958)) + - doc: document how to check locally whether app autoupdate is working ([#1960](http://github.com/YunoHost/yunohost/pull/1960)) + - doc: in autoupgrade doc, double slash ends up as single slash in the doc, but double slash is really needed, so further escape the slashes ([#1965](http://github.com/YunoHost/yunohost/pull/1965)) + - i18n: Translations updated for Basque, Chinese (Simplified), French + + Thanks to all contributors <3 ! (Éric Gaspar, Jinx, Mateusz, oleole39, OniriCorpe, selfhoster1312, Thatoo, tituspijean, xabirequejo) + + -- Alexandre Aubin Tue, 15 Oct 2024 19:50:33 +0200 + +yunohost (11.2.30.2) stable; urgency=low + + - helpers2.1: fix automagic chmod/chown for data_dir during restore (46ddf1e67) + - i18n: Translations updated for Dutch + + Thanks to all contributors <3 ! (Mer) + + -- Alexandre Aubin Mon, 09 Sep 2024 14:25:32 +0200 + +yunohost (11.2.30.1) stable; urgency=low + + - helpers2.1: turns out sudo --preserve-env=PATH still causes issues with 'command not found' despite the command being in the path and 'which' finding the proper executable ... using 'env PATH=$PATH' instead fixes the issue ... (589fda20b) + + -- Alexandre Aubin Fri, 06 Sep 2024 22:13:18 +0200 + +yunohost (11.2.30) stable; urgency=low + + - helpers v2.1: check if patches dir exists before getting realpath ([#1938](http://github.com/YunoHost/yunohost/pull/1938)) + - helpers v2.1: ynh_add_swap and ynh_smart_mktemp (aff885e6b) + - helpers v2.1: fix ynh_restore_everything ([#1943](http://github.com/YunoHost/yunohost/pull/1943)) + - helpers v2.1: fix typo in docs: ynh_install_app_dependencies -> ynh_apt_install_dependencies ([#1939](http://github.com/YunoHost/yunohost/pull/1939)) + - helpers: fix syntax, disambiguate subshell syntax ([#1940](http://github.com/YunoHost/yunohost/pull/1940)) + - quality: Add maintenante/shfmt.sh for shell script formatting (68f35831e) + - quality: Apply shfmt everywhere, fix tabs/space/indent (8a5f2808a, e3ddb1dc4, ef1708276, 38b39ebae, b91e9dd8f) + + Thanks to all contributors <3 ! (Félix Piédallu, Josué Tille, OniriCorpe, selfhoster1312) + + -- Alexandre Aubin Sat, 31 Aug 2024 19:26:59 +0200 + +yunohost (11.2.29) stable; urgency=low + + - apps: generalize replacing __INSTALL_DIR__ and __APP__ in config panel 'bind' statement to any setting (9b0553580) + - apps/config panels: move the computation of the actual 'bind' value to the python core (a6785d34b) + - perf: add cache for _get_app_settings() (c14ebc8be, 7c7906046) + - quality: use _assert_is_installed for consistency instead of if not _is_intalled(app): raise (c409888a4) + - i18n: Translations updated for Basque, French, Galician, Greek, Indonesian + + Thanks to all contributors <3 ! (cjdw, craftrac, José M, ppr, xabirequejo) + + -- Alexandre Aubin Tue, 27 Aug 2024 14:46:26 +0200 + +yunohost (11.2.28) stable; urgency=low + + - ci: various changes due to CI infrastructure changes (200f0272d, 764fe6a7b, 9083a5cc3, d0df3caed, 6733526be, df320a44c, 92f4a605b, f02d4a437, c5953b542) + - apps: exclude .well-known subpaths from conflict checks ([#1647](http://github.com/YunoHost/yunohost/pull/1647)) + - apps: in apt resource, fix empty string in packages_from_raw_bash breaking dpkg-build (a76cd05e8) + - sftp: Tweak umask for SFTP ([#1384](http://github.com/YunoHost/yunohost/pull/1384)) + - mail: Be able to use postfix as a backup ("secondary") MX hosts ([#1253](http://github.com/YunoHost/yunohost/pull/1253)) + - diagnosis: Add check regarding rfkill blocking Wi-Fi card on RPi ([#1841](http://github.com/YunoHost/yunohost/pull/1841)) + - users: trigger hooks when adding or removing user into group (51787a2f8) + - i18n: Translations updated for Basque, French, Indonesian, Russian + + Thanks to all contributors <3 ! (cjdw, Emmanuel Averty, Ivan Davydov, ljf, ppr, Tagada, tituspijean, xabirequejo) + + -- Alexandre Aubin Sun, 25 Aug 2024 13:17:43 +0200 + +yunohost (11.2.27) stable; urgency=low + + - apt resource: fix handling of empty 'packages' list breaking dpkg-deb call (3deffdbd5) + - i18n: Translations updated for Indonesian, Turkish + + Thanks to all contributors <3 ! (Ali Çırçır, cjdw) + + -- Alexandre Aubin Sat, 03 Aug 2024 18:41:27 +0200 + +yunohost (11.2.26) stable; urgency=low + + - bullseye->bookworm: encourage apt to remove luajit if it's installed because for some reason it's causing issues (423e79bd5) + - bullseye->bookworm: have a specific step dedicated to upgrade python3.9 to 3.11 because apt(itude) is derping about it sometimes... (d766f7cdd) + - bullseye->bookworm: boring tweak to remove chattr +i from /etc/resolv.conf otherwise resolvconf will later explode and complain about it (65ea34d7c) + - Fix yunomprompt not being enable after ISO install (fe524dd86) + + -- Alexandre Aubin Thu, 01 Aug 2024 18:05:33 +0200 + +yunohost (11.2.25) stable; urgency=low + + - diagnosis: be more robust when diagnosis DMARC records not containing '=' (d376677db) + - bullseye->bookworm: explicitly import _strptime at the beginning to try to prevent "No module named '_strptime'" during migration (2d3dddc51) + - bullseye->bookworm: explicitly validate that we're on yunohost 12.x at the end of the migration (8b5698317) + - bullseye->bookworm: make sure the non-free / non-free-firmware stuff is idempotent (ad98a10fa) + - bullseye->bookworm: in debian control, add rule that moulinette and ssowat must be < 12 to prevent situation in bullseye->bookworm transition where moulinette gets upgrade but yunohost doesnt and everything explodes (8705dfcf5) + - bullseye->bookworm: more stuff to try to prevent aptitude derping about python dependencies (f4727d3cb) + + -- Alexandre Aubin Tue, 30 Jul 2024 17:12:12 +0200 + +yunohost (11.2.24) stable; urgency=low + + - ci: we don't care about mypy in tests/ folder (ebaecfcbd) + - helpers: fix install from equivs ([#1921](http://github.com/YunoHost/yunohost/pull/1921)) + - bullseye->bookworm: re-add tweak about libluajit2 + be more robust about full-upgrade that may fail if python3.9-venv aint installed (9e1b0561e) + - i18n: Translations updated for Indonesian + + Thanks to all contributors <3 ! (cjdw, Kayou) + + -- Alexandre Aubin Fri, 26 Jul 2024 21:01:23 +0200 + +yunohost (11.2.23) stable; urgency=low + + - helpers2.1: force sourcing getopts before the other helpers to prevent stupid issues (in particular when renaming phpversion to php_version) (3d53cf04) + - diagnosis: Remove SenderScore from the dnsbl_list.yml file ([#1918](http://github.com/YunoHost/yunohost/pull/1918)) + - ldap: make slapd listen also on ipv6 ([#1916](http://github.com/YunoHost/yunohost/pull/1916)) + - log: zzz fix log list again (b2492ffc) + - i18n: Translations updated for Galician, Indonesian, Slovak + + Thanks to all contributors <3 ! (cjdw, Étienne Deparis, José M, Jose Riha, Josué Tille) + + -- Alexandre Aubin Tue, 23 Jul 2024 19:07:20 +0200 + +yunohost (11.2.22) stable; urgency=low + + - logs: fix "log list" : use root_dir for iglob / make sure we use absolute paths ([#1913](http://github.com/YunoHost/yunohost/pull/1913)) + - bullseye->bookworm: remove pkg_resources from pip freeze ([#1912](http://github.com/YunoHost/yunohost/pull/1912)) + - bullseye->bookworm: explicitly install yunohost-portal (4232fc7c) + - bullseye->bookworm: explicitly remove python3.9 and python3.9-venv which seems to confuse aptitude... (079cdc26) + - bullseye->bookworm: try the yunohost upgrade without unholding the app-ynh-deps virtual packages, then after unholding if it didnt work for some reason (a8fd6afe) + - bullseye->bookworm: automatically add non-free-firmware if non-free is enabled (97bb6bde) + - bullseye->bookworm: trigger the 'new' migrations from inside the bullseye->bookworm migration (f11f1197) + - i18n: Translations updated for Basque, French, Indonesian + + Thanks to all contributors <3 ! (Anonymous, cjdw, Kayou, ppr, xabirequejo) + + -- Alexandre Aubin Fri, 19 Jul 2024 16:51:55 +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)) + - appsv2: in perm resource, fix handling of additional urls containing vars to replace (8fbdd228) + - appsv2: fix version-specific upgrade notification hydration ([#1655](https://github.com/yunohost/yunohost/pull/1655)) + - appsv2/regenconf: prevent set -u to be enabled during regen-conf triggered from inside appsv2 scripts (a7350a7e) + - refactoring: various renaming in configpanel ([#1649](https://github.com/yunohost/yunohost/pull/1649)) + - i18n: Translations updated for Arabic, Basque, Indonesian + + Thanks to all contributors <3 ! (axolotle, ButterflyOfFire, Kayou, Neko Nekowazarashi, tituspijean, xabirequejo) + + -- Alexandre Aubin Mon, 08 May 2023 16:04:06 +0200 + +yunohost (11.1.18) stable; urgency=low + + - appsv2: always set an 'app' setting equal to app id to be able to use __APP__ in markdown templates ([#1645](https://github.com/yunohost/yunohost/pull/1645)) + - appsv2: fix edge-case when validating packager-provided infos for permissions resource (aa43e6c2) + - appsv2: Support using any variables/setting in permissions declaration ([#1637](https://github.com/yunohost/yunohost/pull/1637)) + - dns: Add support for Porkbun through Lexicon ([#1638](https://github.com/yunohost/yunohost/pull/1638)) + - diagnosis: Report out-of-catalog/broken/bad quality apps as warning instead of error ([#1641](https://github.com/yunohost/yunohost/pull/1641)) + - user: .ssh directory should be executable ([#1642](https://github.com/yunohost/yunohost/pull/1642)) + - i18n: Translations updated for Arabic, Basque, French, Galician + + Thanks to all contributors <3 ! (ButterflyOfFire, José M, ppr, tituspijean, xabirequejo) + + -- Alexandre Aubin Fri, 14 Apr 2023 17:20:58 +0200 + +yunohost (11.1.17) stable; urgency=low + + - domains: fix autodns for gandi root domain ([#1634](https://github.com/yunohost/yunohost/pull/1634)) + - helpers: fix previous change about using YNH_APP_ACTION ... which is not defined in config panel context (8c25aa9b) + - appsv2: for the dir/subdirs of data_dir, create parent folders if they don't exist (9a4267ff) + - quality: Split utils/config.py ([#1635](https://github.com/yunohost/yunohost/pull/1635)) + - quality: Rework questions/options tests ([#1629](https://github.com/yunohost/yunohost/pull/1629)) + + Thanks to all contributors <3 ! (axolotle, Kayou) + + -- Alexandre Aubin Wed, 05 Apr 2023 16:00:09 +0200 + +yunohost (11.1.16) stable; urgency=low + + - apps: fix i18n panel+section names ([#1630](https://github.com/yunohost/yunohost/pull/1630)) + - appsv2: don't remove yhh-deps virtual package if it doesn't exist. Otherwise when apt fails to install dependency, we end up with another error about failing to remove the ynh-deps package (3656c199) + - appsv2: add validation for expected types for permissions stuff (b2596f32) + - appsv2: add support for subdirs property in data_dir (4b46f322) + - appsv2: various fixes regarding sources toml parsing/caching (14bf2ee4) + - appsv2: add documentation about the new 'autoupdate' mechanism for app sources (63981aac) + - ynh_setup_source: fix buggy checksum mismatch handling, can't compute the sha256sum after we delete the file @_@ (1b2fa91f) + - users: fix quota parsing being wrong by a factor 1000 ... doveadm returns kilos, not bytes (821aedef) + - backup: fix boring issue where archive is a broken symlink... (a95d10e5) + + Thanks to all contributors <3 ! (axolotle) + + -- Alexandre Aubin Sun, 02 Apr 2023 20:29:33 +0200 + +yunohost (11.1.15) stable; urgency=low + + - doc: Fix version number in autogenerated resource doc (5b58e0e6) + - helpers: Fix documentation for ynh_setup_source (7491dd4c) + - helpers: fix ynh_setup_source, 'source_id' may contain slashes x_x (eaf7a290) + - helpers/nodejs: simplify 'n' script install and maintenance ([#1627](https://github.com/yunohost/yunohost/pull/1627)) + + -- Alexandre Aubin Sat, 11 Mar 2023 16:50:50 +0100 + +yunohost (11.1.14) stable; urgency=low + + - helpers: simplify --time display option for ynh_script_progression .. we don't care about displaying time when below 10 sc (8731f77a) + - appsv2: add support for a 'sources' app resources to modernize and replace app.src format ([#1615](https://github.com/yunohost/yunohost/pull/1615)) + - i18n: Translations updated for Arabic, Polish, Ukrainian + + Thanks to all contributors <3 ! (ButterflyOfFire, Grzegorz Cichocki, Tymofii-Lytvynenko) + + -- Alexandre Aubin Thu, 09 Mar 2023 15:34:17 +0100 + +yunohost (11.1.13) stable; urgency=low + + - appsv2: fix port already used detection ([#1622](https://github.com/yunohost/yunohost/pull/1622)) + - appsv2: when hydrating template, the data may be not-string, eg ports are int (72986842) + - [i18n] Translations updated for Arabic, French, Galician, German, Occitan + + Thanks to all contributors <3 ! (ButterflyOfFire, Christian Wehrli, José M, Kay0u, ppr) + + -- Alexandre Aubin Fri, 03 Mar 2023 22:57:14 +0100 + +yunohost (11.1.12.2) stable; urgency=low + + - helpers: omg base64 wraps the output by default :| (d04f2085) + + -- Alexandre Aubin Wed, 01 Mar 2023 22:12:51 +0100 + +yunohost (11.1.12.1) stable; urgency=low + + - helper: fix previous tweak about debugging diff for manually modified files on the CI @_@ (fd304008) + + -- Alexandre Aubin Wed, 01 Mar 2023 08:08:55 +0100 + +yunohost (11.1.12) stable; urgency=low + + - apps: add '--continue-on-failure' to 'yunohost app upgrade ([#1602](https://github.com/yunohost/yunohost/pull/1602)) + - appsv2: Create parent dirs when provisioning install_dir ([#1609](https://github.com/yunohost/yunohost/pull/1609)) + - appsv2: set `w` as default permission on `install_dir` folder ([#1611](https://github.com/yunohost/yunohost/pull/1611)) + - appsv2: Handle undefined main permission url ([#1620](https://github.com/yunohost/yunohost/pull/1620)) + - apps/helpers: tweak behavior of checksum helper in CI context to help debug why file appear as 'manually modified' ([#1618](https://github.com/yunohost/yunohost/pull/1618)) + - apps/helpers: more robust way to grep that the service correctly started ? ([#1617](https://github.com/yunohost/yunohost/pull/1617)) + - regenconf: sometimes ntp doesnt exist (97c0128c) + - nginx/security: fix empty webadmin allowlist breaking nginx conf... (e458d881) + - misc: automatic get rid of /etc/profile.d/check_yunohost_is_installed.sh when yunohost is postinstalled (20e8805e) + - settings: Fix pop3_enabled parsing returning 0/1 instead of True/False ... (b40c0de3) + - [i18n] Translations updated for French, Galician, Italian, Polish + + Thanks to all contributors <3 ! (Éric Gaspar, John Schmidt, José M, Krakinou, Kuba Bazan, Laurent Peuch, ppr, tituspijean) + + -- Alexandre Aubin Tue, 28 Feb 2023 23:08:02 +0100 + +yunohost (11.1.11.2) stable; urgency=low + + - Rebump version to flag as stable, not testing >_> + + -- Alexandre Aubin Fri, 24 Feb 2023 13:09:48 +0100 + +yunohost (11.1.11.1) testing; urgency=low + + - appsv2: fix previous commit about __DOMAIN__ because url may be None x_x (e05df676) + + -- Alexandre Aubin Fri, 24 Feb 2023 01:30:14 +0100 + +yunohost (11.1.11) stable; urgency=low + + - logs: fix decoding errors not handled when trying to read service logs ([#1606](https://github.com/yunohost/yunohost/pull/1606)) + - mail: fix dovecot-pop3d not being installed when enabling pop3 ([#1607](https://github.com/yunohost/yunohost/pull/1607)) + - apps: when creating the app's bash env for script, make sure to use the manifest from the workdir instead of app setting dir, which is important for consistency during edge case when upgrade from v1 to v2 fails (bab27014) + - appsv2: data_dir's owner should have rwx by default (139e54a2) + - appsv2: fix usage of __DOMAIN__ in permission url (943b9ff8) + + Thanks to all contributors <3 ! (Eric Geldmacher, ljf) + + -- Alexandre Aubin Thu, 23 Feb 2023 22:31:02 +0100 + +yunohost (11.1.10) stable; urgency=low + + - apps: add 'YNH_DEBIAN_VERSION' variable in apps contexts (df6a2a2c) + - appsv2: add support for a packages_from_raw_bash option in apt where one can add a multiline bash snippet to echo packages (4dfff201) + - appsv2: fix resource provisioning scripts picking up already-closed operation logger, resulting in confusing debugging output (888593ad) + - appsv2: fix reload_only_if_change option not working as expected, resulting in incorrect 'Firewall reloaded' messages (d725b454) + - appsv2: fix check that postgresql db exists... (1dc8b753) + + -- Alexandre Aubin Tue, 21 Feb 2023 18:57:33 +0100 + +yunohost (11.1.9) stable; urgency=low + + - apps: simplify the redaction of change_url scripts by adding a new ynh_change_url_nginx_config helper + predefining new/old/change domain/path variables (2b70ccbf) + - appsv2: revert commit that adds a bunch of warning about apt/database consistency, it's more relevant to have them in package linter instead (63f0f084) + - appsv2: fix system user group update, broke in commit from earlier (ec4c2684) + - log: Previous trick about getting rid of setting didnt work, forgot to use metadata instead of self.metadata (848adf89) + - ux: Moar boring postgresql messages displayed as warning (290d627f) + + Thanks to all contributors <3 ! (Bram) + + -- Alexandre Aubin Mon, 20 Feb 2023 20:32:28 +0100 + +yunohost (11.1.8.2) stable; urgency=low + + - regenconf: fix undefined var in apt regenconf (343065eb) + + -- Alexandre Aubin Sun, 19 Feb 2023 21:38:59 +0100 + +yunohost (11.1.8.1) stable; urgency=low + + - postgresql: moar regenconf fixes (e6ae3892) + - postgresql: ugly hack to hide boring warning messages when installing postgresql with apt the first time ... (13d50f4f) + + -- Alexandre Aubin Sun, 19 Feb 2023 19:41:05 +0100 + +yunohost (11.1.8) stable; urgency=low + + - apps: don't miserably crash when failing to read .md file such as DESCRIPTION.md (58ac633d) + - apps: fix edge case when upgrading using a local folder not modified since a while (d3ec5d05) + - appsv2: fix system user provisioning ... (d123fd76, 771b801e) + - appsv2: add check about database vs. apt consistency in resource / warn about lack of explicit dependency to mariadb-server (97b69e7c) + - appsv2: add home dir that defaults to /var/www/__APP__ for system user resource (ce7227c0) + - postgresql: fix regenconf hook, the arg format thingy changed a bit at some point ? (8a43b046) + - regenconf: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore (similar to commit e24ddd29) (18e034df) + - postinstall: raise a proper error when trying to use e.g. 'admin' as the first username which will conflict with the admins group mail aliases (475c93d5) + - i18n: Translations updated for Arabic, Basque + + Thanks to all contributors <3 ! (ButterflyOfFire, xabirequejo) + + -- Alexandre Aubin Sun, 19 Feb 2023 18:22:02 +0100 + +yunohost (11.1.7) stable; urgency=low + + - mail: fix complain about unused parameters in postfix: exclude_internal=yes / search_timeout=30 (0da6370d) + - mail: Add push notification plugins in dovecot ([#1594](https://github.com/yunohost/yunohost/pull/1594)) + - diagnosis: fix typo, diagnosis detail should be a list, not a string (d0ca120e) + - helpers: in apt/php stuff, don't try to upgrade-alternatives if the default PHP version ain't available anymore (e24ddd29) + - apps: fix inconsistent app removal during remove-after-failed-upgrade and remove-after-failed-backup contexts (7be7eb11) + - appsv2: we don't want to store user-provided passwords by default, but they should still be set in the env for the script to use it (9bd4344f) + - appsv2: fix i18n for arch mismatch, can't juste join() inside string formated with .format() (aa9bc47a) + - appsv2: missing raw_msg=True for exceptions (1d1a3756) + - appsv2: fix check that main permission url is '/' (ab8a6b94) + - appsv2: mysqlshow is fucking dumb and returns exit code 0 when DB doesnt exists ... (0ab20b73) + - appsv2: also replace __DOMAIN__ in resource properties (0c4a006a) + - appsv2: in php helpers, use the global $phpversion var/setting by default instead of $YNH_PHP_VERSION (60b21795) + - i18n: Translations updated for Arabic, Galician + + Thanks to all contributors <3 ! (ButterflyOfFire, John Hackett, José M) + + -- Alexandre Aubin Wed, 15 Feb 2023 21:08:04 +0100 + +yunohost (11.1.6.2) stable; urgency=low + + - permissions: fix trailing-slash issue in edge case where app has additional urls related to a different domain (a4fa6e07) + - backup: fix postinstall during full restore ... tmp admin user can't be named 'admin' because of conflicting alias with the admins group (65894007) + - doc: improve app resource doc (a154e811) + + -- Alexandre Aubin Thu, 09 Feb 2023 19:00:42 +0100 + +yunohost (11.1.6.1) stable; urgency=low + + - dns: fix CAA recommended DNS conf -> 0 is apparently a more sensible value than 128... (2eb7da06) + - users: Allow digits in user fullname (024db62a) + - backup: fix full backup restore postinstall calls that now need first username+fullname+password (48e488f8) + - i18n: Translations updated for Arabic, Basque, Chinese (Simplified) + + Thanks to all contributors <3 ! (ButterflyOfFire, Poesty Li, xabirequejo) + + -- Alexandre Aubin Wed, 08 Feb 2023 22:50:37 +0100 + +yunohost (11.1.6) stable; urgency=low + + - helpers: allow to use ynh_replace_string with @ ([#1588](https://github.com/yunohost/yunohost/pull/1588)) + - helpers: fix behavior of ynh_write_var_in_file when key is duplicated ([#1589](https://github.com/yunohost/yunohost/pull/1589), [#1591](https://github.com/yunohost/yunohost/pull/1591)) + - helpers: fix composer workdir variable for package v2 ([#1586](https://github.com/yunohost/yunohost/pull/1586)) + - configpanels: properly escape & for values used in ynh_write_var_in_file ([#1590](https://github.com/yunohost/yunohost/pull/1590)) + - appsv2/group question: don't include primary groups in choices (c179d4b8) + - appsv2: when initalizing permission, make sure to add 'all_users' when visitors is chosen (71042f08) + - backup/multimedia: test that /home/yunohots.multimedia does exists to avoid boring warning later (170eaf5d) + - domains: add missing logic to inject translated 'help' keys in config panel like we do for global settings (4dee434e) + - domain/dns: don't miserably crash when the domain is known by lexicon but not in registrar_list.toml (b5b69e95) + - admin->admins migration: try to losen up even more the search for first admin user x_x (1e520342) + - i18n: Translations updated for French, Polish + + Thanks to all contributors <3 ! (Éric Gaspar, Grzegorz Cichocki, Kayou, Krzysztof Nowakowski, ljf, ppr) + + -- Alexandre Aubin Tue, 07 Feb 2023 00:14:17 +0100 + +yunohost (11.1.5.5) stable; urgency=low + + - admin->admins migration: try to handle boring case where the 'first' user cant be identified because it doesnt have the root@ alias (8485ebc7) + - appsv2: ignore the old/ugly/legacy removal of apt deps when removing the php conf, because that's handled by the apt resource (3bbba640) + - appsv2: moar fixes for v1->v2 upgrade not getting the proper env context (fb54da2e) + + -- Alexandre Aubin Sat, 04 Feb 2023 18:51:03 +0100 + +yunohost (11.1.5.4) stable; urgency=low + + - appsv2: typo in ports resource doc x_x (0e787acb) + - appsv2: fix permission provisioning for fulldomain apps + fix apps not properly getting removed after failed resources init (476908bd) + + -- Alexandre Aubin Fri, 03 Feb 2023 20:43:04 +0100 + +yunohost (11.1.5.3) stable; urgency=low + + - helpers/appsv2: replacement of __PHPVERSION__ should use the phpversion setting, not YNH_PHP_VERSION (13d4e16e) + - appv2 resources: document the fact that the apt resource may create a phpversion setting when the dependencies contain php packages (2107a848) + + -- Alexandre Aubin Fri, 03 Feb 2023 03:05:11 +0100 + +yunohost (11.1.5.2) stable; urgency=low + + - maintenance: new year, update copyright header (ba4f1925) + - helpers: fix remaining __FINALPATH__ in php template (note that this is backward compatible because ynh_add_config will replace __INSTALL_DIR__ by $finalpath if $finalpath exists... (9b7668da) + + -- Alexandre Aubin Thu, 02 Feb 2023 23:58:29 +0100 + +yunohost (11.1.5.1) stable; urgency=low + + - debian: Bump moulinette/ssowat requirement to 11.1 (0826a541) + - helpers: Fixes $app unbound when running ynh_secure_remove ([#1582](https://github.com/yunohost/yunohost/pull/1582)) + - log/appv2: don't dump all settings in log metadata (a9ac55e4) + - appv2: resource upgrade will tweak settings, we have to re-update the env_dict after upgrading resources (3110460a) + - appv2: safety-backup-before-upgrade should only contain the app (1c95bcff) + - appv2: fix env not including vars for v1->v2 upgrade (2b2d49a5) + - backup: add name of the backup in create/delete message, otherwise that creates some spooky messages with 'Backup created' directly followed by 'Backup deleted' during safety-backup-before-upgrade in v2 apps (8090acb1) + - [i18n] Translations updated for Arabic, French, Galician, Polish + + Thanks to all contributors <3 ! (ButterflyOfFire, Éric Gaspar, Eryk Michalak, Florent, José M, ppr) + + -- Alexandre Aubin Thu, 02 Feb 2023 23:37:46 +0100 + +yunohost (11.1.5) stable; urgency=low + + - Release as stable ! + + - diagnosis: we can't yield an ERROR if there's no IPv6, otherwise that blocks all subsequent network-related diagnoser because of the dependency system ... (ade92e43) + - domains: fix domain_config.toml typos in conditions (480f7a43) + - certs: Don't try restarting metronome if no domain configured for it (452ba8bb) + + Thanks to all contributors <3 ! (Axolotle) + + -- Alexandre Aubin Wed, 01 Feb 2023 20:21:56 +0100 + +yunohost (11.1.4.1) testing; urgency=low + + - debian: don't dump upgradable apps during postinst's catalog update (82d30f02) + - ynh_setup_source: Output checksums when source is 'corrupt' ([#1578](https://github.com/yunohost/yunohost/pull/1578)) + - metronome: Auto-enable/disable metronome if there's no/at least one domain configured for XMPP (c990cee6) + + Thanks to all contributors <3 ! (tituspijean) + + -- Alexandre Aubin Wed, 01 Feb 2023 17:10:32 +0100 + +yunohost (11.1.4) testing; urgency=low + + - settings: Add DNS exposure setting given the IP version ([#1451](https://github.com/yunohost/yunohost/pull/1451)) + + Thanks to all contributors <3 ! (Tagada) + + -- Alexandre Aubin Mon, 30 Jan 2023 16:28:56 +0100 + +yunohost (11.1.3.1) testing; urgency=low + + - nginx: add xmpp-upload. and muc. server_name only if xmpp_enabled is enabled (c444dee4) + - [i18n] Translations updated for Arabic, Basque, French, Galician, Spanish, Turkish + + Thanks to all contributors <3 ! (Alperen İsa Nalbant, ButterflyOfFire, cristian amoyao, Éric Gaspar, José M, Kayou, ppr, quiwy, xabirequejo) + + -- Alexandre Aubin Mon, 30 Jan 2023 15:44:30 +0100 + +yunohost (11.1.3) testing; urgency=low + + - helpers: Include procedures in MySQL database backup ([#1570](https://github.com/yunohost/yunohost/pull/1570)) + - users: be able to change the loginShell of a user ([#1538](https://github.com/yunohost/yunohost/pull/1538)) + - debian: refresh catalog upon package upgrade (be5b1c1b) + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, ljf, Metin Bektas) + + -- Alexandre Aubin Thu, 19 Jan 2023 23:08:10 +0100 + +yunohost (11.1.2.2) testing; urgency=low + + - Minor technical fixes (b37d4baf, 68342171) + - configpanel: stop the madness of returning a 500 error when trying to load config panel 0.1 ... otherwise this will crash the new app info view ... (f21fbed2) + - apps: fix trick to find the default branch from git repo @_@ (25c10166) + - debian: regen ssowatconf during package upgrade (4615d7b7) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric Gaspar, ppr) + + -- Alexandre Aubin Tue, 10 Jan 2023 13:23:28 +0100 + +yunohost (11.1.2.1) testing; urgency=low + + - i18n: fix (un)defined string issues (dd33476f) + - doc: Revive the old auto documentation of API with swagger + - apps: don't clone 'master' branch by default, use git ls-remote to check what's the default branch instead (a6db52b7) + - ssowat: add use_remote_user_var_in_nginx_conf flag on permission (f258eab6) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Mon, 09 Jan 2023 23:58:51 +0100 + +yunohost (11.1.2) testing; urgency=low + + - apps: Various fixes/improvements for appsv2, mostly related to webadmin integration ([#1526](https://github.com/yunohost/yunohost/pull/1526)) + - domains/regenconf: propagate mail/xmpp enable/disable toggle to actual system configs ([#1541](https://github.com/yunohost/yunohost/pull/1541)) + - settings: Add a virtual setting to enable passwordless sudo for admins (75cb3cb2) + - settings: Add a global setting to choose SSOwat's theme ([#1545](https://github.com/yunohost/yunohost/pull/1545)) + - certs: Improve trick to identify certs as self-signed (c38aba74) + - certs: be more resilient when mail cant be sent to root for some reason .. (d7ee1c23) + - certs/postfix: propagate postfix SNI stuff when renewing certificates (31794008) + - certs/xmpp: add to domain's certificate the alt subdomain muc ([#1163](https://github.com/yunohost/yunohost/pull/1163)) + - conf/ldap: fix issue where sudo doesn't work because sudo-ldap doesn't create /etc/sudo-ldap.conf :/ (d2417c33) + - configpanels: fix custom getter ([#1546](https://github.com/yunohost/yunohost/pull/1546)) + - configpanels: fix inconsistent return format for boolean, sometimes 1/0, sometimes True/False -> force normalization of values when calling get() for a single setting from a config panel (47b9b8b5) + - postfix/fail2ban: Add postfix SASL login failure to a fail2ban jail ([#1552](https://github.com/yunohost/yunohost/pull/1552)) + - mail: Fix flag case sensitivity in dovecot and rspamd sieve filter ([#1450](https://github.com/yunohost/yunohost/pull/1450)) + - misc: Don't disable avahi-daemon by force in conf_regen ([#1555](https://github.com/yunohost/yunohost/pull/1555)) + - misc: Fix yunopaste ([#1558](https://github.com/yunohost/yunohost/pull/1558)) + - misc: Don't take lock for read/GET operations (#1554) (0ac8e66a) + - i18n: Translations updated for Basque, French, Galician, Portuguese, Slovak, Spanish, Ukrainian + + Thanks to all contributors <3 ! (axolotle, DDATAA, Fabian Wilkens, Gabriel, José M, Jose Riha, ljf, Luis H. Porras, ppr, quiwy, Rafael Fontenelle, selfhoster1312, Tymofii-Lytvynenko, xabirequejo, Xavier Brochard) + + -- Alexandre Aubin Fri, 06 Jan 2023 00:12:53 +0100 + +yunohost (11.1.1.2) testing; urgency=low + + - group mailalias: the ldap class is in fact mailGroup, not mailAccount -_- (1cb5e43e) + + -- Alexandre Aubin Sat, 03 Dec 2022 15:57:22 +0100 + +yunohost (11.1.1.1) testing; urgency=low + + - Fix again the legacy patch for yunohost user create @_@ (46d6fab0) + + -- Alexandre Aubin Sat, 03 Dec 2022 14:13:09 +0100 + +yunohost (11.1.1) testing; urgency=low + + - groups: add mail-aliases management (#1539) (0f9d9388) + - apps: Allow apps to be installed on a path sharing a common base, eg /foo and /foo2 (#1537) (ae594111) + - admins/ldap: re-allow member of the admins group to edit ldap db (4f5cc166) + - nginx: Add 502 custom error page (#1530) (5063e128) + - misc/nodejs: Upgrade n to version 9.0.1 ([#1528](https://github.com/yunohost/yunohost/pull/1528)) + - misc/update: add --allow-releaseinfo-change option to apt update to prevent the classic nightmare when debian changes from stable to oldstable (ac6d6871) + - misc/dns: Add Webgo as Registrar to support it via LexiconAdd Webgo as Registrar (#1529) (c50f3771) + - misc/debug: Improve dpkg_is_broken instruction to also mention dpkg --audit (a772153b) + - misc/regeconf: fix yunohost hook incorectly tweaking mdns.yml ownership (9bd98162) + - misc/helpers: fix docker-image-extract helper (#1532) + - misc/yunoprompt: don't display postinstall tip to members of all_users group (because they can't check if /etc/yunohost/installed exists, but if they're member of the all_users group, then postinstall was already done) (4aaa8896) + - misc/diagnosis: make the dnsrecord diagnoser not complain about the damn 128 vs 0 stuff in CAA records (70a8225b) + - misc/settings: fix output format for 'yunohost settings list' (70bf38ce) + - misc/helpers: Better error message when psql is not there for database_exists (#992) (f49c121b) + - misc/multimedia: fix edgecase where setfacl crashes because of broken symlinks (94f21ea2) + - misc/legacy: auto-patch yunohost user create syntax in app scripts to use --fullname instead (d254fb1b) + - [i18n] Translations updated for Arabic, Basque, Chinese (Simplified), Dutch, French, Galician, German, Spanish, Ukrainian + + Thanks to all contributors <3 ! (André Koot, Augustin Trancart, Axolotle, ButterflyOfFire, Christian Wehrli, Éric Gaspar, José M, lee, mod242, quiwy, tituspijean, Tymofii-Lytvynenko, xabirequejo) + + -- Alexandre Aubin Fri, 02 Dec 2022 23:31:28 +0100 + +yunohost (11.1.0.2) testing; urgency=low + + - globalsettings: make sure to run migration 25 prior to the regenconf (f3750598) + - domaininfo: Some apps don't have path ([#1521](https://github.com/yunohost/yunohost/pull/1521)) + - Add sponsors to the README ([#1522](https://github.com/yunohost/yunohost/pull/1522)) + - postfix: fix relay conf not triggered because new setting system now returns '1' and not 'True' (cd43c8bd) + - postfix: fix permission issue preventing to properly create sasl_passwd.db (5394790f) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Félix Piédallu, Florian Masy, ppr, Tagada) + + -- Alexandre Aubin Fri, 04 Nov 2022 13:13:40 +0100 + +yunohost (11.1.0.1) testing; urgency=low + + - Bump version after propagating hotfix on 11.0.10.2 + + -- Alexandre Aubin Thu, 27 Oct 2022 15:46:26 +0200 + +yunohost (11.1.0) testing; urgency=low + + - apps: New 'v2' packaging format ([#1289](https://github.com/yunohost/yunohost/pull/1289)) + - helpers: Upgrade n to version 9.0.0 ([#1477](https://github.com/yunohost/yunohost/pull/1477)) + - helpers: Support extracting source from docker images in ynh_setup_source ([#1505](https://github.com/yunohost/yunohost/pull/1505)) + - configpanels: Refactor global settings the new config panel framework ([#1459](https://github.com/yunohost/yunohost/pull/1459)) + - configpanels: Add support for actions (= button widget) and apply it to domain cert management ([#1436](https://github.com/YunoHost/yunohost/pull/1436)) + - admin: Drop the 'admin' user, have 'admins' be a group of Yunohost users instead ([#1408](https://github.com/yunohost/yunohost/pull/1408)) + - admin: Implement a new 'virtual global setting' to change root password from the global setting config panel ([#1515](https://github.com/yunohost/yunohost/pull/1515)) + - domains: Be able to "list" domain as a tree structure + add new 'domain_info' API endpoint ([#1434](https://github.com/yunohost/yunohost/pull/1434)) + - users: Encourage to define a single 'full display name' instead of separate 'firstname/lastname' ([#1516](https://github.com/yunohost/yunohost/pull/1516)) + - security: Improve most used password check list ([#1517](https://github.com/yunohost/yunohost/pull/1517)) + - i18n: Translations updated for Slovak + + Thanks to all contributors <3 ! (axolotle, Dante, Jose Riha, Tagadda, yalh76) + + -- Alexandre Aubin Tue, 25 Oct 2022 17:57:29 +0200 + +yunohost (11.0.10.2) stable; urgency=low + + - Add another trick to autorestart yunohost-api at the end of the upgrade when ran from the api itself... (6f640c08) + + -- Alexandre Aubin Thu, 27 Oct 2022 15:46:26 +0200 + +yunohost (11.0.10.1) stable; urgency=low + + - self-upgrade: fix yunohost-api restart which was not triggered @_@ (472e9250) + + -- Alexandre Aubin Mon, 17 Oct 2022 23:56:37 +0200 + +yunohost (11.0.10) stable; urgency=low + + - configpanels: fix nested bind statements (0252a6fd) + - ynh_setup_source: Add option to fully replace the destination dir ([#1509](https://github.com/yunohost/yunohost/pull/1509)) + - tools_update: also yield a boolean to easily know if there's a major yunohost upgrade pending + list of pending migrations (cf change in webadmin to encourage people to check the release note on the forum before yoloupgrading) (86e45f9c) + - diagnosis: add reports when apt is configured with the 'testing' channel for yunohost, or with the 'stable' codename for debian (0adff31d) + - [i18n] Translations updated for French, Slovak + + Thanks to all contributors <3 ! (Dante, Jose Riha, ppr, yalh76) + + -- Alexandre Aubin Mon, 17 Oct 2022 16:56:47 +0200 + +yunohost (11.0.9.15) stable; urgency=low + + - [fix] Lidswitch if no reboot ([#1506](https://github.com/yunohost/yunohost/pull/1506)) + - [fix] postinstall: edge case where var would get undefined.. (b7bea608) + - [fix] backup: Try to fix again the infamous issue where from_yunohost_version gets filled with 'BASH_XTRACEFD' (14fb1cfd) + - [fix] Various english wording improvements ([#1507](https://github.com/yunohost/yunohost/pull/1507)) + - [i18n] Translations updated for Arabic, Slovak, Telugu, Turkish + + Thanks to all contributors <3 ! (Alice Kile, ButterflyOfFire, Jose Riha, ljf (zamentur), marty hiatt, Sedat Albayrak) + + -- Alexandre Aubin Fri, 30 Sep 2022 16:24:59 +0200 + +yunohost (11.0.9.14) stable; urgency=low + + - [fix] dns: confusion on XMPP CNAME records for nohost.me & co domains (f6057d25) + - [fix] helper ynh_get_ram: LANG= isn't enough to get en_US output, gotta use LC_ALL (e51cdd98) + + -- Alexandre Aubin Wed, 07 Sep 2022 13:08:31 +0200 + +yunohost (11.0.9.13) stable; urgency=low + + - [fix] defaultapp: domain may not exist in app_map dict output (efe0e601) + - [fix] regenconf: fix a stupid issue with slapcat displaying an error message because grep -q breaks the pipe (503b9031) + - [fix] regenconf: add a timeout to curl inside dnsmasq regenconf to prevent being stuck too long when no network on the machine (b77e8114) + - [fix] ynh_delete_file_checksum with non-existing option in helpers/config ([#1501](https://github.com/YunoHost/yunohost/pull/1501)) + - [i18n] Translations updated for Basque, Galician, Slovak + + Thanks to all contributors <3 ! (José M, Jose Riha, tituspijean, xabirequejo) + + -- Alexandre Aubin Sat, 03 Sep 2022 23:27:56 +0200 + +yunohost (11.0.9.12) stable; urgency=low + + - [fix] postinstall: check all partitions (not only physical ones) ([#1497](https://github.com/YunoHost/yunohost/pull/1497)) + - [i18n] Translations updated for Basque, French, Indonesian, Italian, Slovak + + Thanks to all contributors <3 ! (Salamandar) + + -- Alexandre Aubin Sun, 28 Aug 2022 14:50:38 +0200 + +yunohost (11.0.9.11) stable; urgency=low + + = Merge with Buster branch + - [fix] diagnosis: fix inaccurate message (ae92a0b8) + - [fix] logrotate helpers: getopts miserably explodes if 'legacy_args' is inconsistent with 'args_array' ... (530bf04a) + - [i18n] Translations updated for Basque, French, Indonesian, Italian, Slovak + + Thanks to all contributors <3 ! (Jose Riha, Leandro Noferini, liimee, Stephan Klein, xabirequejo) + + -- Alexandre Aubin Fri, 26 Aug 2022 16:32:19 +0200 + +yunohost (11.0.9.9) stable; urgency=low + + - Sync with Buster branch + - [fix] php7.3->7.4: autopatch nginx configs during restore (18e041c4) + + -- Alexandre Aubin Fri, 19 Aug 2022 20:50:52 +0200 + +yunohost (11.0.9.7) stable; urgency=low + + - [fix] logorate helper: was broken because wrong index é_è (efa80304) + - [i18n] Translations updated for French, Galician, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, José M, Tymofii-Lytvynenko) + + -- Alexandre Aubin Wed, 17 Aug 2022 19:24:11 +0200 + +yunohost (11.0.9.6) stable; urgency=low + + - Sync with Buster branch + - [fix] helpers: logrotate shitty inconsistent handling of 'supposedly legacy' --non-append option ... (8d1c75e7) + - [fix] apps: Better handling of super shitty edge case where an app settings.yml is empty for some unexpected mystic reason ... (9eb123f8) + + -- Alexandre Aubin Wed, 17 Aug 2022 01:26:28 +0200 + +yunohost (11.0.9.5) stable; urgency=low + + - Propagate fixes in buster->bullseye migration + - [fix] venv rebuild: synapse's folder is named matrix-synapse (c8031ace) + + -- Alexandre Aubin Sun, 14 Aug 2022 18:22:30 +0200 + +yunohost (11.0.9.3) stable; urgency=low + + - [fix] postgresql 11->13: Epic typo / missing import (3cb1a41a) + - [i18n] Translations updated for Basque, French, Galician + + Thanks to all contributors <3 ! (Éric Gaspar, José M, Kay0u, punkrockgirl) + + -- Alexandre Aubin Sat, 13 Aug 2022 22:37:05 +0200 + +yunohost (11.0.9.2) stable; urgency=low + + - [fix] venv rebuild: fix yunohost app force upgrade command (5d90971b) + - [fix] apt helpers: simplify ynh_remove_app_dependencies, we don't need to care about removing php-fpm services from yunohost, because 'yunohost service' now dynamically check what relevant phpX.Y-fpm service exist on the system (64e35815) + - [enh] diagnosis: add complains if some app installed are still requiring only yunohost 3.x (31aacb33) + - [fix] venv rebuild: migration should have an empty disclaimer when in auto mode (d2a6dcd4) + - [fix] postgresql 11->13 migration: skip if no yunohost app depend on postgresql (d161da03) + + Thanks to all contributors <3 ! (Éric Gaspar, ljf) + + -- Alexandre Aubin Sat, 13 Aug 2022 20:08:27 +0200 + +yunohost (11.0.9.1) stable; urgency=low + + - [fix] venv rebuild: /opt may not exist ... + + -- Alexandre Aubin Thu, 11 Aug 2022 16:00:40 +0200 + +yunohost (11.0.9) stable; urgency=low + + - [fix] services: Skip php 7.3 which is most likely dead after buster->bullseye migration because users get spooked (51804925) + - [enh] bullseye: add a migration process to automatically attempt to rebuild venvs (3b8e49dc) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric Gaspar, Kayou, ljf, theo-is-taken) + + -- Alexandre Aubin Sun, 07 Aug 2022 23:27:41 +0200 + +yunohost (11.0.8.1) testing; urgency=low + + - Fix tests é_è (7fa67b2b) + + -- Alexandre Aubin Sun, 07 Aug 2022 12:41:28 +0200 + +yunohost (11.0.8) testing; urgency=low + + - [fix] helpers: escape username in ynh_user_exists ([#1469](https://github.com/YunoHost/yunohost/pull/1469)) + - [fix] helpers: in nginx helpers, do not change the nginx template conf, replace #sub_path_only and #root_path_only after ynh_add_config, otherwise it breaks the change_url script (30e926f9) + - [fix] helpers: fix arg parsing in ynh_install_apps ([#1480](https://github.com/YunoHost/yunohost/pull/1480)) + - [fix] postinstall: be able to redo postinstall when the 128+ chars + password error is raised ([#1476](https://github.com/YunoHost/yunohost/pull/1476)) + - [fix] regenconf dhclient/resolvconf: fix weird typo, probably meant 'search' (like in our rpi-image tweaking) (9d39a2c0) + - [fix] configpanels: remove debug message because it floods the regenconf logs (f6cd35d9) + - [fix] configpanels: don't restrict choices if there's no choices specified ([#1478](https://github.com/YunoHost/yunohost/pull/1478) + - [i18n] Translations updated for Arabic, German, Slovak, Telugu + + Thanks to all contributors <3 ! (Alice Kile, ButterflyOfFire, Éric Gaspar, Gregor, Jose Riha, Kay0u, ljf, Meta Meta, tituspijean, Valentin von Guttenberg, yalh76) + + -- Alexandre Aubin Sun, 07 Aug 2022 11:26:54 +0200 + +yunohost (11.0.7) testing; urgency=low + + - [fix] Allow lime2 to upgrade even if kernel is hold ([#1452](https://github.com/YunoHost/yunohost/pull/1452)) + - [fix] Some DNS suggestions for specific domains are incorrect ([#1460](https://github.com/YunoHost/yunohost/pull/1460)) + - [enh] Reorganize PHP-specific code in apt helper (5ca18c5) + - [enh] Implement install and removal of YunoHost apps ([#1445](https://github.com/YunoHost/yunohost/pull/1445)) + - [enh] Add n auto-updater ([#1437](https://github.com/YunoHost/yunohost/pull/1437)) + - [enh] nodejs: Upgrade n to v8.2.0 ([#1456](https://github.com/YunoHost/yunohost/pull/1456)) + - [enh] Improve ynh_string_random to output various ranges of characters ([#1455](https://github.com/YunoHost/yunohost/pull/1455)) + - [enh] Avoid alert for Content Security Policies Report-Only and Websockets ((#1464)[https://github.com/YunoHost/yunohost/pull/1464]) + - [doc] Improve ynh_add_config template doc ([#1463](https://github.com/YunoHost/yunohost/pull/1463)) + - [i18n] Translations updated for Russian and French + + Thanks to all contributors <3 ! (DiesDasJenes, ljf, kayou, yalh, aleks, tituspijean, keomabrun, pp-r, cheredin) + + -- tituspijean Tue, 17 May 2022 23:20:00 +0200 + +yunohost (11.0.6) testing; urgency=low + + - [fix] configpanel: the config panel was not modifying the configuration of the correct app in certain situations ([#1449](http://github.com/YunoHost/yunohost/pull/1449)) + - [fix] debian package: fix for openssl conflict (ec41b697) + - [i18n] Translations updated for Arabic, Basque, Finnish, French, Galician, German, Kabyle, Polish + + Thanks to all contributors <3 ! (3ole, Alexandre Aubin, Baloo, Bartłomiej Garbiec, José M, Kayou, ljf, Mico Hauataluoma, punkrockgirl, Selyan Slimane Amiri, Tagada) + + -- Kay0u Tue, 29 Mar 2022 14:13:40 +0200 + +yunohost (11.0.5) testing; urgency=low + + - [mod] configpanel: improve 'filter' mechanism in AppQuestion ([#1429](https://github.com/YunoHost/yunohost/pull/1429)) + - [fix] postinstall: migrate_to_bullseye should be skipped on bullseye (de684425) + - [enh] security: Enable proc-hidepid by default ([#1433](https://github.com/YunoHost/yunohost/pull/1433)) + - [enh] nodejs: Update n to 8.0.2 ([#1435](https://github.com/YunoHost/yunohost/pull/1435)) + - [fix] postfix: sni tls_server_chain_sni_maps -> tls_server_sni_maps ([#1438](https://github.com/YunoHost/yunohost/pull/1438)) + - [fix] ynh_get_ram: Avoid grep issue with vmstat command ([#1440](https://github.com/YunoHost/yunohost/pull/1440)) + - [fix] ynh_exec_*: ensure the arg message is used ([#1442](https://github.com/YunoHost/yunohost/pull/1442)) + - [enh] helpers: Always activate --time when running inside CI tests ([#1444](https://github.com/YunoHost/yunohost/pull/1444)) + - [fix] helpers: unbound variable in ynh_script_progression (676973a1) + - [mod] quality: Several FIXME fix ([#1441](https://github.com/YunoHost/yunohost/pull/1441)) + + Thanks to all contributors <3 ! (ericgaspar, ewilly, Kayou, Melchisedech, Tagadda) + + -- Alexandre Aubin Tue, 08 Mar 2022 13:01:06 +0100 + +yunohost (11.0.4) testing; urgency=low + + - [mod] certificate: drop unused 'staging' LE mode (4b78e8e3) + - [fix] cli: bash_completion was broken ([#1423](https://github.com/YunoHost/yunohost/pull/1423)) + - [enh] mdns: Wait for network to be fully up to start the service ([#1425](https://github.com/YunoHost/yunohost/pull/1425)) + - [fix] regenconf: make some systemctl enable/disable quiet (bccff1b4, 345e50ae) + - [fix] configpanels: Compute choices for the yunohost admin when installing an app ([#1427](https://github.com/YunoHost/yunohost/pull/1427)) + - [fix] configpanels: optimize _get_toml for domains to not load the whole DNS section stuff when just getting a simple info from another section (bf6252ac) + - [fix] configpanel: oopsies, could only change the default app for domain configs :P (0a59f863) + - [fix] php73_to_php74: another search&replace for synapse (f0a01ba2) + - [fix] php73_to_php74: stopping php7.3 before starting 7.4 should be more robust in case confs are conflicting (9ae7ec59) + - [i18n] Translations updated for French, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, Tagadda, tituspijean, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sat, 29 Jan 2022 19:19:44 +0100 + +yunohost (11.0.3) testing; urgency=low + + - [enh] mail: Add SNI support for postfix and dovecot ([#1413](https://github.com/YunoHost/yunohost/pull/1413)) + - [fix] services: fix a couple edge cases (4571c5b2) + - [fix] services: Do not save php-fpm services in services.yml (5d0f8021) + - [fix] diagnosis: diagnosers were run in a funky order ([#1418](https://github.com/YunoHost/yunohost/pull/1418)) + - [fix] configpanels: config_get should return possible choices for domain, user questions (and other dynamic-choices questions) ([#1420](https://github.com/YunoHost/yunohost/pull/1420)) + - [enh] apps/domain: Clarify the default app mecanism, handle it fron domain config panel ([#1406](https://github.com/YunoHost/yunohost/pull/1406)) + - [fix] apps: When no main app permission found, fallback to default label instead of having a 'None' label to prevent the webadmin from displaying an empty app list (07396b8b) + - [i18n] Translations updated for Galician + + Thanks to all contributors <3 ! (José M, Kay0u, Tagadda, tituspijean) + + -- Alexandre Aubin Tue, 25 Jan 2022 13:06:10 +0100 + +yunohost (11.0.2) testing; urgency=low + + - [mod] Various tweaks for Python 3.9, PHP 7.4, PostgreSQL 13, and other changes related to Buster->Bullseye ecosystem + - [mod] debian: Moved mysql, php, and metronome from Depends to Recommends ([#1369](https://github.com/YunoHost/yunohost/pull/1369)) + - [mod] apt: **Add sury by default** ([#1369](https://github.com/YunoHost/yunohost/pull/1369)) + - [enh] mysql: **Drop super old mysql config, now rely on Debian default** ([44c972f...144126f](https://github.com/YunoHost/yunohost/compare/44c972f2dd65...144126f56a3d)) + - [enh] regenconf/helpers: Better integration for postgresql ([#1369](https://github.com/YunoHost/yunohost/pull/1369)) + - [mod] quality: **Rework repository code architecture** ([#1377](https://github.com/YunoHost/yunohost/pull/1377)) + - [mod] quality: **Rework where yunohost files are deployed** (yunohost now a much closer to a python lib with files in /usr/lib/python3/dist-packages/yunohost/, and other "common" files are in /usr/share/yunohost) ([#1377](https://github.com/YunoHost/yunohost/pull/1377)) + - [enh] upgrade: Try to implement **a smarter self-upgrade mechanism to prevent/limit API downtime and related UX issues** ([#1374](https://github.com/YunoHost/yunohost/pull/1374)) + - [mod] regenconf: store tmp files in /var/cache/yunohost/ instead of the misleading /home/yunohost.conf folder (00d535a6) + - [mod] dyndns: rewrite tsig keygen + nsupdate using full python, now that dnssec-keygen doesnt support hmacsha512 anymore (63a84f53) + - [mod] app: During app scripts (and all stuff run in hook_exec), do not inject the HOME variable if it exists. This aims to prevent inconsistencies between CLI (where HOME usually is defined) and API (where HOME doesnt exists) (f43e567b) + - [mod] quality: **Drop legacy commands or arguments** listed below + - Drop `--other_vars` options in ynh_add_fail2ban_config and systemd_config helpers + - Drop deprecated/superold `ynh_bind_or_cp`, `ynh_mkdir_tmp`, `ynh_get_plain_key` helpers + - Drop obsolete `yunohost-reset-ldap-password` command + - Drop obsolete `yunohost dyndns installcron` and `removecron` commands + - Drop deprecated `yunohost service regen-conf` command (see `tools regen-conf` instead) + - Drop deprecated `yunohost app fetchlist` command + - Drop obsolete `yunohost app add/remove/clearaccess` commands + - Drop deprecated `--installed` and `--filter` options in `yunohost app list` + - Drop deprecated `--apps` and `--system` options in `yunohost tools update/upgrade` (no double dashes anymore) + - Drop deprecated `--status` and `--log_type` options in `yunohost service add` + - Drop deprecated `--mail` option in `yunohost user create` + + -- Alexandre Aubin Wed, 19 Jan 2022 20:52:39 +0100 + +yunohost (4.4.2.14) stable; urgency=low + + - bullseye migration: remove derpy OVH repo... (76014920) + - bullseye migration: improve autofix procedure for the libc6 hell (02b3a138) + + -- Alexandre Aubin Sat, 03 Sep 2022 23:19:08 +0200 + +yunohost (4.4.2.13) stable; urgency=low + + - [fix] bullseye migration: a few annoying issues related to Sury (b5fabc87) + + -- Alexandre Aubin Mon, 29 Aug 2022 15:40:03 +0200 + +yunohost (4.4.2.12) stable; urgency=low + + - bullseye migration: add trick to automagically find the likely log of a previously failed migration to ease support (f5d94509) + + -- Alexandre Aubin Fri, 26 Aug 2022 19:22:30 +0200 + +yunohost (4.4.2.10) stable; urgency=low + + - bullseye migration: add proper explanations and advices after the damn 'The distribution is not Buster' message ... (6a594d0e) + + -- Alexandre Aubin Mon, 22 Aug 2022 10:28:50 +0200 + +yunohost (4.4.2.9) stable; urgency=low + + - apt helper: fix edge case with equivs package being flagged hold because of buster->bullseye migration (b306df2c) + - bullseye migration: fix check about free space in /boot/ ... (a2d4abc1) + + -- Alexandre Aubin Thu, 18 Aug 2022 19:24:47 +0200 + +yunohost (4.4.2.7) stable; urgency=low + + - upgrades: ignore boring insserv warnings during apt commands (87f0eff9) + - bullseye migration: higher treshold for low space detection in /boot/ because some people still experience the issue on 4.4.2.6 (d283c900) + + -- Alexandre Aubin Wed, 17 Aug 2022 01:21:36 +0200 + +yunohost (4.4.2.6) stable; urgency=low + + - [fix] bullseye migration: trash pip freeze stderr because it's confusing users ... (e68fc821) + - [fix] bullseye migration: add a check that there's at least 70MB available in /boot ... (02fcbd97) + - [fix] bullseye migration: better detection mechanism for the libc6 / libgcc hell issue (633a1fbf) + + -- Alexandre Aubin Sun, 14 Aug 2022 18:18:13 +0200 + +yunohost (4.4.2.3) stable; urgency=low + + - [fix] bullseye migration: add fix for stupid dnsmasq not picking new init script (origin/dev, origin/HEAD, dev) + - [fix] bullseye migration: add the patch for the build-essential / libc6-dev / libgcc-8-dev hell ... + - [fix] bullseye migration: add critical fix for RPi failing to get network on reboot + - [fix] bullseye migration: add ffsync to deprecated apps (77c2f5dc) + + -- Alexandre Aubin Sat, 13 Aug 2022 20:06:00 +0200 + +yunohost (4.4.2.1) stable; urgency=low + + - [fix] bullseye migration: /opt may not exist ... (5fd74577) + + -- Alexandre Aubin Thu, 11 Aug 2022 15:56:16 +0200 + +yunohost (4.4.2) stable; urgency=low + + - Release as stable + - [fix] bullseye migration: /etc/apt/sources.list may not exist (b928dd12) + - [fix] bullseye migration: Allow lime2 to upgrade even if kernel is hold (#1452) + - [fix] bullseye migration: Save python apps venv in a requirements file, in order to regenerate it in a follow-up migration ([#1479](https://github.com/YunoHost/yunohost/pull/1479)) + - [fix] bullseye migration: tweak message to prepare for stable release (80015a72) + + Thanks to all contributors <3 ! (ljf, theo-is-taken) + + -- Alexandre Aubin Tue, 09 Aug 2022 16:59:15 +0200 + +yunohost (4.4.1) testing; urgency=low + + - [fix] php helpers: prevent epic catastrophies when the app changes php version (31d3719b) + + Thanks to all contributors <3 ! (Alexandre Aubin) + + -- Kay0u Tue, 29 Mar 2022 14:03:52 +0200 + +yunohost (4.4.0) testing; urgency=low + + - [enh] Add buster->bullseye migration + + -- Alexandre Aubin Wed, 19 Jan 2022 20:45:22 +0100 + +yunohost (4.3.6.3) stable; urgency=low + + - [fix] debian package: backport fix for openssl conflict (1693c831) + + Thanks to all contributors <3 ! (Kay0u) + + -- Kay0u Tue, 29 Mar 2022 13:52:58 +0200 + +yunohost (4.3.6.2) stable; urgency=low + + - [fix] apt helpers: fix bug when var is empty... (7920cc62) + + -- Alexandre Aubin Wed, 19 Jan 2022 20:30:25 +0100 + +yunohost (4.3.6.1) stable; urgency=low + + - [fix] dnsmasq: ensure interface is up ([#1410](https://github.com/YunoHost/yunohost/pull/1410)) + - [fix] apt helpers: fix ynh_install_app_dependencies when an app change his default phpversion (6ea32728) + - [fix] certificates: fix edge case where None is returned, triggering 'NoneType has no attribute get' (019839db) + - [i18n] Translations updated for German + + Thanks to all contributors <3 ! (Gregor, Kay0u) + + -- Alexandre Aubin Wed, 19 Jan 2022 20:05:13 +0100 + +yunohost (4.3.6) stable; urgency=low + + - [enh] ssh: add a new setting to manage PasswordAuthentication in sshd_config ([#1388](https://github.com/YunoHost/yunohost/pull/1388)) + - [enh] upgrades: filter more boring apt messages (3cc1a0a5) + - [fix] ynh_add_config: crons should be owned by root, otherwise they probably don't run? (0973301b) + - [fix] domains: force cert install during domain_add ([#1404](https://github.com/YunoHost/yunohost/pull/1404)) + - [fix] logs: remove 'args' for metadata, may contain unredacted secrets in edge cases + - [fix] helpers, apt: upgrade apt dependencies from extra repos ([#1407](https://github.com/YunoHost/yunohost/pull/1407)) + - [fix] diagnosis: incorrect dns check (relative vs absolute) for CNAME on subdomain (d81b85a4) + - [i18n] Translations updated for Dutch, French, Galician, German, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Boudewijn, Christian Wehrli, Éric Gaspar, Germain Edy, José M, Kay0u, Kayou, ljf, Tagada, Tymofii-Lytvynenko) + + -- Alexandre Aubin Fri, 14 Jan 2022 01:29:58 +0100 + +yunohost (4.3.5) stable; urgency=low + + - [fix] backup: bug in backup_delete when compress_tar_archives is True ([#1381](https://github.com/YunoHost/yunohost/pull/1381)) + - [fix] helpers logrorate: remove permission tweak .. code was not working as expected. To be re-addressed some day ... (0fc209ac) + - [fix] i18n: consistency for deprecation for --apps in 'yunohost tools update/upgrade' ([#1392](https://github.com/YunoHost/yunohost/pull/1392)) + - [fix] apps: typo when deleting superfluous question keys ([#1393](https://github.com/YunoHost/yunohost/pull/1393)) + - [fix] diagnosis: typo in dns record diagnoser (a615528c) + - [fix] diagnosis: tweak treshold for suspiciously high number of auth failure because too many people getting report about it idk (76abbf03) + - [enh] quality: apply pyupgrade ([#1395](https://github.com/YunoHost/yunohost/pull/1395)) + - [enh] quality: add lgtm/code quality badge ([#1396](https://github.com/YunoHost/yunohost/pull/1396)) + - [i18n] Translations updated for Dutch, French, Galician, German, Indonesian, Russian, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Boudewijn, Bram, Christian Wehrli, Colin Wawrik, Éric Gaspar, Ilya, José M, Juan Alberto González, Kay0u, liimee, Moutonjr Geoff, tituspijean, Tymofii Lytvynenko, Valentin von Guttenberg) + + -- Alexandre Aubin Wed, 29 Dec 2021 01:01:33 +0100 + +yunohost (4.3.4.2) stable; urgency=low + + - [fix] yunomdns: Ignore ipv4 link-local addresses (6854f23c) + - [fix] backup: Fix path for multimedia restore ([#1386](https://github.com/YunoHost/yunohost/pull/1386)) + - [fix] helpers apt/php: typo in extra php-fpm yunohost service integration (47f3c00d) + - [enh] helpers: Update n to 8.0.1 (d1ab1f67) + + Thanks to all contributors <3 ! (ericgaspar, Kayou) + + -- Alexandre Aubin Wed, 08 Dec 2021 22:04:04 +0100 + +yunohost (4.3.4.1) stable; urgency=low + + - [fix] regenconf: Force permission on /etc/resolv.dnsmasq.conf to fix an issue on some setup with umask=027 (5881938c) + - [fix] regenconf: Typo in custom mdns alias regen conf (b3df36dd) + - [fix] regenconf: Try to fix the return line bug in dnsmasq conf ([#1385](https://github.com/YunoHost/yunohost/pull/1385)) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Sat, 27 Nov 2021 21:15:29 +0100 + +yunohost (4.3.4) stable; urgency=low + + - [fix] apps: Allow tilde in username/organization for repo URLs ([#1382](https://github.com/YunoHost/yunohost/pull/1382)) + - [fix] misc: /etc/yunohost permissions broken on some setups (6488b4f6) + - [fix] mdns: Don't add yunohost.local in config if it's already among the yunohost domains (c4962834) + - [enh] dnsmasq: Tweak conf for better support of some stuff like the hotspot app ([#1383](https://github.com/YunoHost/yunohost/pull/1383)) + + Thanks to all contributors <3 ! (ljf, tituspijean) + + -- Alexandre Aubin Sat, 27 Nov 2021 00:53:16 +0100 + +yunohost (4.3.3) stable; urgency=low + + - [fix] log: fix dump_script_log_extract_for_debugging displaying wrong log snippet during failed upgrade ([#1376](https://github.com/YunoHost/yunohost/pull/1376)) + - [fix] certificate: fix stupid certificate/diagnosis issue with subdomains of ynh domains (7c569d16) + - [fix] diagnosis: Read DNS Blacklist answer and compare it against list of non-BL codes ([#1375](https://github.com/YunoHost/yunohost/pull/1375)) + - [enh] helpers: Update n to 8.0.0 ([#1372](https://github.com/YunoHost/yunohost/pull/1372)) + - [fix] helpers: Make ynh_add_fpm_config more robust to some edge cases (51d5dca0) + - [fix] backup: conf_ynh_settings backup/restore hook, /etc/yunohost/domains may not exist (38f5352f) + - [i18n] Translations updated for Basque, Chinese (Simplified), Indonesian, Italian, Ukrainian + + Thanks to all contributors <3 ! (dagangtie, ericgaspar, Félix Piédallu, Flavio Cristoforetti, liimee, punkrockgirl, Romain Thouvenin, Tommi, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sun, 14 Nov 2021 22:55:16 +0100 + +yunohost (4.3.2.2) stable; urgency=low + + - [fix] nginx: Try to fix again the webadmin cache hell (74e2a51e) + + -- Alexandre Aubin Sat, 06 Nov 2021 17:39:58 +0100 + +yunohost (4.3.2.1) stable; urgency=low + + - [enh] mdns: Add possibility to manually add .local aliases via /etc/yunohost/mdns.aliases (meant for internetcube) (3da2df6e) + - [fix] debian: Fix conflict with redis-server (6558b23d) + - [fix] nginx: Refine experimental CSP header (in the end still gotta enable unsafe-inline and unsafe-eval for a bunch of things, but better than no policy at all...) (1cc3e440) + + -- Alexandre Aubin Sat, 06 Nov 2021 16:58:07 +0100 + +yunohost (4.3.2) stable; urgency=low + + - Release as stable + - [i18n] Translations updated for Basque, Occitan + + Thanks to all contributors <3 ! (punkrockgirl, Quentí) + + -- Alexandre Aubin Fri, 05 Nov 2021 02:32:56 +0100 + +yunohost (4.3.1.8) testing; urgency=low + + - [enh] dyndns: Drop some YAGNI + improve IPv6-only support + resilience w.r.t. ns0 / ns1 being down (a61d0231, [#1367](https://github.com/YunoHost/yunohost/pull/1367)) + - [fix] helpers: improve composer debug when it can't install dependencies (4ebcaf8d) + - [enh] helpers: allow to get/set/delete app settings without explicitly passing app id everytime... (fcd2ef9d) + - [fix] helpers: Don't say the 'app was restored' when restore failed after failed upgrade (019d207c) + - [enh] helpers: temporarily auto-add visitors during ynh_local_curl if needed ([#1370](https://github.com/YunoHost/yunohost/pull/1370)) + - [enh] apps: Add YNH_ARCH to app script env for easier debugging and arch check in script (85eb43a7) + - [mod] misc fixes/enh (2687121f, 146fba7d, 86a9cb37, 4e917b5e, 974ea71f, edc5295d, ba489bfc) + - [i18n] Translations updated for Basque, French, Spanish + + Thanks to all contributors <3 ! (ljf, Page Asgardius, ppr, punkrockgirl) + + -- Alexandre Aubin Wed, 03 Nov 2021 18:35:18 +0100 + +yunohost (4.3.1.7) testing; urgency=low + + - [fix] configpanel: Misc technical fixes ... (341059d0, 9c22329e) + - [i18n] Translations updated for Basque, French + + Thanks to all contributors <3 ! (ljf, ppr, punkrockgirl) + + -- Alexandre Aubin Tue, 19 Oct 2021 15:30:50 +0200 + +yunohost (4.3.1.6) testing; urgency=low + + - [fix] configpanel: Various technical fixes (07c1ddce, eae826b2, ff69067d) + - [i18n] Translations updated for Basque, Galician, German, Russian, Ukrainian + + Thanks to all contributors <3 ! (Colin Wawrik, Daniel, José M, ljf, punkrockgirl, Semen Turchikhin, Tymofii-Lytvynenko) + + -- Alexandre Aubin Mon, 18 Oct 2021 18:50:00 +0200 + +yunohost (4.3.1.5) testing; urgency=low + + - [enh] configpanel: Add hook mecanism between questions (9f7fb61b) + - [fix] configpanel: Issue with visible-if context missing between section + - [mod] Force-disable old avahi-daemon (af3d6dd7, 3a07a780) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Sun, 17 Oct 2021 20:44:33 +0200 + +yunohost (4.3.1.4) testing; urgency=low + + - [mod] codequality: Safer, clearer ynh_secure_remove ([#1357](https://github.com/YunoHost/yunohost/pull/1357)) + - [mod] codequality: Lint/autoformat helpers, hooks and debian scripts ([#1356](https://github.com/YunoHost/yunohost/pull/1356)) + - [mod] helpers: Flag ynh_print_ON/OFF as internal to not advertise them in the doc (fe959bd7) + - [fix] helpers: Eval mecanism in ynh_exec_* lead to epic bugs ([#1358](https://github.com/YunoHost/yunohost/pull/1358)) + - [enh] dyndns: validate that we're connected to the internet before triggering yunohost dyndns update (55bacd74) + - [enh] regenconf/dyndns: Delete dyndns cron in regenconf if no dyndns domain found (cb835a2d) + - [fix] regenconf/dovecot: add conf snippet to get rid of stupid stats-writer errors in mail.log (dab3dc6f) + - [enh] regenconf/dnsmasq: Don't generate dnsmasq conf for .local domains (df02f898) + + -- Alexandre Aubin Wed, 13 Oct 2021 15:41:21 +0200 + +yunohost (4.3.1.3) testing; urgency=low + + - [fix] app: repo url branch names may contain dots (38cff4a9) + + -- Alexandre Aubin Thu, 07 Oct 2021 18:31:09 +0200 + +yunohost (4.3.1.2) testing; urgency=low + + - [fix] apps: upgrade was broken because of typo ([#1350](https://github.com/YunoHost/yunohost/pull/1350)) + - [enh] apps: in app_info, return a new is_webapp info meant to be used by API/webadmin (4cd5e9b6) + - [fix] configpanel: handle case where file question didnt get modified from webadmin, in which case self.value contains a path (54d901ad) + - [fix] configpanel: bind_key -> bind_key_ to prevent yunohost from redacting key names which leads to broken log metadata.yml somehow (941cc294) + - [enh] questions: Add visible attribute support in cli (74256845) + - [enh] helpers: Simplify apt/php dependencies helpers ([#1018](https://github.com/YunoHost/yunohost/pull/1018)) + - [enh] helpers: In logrotate helper, enforce decent permissions on log file if app user exists ([#1352](https://github.com/YunoHost/yunohost/pull/1352)) + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, ljf) + + -- Alexandre Aubin Thu, 07 Oct 2021 10:42:06 +0200 + +yunohost (4.3.1.1) testing; urgency=low + + - [enh] app helpers: Update n version ([#1347](https://github.com/YunoHost/yunohost/pull/1347)) + - [enh] Misc app.py refactoring + Prevent change_url from being used to move a fulldomain app to a subpath ([#1346](https://github.com/YunoHost/yunohost/pull/1346)) + - [i18n] Translations updated for French, Galician, Portuguese, Ukrainian + + Thanks to all contributors <3 ! (Éric Gaspar, José M, mifegui, ppr, Tymofii-Lytvynenko) + + -- Alexandre Aubin Mon, 04 Oct 2021 01:33:22 +0200 + +yunohost (4.3.1) testing; urgency=low + + - [fix] diagnosis: new app diagnosis grep reporing comments as issues ([#1333](https://github.com/YunoHost/yunohost/pull/1333)) + - [enh] configpanel: Bind function for hotspot (79126809) + - [enh] cli: Rework/improve prompt mecanic ([#1338](https://github.com/YunoHost/yunohost/pull/1338)) + - [fix] dyndns update broke because of buggy dns record names (da1b9089) + - [enh] dns: general improvement for special-use TLD / ynh dyndns domains (17aafe6f) + - [fix] yunomdns: various fixes/improvements ([#1335](https://github.com/YunoHost/yunohost/pull/1335)) + - [fix] certs: Adapt ready_for_ACME check to the new dnsrecord result format... (d75c1a61) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric Gaspar, Félix Piédallu, Kayou, ljf, tituspijean) + + -- Alexandre Aubin Wed, 29 Sep 2021 22:22:42 +0200 + +yunohost (4.3.0) testing; urgency=low + + - [users] Import/export users from/to CSV ([#1089](https://github.com/YunoHost/yunohost/pull/1089)) + - [domain] Add mDNS for .local domains / replace avahi-daemon ([#1112](https://github.com/YunoHost/yunohost/pull/1112)) + - [settings] new setting to enable experimental security features ([#1290](https://github.com/YunoHost/yunohost/pull/1290)) + - [settings] new setting to handle https redirect ([#1304](https://github.com/YunoHost/yunohost/pull/1304)) + - [diagnosis] add an "app" section to check that app are in catalog with good quality, check for deprecated practices ([#1217](https://github.com/YunoHost/yunohost/pull/1217)) + - [diagnosis] report suspiciously high number of auth failures ([#1292](https://github.com/YunoHost/yunohost/pull/1292)) + - [refactor] Rework the authentication system ([#1183](https://github.com/YunoHost/yunohost/pull/1183)) + - [enh] New config-panel mechanism ([#987](https://github.com/YunoHost/yunohost/pull/987)) + - [enh] Add backup for multimedia files (88063dc7) + - [enh] Configure automatically the DNS records using lexicon ([#1315](https://github.com/YunoHost/yunohost/pull/1315)) + - also brings domain settings, domain config panel, subdomain awareness, improvements in dns recommended conf + - [i18n] Translations updated for Catalan, Chinese (Simplified), Czech, Esperanto, French, Galician, German, Italian, Occitan, Persian, Portuguese, Spanish, Ukrainian + + Thanks to all contributors <3 ! (Corentin Mercier, Daniel, Éric Gaspar, Flavio Cristoforetti, Gregor Lenz, José M, Kay0u, ljf, MercierCorentin, mifegui, Paco, Parviz Homayun, ppr, tituspijean, Tymofii-Lytvynenko) + + -- Alexandre Aubin Sun, 19 Sep 2021 23:55:21 +0200 + +yunohost (4.2.8.3) stable; urgency=low + + - [fix] mysql: Another bump for sort_buffer_size to make Nextcloud 22 work (34e9246b) + + Thanks to all contributors <3 ! (ljf (zamentur)) + + -- Kay0u Fri, 10 Sep 2021 10:40:38 +0200 + +yunohost (4.2.8.2) stable; urgency=low + + - [fix] mysql: Bump sort_buffer_size to 256K to fix Nextcloud 22 installation (d8c49619) + + Thanks to all contributors <3 ! (ericg) + + -- Alexandre Aubin Tue, 07 Sep 2021 23:23:18 +0200 + +yunohost (4.2.8.1) stable; urgency=low + + - [fix] Safer location for slapd backup during hdb/mdb migration (3c646b3d) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Fri, 27 Aug 2021 01:32:16 +0200 + +yunohost (4.2.8) stable; urgency=low + + - [fix] ynh_permission_has_user not behaving properly when checking if a group is allowed (f0590907) + - [enh] use yaml safeloader everywhere ([#1287](https://github.com/YunoHost/yunohost/pull/1287)) + - [enh] Add --no-safety-backup option to "yunohost app upgrade" ([#1286](https://github.com/YunoHost/yunohost/pull/1286)) + - [enh] Add --purge option to "yunohost app remove" ([#1285](https://github.com/YunoHost/yunohost/pull/1285)) + - [enh] Multimedia helper: check that home folder exists ([#1255](https://github.com/YunoHost/yunohost/pull/1255)) + - [i18n] Translations updated for French, Galician, German, Portuguese + + Thanks to all contributors <3 ! (José M, Kay0u, Krakinou, ljf, Luca, mifegui, ppr, sagessylu) + + -- Alexandre Aubin Thu, 19 Aug 2021 19:11:19 +0200 + +yunohost (4.2.7) stable; urgency=low + + Notable changes: + - [fix] app: 'yunohost app search' was broken (8cf92576) + - [fix] app: actions were broken, fix by reintroducing user arg in hook exec ([#1264](https://github.com/yunohost/yunohost/pull/1264)) + - [enh] app: Add check for available disk space before app install/upgrade ([#1266](https://github.com/yunohost/yunohost/pull/1266)) + - [enh] domains: Better support for non latin domain name ([#1270](https://github.com/yunohost/yunohost/pull/1270)) + - [enh] security: Add settings to restrict webadmin access to a list of IPs ([#1271](https://github.com/yunohost/yunohost/pull/1271)) + - [enh] misc: Avoid to suspend server if we close lidswitch ([#1275](https://github.com/yunohost/yunohost/pull/1275)) + - [i18n] Translations updated for French, Galician, German + + Misc fixes, improvements: + - [fix] logs: Sometimes metadata ends up being empty for some reason and ends up being loaded as None, making the "in" operator crash :| (50129f3a) + - [fix] nginx: Invalid HTML in yunohost_panel #1837 (1c15f644) + - [fix] ssh: set .ssh folder permissions to 600 ([#1269](https://github.com/yunohost/yunohost/pull/1269)) + - [fix] firewall: upnpc.getspecificportmapping expects an int, can't handle port ranges ? (ee70dfe5) + - [fix] php helpers: fix conf path for dedicated php server (7349b229) + - [fix] php helpers: Increase memory limit for composer ([#1278](https://github.com/yunohost/yunohost/pull/1278)) + - [enh] nodejs helpers: Upgrade n version to 7.3.0 ([#1262](https://github.com/yunohost/yunohost/pull/1262)) + - [fix] doc: Example command in yunopaste usage was outdated (a8df60da) + - [fix] doc, diagnosis: update links to SMTP relay configuration ([#1277](https://github.com/yunohost/yunohost/pull/1277)) + - [i18n] Translations fixes/cleanups (780c3cb8, b61082b1, 271e3a26, 4e4173d1, fab248ce, d49ad748) + + Thanks to all contributors <3 ! (Bram, Christian Wehrli, cyxae, Éric Gaspar, José M, Kay0u, Le Libre Au Quotidien, ljf, Luca, Meta Meta, ppr, Stylix58, Tagada, yalh76) + + -- Alexandre Aubin Sun, 08 Aug 2021 19:27:27 +0200 + +yunohost (4.2.6.1) stable; urgency=low + + - [fix] Remove invaluement from free dnsbl list (71489307) + - [i18n] Remove stale strings (079f6762) + - [i18n] Translations updated for Esperanto, French, Galician, German, Greek + + Thanks to all contributors <3 ! (amirale qt, Christian Wehrli, Éric Gaspar, José M, ljf, ppr, qwerty287) + + -- Alexandre Aubin Sat, 19 Jun 2021 17:18:13 +0200 + +yunohost (4.2.6) stable; urgency=low + + - [fix] metronome/xmpp: deactivate stanza mention optimization / have quick notification in chat group ([#1164](https://github.com/YunoHost/yunohost/pull/1164)) + - [enh] metronome/xmpp: activate module pubsub ([#1170](https://github.com/YunoHost/yunohost/pull/1170)) + - [fix] upgrade: undefined 'apps' variable (923f703e) + - [fix] python3: fix string split in postgresql migration (14d4cec8) + - [fix] python3: python2 was still used in helpers (bd196c87) + - [fix] security: fail2ban rule for yunohost-api login (b837d3da) + - [fix] backup: Apply realpath to find mounted points to unmount ([#1239](https://github.com/YunoHost/yunohost/pull/1239)) + - [mod] dnsmasq: Remove LDN from resolver list (a97fce05) + - [fix] logs: redact borg's passphrase (dbe5e51e, c8d4bbf8) + - [i18n] Translations updated for Galician, German, Italian + - Misc fixes/enh for tests and CI (8a5213c8, e5a03cab, [#1249](https://github.com/YunoHost/yunohost/pull/1249), [#1251](https://github.com/YunoHost/yunohost/pull/1251)) + + Thanks to all contributors <3 ! (Christian Wehrli, Flavio Cristoforetti, Gabriel, José M, Kay0u, ljf, tofbouf, yalh76) + + -- Alexandre Aubin Fri, 11 Jun 2021 20:12:20 +0200 + +yunohost (4.2.5.3) stable; urgency=low + + - [fix] doc, helpers: Helper doc auto-generation job (f2886510) + - [fix] doc: Manpage generation ([#1237](https://github.com/yunohost/yunohost/pull/1237)) + - [fix] misc: Yunohost -> YunoHost ([#1235](https://github.com/yunohost/yunohost/pull/1235)) + - [enh] email: Accept attachment of 25MB instead of 21,8MB ([#1243](https://github.com/yunohost/yunohost/pull/1243)) + - [fix] helpers: echo -n is pointless in ynh_systemd_action ([#1241](https://github.com/yunohost/yunohost/pull/1241)) + - [i18n] Translations updated for Chinese (Simplified), French, Galician, German, Italian + + Thanks to all contributors <3 ! (Éric Gaspar, José M, Kay0u, Leandro Noferini, ljf, Meta Meta, Noo Langoo, qwerty287, yahoo~~) + + -- Alexandre Aubin Wed, 02 Jun 2021 20:20:54 +0200 + +yunohost (4.2.5.2) stable; urgency=low + + - Fix install in chroot ... *again* (806b7acf) + + -- Alexandre Aubin Mon, 24 May 2021 22:11:02 +0200 + +yunohost (4.2.5.1) stable; urgency=low + + - Releasing as stable + + -- Alexandre Aubin Mon, 24 May 2021 19:36:35 +0200 + +yunohost (4.2.5) testing; urgency=low + + - [fix] backup: Also catch tarfile.ReadError as possible archive corruption error (4aaf0154) + - [enh] helpers: Update n to version 7.2.2 ([#1224](https://github.com/yunohost/yunohost/pull/1224)) + - [fix] helpers: Define ynh_node_load_path to be compatible with ynh_replace_vars (06f8c1cc) + - [doc] helpers: Add requirements for new helpers (2b0df6c3) + - [fix] helpers: Set YNH_APP_BASEDIR as an absolute path ([#1229](https://github.com/yunohost/yunohost/pull/1229), 27300282) + - [fix] Tweak yunohost-api systemd config as an attempt to fix the API being down after yunohost upgrades (52e30704) + - [fix] python3: encoding issue in nftable migrations (0f10b91f) + - [fix] python3: Email on certificate renewing failed ([#1227](https://github.com/yunohost/yunohost/pull/1227)) + - [fix] permissions: Remove warnings about legacy permission system (now reported in the linter) ([#1228](https://github.com/yunohost/yunohost/pull/1228)) + - [fix] diagnosis, mail: Remove SPFBL because it triggers false positive ([#1231](https://github.com/yunohost/yunohost/pull/1231)) + - [fix] diagnosis: DNS diagnosis taking an awful amount of time because of timeout ([#1233](https://github.com/yunohost/yunohost/pull/1233)) + - [fix] install: Be able to init slapd in a chroot ([#1230](https://github.com/yunohost/yunohost/pull/1230)) + - [i18n] Translations updated for Catalan, Chinese (Simplified), Czech, French, Galician, German + + Thanks to all contributors <3 ! (Christian Wehrli, Éric Gaspar, José M, ljf, Radek S, Salamandar, Stephan Schneider, xaloc33, yahoo~~) + + -- Alexandre Aubin Mon, 24 May 2021 17:20:47 +0200 + +yunohost (4.2.4) stable; urgency=low + + - python3: smtplib's sendmail miserably crashes with encoding issue if accent in mail body (af567c6f) + - ssh_config: add conf block for sftp apps (51478d14) + - ynh_systemd_action: Fix case where service is already stopped ([#1222](https://github.com/yunohost/yunohost/pull/1222)) + - [i18n] Translations updated for German, Italian, Occitan + - Releasing as stable + + Thanks to all contributors <3 ! (Christian Wehrli, Flavio Cristoforetti, Quentí, yalh76) + + -- Alexandre Aubin Sat, 08 May 2021 15:05:43 +0200 + +yunohost (4.2.3.1) testing; urgency=low + + - [fix] Recreate the admins group which for some reason didnt exist on old setups .. (ee83c3f9) + - [i18n] Translations updated for French + + Thanks to all contributors <3 ! (Éric G., ppr) + + -- Alexandre Aubin Wed, 28 Apr 2021 17:59:14 +0200 + +yunohost (4.2.3) testing; urgency=low + + - Fix a stupid issue where an app's tmp work dir would be deleted during upgrade because of the backup process (50af0393) + - cli ux: Don't suggest that we can remove multiple apps (4ae72cc3) + - ynh_port_available: also check ports used by other apps in settings.yml (381f789f) + - ssh: Add ssh.app, sftp.app groups to cover my_webapp and borg needing ssh access ([#1216](https://github.com/yunohost/yunohost/pull/1216)) + - i18n: Translations updated for German + + Thanks to all contributors <3 ! (Bram, Christian W.) + + -- Alexandre Aubin Mon, 26 Apr 2021 16:29:17 +0200 + +yunohost (4.2.2) testing; urgency=low + + - permissions: Add SFTP / SSH permissions ([#606](https://github.com/yunohost/yunohost/pull/606)) + - refactoring: Uniformize API routes ([#1192](https://github.com/yunohost/yunohost/pull/1192)) + - settings: New setting to disable the 'YunoHost' panel overlay in apps ([#1071](https://github.com/yunohost/yunohost/pull/1071), 08fbfa2e) + - settings: New setting for custom ssh port ([#1209](https://github.com/yunohost/yunohost/pull/1209), 37c0825e, 95999fea) + - security: Redact 'passphrase' settings from logs ([#1206](https://github.com/yunohost/yunohost/pull/1206)) + - security: Sane default permissions for files added using ynh_add_config and ynh_setup_source ([#1188](https://github.com/yunohost/yunohost/pull/1188)) + - backup: Support having .tar / .tar.gz in the archive name arg of backup_info/restore (00ec7b2f) + - backup: Don't backup crons + manage crons from the regenconf ([#1184](https://github.com/yunohost/yunohost/pull/1184)) + - backup: Drop support for archive restore from prior 3.8 ([#1203](https://github.com/yunohost/yunohost/pull/1203)) + - backup: Introduce hooks during restore to apply migrations between archive version and current version ([#1203](https://github.com/yunohost/yunohost/pull/1203)) + - backup: Create a proper operation log for backup_create (fe9f0731) + - backup: Improve error management for app restore ([#1191](https://github.com/yunohost/yunohost/pull/1191)) + - backup: Rework content of system backups ([#1185](https://github.com/yunohost/yunohost/pull/1185)) + - backup: Add a --dry-run option to backup_create to fetch an estimate of the backup size ([#1205](https://github.com/yunohost/yunohost/pull/1205)) + - helpers: Add --keep option to ynh_setup_source to keep files that may be overwritten during upgrade ([#1200](https://github.com/yunohost/yunohost/pull/1200)) + - helpers: Bump 'n' to version 7.1.0 ([#1197](https://github.com/yunohost/yunohost/pull/1197)) + - mail: Support SMTPS Relay ([#1159](https://github.com/yunohost/yunohost/pull/1159)) + - nginx: add header to disallow FLoC ([#1211](https://github.com/yunohost/yunohost/pull/1211)) + - app: Add route to fetch app manifest for custom app installs in a forge-agnostic way ([#1213](https://github.com/yunohost/yunohost/pull/1213)) + - perf: add optional 'apps' argument to user_permission_list to speed up user_info / user_list (e6312db3) + - ux: Add '--human-readable' to recommended command to display diagnosis issues in cli ([#1207](https://github.com/yunohost/yunohost/pull/1207)) + - Misc enh/fixes, code quality (42f8c9dc, 86f22d1b, 1468073f, b33e7c16, d1f0064b, c3754dd6, 02a30125, aabe5f19, ce9f6b3d, d7786662, f9419c96, c92e495b, 0616d632, 92eb9704, [#1190](https://github.com/yunohost/yunohost/pull/1190), [#1201](https://github.com/yunohost/yunohost/pull/1201), [#1210](https://github.com/yunohost/yunohost/pull/1210), [#1214](https://github.com/yunohost/yunohost/pull/1214), [#1215](https://github.com/yunohost/yunohost/pull/1215)) + - i18n: Translations updated for French, German + + Thanks to all contributors <3 ! (axolotle, Bram, cyxae, Daniel, Éric G., grenagit, Josué, Kay0u, lapineige, ljf, Scapharnaum) + + -- Alexandre Aubin Sat, 17 Apr 2021 03:45:49 +0200 + +yunohost (4.2.1.1) testing; urgency=low + + - [fix] services.py, python3: missing decode() in subprocess output fetch (357c151c) + - [fix] log.py: don't inject log_ref if the operation didnt start yet (f878d61f) + - [fix] dyndns.py: Missing raw_msg=True (008e9f1d) + - [fix] firewall.py: Don't miserably crash when there are port ranges (6fd5f7e8) + - [fix] nginx conf: CSP rules for admin was blocking small images used for checkboxes, radio, pacman in the new webadmin (575fab8a) + + -- Alexandre Aubin Sun, 11 Apr 2021 20:15:11 +0200 + +yunohost (4.2.1) testing; urgency=low + + - security: Various permissions tweaks to protect from malicious yunohost users (aefc100a, fc26837a) + + -- Alexandre Aubin Sat, 10 Apr 2021 01:08:04 +0200 + +yunohost (4.2.0) testing; urgency=low + + - [mod] Python2 -> Python3 ([#1116](https://github.com/yunohost/yunohost/pull/1116), a97a9df3, 1387dff4, b53859db, f5ab4443, f9478b93, dc6033c3) + - [mod] refactoring: Drop legacy-way of passing arguments in hook_exec, prevent exposing secrets in command line args ([#1096](https://github.com/yunohost/yunohost/pull/1096)) + - [mod] refactoring: use regen_conf instead of service_regen_conf in settings.py (9c11fd58) + - [mod] refactoring: More consistent local CA management for simpler postinstall ([#1062](https://github.com/yunohost/yunohost/pull/1062)) + - [mod] refactoring: init folders during .deb install instead of regen conf ([#1063](https://github.com/yunohost/yunohost/pull/1063)) + - [mod] refactoring: init ldap before the postinstall ([#1064](https://github.com/yunohost/yunohost/pull/1064)) + - [mod] refactoring: simpler and more consistent logging initialization ([#1119](https://github.com/yunohost/yunohost/pull/1119), 0884a0c1) + - [mod] code-quality: add CI job to auto-format code, fix linter errors ([#1142](https://github.com/yunohost/yunohost/pull/1142), [#1161](https://github.com/yunohost/yunohost/pull/1161), 97f26015, [#1162](https://github.com/yunohost/yunohost/pull/1162)) + - [mod] misc: Prevent the installation of apache2 ... ([#1148](https://github.com/yunohost/yunohost/pull/1148)) + - [mod] misc: Drop old cache rules for .ms files, not relevant anymore ([#1150](https://github.com/yunohost/yunohost/pull/1150)) + - [fix] misc: Abort postinstall if /etc/yunohost/apps ain't empty ([#1147](https://github.com/yunohost/yunohost/pull/1147)) + - [mod] misc: No need for mysql root password anymore ([#912](https://github.com/YunoHost/yunohost/pull/912)) + - [fix] app operations: wait for services to finish reloading (4a19a60b) + - [enh] ux: Improve error semantic such that the webadmin can autoredirect to the proper log view ([#1077](https://github.com/yunohost/yunohost/pull/1077), [#1187](https://github.com/YunoHost/yunohost/pull/1187)) + - [mod] cli/api: Misc command and routes renaming / aliasing ([#1146](https://github.com/yunohost/yunohost/pull/1146)) + - [enh] cli: Add a new "yunohost app search" command ([#1070](https://github.com/yunohost/yunohost/pull/1070)) + - [enh] cli: Add '--remove-apps' (and '--force') options to "yunohost domain remove" ([#1125](https://github.com/yunohost/yunohost/pull/1125)) + - [enh] diagnosis: Report low total space for rootfs ([#1145](https://github.com/yunohost/yunohost/pull/1145)) + - [fix] upnp: Handle port closing ([#1154](https://github.com/yunohost/yunohost/pull/1154)) + - [fix] dyndns: clean old madness, improve update strategy, improve cron management, delete dyndns key upon domain removal ([#1149](https://github.com/yunohost/yunohost/pull/1149)) + - [enh] helpers: Adding composer helper ([#1090](https://github.com/yunohost/yunohost/pull/1090)) + - [enh] helpers: Upgrade n to v7.0.2 ([#1178](https://github.com/yunohost/yunohost/pull/1178)) + - [enh] helpers: Add multimedia helpers and hooks ([#1129](https://github.com/yunohost/yunohost/pull/1129), 47420c62) + - [enh] helpers: Normalize conf template handling for nginx, php-fpm, systemd and fail2ban using ynh_add_config ([#1118](https://github.com/yunohost/yunohost/pull/1118)) + - [fix] helpers, doc: Update template for the new doc (grav) ([#1167](https://github.com/yunohost/yunohost/pull/1167), [#1168](https://github.com/yunohost/yunohost/pull/1168), 59d3e387) + - [enh] helpers: Define YNH_APP_BASEDIR to be able to properly point to conf folder depending on the app script we're running ([#1172](https://github.com/yunohost/yunohost/pull/1172)) + - [enh] helpers: Use jq / output-as json to get info from yunohost commands instead of scraping with grep ([#1160](https://github.com/yunohost/yunohost/pull/1160)) + - [fix] helpers: Misc fixes/enh (b85d959d, db93b82b, ce04570b, 07f8d6d7) + - [fix] helpers: download ynh_setup_source stuff in /var/cache/yunohost to prevent situations where it ends up in /etc/yunohost/apps/ (d98ec6ce) + - [i18n] Translations updated for Catalan, Chinese (Simplified), Czech, Dutch, French, German, Italian, Occitan, Polish + + Thanks to all contributors <3 ! (Bram, Christian W., Daniel, Dave, Éric G., Félix P., Flavio C., Kay0u, Krzysztof N., ljf, Mathieu M., Miloš K., MrMorals, Nils V.Z., penguin321, ppr, Quentí, Radek S, Scapharnaum, Sébastien M., xaloc33, yalh76, Yifei D.) + + -- Alexandre Aubin Thu, 25 Mar 2021 01:00:00 +0100 + +yunohost (4.1.7.4) stable; urgency=low + + - [fix] sec: Enforce permissions for /home/yunohost.backup and .conf (41b5a123) + + -- Alexandre Aubin Thu, 11 Mar 2021 03:08:10 +0100 + +yunohost (4.1.7.3) stable; urgency=low + + - [fix] log: Some secrets were not redacted (0c172cd3) + - [fix] log: For some reason sometimes we were redacting 'empty string' which made everything explode (88b414c8) + - [fix] helpers: Various fixes for ynh_add_config / ynh_replace_vars (a43cd72c, 2728801d, 9bbc3b72, 2402a1db, 6ce02270) + - [fix] helpers: Fix permission helpers doc format (d12f403f) + - [fix] helpers: ynh_systemd_action did not properly clean the 'tail' process when service action failed (05969184) + - [fix] i18n: Translation typo in italian translation ... (bd8644a6) + + Thanks to all contributors <3 ! (Kay0u, yalh76) + + -- Alexandre Aubin Tue, 02 Mar 2021 02:03:35 +0100 + +yunohost (4.1.7.2) stable; urgency=low + + - [fix] When migration legacy protected permissions, all users were allowed on the new perm (29bd3c4a) + - [fix] Mysql is a fucking joke (... trying to fix the mysql issue on RPi ...) (cd4fdb2b) + - [fix] Replace \t when converting legacy conf.json.persistent... (f398f463) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Sun, 21 Feb 2021 05:25:49 +0100 + +yunohost (4.1.7.1) stable; urgency=low + + - [enh] helpers: Fix ynh_exec_as regression (ac38e53a7) + + -- Alexandre Aubin Wed, 03 Feb 2021 16:59:05 +0100 + +yunohost (4.1.7) stable; urgency=low + + - [fix] diagnosis: Handle case where DKIM record is split into several pieces (4b876ff0) + - [fix] i18n: de locale was broken (4725e054) + - [enh] diagnosis: Ignore /dev/loop devices in systemresources (536fd9be) + - [fix] backup: fix a small issue dur to var not existing in some edge case ... (2fc016e3) + - [fix] settings: service_regen_conf is deprecated in favor of regen_conf (62e84d8b) + - [fix] users: If uid is less than 1001, nsswitch ignores it (4e335e07, aef3ee14) + - [enh] misc: fixes/enh in yunoprompt (5ab5c83d, 9fbd1a02) + - [enh] helpers: Add ynh_exec_as (b94ff1c2, 6b2d76dd) + - [fix] helpers: Do not ynh_die if systemctl action fails, to avoid exiting during a remove script (29fe7c31) + - [fix] misc: logger.exception -> logger.error (08e7b42c) + + Thanks to all contributors <3 ! (ericgaspar, Kayou, ljf) + + -- Alexandre Aubin Tue, 02 Feb 2021 04:18:01 +0100 + +yunohost (4.1.6) stable; urgency=low + + - [fix] Make dyndns update more resilient to ns0.yunohost.org being down ([#1140](https://github.com/yunohost/yunohost/pull/1140)) + - [fix] Stupid yolopatch for not-normalized app path settings ([#1141](https://github.com/yunohost/yunohost/pull/1141)) + - [i18n] Update translations for German + + Thanks to all contributors <3 ! (Christian W., Daniel, penguin321) + + -- Alexandre Aubin Wed, 20 Jan 2021 01:46:02 +0100 + +yunohost (4.1.5) stable; urgency=low + + - [fix] Update helpers ([#1136](https://github.com/yunohost/yunohost/pull/11346)) + - [fix] Certificate during regen conf on some setup (1d2b1d9) + - [fix] Empty password is not an error if it's optional ([#1135](https://github.com/yunohost/yunohost/pull/11345)) + - [fix] Remove useless warnings during system backup ([#1138](https://github.com/yunohost/yunohost/pull/11348)) + - [fix] We can now use "true" or "false" for a boolean ([#1134](https://github.com/yunohost/yunohost/pull/1134)) + - [i18n] Translations updated for Catalan, French, Italian, Spanish + + Thanks to all contributors <3 ! (Aleks, Kay0u, Omnia89, jorge-vitrubio, YohannEpitech, xaloc33) + + -- Kayou Thu, 14 Jan 2021 21:23:39 +0100 + +yunohost (4.1.4.4) stable; urgency=low + + - [fix] Add the -F flag to grep command for fixed string mode, prevent special chars in the password to be interpreted as regex pattern ([#1132](https://github.com/yunohost/yunohost/pull/1132)) + - [fix] apt helpers: explicitly return 0, otherwise the return code of last command is used, which in that case is 1 ... (c56883d0) + + Thanks to all contributors <3 ! (Saxodwarf) + + -- Alexandre Aubin Mon, 11 Jan 2021 14:17:37 +0100 + +yunohost (4.1.4.3) stable; urgency=low + + - [fix] ynh_replace_vars in case var is defined but empty (30dde208) + + -- Alexandre Aubin Sun, 10 Jan 2021 01:58:35 +0100 + +yunohost (4.1.4.2) stable; urgency=low + + - [fix] Prevent info from being redacted (because of foobar_key=) by the logging system (8f1b05f3) + - [fix] For some reason sometimes submetadata is None ... (00508c96) + - [enh] Reduce the noise in logs because of ynh_app_setting (ac4b62ce) + + -- Alexandre Aubin Sat, 09 Jan 2021 18:59:01 +0100 + +yunohost (4.1.4.1) stable; urgency=low + + - [hotfix] Postfix conf always included the relay snippets (b25cde0b) + + -- Alexandre Aubin Fri, 08 Jan 2021 16:21:07 +0100 + +yunohost (4.1.4) stable; urgency=low + + - [fix] firewall: force source port for UPnP. ([#1109](https://github.com/yunohost/yunohost/pull/1109)) + - Stable release + + Thanks to all contributors <3 ! (Léo Le Bouter) + + -- Alexandre Aubin Fri, 08 Jan 2021 03:09:14 +0100 + +yunohost (4.1.3) testing; urgency=low + + - [enh] Do not advertise upgrades for bad-quality apps ([#1066](https://github.com/yunohost/yunohost/pull/1066)) + - [enh] Display domain_path of app in the output of app list ([#1120](https://github.com/yunohost/yunohost/pull/1120)) + - [enh] Diagnosis: report usage of backports repository in apt's sources.list ([#1069](https://github.com/yunohost/yunohost/pull/1069)) + - [mod] Code cleanup, misc fixes (165d2b32, [#1121](https://github.com/yunohost/yunohost/pull/1121), [#1122](https://github.com/yunohost/yunohost/pull/1122), [#1123](https://github.com/yunohost/yunohost/pull/1123), [#1131](https://github.com/yunohost/yunohost/pull/1131)) + - [mod] Also display app label on remove_domain with apps ([#1124](https://github.com/yunohost/yunohost/pull/1124)) + - [enh] Be able to change user password in CLI without writing it in clear ([#1075](https://github.com/YunoHost/yunohost/pull/1075)) + - [enh] New permissions helpers ([#1117](https://github.com/yunohost/yunohost/pull/1117)) + - [i18n] Translations updated for French, German + + Thanks to all contributors <3 ! (C. Wehrli, cricriiiiii, Kay0u, Bram, ljf, ppr) + + -- Alexandre Aubin Thu, 07 Jan 2021 00:46:09 +0100 + +yunohost (4.1.2) testing; urgency=low + + - [enh] diagnosis: Detect moar hardware name (b685a274) + - [fix] permissions: Handle regexes that may start with ^ or \ (bdff5937) + - [fix] permissions: Tile/protect status for legacy migration ([#1113](https://github.com/yunohost/yunohost/pull/1113)) + - [fix] domain: double return prevent new code from working (0c977d8c) + - [fix] settings: When encountering unknown setting, also save the regular setting so we don't re-encounter the unknown settings everytime (d77d5afb) + - [fix] users: only ask for one letter for first/last name ([#1114](https://github.com/yunohost/yunohost/pull/1114)) + - [fix] apt/sury: Tweak app helpers to not mess with Sury's pinning ([#1110](https://github.com/yunohost/yunohost/pull/1110)) + - [i18n] Translations updated for German + + Thanks to all contributors <3 ! (Bram, C. Wehrli, Kayou) + + -- Alexandre Aubin Thu, 31 Dec 2020 16:26:51 +0100 + +yunohost (4.1.1) testing; urgency=low + + - [fix] Backup/restore DKIM keys ([#1098](https://github.com/yunohost/yunohost/pull/1098), [#1100](https://github.com/yunohost/yunohost/pull/1100)) + - [fix] Backup/restore Dyndns keys ([#1101](https://github.com/yunohost/yunohost/pull/1101)) + - [fix] mail: Add a max limit to number of recipients ([#1094](https://github.com/yunohost/yunohost/pull/1094)) + - [fix] mail: Do not enforce encryption for relays .. some don't support it ... (11fe9d7e) + - [i18n] Translations updated for French, German, Italian, Occitan + + Misc small fixes: + + - [fix] misc: Prevent running `yunohost domain dns-conf` on arbirary domains ([#1099](https://github.com/yunohost/yunohost/pull/1099)) + - [enh] misc: We don't care that 'apt-key output should not be parsed' (5422a49d) + - [fix] dnsmasq: Avoid to define wildcard records locally ([#1102](https://github.com/yunohost/yunohost/pull/1102)) + - [fix] ssowat: Fix indent ([#1103](https://github.com/yunohost/yunohost/pull/1103)) + - [fix] nginx: Force-disable gzip for acme-challenge (c5d06af2) + - [enh] app helpers: Handle change php version ([#1107](https://github.com/yunohost/yunohost/pull/1107)) + - [fix] permissions: Misc fixes ([#1104](https://github.com/yunohost/yunohost/pull/1104), [#1105](https://github.com/yunohost/yunohost/pull/1105)) + - [fix] certificates: Use organization name to check if from Lets Encrypt ([#1093](https://github.com/yunohost/yunohost/pull/1093)) + - [enh] ldap: Increase ldap search size limit? ([#1074](https://github.com/yunohost/yunohost/pull/1074)) + - [fix] app helpers: Avoid unecessarily reloading php7.3 too fast ([#1108](https://github.com/yunohost/yunohost/pull/1108)) + - [fix] log: Fix a small issue where metadata could be None (because of empty yaml maybe?) (f9143d53) + + Thanks to all contributors <3 ! (Christian Wehrli, Eric COURTEAU, Flavio Cristoforetti, Kay0u, Kayou, ljf, ljf (zamentur), Quentí) + + -- Alexandre Aubin Sat, 19 Dec 2020 01:33:36 +0100 + +yunohost (4.1.0) testing; urgency=low + + - [enh] Extends permissions features, improve legacy settings handling (YunoHost#861) + - [enh] During app installs, add a default answer for user-type questions (YunoHost#982) + - [enh] Default questions for common app manifest arguments (YunoHost#981) + - [enh] Only upgrade apps if version actually changed (YunoHost#864) + - [enh] Create uncompressed backup archives by default (instead of .tar.gz) (YunoHost#1020) + - [enh] Add possibility to download backups (YunoHost#1046) + - [enh] Asking an email address during user creation was confusing, now define it a username@domain by default (admin only chooses the domain) (YunoHost#962) + - [enh] Be able to configure an smtp relay (YunoHost#773) + - [enh] Add a diagnosis to detect processes rencently killed by oom_reaper (YunoHost/f5acbffb) + - [enh] Simplify operation log list (YunoHost#955) + - [enh] Smarter sorting of domain list (YunoHost#860) + - [fix] Accept '+' sign in mail forward adresses (YunoHost#818) + - [enh] Add x509 fingerprint in /etc/issue (YunoHost#1056) + - [enh] Add ynh_add_config helper (YunoHost#1055) + - [enh] Upgrade n version (YunoHost#1073) + - [enh] Clean /usr/bin/yunohost, make it easier to use yunohost as a python lib (YunoHost#922) + - [enh] Lazy loading of smtplib to reduce memory footprint a bit (0f2e9ab1) + - [enh] Refactor manifest arguments parsing (YunoHost#1013) + - [enh] Detect misformated arguments in getopts (YunoHost#1052) + - [enh] Refactor app download process, make it github-independent (YunoHost#1049) + - [fix] Test at the beginning of postinstall that iptables is working instead of miserably crashing later (YunoHost/f73ae4ee) + - [enh] Service logs: journalctl -x in fact makes everything bloated, the supposedly additional info it displays does not contains anything relevant... (YunoHost/452b178d) + - [enh] Add redis hook to enforce permissions on /var/log/redis (YunoHost/a1c1057a) + - [enh] Add configuration tests for dnsmasq, fail2ban, slapd (YunoHost/6e69df37) + - [enh] Remove some old fail2ban jails that do not exists anymore (YunoHost/2c6736df) + - [enh] Get rid of yunohost.local in main domain nginx conf (YunoHost/ba884d5b) + - [enh] Ignore some unimportant apt warnings (YunoHost/199cc50) + - [enh] Create the helper doc on new version (YunoHost#1080) + - [enh] The email "abuse@you_domain.tld" is now unavailable for security reason (YunoHost/67e03e6) + - [enh] Remove some warnings during backup (YunoHost#1047) + - [i18n] Translations updated for Catalan, Chinese (Simplified), French, German, Italian, Occitan, Portuguese + + Thanks to all contributors <3 ! (Aleks, Augustin T., Baptiste W., Bram, Christian W., Colin W., cyxae, ekhae, Éric G., Félix P., Josué, Julien J., Kayou, Leandro N., ljf, Maniack C, ppr, Quentí, Quentin D., SiM, yalh76, Yifei D., xaloc33) + + -- Kay0u Thu, 03 Dec 2020 16:34:38 +0100 + +yunohost (4.0.8.3) stable; urgency=low + + - [fix] Certificate renewal for LE (#1092) + + Thanks to all contributors <3 ! (frju365) + + -- Kay0u Thu, 03 Dec 2020 14:01:03 +0000 + +yunohost (4.0.8.2) stable; urgency=low + + - [fix] intermediate_certificate is now included in signed certificate (#1067) + + Thanks to all contributors <3 ! (Bram) + + -- Alexandre Aubin Wed, 04 Nov 2020 23:32:16 +0100 + +yunohost (4.0.8.1) stable; urgency=low + + - [fix] App installs logs were still disclosing secrets when shared sometimes ... + + -- Alexandre Aubin Wed, 04 Nov 2020 17:24:52 +0100 + +yunohost (4.0.8) stable; urgency=low + + - [fix] Diagnose ssl libs installed from sury (#1053) + - [enh] Better problematic apt dependencies auto-investigation mechanism (#1051, 8d4f36e1) + - [fix] Force locale to C during postgresql migration to avoid some stupid issue related to locale (d532cd5e) + - [fix] Use php7.3 by default in CLI (82c0cc92) + - [fix] Typo in fpm_config helper led to install process hanging forever (7dcf4b00) + + Thanks to all contributors <3 ! (Kayou) + + -- Alexandre Aubin Wed, 16 Sep 2020 16:23:04 +0200 + +yunohost (4.0.7.1) stable; urgency=low + + - Forbid users from using SSH as a VPN (even if SSH login is disabled) (#1050) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Fri, 11 Sep 2020 21:06:09 +0200 + +yunohost (4.0.7) stable; urgency=low + + - [fix] Require explicitly php7.3-foo packages because in some cases Sury's php7.4- packages are installed and php7.3-fpm doesn't get installed ... (1288159a) + - [fix] Make sure app nginx confs do not prevent the loading of /yunohost/sso (#1044) + + Thanks to all contributors <3 ! (Kayou, ljf) + + -- Alexandre Aubin Fri, 04 Sep 2020 14:32:07 +0200 + +yunohost (4.0.6.1) stable; urgency=low + + - [fix] Stupid syntax issue in dovecot conf + + -- Alexandre Aubin Tue, 01 Sep 2020 02:00:19 +0200 + +yunohost (4.0.6) stable; urgency=low + + - [mod] Add apt conf regen hook to manage sury pinning policy (#1041) + - [fix] Use proper templating + handle xmpp-upload.domain.tld in dnsmasq conf (bc7344b6, 503e08b5) + - [fix] Explicitly require php-fpm >= 7.3 ... (41813744) + - [i18n] Translations updated for Catalan, French, German + + Thanks to all contributors <3 ! (Christian W., Titus PiJean, xaloc33) + + -- Alexandre Aubin Mon, 31 Aug 2020 19:57:24 +0200 + +yunohost (4.0.5) testing; urgency=low + + - [enh] Update postfix, dovecot, nginx configuration according to Mozilla guidelines (Buster + DH params) (f3a4334a, 89bcf1ba, 2d661737) + - [enh] Update acme_tiny to 4.1.0 (#1037) + - [fix] ref to variable in i18n string (c.f. issue 1647) (7b1f02e0) + - [fix] Recursively enforce ownership for rspamd (8454f2ec) + - [fix] Stupid encoding issue when fetching service description (6ec0e7b6) + - [fix] Misc fixes for CI (ca0a42f2, 485c65a9, #1038, a891d20a) + + Thanks to all contributors <3 ! (Eric G., Kay0u) + + -- Alexandre Aubin Tue, 25 Aug 2020 19:32:27 +0200 + +yunohost (4.0.4) stable; urgency=low + + - Debugging and robustness improvements for postgresql 9.6 -> 11 and xtables->nftables migrations (accc2da4, 59bd7d66, 4cb6f7fd, 4b14402c) + + -- Alexandre Aubin Wed, 12 Aug 2020 18:14:00 +0200 + +yunohost (4.0.3) stable; urgency=low + + - Bump version number for stable release + + -- Alexandre Aubin Wed, 29 Jul 2020 17:00:00 +0200 + +yunohost (4.0.2~beta) testing; urgency=low + + - [mod] Rebase on stretch-unstable to include recent changes + - [fix] Create admin's home during postinstall (#1021) + + Thanks to all contributors <3 ! (Kay0u) + + -- Alexandre Aubin Fri, 19 Jun 2020 15:16:26 +0200 + +yunohost (4.0.1~alpha) testing; urgency=low + + - [fix] It just make no sense to backup/restore the mysql password... (#911) + - [fix] Fix getopts and helpers (#885, #886) + - [fix] Explicitly create home using mkhomedir_helper instead of obscure pam rule that doesn't work anymore (b67ff314) + - [fix] Ldap interface seems to expect lists everywhere now? (fb8c2b7b) + - [deb] Clean control file, remove some legacy Conflicts and Replaces (ca0d4933) + - [deb] Add conflicts with versions from backports for critical dependencies (#967) + - [cleanup] Stale / legacy code (217aaa36, d77da6a0, af047468, 82d468a3) + - [conf] Automatically disable/stop systemd-resolved that conflicts with dnsmasq on fresh setups ... (e7214b37) + - [conf] Remove deprecated option in sshd conf, c.f. https://patchwork.openembedded.org/patch/139981/ (2723d245) + - [conf] Small tweak in dovecot conf (deprecated settings) (dc0481e2) + - [conf] Update nslcd and nsswitch stuff using new Buster's default configs + get rid of nslcd service, only keep the regen-conf part (6ef3520f) + - [php] Migrate from php7.0 to php7.3 (3374e653, 9be10506, dd9564d3, 9679c291, 212a15e4, 25fcaa19, c4ad66f5) + - [psql] Migrate from psql 11 to 9.6 (e88aed72, 4920d4f9, c70b0ae4) + - [firewall] Migrate from xtable to nftable (05fb58f2, 2c4a8b73, 625d5372) + - [slapd] Rework slapd regenconf to use new backend (#984) + + Thanks to all contributors <3 ! (Étienne M., Josué, Kay0u) + + -- Alexandre Aubin Fri, 05 Jun 2020 03:10:09 +0200 + +yunohost (3.8.5.5) stable; urgency=low + + - [enh] Allow to extend the nginx default_server configuration (f1bfc521) + - [mod] Move redirect to /yunohost/admin to a separate nginx conf file to allow customizing it more easily (ac9182d6) + - [enh] Make sure to validate/upgrade that we don't have any active weak certificate used by nginx at the beginning of the buster migration, otherwise nginx will later miserably fail to start (d4358897) + - [fix] get_files_diff crashing if {orig,new}_file is None (7bfe564a) + - [enh] Remove some useless message about file that "wasn't deleted because it doesn't exist." (#1024) + - [mod] Remove useless robot protection code (#1026) + - [fix] Let's not redefine the value for the 'service' var ... (1a2f26dc) + - [fix] More general stretch->buster patching for sources.list (#1028) + - [mod] Tweak custom disclaimer about the migration still being a bit touchy in preparation for stable release (852dea07) + - [mod] Typo/wording in en.json (#1030) + - [i18n] Translations updated for Catalan, French, Italian, Occitan + + Thanks to all contributors <3 ! (É. Gaspar, Kay0u, L. Noferini, ppr, Quentí, xaloc33) + + -- Alexandre Aubin Mon, 27 Jul 2020 19:03:33 +0200 + +yunohost (3.8.5.4) testing; urgency=low + + - [fix] Fix unscd version parsing *again* + - [fix] Enforce permissions on rspamd log directory + - [enh] Ignore stupid warnings about sudo-ldap that is already provided + + -- Alexandre Aubin Sun, 21 Jun 2020 23:37:09 +0200 + +yunohost (3.8.5.3) testing; urgency=low + + - [fix] Fix the fix about unscd downgrade :/ + + -- Alexandre Aubin Fri, 19 Jun 2020 18:50:58 +0200 + +yunohost (3.8.5.2) testing; urgency=low + + - [fix] Small issue with unscd upgrade/downgrade ... new version ain't always 0.53.1, so find it using dirty scrapping + + -- Alexandre Aubin Thu, 18 Jun 2020 16:19:35 +0200 + +yunohost (3.8.5.1) testing; urgency=low + + - [fix] Update Stretch->Buster migration disclaimer to make it clear that this is alpha-stage + + -- Alexandre Aubin Sat, 06 Jun 2020 03:30:00 +0200 + +yunohost (3.8.5) testing; urgency=low + + - [enh] Add migration procedure for Stretch->Buster (a2b83c0f, a26411db, 9f1211e9, e544bf3e, a0511cca) + - [fix] Disable/skip ntp when inside a container (9d0c0924) + + -- Alexandre Aubin Sat, 06 Jun 2020 02:11:51 +0200 + +yunohost (3.8.4.9) stable; urgency=low + + - [fix] Force lowercase on domain names (804f4b3e) + - [fix] Add dirmngr to Depends:, needed for apt-key / gpg (cd115ed8) + - [fix] Improve debugging when diagnosis ain't happy when renewing certs (0f0194be) + - [enh] Add yunohost version to logs metadata (d615546b) + - [enh] Alway filter irrelevant log lines when sharing it (38704cba, 51d53be5) + - [fix] Regen-conf outputing many 'forget-about-it' because of files flagged as to be removed (f4525488) + - [fix] postfix per-domain destination concurrency (#988) + - [fix] Call regenconf for ssh before the general regenconf during the postinstall to avoid an irrelevant warning (7805837b) + - [i18n] Translations updated for Catalan, French, German + + Thanks to all contributors <3 ! (taziden, ljf, ppr, xaloc33, Yasss Gurl) + + -- Alexandre Aubin Thu, 18 Jun 2020 15:13:01 +0200 + +yunohost (3.8.4.8) stable; urgency=low + + - [fix] Don't add unprotected_urls if it's already in skipped_urls (#1005) + - [enh] Add pre-defined DHE group and set up Nginx to use it (#1007) + - [fix] Make sure to propagate change in slapd systemd conf during initial install (2d42480f) + - [fix] More accurate grep to avoid mistakenly grepping commented lines... (2408a620) + - [enh] Update n to 6.5.1 (#1012) + - [fix] Set sury default pinning to 600 (653c5fde) + - [enh] Clean stale file/hashes in regen-conf (#1009) + - [fix] Weirdness in regen-conf mechanism for SSH conf (#1014) + + Thanks to all contributors <3 ! (É. Gaspar, Josué, SohKa) + + -- Alexandre Aubin Sat, 06 Jun 2020 01:59:08 +0200 + +yunohost (3.8.4.7) stable; urgency=low + + - [fix] Remove some remains of glances (17eec25e) + - [fix] Force external resolution for reverse DNS dig (852cd14c) + - [fix] Make sure mysql is an alias to mariadb (e24191ce, ca89607d) + - [fix] Path for ynh_add_fpm_config template in restore (#1001) + - [fix] Add -o Acquire::Retries=3 to fix some stupid network issues happening sometimes with apt (03432349) + - [fix] ynh_setup_source: Retry wget on non-critical failures to try to avoid tmp dns issues (3d66eaec) + - [fix] ynh_setup_source: Calling ynh_print_err in case of error didn't work, and we probably want a ynh_die here (55036fad) + - [i18n] Translations updated for Catalan, French, Italian, Occitan + + Thanks to all contributors <3 ! (JimboJoe, Leandro N., ppr, Quentí, xaloc33, yalh76) + + -- Alexandre Aubin Thu, 04 Jun 2020 02:28:33 +0200 + +yunohost (3.8.4.6) stable; urgency=low + + - [fix] Bump server_names_hash_bucket_size to 128 to avoid nginx exploding for stupid reasons (b3db4d92) + - [fix] More sensible strategy for sury pinning (#1006) + - [fix] Stop trying to fetch log categories that are not implemented yet T.T (77bd9ae3) + - [enh] Add logging and persistent as default config for new muc room (#1008) + - [tests] Moar tests for app args parsing (#1004) + + Thanks to all contributors <3 ! (Gabriel, Kay0u, Bram) + + -- Alexandre Aubin Thu, 28 May 2020 00:22:10 +0200 + +yunohost (3.8.4.5) stable; urgency=low + + - [enh] Tell systemctl to stfu about creating symlinks when enabling/disabling services (6637c8a8) + - [enh] Add maindomain in diagnosis email subject (e30e25fa) + - [fix] Webpath should also be normalized for args_list, so that we can get rid of the 'malformed path' check of the CI... (58ce6e5e) + - [fix] Increase time window for auto diagnosis cron to avoid remote diagnosis server overload (dc221495) + - [fix] encoding bullshit (4c600125, 64596bc1) + - [fix] Typo in diagnosis message + fix FR translation report format of bad DNS conf (#1002, b8f8ea14) + - [fix] Flag old etckeeper.conf as 'should not exist' in regenconf (5a3b382f) + - [enh] Detect dyndns-domains managed by yunohost and advice to use yunohost dyndns update --force (8b169f13) + - [enh] Complain if apps savagely edit system configurations during install and upgrade (a23f02db) + - [i18n] Translations updated for Arabic, Catalan, French, German, Italian + - [tests] CI V2 : Rework CI workflow (#991) + + Thanks to all contributors <3 ! (ButterflyOfFire, Kay0u, L. Noferini, rynas, V. Rubiolo, xaloc33, Yasss Gurl) + + -- Alexandre Aubin Tue, 26 May 2020 03:20:39 +0200 + +yunohost (3.8.4.4) stable; urgency=low + + - [fix] Crash when the services file is empty (85f1802) + - [fix] IPv6 detection when using wg-quick (#997) + - [fix] Use a .get() to avoid crash if key doesn't exist (1f1b2338) + - [enh] Don't display the hostname when calling journalctl, this takes horizontal space for nothing (2bcfb5a1) + - [fix] Add --quiet, otherwise getopts is confused by "-- Logs" at the beginning (bdbf1822) + - [mod] We don't need those color codes... and warnings are already warnings... (2a631fa2) + - [fix] psql_setup_db: Do not create a new password if the user already exists (#998) + - [enh] Add an exception if packaging format is not recognized (f0cc6798) + + Thanks to all contributors <3 ! (Aleks, Julien Rabier, Kayou) + + -- Kay0u Fri, 22 May 2020 19:26:05 +0000 + +yunohost (3.8.4.3) stable; urgency=low + + - [fix] Workaround for the sury pinning issues when installing dependencies + - [i18n] Translations updated for Catalan, French, Occitan + + Thanks to all contributors <3 ! (Aleks, clecle226, Kay0u, ppr, Quenti) + + -- Kay0u Wed, 20 May 2020 18:41:49 +0000 + +yunohost (3.8.4.2) testing; urgency=low + + - [enh] During failed upgrades: Only mention packages that couldn't be upgraded (26fcfed7) + - [enh] Also run dpkg --audit to check if dpkg is in a broken state (09d8500f, 97199d19) + - [enh] Improve logs readability (c6f18496, 9cbd368d, 5850bf61, 413778d2, 5c8c07b8, f73c34bf, 94ea8265) + - [enh] Crash early about apps already installed when attempting to restore (f9e4c96c) + - [fix] Add the damn short hostname to /etc/hosts automagically (c.f. rabbitmq-server) (e67dc791) + - [fix] Don't miserably crash if doveadm fails to run (c9b22138) + - [fix] Diagnosis: Try to not have weird warnings if no diagnosis ran yet... (65c87d55) + - [fix] Diagnosis: Change logic of --email to avoid sending empty mail if some issues are found but ignored (4cd4938e) + - [enh] Diagnosis/services: Report the service status as warning/unknown if service type is oneshot and status exited (dd09758f, 1cd7ffea) + - [fix] Rework ynh_psql_test_if_first_run (#993) + - [tests] Tests for args parsing (#989, 108a3ca4) + + Thanks to all contributors <3 ! (Bram, Kayou) + + -- Alexandre Aubin Tue, 19 May 2020 20:08:47 +0200 + +yunohost (3.8.4.1) testing; urgency=low + + - [mod] Tweak diagnosis threshold for swap warning (429df8c4) + - [fix] Make sure we have a list for log_list + make sure item is in list before using .remove()... (afbeb145, 43facfd5) + - [fix] Sometimes tree-model has a weird \x00 which breaks yunopaste (c346f5f1) + + -- Alexandre Aubin Mon, 11 May 2020 00:50:34 +0200 + +yunohost (3.8.4) testing; urgency=low + + - [fix] Restoration of custom hooks / missing restore hooks (#927) + - [enh] Real CSP headers for the webadmin (#961) + - [enh] Simplify / optimize reading version of yunohost packages... (#968) + - [fix] handle new auto restart of ldap in moulinette (#975) + - [enh] service.py cleanup + add tests for services (#979, #986) + - [fix] Enforce permissions for stuff in /etc/yunohost/ (#963) + - [mod] Remove security diagnosis category for now, Move meltdown check to base system (a799740a) + - [mod] Change warning/errors about swap as info instead ... add a tip about the fact that having swap on SD or SSD is dangerous (23147161) + - [enh] Improve auto diagnosis cron UX, add a --human-readable option to diagnosis_show() (aecbb14a) + - [enh] Rely on new diagnosis for letsencrypt elligibility (#985) + - [i18n] Translations updated for Catalan, Esperanto, French, Spanish + + Thanks to all contributors <3 ! (amirale qt, autra, Bram, clecle226, I. Hernández, Kay0u, xaloc33) + + -- Alexandre Aubin Sat, 09 May 2020 21:20:00 +0200 + +yunohost (3.8.3) testing; urgency=low + + - [fix] Remove dot in reverse DNS check + - [fix] Upgrade of multi-instance apps was broken (#976) + - [fix] Check was broken if an apps with no domain setting was installed (#978) + - [enh] Add a timeout to wget (#972) + - [fix] ynh_get_ram: Enforce choosing --free or --total (#972) + - [fix] Simplify / improve robustness of backup list + - [enh] Make nodejs helpers easier to use (#939) + - [fix] Misc tweak for disk usage diagnosis, some values were inconsistent / bad UX / ... + - [enh] Assert slapd is running to avoid miserably crashing with weird ldap errors + - [enh] Try to show smarter / more useful logs by filtering irrelevant lines like set +x etc + - Technical tweaks for metronome 3.14.0 support + - Misc improvements for tests and linters + + Thanks to all contributors <3 ! (Bram, Kay0u, Maniack C., ljf, Maranda) + + -- Alexandre Aubin Thu, 07 Apr 2020 04:00:00 +0000 + +yunohost (3.8.2.2) testing; urgency=low + + Aleks broke everything /again/ *.* + + -- Alexandre Aubin Thu, 30 Apr 2020 18:05:00 +0000 + +yunohost (3.8.2.1) testing; urgency=low + + - [fix] Make sure DNS queries are dong using absolute names to avoid stupid issues + - [fix] More reliable way to fetch PTR record / reverse DNS + - [fix] Propagate IPv6 default route check to ip diagnoser code as well + + Thanks to ljf for the tests and fixes ! + + -- Alexandre Aubin Thu, 30 Apr 2020 17:30:00 +0000 + +yunohost (3.8.2) testing; urgency=low + + ### Diagnosis + + - [fix] Some DNS queries triggered false negatives about CNAME/A record and email blacklisting (#943) + - [enh] Add a check about domain expiration (#944) + - [enh] Dirty hack to automatically find custom SSH port and diagnose it instead of 22 (b78d722) + - [enh] Add a tip / explanation when IPv6 ain't working / available (426d938) + - [fix] Small false-negative about not having IPv6 when it's actually working (822c731) + + ### Helpers + + - [fix] When setting up a new db, corresponding user should be declared as owner (#813) + - [enh] Add dynamic variables to systemd helper (#937) + - [enh] Clean helpers (#947) + - [fix] getopts behaved in weird way when fed empty parameters (#948) + - [fix] Use ynh_port_available in ynh_find_port (#957) + + ### Others + + - [enh] Setup all XMPP components for each "parent" domains (#916) + - [fix] Previous change in Postfix ciphers broke TLS (#949) + - [fix] Update ACME snippet detection following previous change (#950) + - [fix] Trying to install apps with unpatchable legacy helpers was breaking stuff (#954) + - [fix] Patch usage of old 'yunohost tools diagnosis' (#954) + - [enh] Misc optimizations to speed up regen-conf and other things (#958) + - [enh] When sharing logs, also anonymize folder name containing %2e instead of dot (b392efd) + - [enh] Keep track of yunohost version a backup was made from (54cc684) + - [fix] Re-add 'app fetchlist', 'app list -i', 'app list' filter for backward compatibility... (69938c3) + - [i18n] Improve translations for Catalan, German, French, Esperanto, Spanish, Greek, Nepali, Occitan + + Thanks to all contributors <3 ! (Bram, C. Wehrli, Kay0u, Maniack C., Quentí, Zeik0s, amirale qt, ljf, pitchum, tituspijean, xaloc33, Éric G.) + + -- Alexandre Aubin Wed, 29 Apr 2020 23:15:00 +0000 + +yunohost (3.8.1.1) testing; urgency=low + + - [fix] Stupid issue about path in debian/install ... + + -- Alexandre Aubin Sun, 19 Apr 2020 07:04:00 +0000 + +yunohost (3.8.1) testing; urgency=low + + ## Helpers (PHP, apt) + + - New helpers for extra apt repo, PHP version install, and PHP fpm (#881, #928, #929) + - Pave the way to migration to php7.3 and future ones (#880, #926) + - Option in PHP helper to use a dedicated php service (#915) + + ## Diagnosis + + - Many improvements in diagnosis mechanism (#923, #921, #940) + + ## Misc fixes, improvements + - custom_portal and custom_overlay redirect (#925) + - Improve systemd settings for slapd (#933) + - Spelling and typo corrections (#931) + - Improve translations for French, German, Catalan + + Thanks to all contributors <3 ! (Kay0u, Maniack Crudelis, ljf, E.Gaspar, + xaloc33) + + -- Alexandre Aubin Sun, 19 Apr 2020 06:20:00 +0000 + +yunohost (3.8.0) testing; urgency=low + + # Major stuff + + - [enh] New diagnosis system (#534, #872, #919, a416044, a354425, 4ab3653, decb372, e686dc6, b5d18d6, 69bc124, 937d339, cc2288c, aaa9805, 526a3a2) + - [enh] App categories (#778, #853) + - [enh] Support XMPP http upload (#831) + - [enh] Many small improvements in the way we manage services (#838, fa5c0e9, dd92a34, c97a839) + - [enh] Add subcategories management in bash completion (#839) + - [mod] Add conflict with apache2 and bind9, other minor changes in Depends (#909, 3bd6a7a, 0a482fd) + - [enh] Setting to enable POP3 in email stack (#791) + - [enh] Better UX for CLI/API to change maindomain (#796) + + # Misc technical + + - Update ciphers for nginx, postfix and dovecot according to new Mozilla recommendation (#913, #914) + - Get rid of domain-specific acme-challenge snippet, use a single snippet included in every conf (#917) + - [enh] Persist cookies between multiple ynh_local_curl calls for the same app (#884, #903) + - [fix] ynh_find_port didn't detect port already used on UDP (#827, #907) + - [fix] prevent firefox to mix CA and server certificate (#857) + - [enh] add operation logger for config panel (#869) + - [fix] psql helpers: Revoke sessions before dropping tables (#895) + - [fix] moulinette logs were never displayed #lol (#758) + + # Tests, cleaning, refactoring + + - Add core CI, improve/fix tests (#856, #863, 6eb8efb, c4590ab, 711cc35, 6c24755) + - Refactoring (#805, 101d3be, #784) + - Drop some very-old deprecated app helpers (though still somewhat supporting them through hacky patching) (#780) + - Drop glances and the old monitoring system (#821) + - Drop app_debug (#824) + - Drop app's status.json (#834) + - Drop ynh_add_skipped/(un)protected_uris helpers (#910) + - Use a common security.conf.inc instead of having cipher setting in each nginx's domain file (1285776, 4d99cbe, be8427d, 22b9565) + - Don't add weird tmp redirected_urls after postinstall (#902) + - Don't do weird stuff with yunohost-firewall during debian's postinst (978d9d5) + + # i18n, messaging + + - Unit tests / lint / cleaning for translation files (#901) + - Improve message wording, spelling (8b0c9e5, 9fe43b1, f69ab4c, 0decb64, 986f38f, 8d40c73, 8fe343a, 1d84f17) + - Improve translations for French, Catalan, Bengali (Bangladesh), Italian, Dutch, Norwegian Bokmål, Chinese, Occitan, Spanish, Esperanto, German, Nepali, Portuguese, Arabic, Russian, Hungarian, Hindi, Polish, Greek + + Thanks to all contributors <3 ! (Aeris One, Aleks, Allan N., Alvaro, Armando F., Arthur L., Augustin T., Bram, ButterflyOfFire, Damien P., Gustavo M., Jeroen F., Jimmy M., Josué, Kay0u, Maniack Crudelis, Mario, Matthew D., Mélanie C., Patrick B., Quentí, Yasss Gurl, amirale qt, Elie G., ljf, pitchum, Romain R., tituspijean, xaloc33, yalh76) + + -- Kay0u Thu, 09 Apr 2020 19:59:18 +0000 + +yunohost (3.7.1.3) stable; urgency=low + + - [fix] Fix the hotfix about trailing slash, it was breaking access to app installed on domain root.. + + -- Alexandre Aubin Thu, 28 Apr 2020 19:00:00 +0000 + +yunohost (3.7.1.2) stable; urgency=low + + - [fix] Be more robust against some situation where some archives are corrupted + - [fix] Make nginx regen-conf more robust against broken config or service failing to start, show info to help debugging + - [fix] Force-flush the regen-conf for nginx domain conf when adding/removing a domain... + - [fix] app_map : Make sure to return / and not empty string for stuff on domain root + - [fix] Improve ynh_systemd_action to wait for fail2ban to reload + - [fix] Improper use of logger.exception in app.py leading to infamous weird "KeyError: label" + + -- Alexandre Aubin Mon, 27 Apr 2020 23:50:00 +0000 + +yunohost (3.7.1.1) stable; urgency=low + + - [fix] lxc uid number is limited to 65536 by default (0c9a4509) + - [fix] also invalidate group cache when creating users (aaabf8c7) + - [fix] Make sure to have a path that include sbin for stupid cron jobs (f03bb82a) + + -- Alexandre Aubin Sun, 12 Apr 2020 23:15:00 +0000 + +yunohost (3.7.1) stable; urgency=low + + - [enh] Add ynh_permission_has_user helper (#905) + - [mod] Change behavior of ynh_setting_delete to try to make migrating away from legacy permissions easier (#906) + - [fix] app_config_apply should also return 'app' info (#918) + - [fix] uid/gid conflicts in user_create because of inconsistent comparison (#924) + - [fix] Ensure metronome owns its directories (1f623830, 031f8a6e) + - [mod] Remove useless sudos in helpers (be88a283) + - [enh] Improve message wording for services (3c844292) + - [enh] Attempt to anonymize data pasted to paste.yunohost.org (f56f4724) + - [enh] Lazy load yunohost.certificate to possibly improve perfs (af8981e4) + - [fix] Improve logging / debugging (1eef9b67, 7d323814, d17fcaf9, 210d5f3f) + + Thanks to all contributors <3 ! (Bram, Kay0u, Maniack, Matthew D.) + + -- Alexandre Aubin Thu, 9 Apr 2020 14:52:00 +0000 + +yunohost (3.7.0.12) stable; urgency=low + + - Fix previous buggy hotfix about deleting existing primary groups ... + + -- Alexandre Aubin Sat, 28 Mar 2020 14:52:00 +0000 + +yunohost (3.7.0.11) stable; urgency=low + + - [fix] Mess due to automatic translation tools ~_~ + + -- Kay0u Fri, 27 Mar 2020 23:49:45 +0000 + +yunohost (3.7.0.10) stable; urgency=low + + - [fix] On some weird setup, this folder and content ain't readable by group ... gotta make sure to make rx for group other slapd will explode + + -- Alexandre Aubin Fri, 27 Mar 2020 21:45:00 +0000 + +yunohost (3.7.0.9) stable; urgency=low + + - [fix] Automatically remove existing system group if it exists when creating primary groups + - [fix] Require moulinette and ssowat to be at least 3.7 to avoid funky situations where regen-conf fails because moulinette ain't upgraded yet + - [i18n] Improve translations for Arabic, Bengali, Catalan, Chinese, Dutch, Esperanto, French, German, Greek, Hindi, Hungarian, Italian, Norwegian Bokmål, Occitan, Polish, Portuguese, Russian, Spanish + + Thanks to all contributors <3 ! (Aeris One, Allan N., Alvaro, amirale qt, Armando F., ButterflyOfFire, Elie G., Gustavo M., Jeroen F., Kayou, Mario, Mélanie C., Patrick B., Quentí, tituspijean, xaloc33, yalh76, Yasss Gurl) + + -- Alexandre Aubin Fri, 27 Mar 2020 21:00:00 +0000 + +yunohost (3.7.0.8) stable; urgency=low + + - [fix] App_setting delete add if the key doesn't exist + + -- Kay0u Fri, 27 Mar 2020 00:36:46 +0000 + +yunohost (3.7.0.7) stable; urgency=low + + - [fix] Allow public apps with no sso tile (#894) + - [fix] Slapd now index permission to avoid log error + + Thanks to all contributors <3 ! (Aleks, Kay0u) + + -- Kay0u Thu, 26 Mar 2020 21:53:22 +0000 + +yunohost (3.7.0.6) testing; urgency=low + + - [fix] Make sure the group permission update contains unique elements + + Thanks to all contributors <3 ! (Aleks) + + -- Kay0u Sun, 15 Mar 2020 22:34:27 +0000 + +yunohost (3.7.0.5) testing; urgency=low + + - [fix] Permission url (#871) + - [fix] DNS resolver (#859) + - [fix] Legacy permission management (#868, #855) + - [enh] More informations in hooks permission (#877) + + Thanks to all contributors <3 ! (Bram, ljf, Aleks, Josué, Maniack, Kay0u) + + -- Kay0u Sun, 15 Mar 2020 15:07:24 +0000 + +yunohost (3.7.0.4) testing; urgency=low + + - [fix] Also add all_users when allowing visitors (#855) + - [fix] Fix handling of skipped_uris (c.f. also SSOwat#149) + - [i18n] Improve translations for Catalan + + -- Alexandre Aubin Mon, 2 Dec 2019 20:44:00 +0000 + +yunohost (3.7.0.3) testing; urgency=low + + - [mod] Some refactoring for permissions create/update/reset (#837) + - [fix] Fix some edge cases for ynh_secure_remove and ynh_clean_check_starting + - [i18n] Improve translations for French, Catalan + + -- Alexandre Aubin Sat, 23 Nov 2019 19:30:00 +0000 + +yunohost (3.7.0.2) testing; urgency=low + + - [fix] Make sure the users actually exists when migrating legacy custom permissions + - [mod] Move debug log dump from ynh_exit_properly to the core after failed app operation (#833) + - [enh] Improve app_upgrade error management (#832) + - [mod] Refactor group permission (#837) + - [enh] Add permission name in permission callback when adding/removing allowed users (#836) + - [enh] Improve permission helpers (#840) + - [i18n] Improve translations for German, Catalan, Swedish, Spanish, Turkish, Basque, French, Esperanto, Occitan + + -- Alexandre Aubin Fri, 15 Nov 2019 16:45:00 +0000 + +yunohost (3.7.0.1) testing; urgency=low + + - Hotfix to avoid having a shitload of warnings displayed during the permission migration + + -- Alexandre Aubin Thu, 31 Oct 2019 20:35:00 +0000 + +yunohost (3.7.0) testing; urgency=low + + # ~ Major stuff + + - [enh] Add group and permission mechanism (YunoHost#585, YunoHost#763, YunoHost#789, YunoHost#790, YunoHost#795, YunoHost#797, SSOwat#147, Moulinette#189, YunoHost-admin#257) + - [mod] Rework migration system to have independent migrations (YunoHost#768, YunoHost#774, YunoHost-admin#258) + - [enh] Many improvements in the way app action failures are handled (YunoHost#769, YunoHost#811) + - [enh] Improve checks for system anomalies after app operations (YunoHost#785) + - [mod] Spookier warnings for dangerous app installs (YunoHost#814, Moulinette/808f620) + - [enh] Support app manifests in toml (YunoHost#748, Moulinette#204, Moulinette/55515cb) + - [mod] Get rid of etckeeper (YunoHost#803) + - [enh] Quite a lot of messages improvements, string cleaning, language rework... (YunoHost#793, YunoHost#799, YunoHost#823, SSOwat#143, YunoHost#766, YunoHost#767, YunoHost/fd99ef0, YunoHost/92a6315, YunoHost-admin/10ea04a, Moulinette/599bec3, Moulinette#208, Moulinette#213, Moulinette/b7d415d, Moulinette/a8966b8, Moulinette/fdf9a71, Moulinette/d895ae3, Moulinette/bdf0a1c, YunoHost#817, YunoHost#823, YunoHost/79627d7, YunoHost/9ee3d23, YunoHost-admin#265) + - [i18n] Improved translations for Catalan, Occitan, French, Esperanto, Arabic, German, Spanish, Norwegian Bokmål, Portuguese + + # Smaller or pretty technical fix/enh + + - [enh] Add unit/functional tests for apps + improve other tests (YunoHost#779, YunoHost#808) + - [enh] Preparations for moulinette Python3 migration (Tox, Pytest and unit tests) (Moulinette#203, Moulinette#206, Moulinette#207, Moulinette#210, Moulinette#211 Moulinette#212, Moulinette/2403ee1, Moulinette/69b0d49, Moulinette/49c749c, Moulinette/2c84ee1, Moulinette/cef72f7, YunoHost/6365a26) + - [enh] Support python hooks (YunoHost#747) + - [enh] Upgrade n version + compatibility with arm64 (YunoHost#753) + - [enh] Add OpenLDAP TLS support (YunoHost#755, YunoHost/0a2d1c7, YunoHost/2dc8095) + - [enh] Improve PostgreSQL password security (YunoHost#762) + - [enh] Integrate actions/config-panel into operation logs (YunoHost#764) + - [mod] Assume that apps without any 'path' setting defined aren't webapps (YunoHost#765) + - [fix] Set dpkg vendor to YunoHost (YunoHost#749, YunoHost#772) + - [enh] Adding variable 'token' to data to redact from logs (YunoHost#783) + - [enh] Add --force and --dry-run options to 'yunohost dyndns update' (YunoHost#786) + - [fix] Don't throw a fatal error if we can't change the hostname (YunoHost/fe3ecd7) + - [enh] Dynamically evaluate proper mariadb-server- (YunoHost/f0440fb) + - [fix] Bad format for backup info.json ... (YunoHost/7d0119a) + - [fix] Inline buttons responsiveness on migration screen (YunoHost-admin#259) + - [enh] Add debug logs to SSOwat (SSOwat#145) + - [enh] Add a write_to_yaml utility similar to write_to_json (Moulinette/2e2e627) + - [enh] Warn the user about long locks (Moulinette#205) + - [mod] Tweak stuff about setuptools and moulinette deps? (Moulinette/b739f27, Moulinette/da00fc9, Moulinette/d8cbbb0) + - [fix] Misc micro bugfixes or improvements (YunoHost#743, YunoHost#792, YunoHost/6f48d1d, YunoHost/d516cf8, YunoHost#819, Moulinette/83d9e77, YunoHost/63d364e, YunoHost/68e9724, YunoHost/0849adb, YunoHost/19dbe87, YunoHost/61931f2, YunoHost/6dc720f, YunoHost/4def4df, SSOwat#140, SSOwat#141, YunoHost#829) + - [doc] Fix doc building + add doc build tests with Tox (Moulinette/f1ac5b8, Moulinette/df7d478, Moulinette/74c8f79, Moulinette/bcf92c7, Moulinette/af2c80c, Moulinette/d52a574, Moulinette/307f660, Moulinette/dced104, Moulinette/ed3823b) + - [enh] READMEs improvements (YunoHost/b3398e7, SSOwat/ee67b6f, Moulinette/1541b74, Moulinette/ad1eeef, YunoHost/25afdd4, YunoHost/73741f6) + + Thanks to all contributors <3 ! (accross all repo: Yunohost, Moulinette, SSOwat, Yunohost-admin) : advocatux, Aksel K., Aleks, Allan N., amirale qt, Armin P., Bram, ButterflyOfFire, Carles S. A., chema o. r., decentral1se, Emmanuel V., Etienne M., Filip B., Geoff M., htsr, Jibec, Josué, Julien J., Kayou, liberodark, ljf, lucaskev, Lukas D., madtibo, Martin D., Mélanie C., nr 458 h, pitfd, ppr, Quentí, sidddy, troll, tufek yamero, xaloc33, yalh76 + + -- Alexandre Aubin Thu, 31 Oct 2019 18:00:00 +0000 + +yunohost (3.6.5.3) stable; urgency=low + + - [fix] More general grep for the php/sury dependency nightmare fix (followup of #809) + + -- Alexandre Aubin Tue, 29 Oct 2019 03:48:00 +0000 + +yunohost (3.6.5.2) stable; urgency=low + + - [fix] Alex was drunk and released an epic stupid bug in stable (2623d385) + + -- Alexandre Aubin Thu, 10 Oct 2019 01:00:00 +0000 + +yunohost (3.6.5.1) stable; urgency=low + + - [mod] Change maxretry of fail2ban from 6 to 10 (fe8fd1b) + + -- Alexandre Aubin Tue, 08 Oct 2019 20:00:00 +0000 + +yunohost (3.6.5) stable; urgency=low + + - [enh] Detect and warn early about unavailable full domains... (#798) + - [mod] Change maxretry of fail2ban from 6 to 10 (#802) + - [fix] Epicly ugly workaround for the goddamn dependency nighmare about sury fucking up php7.0 dependencies (#809) + - [fix] Support logfiles not ending with .log in logrotate ... (#810) + + -- Alexandre Aubin Tue, 08 Oct 2019 19:00:00 +0000 + +yunohost (3.6.4.6) stable; urgency=low + + - [fix] Hopefully fix the issue about corrupted logs metadata files (d507d447, 1cec9d78) + + -- Alexandre Aubin Mon, 05 Aug 2019 18:37:00 +0000 + +yunohost (3.6.4.5) stable; urgency=low + + - [fix] Typo in hotfix... + + -- Alexandre Aubin Sun, 04 Aug 2019 18:45:00 +0000 + +yunohost (3.6.4.4) stable; urgency=low + + - [fix] Small typo breaking experimental config panel for apps (1224380) + - [mod] Remove the old ugly trick to change the admin password, not needed anymore (1cb0a26) + - [fix] Legit variable getting caught as an info to be redacted by the core (8212010) + - [fix] Exception handling for corrupted metadata about operation logs (#754) + + Contributors: Aleks, Bram, ljf + + -- Alexandre Aubin Sun, 04 Aug 2019 18:20:00 +0000 + +yunohost (3.6.4.3) stable; urgency=low + + - [hotfix] Fix some password-redacting cases that weren't caught up + + -- Alexandre Aubin Sat, 06 Jul 2019 19:35:00 +0000 + +yunohost (3.6.4.2) stable; urgency=low + + - [hotfix] Use the acme-v02 API to fix the newAccount keyError in acme_tiny + + -- Alexandre Aubin Sat, 06 Jul 2019 18:40:00 +0000 + +yunohost (3.6.4.1) stable; urgency=low + + - [hotfix] Slapd not being able to start on ipv4-only instances + + -- Alexandre Aubin Fri, 05 Jul 2019 20:50:00 +0000 + +yunohost (3.6.4) stable; urgency=low + + Minor fixes + bumping version for stable release + + -- Alexandre Aubin Thu, 04 Jul 2019 23:30:00 +0000 + +yunohost (3.6.3) testing; urgency=low + + - [fix] Less logging madness due ynh_script_progression building progress bar (#741) + - [fix] Update acme-tiny to 4.0.4 (#740) + - [fix] Missing old internet cube list in migration to unified apps.json (#745) + - [enh] Add manpage for Yunohost ! (#682) + - [enh] Config panel : use manifest.json/actions.json args format for config_panel.toml (#734) + - [enh] Allow to describe actions through toml file instead of json (#744) + - [mod] Proper return interface for app config panel (#739) + - [fix] Add mechanism to automatically detect and redact passwords from operation logs (#742) + + Thanks to all contributors <3 ! (Aleks, Bram, ljf, toitoinebzh) + + -- Alexandre Aubin Tue, 02 Jul 2019 11:10:00 +0000 + +yunohost (3.6.2) testing; urgency=low + + - [fix] Use systemd-run for more robust self-upgrade mechanism (158aa08) + - [enh] Add a do_not_backup_data app setting to avoid backing up data (#731) + - [enh] support config_panel in TOML format (#732) + - [fix] ynh_print_OFF when set -x is used in other helpers (#733) + - [enh] Add current and new version for apps in tools_update output (#735) + - [fix] Backup delete should delete symlink target (#738) + - [i18n] Improve translation for Occitan, French + + Thanks to all contributors <3 ! (Aleks, Bram, kay0u, locness3, Maniack, Quentí) + + -- Alexandre Aubin Mon, 24 Jun 2019 18:00:00 +0000 + +yunohost (3.6.1.3) testing; urgency=low + + - [fix] Missing quotes led to an issue during when upgrading postsrsd + - [fix] Running slapindex seems to fix the previous issues about LDAP indexing stuff + + -- Alexandre Aubin Fri, 07 Jun 2019 06:38:00 +0000 + +yunohost (3.6.1.2) testing; urgency=low + + - [fix] More weird issues with slapd indexation ... + - [fix] Small issue with operation logging during failed upgrade (success status set to true) + + -- Alexandre Aubin Wed, 05 Jun 2019 16:25:00 +0000 + +yunohost (3.6.1.1) testing; urgency=low + + - [fix] Weird issue in slapd triggered by indexing uidNumber / gidNumber + + -- Alexandre Aubin Tue, 04 Jun 2019 15:10:00 +0000 + +yunohost (3.6.1) testing; urgency=low + + - [fix] current version in app_info (#730) + - [fix] Add indexes for fields listed by slapd in the logs (#729) + - [fix] Allow to display logs when postinstall fails (#728) + - [fix] Stupid issue with files inside tar : foo is not the same as ./foo (#726) + - [enh] Remove unecessary log messages (#724) + - [enh] Check for obvious conflict with already running apt/dpkg commands when running yunohost upgrade (d0c982a) + + Thanks to all contributors <3 ! (Aleks, Kay0u, Bram, L. Murphy, MCMic) + + -- Alexandre Aubin Tue, 04 Jun 2019 13:20:00 +0000 + +yunohost (3.6.0) testing; urgency=low + + ## Major changes + + - [enh] Simplify the whole LDAP interface thing (#721) + - [enh] Rework how system upgrade is handled (#692) + - [enh] Properly reimplement bash completion for yunohost cli (#678) + - [enh] Migrate to apps.json / use it as default list (#666, #665) + - [enh] Decouple the regen-conf mechanism from services (#653) + - [i18n] Update translations for Catalan, Occitan, French, Italian, Spanish, Arabic + + ## App helpers + + - [mod] Set min version to 3.5.0 for helpers (#725) + - [enh] Add helpers for sso config (#720) + - [enh] Reorganize helpers (#717) + - [enh] Add the ongoing part to the progression bar when using ynh_script_progression (#715) + - [fix] postgresql helpers : force disconnection of all clients connected to the database (#713) + - [enh] Use printers in helpers (#712) + - [enh] Use ynh_systemd_action in helpers (#711) + - [fix] Fix extraction of weight value for ynh_script_progression (#710) + - [enh] Add support for ynh_setup_source in restore script (#703) + + # Other changes + + - [fix] Update censurfridns ipv6 (#727) + - [enh] Optimize ynh_script_progression (#723) + - [enh] Disable VRFY command in Postfix command (#722) + - [enh] Add a --with-details option for log list (#716) + - [enh] Specify -a parameter on dovecot lda for Sieve (#709) + - [fix] Fix an issue with config panels following changes in hook_exec (#707) + - [enh] Don't expose LDAP server to the outside world (#706) + - [fix] Remove backup hook warning about cron file (#704) + - [enh] Update nginx conf to handle WebSocket proxying (#701) + - [enh] Add size of apps in backup_info result (#699) + - [enh] Add a setting to remove support for TLSv1 and TLSv1.1 in Postfix (#696) + - [enh] Mark YunoHost as essential to avoid removing it inadvertenly (#694) + - [enh] Avoid to send simultaneously too many emails (#691) + - [enh] Dump log when an app script fails in CLI to help with debugging (#687) + - [fix] Many small technical fixes (ec48edf,251a338,d11d31d,3668bf7,c7eb5bb,9b08afc,cecaee4,95fdfb3,2bc0deb) + + Thanks to all contributors : Aleks, Benoît, Bram, ButterflyOfFire, C. Vuillot, Josue, J. Maulny, Kayou, L. Noferini, Maniack, M. Thiel, Quentí, R. du Song, Sylkevicious, ljf, xaloc33, yalh76 ! <3 + + -- Alexandre Aubin Wed, 22 May 2019 19:10:00 +0000 + +yunohost (3.5.2.2) stable; urgency=low + + - Hotfix for ynh_psql_remove_db (from ljf) + + -- Alexandre Aubin Thu, 18 Apr 2019 17:32:00 +0000 + +yunohost (3.5.2.1) stable; urgency=low + + - [fix] Fresh install was broken because of yunohost_admin.conf initialization + + -- Alexandre Aubin Thu, 11 Apr 2019 14:38:00 +0000 + +yunohost (3.5.2) stable; urgency=low + + - Release as stable ! + - [doc] Update script to automatically generate helper doc + - [i18n] Update translations for Catalan, Arabic, Italian + + Thanks to all contributors: Aleks, xaloc, BoF, silkevicious ! <3 + + -- Alexandre Aubin Wed, 10 Apr 2019 01:53:00 +0000 + +yunohost (3.5.1.1) testing; urgency=low + + - [fix] enabled/disabled status for sysv services + - [fix] Nodejs helpers : use YNH_APP_INSTANCE_NAME instead of YNH_APP_ID (#700) + - [fix] nginx diagnosis when there's an error throwing a huge useless traceback. Use Popen instead to display the real error + - [fix] service_status returns different type of data if you ask for one or multiple services + + -- Alexandre Aubin Wed, 03 Apr 2019 17:28:00 +0000 + +yunohost (3.5.1) testing; urgency=low + + - [fix] Fix the dbus interface to get info for services (#698) + - [mod] Use ask key for display_text instead and support i18n (#697) + - [fix] Rework tools update (#695) + - [enh] Nginx conf tweaks for theme (#689) + - [fix] Fix argument escaping in getopts (#685, #683) + - [enh] Support php versions in ynh_add_fpm_config (#674) + - [enh] Check that required services are up before running app install and upgrade (#670) + - [doc] Add min version for all helpers (#664) + - [enh] Add a setting to control compatibility/security tradeoff for nginx and ssh configurations (#640) + - [enh] Hooks to allow apps to extend the recommended DNS configuration (#517) + - Misc technical fixes / improvements (0bd781b, fad3edf, 1268872, 847ceca, 26e77b7, b6cff68) + - [i18n] Update translation for French, Catalan, Esperanto, Occitan + + Thanks to all contributors: Aleks, Bram, Gabriel Corona, Jibec, Josue, Maniack C, Mélanie C., Quentí, Romuald du Song, ljf, ppr, Xaloc ! <3 + + -- Alexandre Aubin Wed, 03 Apr 2019 02:13:00 +0000 + +yunohost (3.5.0.2) testing; urgency=low + + - [fix] Make sure that `ynh_system_user_delete` also deletes the group (#680) + - [enh] `ynh_systemd_action` : reload-or-restart instead of just reload (#681) + + Last minute fixes by Maniack ;) + + -- Alexandre Aubin Thu, 14 Mar 2019 03:45:00 +0000 + +yunohost (3.5.0.1) testing; urgency=low + + - [fix] #675 introduced a bug in nginx conf ... + + -- Alexandre Aubin Wed, 13 Mar 2019 19:23:00 +0000 + +yunohost (3.5.0) testing; urgency=low + + Core + ---- + + - [fix] Disable gzip entirely to avoid BREACH attacks (#675) + - [fix] Backup tests were broken (#673) + - [fix] Backup fails because output directory not empty (#672) + - [fix] Reject app password if they contains { or } (#671) + - [enh] Allow `display_text` 'fake' argument in manifest.json (#669) + - [fix] Optimize dyndns requests (#662) + - [enh] Don't add Strict-Transport-Security header in nginx conf if using a selfsigned cert (#661) + - [enh] Add apt-transport-https to dependencies (#658) + - [enh] Cache results from meltdown vulnerability checker (#656) + - [enh] Ensure the tar file is closed during the backup (#655) + - [enh] Be able to define hook to trigger when changing a setting (#654) + - [enh] Assert dpkg is not broken before app install (#652) + - [fix] Loading only one helper file leads to errors because missing getopts (#651) + - [enh] Improve / add some messages to improve UX (#650) + - [enh] Reload fail2ban instead of restart (#649) + - [enh] Add IPv6 resolvers from diyisp.org to resolv.dnsmasq.conf (#639) + - [fix] Remove old SMTP port (465) from fail2ban jail.conf (#637) + - [enh] Improve protection against indexation from the robots. (#622) + - [enh] Allow hooks to return data (#526) + - [fix] Do not make version number available from web API to unauthenticated users (#291) + - [i18n] Improve Russian and Chinese (Mandarin) translations + + App helpers + ----------- + + - [enh] Optimize app setting helpers (#663, #676) + - [enh] Handle `ynh_install_nodejs` for arm64 / aarch64 (#660) + - [enh] Update postgresql helpers (#657) + - [enh] Print diff of files when backup by `ynh_backup_if_checksum_is_different` (#648) + - [enh] Add app debugger helper (#647) + - [fix] Escape double quote before eval in getopts (#646) + - [fix] `ynh_local_curl` not using the right url in some cases (#644) + - [fix] Get rid of annoying 'unable to initialize frontend' messages (#643) + - [enh] Check if dpkg is not broken when calling `ynh_wait_dpkg_free` (#638) + - [enh] Warn the packager that `ynh_secure_remove` should be used with only one arg… (#635, #642) + - [enh] Add `ynh_script_progression` helper (#634) + - [enh] Add `ynh_systemd_action` helper (#633) + - [enh] Allow to dig deeper into an archive with `ynh_setup_source` (#630) + - [enh] Use getops (#561) + - [enh] Add `ynh_check_app_version_changed` helper (#521) + - [enh] Add fail2ban helpers (#364) + + Contributors: Alexandre Aubin, Jimmy Monin, Josué Tille, Kayou, Laurent Peuch, Lukas Fülling, Maniack Crudelis, Taekiro, frju365, ljf, opi, yalh76, Алексей + + -- Alexandre Aubin Wed, 13 Mar 2019 16:10:00 +0000 + +yunohost (3.4.2.4) stable; urgency=low + + - [fix] Meltdown vulnerability checker something outputing trash instead of pure json + + -- Alexandre Aubin Tue, 19 Feb 2019 19:11:38 +0000 + +yunohost (3.4.2.3) stable; urgency=low + + - [fix] Admin password appearing in logs after logging in on webadmin + - [fix] Update friendly DNS resolver list + + -- Alexandre Aubin Thu, 07 Feb 2019 03:20:10 +0000 + +yunohost (3.4.2.2) stable; urgency=low + + - Silly bug in migraton 8 :| + + -- Alexandre Aubin Wed, 30 Jan 2019 21:17:00 +0000 + +yunohost (3.4.2.1) stable; urgency=low + + Small issues + - Fix parsing of the Meltdown vulnerability checker (ignore stderr :/) + - Mail autoconfig was broken, follow-up of #564 + - Handle the fact that the archive folder might not exist, in migration 0008 + + -- Alexandre Aubin Wed, 30 Jan 2019 16:37:00 +0000 + +yunohost (3.4.2) stable; urgency=low + + - [fix] Do not log stretch migration in /tmp/ (#632) + - [fix] Some issues with ynh_handle_getopts_args (#628) + - [fix] Revert some stuff about separates php-ini file (c.f. #548) (#627) + - [fix] App conflicted with itself during change_url (#626) + - [fix] Improve `ynh_package_install_from_equivs` debuggability (#625) + - [enh] Add systemd log handling (#624) + - [enh] Update spectre meltdown checker (#620) + - [fix] Propagate HTTP2, more_set_headers and ecdh_curve changes to webadmin (#618) + - [enh] Control the login shell when creating users in ynh_system_user_create (#455, #629) + - [fix] Postgresql-9.4 was being detected as installed whereas it was in fact not (969577b) + - [fix] Restoring system failed because of temporary dumb password being refused (51712f9) + + Thanks to all contributors (Aleks, frju365, JimboJoe, kay0u, Maniack, opi) ! <3 + + -- Alexandre Aubin Tue, 29 Jan 2019 16:42:00 +0000 + +yunohost (3.4.1) testing; urgency=low + + * [fix] `_run_service_command` not properly returning False if command fails (#616) + * [enh] Change git clone for gitlab working with branch (#615) + * [fix] Set owner of archives folder to 'admin' (#613) + * [enh] Add reload and restart actions to 'yunohost service' (#611) + * [fix] propagate --no-checks cert-install option to renew crontab (#610) + * [fix] Several issues with bootprompt (#609) + * [fix] Fix the way change_url updates the domain/path (#608) + * [fix] Repair tests (#607) + * [fix] Explicit dependance to iptables (1667ba1) + * [i18n] Tiny typographic changes (#612) + * [i18n] Improve translations for Hungarian, Esperanto, German + * Misc minor fixes and improvements. + + Thanks to all contributors (Aleks, Bram, J. Meggyeshazi, Jibec, Josué, M. Martin, P. Bourré, anubis) ! <3 + + -- Alexandre Aubin Thu, 17 Jan 2019 22:16:00 +0000 + +yunohost (3.4.0) testing; urgency=low + + * Misc fixes (#601, #600, #593) + * [fix] DEBUG-level messages not appearing in actions performed via the API (#603) + * [enh] Also remove /var/mail/ directory on user delete (with --purge option) (#602) + * [enh] Ask confirmation before installing low-quality, experimental or third party apps (#598) + * [fix] Repair tests (#595) + * [enh] Clean + harden sshd config using Mozilla recommendation (#590 + * [fix] Add libpam-ldapd as dependency to be able to login through SSH with LDAP? (#587) + * [enh] Add post_cert_update hook each time certificate is updated (#586) + * [enh] Enable HTTP2 (#580) + * [enh] Update ECDH curves recommended by Mozilla, now that we are on stretch (#579) + * [enh] Allow to not fail on backup and restore for non-mandatory files (#576) + * [enh] Simplify error management (#574) + * [enh] Use more_set_headers in nginx config + fixes for path traversal issues (#564) + * [enh] Display human readable date and clarify timezone handling (#552) + * [fix] Do not use separate ini file for php pools anymore (#548) + * [enh] Improve UPnP support (#542) + * [fix] Standardize sshd configuration (#518) + * [fix] DKIM keys for new domains werent generated (0445aed) + * [i18n] Improve translations for Arabic, Italian and Spanish + + Thanks to all contributors (Aleks, A. Pierré, ButterflyOfFire, Bram, irina11y, Josué, Maniack Crudelis, Sylkevicious, T. Hill, chateau, frju365, gdayon, liberodark, ljf, nqb, wilPoly) ! <3 + + -- Alexandre Aubin Thu, 20 Dec 2018 22:13:00 +0000 + +yunohost (3.3.4) stable; urgency=low + + * [fix] Use --force-confold and noninteractive debian frontend during core upgrade (#614) + + -- Alexandre Aubin Thu, 17 Jan 2019 02:00:00 +0000 + +yunohost (3.3.3) stable; urgency=low + + * [fix] ynh_wait_dpkg_free displaying a warning despite everything being okay (#593) + * [fix] Quotes for recommended CAA DNS record (#596) + * [fix] Manual migration and disclaimer behaviors (#594) + * [fix] Explicit root password change each time admin password is changed + + -- Alexandre Aubin Sun, 09 Dec 2018 20:58:00 +0000 + +yunohost (3.3.2) stable; urgency=low + + * [fix] Regen nginx conf to be sure it integrates OCSP Stapling (#588) + * [fix] Broken new settings and options to control passwords checks / constrains (#589) + * [fix] Log dyndns update only if we really update something (#591) + + -- Alexandre Aubin Sun, 02 Dec 2018 17:23:00 +0000 + +yunohost (3.3.1) stable; urgency=low + + * [fix] Wait for dpkg lock to be free in apt helpers (#571) + * [fix] app_removeaccess call set.add (#573) + * [fix] Fix app_addaccess behaviour when 'allowed_users' is initially empty (#575) + * [fix] Typo in user_update when update password (#577) + * [fix] Do not fail on missing fail2ban config during the backup (#558) + * [fix] Generate a random serial for local certification auth (followup of #557) + * [i18n] Update Italian, Occitan, French translations + + Thanks to all contributors (Maniack, airwoodix, Aleks, ljf, silkevicious, Quent-in, Jibec) <3 ! + + -- Alexandre Aubin Fri, 23 Nov 2018 15:58:00 +0000 + +yunohost (3.3.0) testing; urgency=low + + Highlights + ========== + + * [enh] Synchronize root password with admin password (#527) + * [enh] Check for weak passwords whenever a password is defined (#196) + * [fix] 'dyndns update' now checks the upstream DNS record (#519) + * [fix] Update Metronome configuration file to v3.11 standard (#559) + * [fix] Some php conf files wre not properly removed when an app was uninstalled (#566) + * [i18n] Improve Catalan, French, Occitan, Portuguese, Arabic, Italian translations + + Misc + ==== + + * [enh] Add OCSP Stapling to nginx configuration if using Lets Encrypt (#533) + * [enh] Add CAA record in recommended DNS conf (#528) + * [helpers] Add `ynh_delete_file_checksum` (#524) + * [helpers] When using `ynh_setup_source`, silent unecessary messages (#545) + * [helpers] Use more blocks for dd in `ynh_string_random` (#569) + * [fix] Potential key error when retrieving install_time (#551) + * [fix] Allow `-` in user last names (#565) + * [fix] Fix possible HTTP2 issue with curl (#547) + * [fix] Fix BASE/URI in ldap conf (#554) + * [fix] Use random serial number for CA (prevent browser from complaining about some selfsigned certs) (#557) + * [enh] Pass Host header to YunoHost API (#560) + * [enh] Sort backup list according to their date (#562) + * [fix] Improve UX when admin tries to allocate reserved email alias (#553) + + Thanks to all contributors (ljf, irinia11y, Maniack, xaloc33, Bram, flashemade, Maranda, Josue, frju365, Aleks, randomstuff, jershon, Genma, Quent-in, MyNameIsTroll, ButterflyOfFire, Jibec, silkevicious) ! <3 + + -- Alexandre Aubin Thu, 08 Nov 2018 17:09:00 +0000 + +yunohost (3.2.2) stable; urgency=low + + * [hotfix] mod_auth_ldap: reflect SASL API changes in latest Metronome (#546) + * [enh] Add the internal helper ynh_handle_getopts_args (#520) + + Thanks to all contributors (Maranda, Maniack) ! <3 + + -- Alexandre Aubin Fri, 28 Sep 2018 23:04:00 +0000 + +yunohost (3.2.1) stable; urgency=low + + * Don't send an email if no certificate needs to be renewed (#540) + * Fix an issue with home backups (#541) + * Fix an issue with installs on OVH VPS + * Tell the user about post-install available in browser in bootprompt (#544) + * Improve Arabic translation + + Thanks to all contributors (BoF, ljf, Aleks) ! <3 + + -- Alexandre Aubin Mon, 17 Sep 2018 18:06:00 +0000 + +yunohost (3.2.0) stable; urgency=low + + * Add many print and exec helpers (#523) + * Add su directive as option for logrotate helper (#511) + * Add equivs, fake-hwclock and jq as base dependencies (#515, #514, #532) + * Allow to add a service description on "yunohost service add" (#529) + * Add option '--need-lock' to 'yunohost service add' (#530) + * Don't backup user home with .nobackup file (#536) + * Add a script to automatically generate helpers documentation (#538) + * [i18n] Improve Arabic translation + + Thanks to all contributors (Bram, Maniack, irina11y, Josue, BoF, ljf, Aleks) ! <3 + + -- Alexandre Aubin Tue, 11 Sep 2018 16:30:00 +0000 + +yunohost (3.2.0~testing1) testing; urgency=low + + * Add logging system of every unit operation (#165) + * Add a helper `ynh_info` for apps, so that they can comment on what is going on during scripts execution (#383) + * Fix the Sender Rewriting Scheme (#331) + * Add `ynh_render_template` to be able to render Jinja 2 templates (#463) + + Thanks to all contributors : Bram, ljf, Aleks ! + + -- Alexandre Aubin Thu, 23 Aug 2018 21:45:00 +0000 + +yunohost (3.1.0) stable; urgency=low + + Highlights + ========== + + * Add MUA autoconfiguration (e.g. for Thunderbird) (#495) + * Experimental : Configuration panel for applications (#488) + * Experimental : Allow applications to ship custom actions (#486, #505) + + Other fixes / improvements + ========================== + + * Fix an issue with mail permission after restoring them (#496) + * Optimize imports in certificate.py (#497) + * Add timeout to get_public_ip so that 'dyndns update' don't get stuck (#502) + * Use human-friendly choices for booleans during apps installations (#498) + * Fix the way we detect we're inside a container (#508) + * List existing users during app install if the app ask for a user (#506) + * Allow apps to tell they don't want to be displayed in the SSO (#507) + * After postinstall, advice the admin to create a first user (#510) + * Disable checks in acme_tiny lib is --no-checks is used (#509) + * Better UX in case of url conflicts when installing app (#512) + * Misc fixes / improvements + + Thanks to all contributors : pitchum, ljf, Bram, Josue, Aleks ! + + -- Alexandre Aubin Wed, 15 Aug 2018 21:34:00 +0000 + +yunohost (3.0.0.1) stable; urgency=low + + * Fix remaining use of --verbose and --ignore-system during backup/restore + of app upgrades + + -- Alexandre Aubin Mon, 18 Jun 2018 18:31:00 +0000 + +yunohost (3.0.0) stable; urgency=low + + * Merge with jessie's branches + * Release as stable + + -- Alexandre Aubin Sun, 17 Jun 2018 03:25:00 +0000 + +yunohost (3.0.0~beta1.7) testing; urgency=low + + * Merge with jessie's branches + * Set verbose by default + * Remove archivemount stuff + * Correctly patch php5/php7 stuff when doing a backup restore + * Fix counter-intuitive backup API + + -- Alexandre Aubin Sat, 16 Jun 2018 16:20:00 +0000 + +yunohost (3.0.0~beta1.6) testing; urgency=low + + * [fix] Service description for php7.0-fpm + * [fix] Remove old logrotate for php5-fpm during migration + * [fix] Explicitly enable php7.0-fpm and disable php5-fpm during migration + * [fix] Don't open the old SMTP port anymore (465) + * [enh] Check space available before running the postgresql migration + + -- Alexandre Aubin Tue, 12 Jun 2018 01:00:00 +0000 + +yunohost (3.0.0~beta1.5) testing; urgency=low + + * (c.f. 2.7.13.4) + + -- Alexandre Aubin Mon, 02 Jun 2018 00:14:00 +0000 + +yunohost (3.0.0~beta1.4) testing; urgency=low + + * Merge with jessie's branches + + -- Alexandre Aubin Mon, 28 May 2018 02:30:00 +0000 + +yunohost (3.0.0~beta1.3) testing; urgency=low + + * Use mariadb 10.1 now + * Convert old php comment starting with # for php5->7 migration + + -- Alexandre Aubin Sat, 12 May 2018 19:26:00 +0000 + +yunohost (3.0.0~beta1.2) testing; urgency=low + + Removing http2 also from yunohost_admin.conf since there still are some + issues with wordpress ? + + -- Alexandre Aubin Tue, 08 May 2018 05:52:00 +0000 + +yunohost (3.0.0~beta1.1) testing; urgency=low + + Fixes in the postgresql migration + + -- Alexandre Aubin Sun, 06 May 2018 03:06:00 +0000 + +yunohost (3.0.0~beta1) testing; urgency=low + + Beta release for Stretch + + -- Alexandre Aubin Thu, 03 May 2018 03:04:45 +0000 + +yunohost (2.7.14) stable; urgency=low + + * Last minute fix : install php7.0-acpu to hopefully make stretch still work after the upgrade + * Improve Occitan, French, Portuguese, Arabic translations + * [fix] local variables and various fix on psql helpers + + -- Alexandre Aubin Sun, 17 Jun 2018 01:16:13 +0000 + +yunohost (2.7.13.6) testing; urgency=low + + * Misc fixes + * [stretch-migration] Disable predictable network interface names + + Fixes by Bram and Aleks + + -- Alexandre Aubin Fri, 15 Jun 2018 16:20:00 +0000 + +yunohost (2.7.13.5) testing; urgency=low + + * [fix] a bug when log to be fetched is empty + * [fix] a bug when computing diff in regen_conf + * [stretch-migration] Tell postgresql-common to not send an email about 9.4->9.6 migration + * [stretch-migration] Close port 465 / open port 587 during migration according to SMTP port change in postfix + * [stretch-migration] Rely on /etc/os-release to get debian release number + + Fixes by Bram and Aleks + + -- Alexandre Aubin Tue, 12 Jun 2018 01:00:00 +0000 + +yunohost (2.7.13.4) testing; urgency=low + + * Fix a bug for services with alternate names (mysql<->mariadb) + * Fix a bug in regen conf when computing diff with files that don't exists + * Increase backup filename length + + (Fixes by Bram <3) + + -- Alexandre Aubin Tue, 05 Jun 2018 18:22:00 +0000 + +yunohost (2.7.13.3) testing; urgency=low + + * [enh] Add postgresql helpers (#238) + * [enh] Bring back the bootprompt (#363) + * [enh] Allow to disable the backup during the upgrade (#431) + * [fix] Remove warning from equivs (#439) + * [enh] Add SOURCE_EXTRACT (true/false) in ynh_setup_source (#460) + * [enh] More debug output in services.py (#468) + * [enh] Be able to use more variables in template for nginx conf (#462) + * [enh] Upgrade Meltdown / Spectre diagnosis (#464) + * [enh] Check services status via dbus (#469, #478, #479) + * [mod] Cleaning in services.py code (#470, #472) + * [enh] Improvate and translate service descriptions (#476) + * [fix] Fix "untrusted TLS connection" in mail logs (#471) + * [fix] Make apt-get helper not quiet so we can debug (#475) + * [i18n] Improve Occitan, Portuguese, Arabic, French translations + + Contributors : ljf, Maniack, Josue, Aleks, Bram, Quent-in, itxtoledo, ButterflyOfFire, Jibec, ariasuni, Haelwenn + + -- Alexandre Aubin Mon, 28 May 2018 02:23:00 +0000 + +yunohost (2.7.13.2) testing; urgency=low + + * [fix] Fix an error with services marked as None (#466) + * [fix] Issue with nginx not upgrading correctly /etc/nginx/nginx.conf if it was manually modified + + -- Alexandre Aubin Fri, 11 May 2018 02:06:42 +0000 + +yunohost (2.7.13.1) testing; urgency=low + + * [fix] Misc fixes on stretch migration following feedback + + -- Alexandre Aubin Wed, 09 May 2018 00:44:50 +0000 + +yunohost (2.7.13) testing; urgency=low + + * [enh] Add 'manual migration' mechanism to the migration framework (#429) + * [enh] Add Stretch migration (#433) + * [enh] Use recommended ECDH curves (#454) + + -- Alexandre Aubin Sun, 06 May 2018 23:10:13 +0000 + +yunohost (2.7.12) stable; urgency=low + + * [i18n] Improve translation for Portuguese + * Bump version number for stable release + + -- Alexandre Aubin Sun, 06 May 2018 16:40:11 +0000 + +yunohost (2.7.11.1) testing; urgency=low + + * [fix] Nginx Regression typo (#459) + + -- Alexandre Aubin Wed, 02 May 2018 12:12:45 +0000 + +yunohost (2.7.11) testing; urgency=low + + Important changes / fixes + ------------------------- + + * [enh] Add commands to manage user ssh accesses and keys (#403, #445) + * [fix] Fix Lets Encrypt install when an app is installed at root (#428) + * [enh] Improve performances by lazy-loading some modules (#451) + * [enh] Use Mozilla's recommended headers in nginx conf (#399, #456) + * [fix] Fix path traversal issues in yunohost admin nginx conf (#420) + * [helpers] Add nodejs helpers (#441, #446) + + Other changes + ------------- + + * [enh] Enable gzip compression for common text mimetypes in nginx (#356) + * [enh] Add 'post' hooks on app management operations (#360) + * [fix] Fix an issue with custom backup methods and crons (#421) + * [mod] Simplify the way we fetch and test global ip (#424) + * [enh] Manage etckeeper.conf to make etckeeper quiet (#426) + * [fix] Be able to access conf folder in change_url scripts (#427) + * [enh] Verbosify backup/restores that are performed during app upgrades (#432) + * [enh] Display debug information on cert-install/renew failure (#447) + * [fix] Add mailutils and wget as a dependencies + * [mod] Misc tweaks to display more info when some commands fail + * [helpers] More explicit depreciation warning for 'app checkurl' + * [helpers] Fix an issue in ynh_restore_file if destination already exists (#384) + * [helpers] Update php-fpm helpers to handle stretch/php7 and a smooth migration (#373) + * [helpers] Add helper 'ynh_get_debian_release' (#373) + * [helpers] Trigger an error when failing to install dependencies (#381) + * [helpers] Allow for 'or' in dependencies (#381) + * [helpers] Tweak the usage of BACKUP_CORE_ONLY (#398) + * [helpers] Tweak systemd config helpers (optional service name and template name) (#425) + * [i18n] Improve translations for Arabic, French, German, Occitan, Spanish + + Thanks to all contributors (ariasuni, ljf, JimboJoe, frju365, Maniack, J-B Lescher, Josue, Aleks, Bram, jibec) and the several translators (ButterflyOfFire, Eric G., Cedric, J. Keerl, beyercenter, P. Gatzka, Quenti, bjarkan) <3 ! + + -- Alexandre Aubin Tue, 01 May 2018 22:04:40 +0000 + +yunohost (2.7.10) stable; urgency=low + + * [fix] Fail2ban conf/filter was not matching failed login attempts... + + -- Alexandre Aubin Wed, 07 Mar 2018 12:43:35 +0000 + +yunohost (2.7.9) stable; urgency=low + + (Bumping version number for stable release) + + -- Alexandre Aubin Tue, 30 Jan 2018 17:42:00 +0000 + +yunohost (2.7.8) testing; urgency=low + + * [fix] Use HMAC-SHA512 for DynDNS TSIG + * [fix] Fix ynh_restore_upgradebackup + * [i18n] Improve french translation + + Thanks to all contributors (Bram, Maniack, jibec, Aleks) ! <3 + + -- Alexandre Aubin Wed, 24 Jan 2018 12:15:12 -0500 + +yunohost (2.7.7) stable; urgency=low + + (Bumping version number for stable release) + + -- Alexandre Aubin Thu, 18 Jan 2018 17:45:21 -0500 + +yunohost (2.7.6.1) testing; urgency=low + + * [fix] Fix Meltdown diagnosis + * [fix] Improve error handling of 'nginx -t' and Metdown diagnosis + + -- Alexandre Aubin Wed, 17 Jan 2018 13:11:02 -0500 + +yunohost (2.7.6) testing; urgency=low + + Major changes: + + * [enh] Add new api entry point to check for Meltdown vulnerability + * [enh] New command 'app change-label' + + Misc fixes/improvements: + + * [helpers] Fix upgrade of fake package + * [helpers] Fix ynh_use_logrotate + * [helpers] Fix broken ynh_replace_string + * [helpers] Use local variables + * [enh/fix] Save the conf/ directory of app during installation and upgrade + * [enh] Improve UX for app messages + * [enh] Keep SSH sessions alive + * [enh] --version now display stable/testing/unstable information + * [enh] Backup: add ability to symlink the archives dir + * [enh] Add regen-conf messages, nginx -t and backports .deb to diagnosis output + * [fix] Comment line syntax for DNS zone recommendation (use ';') + * [fix] Fix a bug in disk diagnosis + * [mod] Use systemctl for all service operations + * [i18n] Improved Spanish and French translations + + Thanks to all contributors (Maniack, Josue, Bram, ljf, Aleks, Jocelyn, JimboeJoe, David B, Lapineige, ...) ! <3 + + -- Alexandre Aubin Tue, 16 Jan 2018 17:17:34 -0500 + +yunohost (2.7.5) stable; urgency=low + + (Bumping version number for stable release) + + -- Alexandre Aubin Sat, 02 Dec 2017 12:38:00 -0500 + +yunohost (2.7.4) testing; urgency=low + + * [fix] Update acme-tiny as LE updated its ToS (#386) + * [fix] Fix helper for old apps without backup script (#388) + * [mod] Remove port 53 from UPnP (but keep it open on local network) (#362) + * [i18n] Improve French translation + + Thanks to all contributors <3 ! (jibec, Moul, Maniack, Aleks) + + -- Alexandre Aubin Tue, 28 Nov 2017 19:01:41 -0500 + +yunohost (2.7.3) testing; urgency=low + + Major changes : + + * [fix] Refactor/clean madness related to DynDNS (#353) + * [i18n] Improve french translation (#355) + * [fix] Use cryptorandom to generate password (#358) + * [enh] Support for single app upgrade from the webadmin (#359) + * [enh] Be able to give lock to son processes detached by systemctl (#367) + * [enh] Make MySQL dumps with a single transaction to ensure backup consistency (#370) + + Misc fixes/improvements : + + * [enh] Escape some special character in ynh_replace_string (#354) + * [fix] Allow dash at the beginning of app settings value (#357) + * [enh] Handle root path in nginx conf (#361) + * [enh] Add debugging in ldap init (#365) + * [fix] Fix app_upgrade_string with missing key + * [fix] Fix for change_url path normalizing with root url (#368) + * [fix] Missing 'ask_path' string (#369) + * [enh] Remove date from sql dump (#371) + * [fix] Fix unicode error in backup/restore (#375) + * [fix] Fix an error in ynh_replace_string (#379) + + Thanks to all contributors <3 ! (Bram, Maniack C, ljf, JimboJoe, ariasuni, Jibec, Aleks) + + -- Alexandre Aubin Thu, 12 Oct 2017 16:18:51 -0400 + +yunohost (2.7.2) stable; urgency=low + + * [mod] pep8 + * [fix] Explicitly require moulinette and ssowat >= 2.7.1 + * [fix] Set firewall start as background task (to be done right after postinstall) to avoid lock issues + + Thanks to all contributors <3 ! (Bram, Alex) + + -- Alexandre Aubin Tue, 22 Aug 2017 21:25:17 -0400 + +yunohost (2.7.1) testing; urgency=low + + ## Security: uses sha-512 to store password and auto upgrade old password on login + * [fix] use real random for hash selection (Laurent Peuch) + * [enh] use the full length of available chars for salt generation (Laurent Peuch) + * [mod] add more salt because life is miserable (Laurent Peuch) + * [fix] move to sh512 because it's fucking year 2017 (Laurent Peuch) + * [enh] according to https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html we can go up to 16 salt caracters (Laurent Peuch) + * [fix] also uses sha512 in user_update() (Laurent Peuch) + * [fix] uses strong hash for admin password (Laurent Peuch) + + ## Add a reboot/shutdown action + * [enh] Add reboot/shutdown actions in tools (#190) (Laurent Peuch, opi) + + ## Change lock mechanism + * Remove old 'lock' configuration (Alexandre Aubin) + * Removed unusted socket import (Alexandre Aubin) + + ## Various fix + ### backup + * [fix] Remove check that domain is resolved locally (Alexandre Aubin) + * [fix] Tell user that domain dns-conf shows a recommendation only (Alexandre Aubin) + * [fix] Backup without info.json (#342) (ljf) + * [fix] Make read-only mount bind actually read-only (#343) (ljf) + ### dyndns + * Regen dnsmasq conf if it's not up to date :| (Alexandre Aubin) + * [fix] timeout on request to avoid blocking process (Laurent Peuch) + * Put request url in an intermediate variable (Alexandre Aubin) + ### other + * clean users.py (Laurent Peuch) + * clean domains.py (Laurent Peuch) + * [enh] add 'yunohost tools shell' (Laurent Peuch) + * Use app_ssowatconf instead of os.system call (Alexandre Aubin) + + Thanks to all contributors <3 ! (Bram, ljf, Aleks, opi) + + -- Laurent Peuch Sat, 19 Aug 2017 23:16:44 +0000 + +yunohost (2.7.0) testing; urgency=low + + Thanks to all contributors <3 ! (Bram, Maniack C, ljf, Aleks, JimboJoe, anmol26s, e-lie, Ozhiganov) + + Major fixes / improvements + ========================== + + * [enh] Add a migration framework (#195) + * [enh] Remove m18n (and other globals) black magic (#336) + * [fix] Refactor DNS conf management for domains (#299) + * [enh] Support custom backup methods (#326) + + App helpers + =========== + + * New helper autopurge (#321) + * New helpers ynh_add_fpm_config and ynh_remove_fpm_config (#284) + * New helpers ynh_restore_upgradebackup and ynh_backup_before_upgrade (#289) + * New helpers ynh_add_nginx_config and ynh_remove_nginx_config (#285) + * New helpers ynh_add_systemd_config and ynh_remove_systemd_config (#287) + + Smaller fixes / improvements + ============================ + + * [fix] Run change_url scripts as root as a matter of homogeneity (#329) + * [fix] Don't verify SSL during changeurl tests :/ (#332) + * [fix] Depreciation warning for --hooks was always shown (#333) + * [fix] Logrotate append (#328) + * [enh] Check that url is available and normalize path before app install (#304) + * [enh] Check that user is legitimate to use an email adress when sending mail (#330) + * [fix] Properly catch Invalid manifest json with ValueError. (#324) + * [fix] No default backup method (redmine 968) (#339) + * [enh] Add a script to test m18n keys usage (#308) + * [i18] Started russian translation (#340) + + -- Alexandre Aubin Mon, 07 Aug 2017 13:16:08 -0400 + +yunohost (2.6.5) stable; urgency=low + + Minor fix + --------- + + * Do not crash backup restore if archivemount is not there (#325) + + -- Alexandre Aubin Wed, 26 Jul 2017 11:56:09 -0400 + +yunohost (2.6.4) stable; urgency=low + + Changes + ------------- + + * Misc fixes here and there + * [i18n] Update Spanish, German and French translations (#323) + + Thanks to all contributors : opi, Maniack C, Alex, JuanuSt, franzos, Jibec, Jeroen and beyercenter ! + + -- ljf Wed, 21 Jun 2017 17:18:00 -0400 + +yunohost (2.6.3) testing; urgency=low + + Major changes + ------------- + + * [love] Add missing contributors & translators. + * [enh] Introduce global settings (#229) + * [enh] Refactor backup management to pave the way to borg (#275) + * [enh] Changing nginx ciphers to intermediate compatiblity (#298) + * [enh] Use ssl-cert group for certificates, instead of metronome (#222) + * [enh] Allow regen-conf to manage new files already present on the system (#311) + * [apps] New helpers + * ynh_secure_remove (#281) + * ynh_setup_source (#282) + * ynh_webpath_available and ynh_webpath_register (#235) + * ynh_mysql_generate_db and ynh_mysql_remove_db (#236) + * ynh_store_file_checksum and ynh_backup_if_checksum_is_different (#286) + * Misc fixes here and there + * [i18n] Update Spanish, German and French translations (#318) + + Thanks to all contributors : Bram, ljf, opi, Maniack C, Alex, JimboJoe, Moul, Jibec, JuanuSt and franzos ! + + -- Alexandre Aubin Fri, 02 Jun 2017 09:15:05 -0400 + +yunohost (2.6.2) testing; urgency=low + + New Features + ------------ + + * [enh] Allow applications to ship a script to change its url (#185) + * New helper ynh_replace_string (#280) + * New helper ynh_local_curl (#288) + + Fixes + ----- + + * Fix for missing YunoHost tiles (#276) + * [fix] Properly define app upgradability / Fix app part of tools update (#255) + * [fix] Properly manage resolv.conf, dns resolvers and dnsmasq (#290) + * [fix] Add random delay to app fetchlist cron job (#297) + + Improvements + ------------- + + * [fix] Avoid to remove a apt package accidentally (#292) + * [enh] Refactor applist management (#160) + * [enh] Add libnss-mdns as Debian dependency. (#279) + * [enh] ip6.yunohost is now served through HTTPS. + * [enh] Adding new port availability checker (#266) + * [fix] Split checkurl into two functions : availability + booking (#267) + * [enh] Cleaner postinstall logs during CA creation (#250) + * Allow underscore in backup name + * Rewrite text for "appslist_retrieve_bad_format" + * Rewrite text for "certmanager_http_check_timeout" + * Updated Spanish, German, Italian, French, German and Dutch translations + + -- Alexandre Aubin Mon, 24 Apr 2017 09:07:51 -0400 + +yunohost (2.6.1) testing; urgency=low + + [ Maniack Crudelis ] + * Hack dégueux pour éviter d'écrire dans le log cli + * [enh] New helpers for equivs use + * [enh] New helpers for logrotate + * Update package + * Restore use of subshell + + [ Trollken ] + * [i18n] Translated using Weblate (Portuguese) + + [ rokaz ] + * [i18n] Translated using Weblate (Spanish) + * [i18n] Translated using Weblate (French) + + [ Jean-Baptiste Holcroft ] + * [i18n] Translated using Weblate (French) + + [ rokaz ] + * [i18n] Translated using Weblate (English) + + [ Fabian Gruber ] + * [i18n] Translated using Weblate (German) + + [ bricabraque ] + * [i18n] Translated using Weblate (Italian) + + [ Trollken ] + * [i18n] Translated using Weblate (Portuguese) + + [ rokaz ] + * [i18n] Translated using Weblate (Spanish) + + [ Fabian Gruber ] + * [i18n] Translated using Weblate (German) + * [i18n] Translated using Weblate (German) + + [ bricabraque ] + * [i18n] Translated using Weblate (Italian) + + [ Fabian Gruber ] + * [i18n] Translated using Weblate (German) + + [ Lapineige ] + * [i18n] Translated using Weblate (French) + + [ Laurent Peuch ] + * [enh] upgrade ciphers suit to more secure ones + + [ ljf (zamentur) ] + * [fix] Can't use common.sh on restore operation (#246) + + [ thardev ] + * show fail2ban logs on admin web interface + + [ Maniack Crudelis ] + * Fix ynh_app_dependencies + * Fix ynh_remove_app_dependencies too... + + [ Moul ] + * [mod] dnsmasq conf: remove deprecated XMPP DNS record line. + * [fix] dnsmasq conf: remove 'resolv-file' line. - there is no file specified for this line. - dns resolution isn't working on some cases: - metronome could not works. - https://forum.yunohost.org/t/xmpp-cant-connect-to-conference-yunohost-org/2142 + + [ Laurent Peuch ] + * [enh] defaulting running hook_exec as root + * [mod] change behavior, admin by default, as to explicitly set root as user + * [enh] use root for app related hook_exec + * [mod] remove unused import + * [fix] run missing backup scripts as root + * [enh] run hooks as root + * [mod] try to clean a bit app_list code + + [ Alexandre Aubin ] + * Trying to add comments and simplify some overly complicated parts + * Trying to make offset / limit consistent + + [ Laurent Peuch ] + * [mod] remove useless addition + * [fix] if a service don't have a 'status' entry, don't list it + * [fix] nsswitch and udisks2 aren't used anymore + * [fix] we don't use bind9, add null entry to remove it from old services.yml + * [enh] add other services to remove + * [fix] launch ssowatconf at the end of a broken install to avoid sso bad state + + [ opi ] + * [love] adding thardev to contributors + + [ Alexandre Aubin ] + * [enh] Trigger exception during unit tests if string key aint defined (#261) + * Updating ciphers with recommendation from mozilla with modern compatibility + + [ Maniack Crudelis ] + * Failed if $1 not set + + [ Laurent Peuch ] + * [mod] remove offset/limit from app_list, they aren't used anymore + * [mod] implement ljf comment + + [ Maniack Crudelis ] + * Remove use of deprecated helper + + [ opi ] + * [enh] Use _get_maindomain helper. + + [ Maniack Crudelis ] + * Add app setting + + [ opi ] + * [fix] Regenerate SSOwat conf during main_domain operation. #672 + + [ Maniack Crudelis ] + * Nouveau helper ynh_normalize_url_path (#234) + * Prevent to rewrite the previous control file + + [ Alexandre Aubin ] + * Rename ynh_app_dependencies to ynh_install_app_dependencies + + [ Maniack Crudelis ] + * [enh] New helper ynh_abort_if_errors (#245) + + [ ljf ] + * [fix] Apply cipher suite into webadmin nginx conf + + [ Laurent Peuch ] + * [fix] only remove a service if it is setted to null + + [ Moul ] + + -- Moul Thu, 23 Mar 2017 09:53:06 +0000 + +yunohost (2.6.0) testing; urgency=low + + Important changes + + - [enh] Add unit test mechanism (#254) + - [fix] Any address in the range 127.0.0.0/8 is a valid loopback address for localhost + - [enh] include script to reset ldap password (#217) + - [enh] Set main domain as hostname (#219) + - [enh] New bash helpers for app scripts: ynh_system_user_create, ynh_system_user_delete, helper ynh_find_port + + Thanks to every contributors (Bram, Aleks, Maniack Crudelis, ZeHiro, opi, julienmalik + + Full changes log: + + 8486f440fb18d513468b696f84c0efe833298d77 [enh] Add unit test mechanism (#254) + 45e85fef821bd8c60c9ed1856b3b7741b45e4158 Merge pull request #252 from ZeHiro/fix-785 + 834cf459dcd544919f893e73c6be6a471c7e0554 Please Bram :D + 088abd694e0b0be8c8a9b7d96a3894baaf436459 Merge branch 'testing' into unstable + f80653580cd7be31484496dbe124b88e34ca066b Merge pull request #257 from YunoHost/fix_localhost_address_range + f291d11c844d9e6f532f1ec748a5e1eddb24c2f6 [fix] cert-renew email headers appear as text in the body + accb78271ebefd4130ea23378d6289ac0fa9d0e4 [fix] Any address in the range 127.0.0.0/8 is a valid loopback address + cc4451253917040c3a464dce4c12e9e7cf486b15 Clean app upgrade (#193) + d4feb879d44171447be33a65538503223b4a56fb [enh] include script to reset ldap password (#217) + 1d561123b6f6fad1712c795c31409dedc24d0160 [enh] Set main domain as hostname (#219) + 0e55b17665cf1cd05c157950cbc5601421910a2e Fixing also get_conf_hashes + 035100d6dbcd209dceb68af49b593208179b0595 Merge pull request #251 from YunoHost/uppercase_for_global_variables + f28be91b5d25120aa13d9861b0b3be840f330ac0 [fix] Uppercase global variable even in comment. + 5abcaadaeabdd60b40baf6e79fff3273c1dd6108 [fix] handle the case where services[service] is set to null in the services.yml. Fix #785 + 5d3e1c92126d861605bd209ff56b8b0d77d3ff39 Merge pull request #233 from YunoHost/ynh_find_port + 83dca8e7c6ec4efb206140c234f51dfa5b3f3bf7 Merge pull request #237 from YunoHost/ynh_system_user_create_delete + f6c7702dfaf3a7879323a9df60fde6ac58d3aff7 [mod] rename all global variables to uppercase + 3804f33b2f712eb067a0fcbb6fb5c60f3a813db4 Merge pull request #159 from YunoHost/clean_app_fetchlist + 8b44276af627ec05ac376c57e098716cacd165f9 Merge branch 'testing' into unstable + dea89fc6bb209047058f050352e3c082b9e62f32 Merge pull request #243 from YunoHost/fix-rspamd-rmilter-status + dea6177c070b9176e3955c4f32b8a602977cf424 Merge pull request #244 from YunoHost/fix-unattended-upgrade-syntax + a61445c9c3d231b9248fd247a0dd3345fc0ac6df Checking for 404 error and valid json format + 991b64db92e60f3bc92cb1ba4dc25f7e11fb1a8d Merge branch 'unstable' into clean_app_fetchlist + 730156dd92bbd1b0c479821ffc829e8d4f3d2019 Using request insteqd of urlretrieve, to have timeout + 5b006dbf0e074f4070f6832d2c64f3b306935e3f Adding info/debug message for fetchlist + 98d88f2364eda28ddc6b98d45a7fbe2bbbaba3d4 [fix] Unattended upgrades configuration syntax. + 7d4aa63c430516f815a8cdfd2f517f79565efe2f [fix] Rspamd & Rmilter are no more sockets + 5be13fd07e12d95f05272b9278129da4be0bc2d7 Merge pull request #220 from YunoHost/conf-hashes-logs + 901e3df9b604f542f2c460aad05bcc8efc9fd054 Pas de correction de l'argument + cd93427a97378ab635c85c0ae9a1e45132d6245c Retire la commande ynh + abb9f44b87cfed5fa14be9471b536fc27939d920 Nouveaux helpers ynh_system_user_create et ynh_system_user_delete + 3e9d086f7ff64f923b2d623df41ec42c88c8a8ef Nouveau helper ynh_find_port + 0b6ccaf31a8301b50648ec0ba0473d2190384355 Implementing comments + 5b7536cf1036cecee6fcc187b2d1c3f9b7124093 Style for Bram :) + e857f4f0b27d71299c498305b24e4b3f7e4571c4 [mod] Cleaner logs for _get_conf_hashes + 99f0f761a5e2737b55f9f8b6ce6094b5fd7fb1ca [mod] include execption into appslist_retrieve_error message + 2aab7bdf1bcc6f025c7c5bf618d0402439abd0f4 [mod] simplify code + 97128d7d636836068ad6353f331d051121023136 [mod] exception should only be used for exceptional situations and not when buildin functions allow you to do the expected stuff + d9081bddef1b2129ad42b05b28a26cc7680f7d51 [mod] directly use python to retreive json list + c4cecfcea5f51f1f9fb410358386eb5a6782cdb2 [mod] use python instead of os.system + cf3e28786cf829bc042226283399699195e21d79 [mod] remove useless line + + + -- opi Mon, 20 Feb 2017 16:31:52 +0100 + +yunohost (2.5.6) stable; urgency=low + + [ julienmalik ] + * [fix] Any address in the range 127.0.0.0/8 is a valid loopback address + + [ opi ] + * [fix] Update Rmilter configuration to fix dkim signing. + + -- opi Sat, 18 Feb 2017 15:51:13 +0100 + +yunohost (2.5.5) stable; urgency=low + + Hotfix release + + [ ljf ] + * [fix] Permission issue on install of some apps 778 + + -- opi Thu, 09 Feb 2017 22:27:08 +0100 + +yunohost (2.5.4) stable; urgency=low + + [ Maniack Crudelis ] + * Remove helper ynh_mkdir_tmp + * Update filesystem + + [ opi ] + * [enh] Add warning about deprecated ynh_mkdir_tmp helper + * [enh] Increase fail2ban maxretry on user login, narrow nginx log files + + [ Juanu ] + * [i18n] Translated using Weblate (Spanish) + + [ Jean-Baptiste Holcroft ] + * [i18n] Translated using Weblate (French) + + [ Laurent Peuch ] + * [mod] start putting timeout in certificate code + + [ Alexandre Aubin ] + * Implement timeout exceptions + * Implementing opi's comments + + [ JimboJoe ] + * ynh_backup: Fix error message when source path doesn't exist + + [ paddy ] + * [i18n] Translated using Weblate (Spanish) + * [i18n] Translated using Weblate (French) + + -- opi Thu, 02 Feb 2017 11:24:55 +0100 + +yunohost (2.5.3.1) testing; urgency=low + + * super quickfix release for a typo that break LE certificates + + -- Laurent Peuch Tue, 10 Jan 2017 02:58:56 +0100 + +yunohost (2.5.3) testing; urgency=low + + Love: + * [enh][love] Add CONTRIBUTORS.md + + LE: + * Check acme challenge conf exists in nginx when renewing cert + * Fix bad validity check.. + + Fix a situation where to domain for the LE cert can't be locally resolved: + * Adding check that domain is resolved locally for cert management + * Changing the way to check domain is locally resolved + + Fix a situation where a cert could end up with bad perms for metronome: + * Attempt to fix missing perm for metronome in weird cases + + Rspamd cannot be activate on socket anymore: + * [fix] new rspamd version replace rspamd.socket with rspamd.service + * [fix] Remove residual rmilter socket file + * [fix] Postfix can't access rmilter socket due to chroot + + Various: + * fix fail2ban rules to take into account failed loggin on ssowat + * [fix] Ignore dyndns option is not needed with small domain + * [enh] add yaml syntax check in travis.yml + * [mod] autopep8 on all files that aren't concerned by a PR + * [fix] add timeout to fetchlist's wget + + Thanks to all contributors: Aleks, Bram, ju, ljf, opi, zimo2001 and to the + people who are participating to the beta and giving us feedback <3 + + + -- Laurent Peuch Mon, 09 Jan 2017 18:38:30 +0100 + +yunohost (2.5.2) testing; urgency=low + + LDAP admin user: + * [fix] wait for admin user to be available after a slapd regen-conf, this fix install on slow hardware/vps + + Dovecot/emails: + * [enh] reorder dovecot main configuration so that it is easier to read and extend + * [enh] Allow for dovecot configuration extensions + * [fix] Can't get mailbos used space if dovecot is down + + Backup: + * [fix] Need to create archives_path even for custom output directory + * Keep track of backups with custom directory using symlinks + + Security: + * [fix] Improve dnssec key generation on low entropy devices + * [enh] Add haveged as dependency + + Random broken app installed on slow hardware: + * [enh] List available domains when installing an app by CLI. + + Translation: + * French by Jibec and Genma + * German by Philip Gatzka + * Hindi by Anmol + * Spanish by Juanu + + Other fixes and improvements: + * [enh] remove timeout from cli interface + * [fix] #662: missing 'python-openssl' dependency for Let's Encrypt integration. + * [fix] --no-remove-on-failure for app install should behave as a flag. + * [fix] don't remove trailing char if it's not a slash + + Thanks to all contributors: Aleks, alex, Anmol, Bram, Genma, jibec, ju, + Juanu, ljf, Moul, opi, Philip Gatzka and to the people who are participating + to the beta and giving us feedback <3 + + -- Laurent Peuch Fri, 16 Dec 2016 00:49:08 +0100 + +yunohost (2.5.1) testing; urgency=low + + * [fix] Raise error on malformed SSOwat persistent conf. + * [enh] Catch SSOwat persistent configuration write error. + * [fix] Write SSOwat configuration file only if needed. + * [enh] Display full exception error message. + * [enh] cli option to avoid removing an application on installation failure + * [mod] give instructions on how to solve the conf.json.persistant parsing error + * [fix] avoid random bug on post-install due to nscd cache + * [enh] Adding check that user is actually created + minor refactor of ldap/auth init + * [fix] Fix the way name of self-CA is determined + * [fix] Add missing dependency to nscd package #656 + * [fix] Refactoring tools_maindomain and disabling removal of main domain to avoid breaking things + * [fix] Bracket in passwd from ynh_string_random + + Thanks to all contributors: Aleks, Bram, ju, jibec, ljf, M5oul, opi + + -- Laurent Peuch Sun, 11 Dec 2016 15:26:21 +0100 + +yunohost (2.5.0) testing; urgency=low + + * Certificate management integration (e.g. Let's Encrypt certificate install) + * [fix] Support git ynh app with submodules #533 (#174) + * [enh] display file path on file_not_exist error + * [mod] move a part of os.system calls to native shutil/os + * [fix] Can't restore app on a root domain + + Miscellaneous + + * Update backup.py + * [mod] autopep8 + * [mod] trailing spaces + * [mod] pep8 + * [mod] remove useless imports + * [mod] more pythonic and explicit tests with more verbose errors + * [fix] correctly handle all cases + * [mod] simplier condition + * [fix] uses https + * [mod] uses logger string concatenation api + * [mod] small opti, getting domain list can be slow + * [mod] pylint + * [mod] os.path.join + * [mod] remove useless assign + * [enh] include tracebak into error email + * [mod] remove the summary code concept and switch to code/verbose duet instead + * [mod] I only need to reload nginx, not restart it + * [mod] top level constants should be upper case (pep8) + * Check that the DNS A record matches the global IP now using dnspython and FDN's DNS + * Refactored the self-signed cert generation, some steps were overly complicated for no reason + * Using a single generic skipped regex for acme challenge in ssowat conf + * Adding an option to use the staging Let's Encrypt CA, sort of a dry-run + * [enh] Complete readme (#183) + * [fix] avoid reverse order log display on web admin + + Thanks to all contributors: Aleks, Bram, JimboJoe, ljf, M5oul + Kudos to Aleks for leading the Let's Encrypt integration to YunoHost core \o/ + + -- opi Thu, 01 Dec 2016 21:22:19 +0100 + +yunohost (2.4.2) stable; urgency=low + + [ Laurent Peuch ] + * [enh] add empty file for hindie to enable it in weblate + + [ opi ] + * [fix] Documentation typo + + [ Laurent Peuch ] + * [fix] ensure that multi_instance key value is always a boolean + + -- Laurent Peuch Sun, 14 Aug 2016 18:55:10 +0200 + +yunohost (2.4.1) stable; urgency=low + + [ Bugsbane ] + * [i18n] Translated using Weblate (English) + + [ DUBWiSE ] + * [i18n] Translated using Weblate (Dutch) + + [ Jean-Baptiste ] + * [i18n] Translated using Weblate (French) + + [ jellium ] + * [fix] Replace deprecated psutil.BOOT_TIME attribute + + [ Jérôme Lebleu ] + * [fix] Set empty app argument value only when it's None + * [i18n] Translated using Weblate (English & French) + + [ Juanu ] + * [i18n] Translated using Weblate (Spanish) + + [ Laurent Peuch ] + * [fix] Use a local variable for extracted app dir (bugfix #326) + + [ vetetix ] + * fix issue in dkim dns setting + + -- Jérôme Lebleu Thu, 14 Jul 2016 12:05:35 +0200 + +yunohost (2.4.0.7) stable; urgency=low + + * [fix] Allow - in app id when parsing app instance name + * [ref] Invert no-stats option to with-stats in monitor_enable + * [fix] Set /var/mail folder owners and permissions + + -- Jérôme Lebleu Sun, 12 Jun 2016 15:58:26 +0200 + +yunohost (2.4.0.6) stable; urgency=low + + [ Bugsbane ] + * [i18n] Fixed minor English grammar errors + + [ Jérôme Lebleu ] + * [fix] Harden backup hooks with set options and use ynh_backup + * [fix] Set default value for YNH_APP_BACKUP_DIR in ynh_backup helper + * [fix] Raise proper MoulinetteError exception in hook_exec + * [fix] Use the classic way to create read-only bind mount in ynh_backup + * [fix] Escape arguments and env values in hook_exec (bugfix #377) + + [ opi ] + * [enh] Comments will save us. + * [enh] Use 'source' instead of dot notation, more explicit. + + -- Jérôme Lebleu Mon, 30 May 2016 12:15:21 +0200 + +yunohost (2.4.0.5) stable; urgency=low + + * [enh] Call iptables/ip6tables with --wait option (close #325) + * [fix] Catch not implemented prompt signal in app arguments parsing + + -- Jérôme Lebleu Sat, 28 May 2016 22:02:02 +0200 + +yunohost (2.4.0.4) stable; urgency=low + + * [fix] Print string error of MoulinetteError in hook_callback + * [fix] Hide cat error if tmp_backup_dir_file doesn't exist in conf_regen + * [fix] Rely on systemd is-active to check if mysql is running + + -- Jérôme Lebleu Sun, 22 May 2016 16:48:06 +0200 + +yunohost (2.4.0.3) stable; urgency=low + + [ Laurent Peuch ] + * [fix] exit if not run as root instead of raising an obscur exception + + [ Jérôme Lebleu ] + * [enh] Add ynh_apt wrapper helper and make use of it + * [fix] Save LDAP base before any conf changes in conf_regen hook + + -- Jérôme Lebleu Sat, 21 May 2016 18:00:32 +0200 + +yunohost (2.4.0.2) stable; urgency=low + + [ Jérôme Lebleu ] + * [fix] Update argument with empty value adding for OrderedDict usage + * [fix] Ensure that index.txt CA database exists at SSL regen-conf + + [ opi ] + * [fix] Restart Nginx breaks web admin. Reload instead and fixes #330. + + -- Jérôme Lebleu Wed, 18 May 2016 11:12:52 +0200 + +yunohost (2.4.0.1) stable; urgency=low + + * [fix] Use ps to check if MySQL is running in conf_regen hook (fix #232) + * [fix] Copy app remove script in a tmp file at restoration failure + + -- Jérôme Lebleu Sat, 14 May 2016 14:30:48 +0200 + +yunohost (2.4.0) stable; urgency=low + + [ Jérôme Lebleu ] + * [enh] Add app hooks after the install to allow modifications + * [enh] Also add app hooks after successful upgrade + * [enh] Handle password argument type at prompt from app manifest + * [enh] Try to remount directory as read-only in ynh_backup (wip #298) + * [fix] Prepend backup dir to relative path only and allow absolute in + ynh_backup + * [fix] Update data_home/mail backup hooks to use ynh_backup helper + + [ opi ] + * [fix] Can install app on domain root even if another app is installed + in a sub folder. + + -- Jérôme Lebleu Sun, 08 May 2016 00:47:49 +0200 + +yunohost (2.3.15) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Create backup archives path depending of output directory + * [enh] Check free space in output directory before backup archive creation + * [enh] Create ynh_backup helper based on ynh_bind_or_cp + * [enh] Add ynh_die helper to print error message and exit + * [enh] Do not clean whole pending conf dir when names are given at regen-conf + * [enh] Remove empty pending conf directory at regen-conf + * [fix] Handle when new conf is as current system conf in regen-conf + * [fix] Remove the app if it restoration failed + * [i18n] Rename backup_complete and backup_failed strings + + [ opi ] + * [fix] Pass app instance environment variables to remove script. + * [fix] Catch IOError on tar creation (backup). + + -- Jérôme Lebleu Fri, 06 May 2016 20:31:12 +0200 + +yunohost (2.3.14) testing; urgency=low + + [ Jean-Baptiste ] + * [i18n] Translated using Weblate (French) + + [ Jérôme Lebleu ] + * [enh] Remove client certificate verification from Dovecot and Postfix + * [enh] Allow to set env var for executed hooks in hooks_callback + * [enh] Do not bind mounting if no backup archive is created (wip #298) + * [fix] Do not set default value to mailbox-quota at user_update + * [fix] Clean properly backup tmp directory if it already exists (wip #298) + * [fix] Remove legacy slapd file after directory creation at regen-conf + * [fix] Remove old PAM config file at package postinstall + * [i18n] Translated using Weblate (French) + + [ Julien Malik ] + * [enh] Support passing env var to hook_exec + * [enh] Set env var for each app script and rename app variables + + -- Jérôme Lebleu Sat, 30 Apr 2016 20:59:28 +0200 + +yunohost (2.3.13) testing; urgency=low + + * [cli] Deprecate app_initdb action in flavour of helpers + * [cli] Deprecate and rename regenconf action to regen-conf + * [enh] Add pre/post script execution callbacks to hook_callback + * [enh] Refactor the conf regen for better conflicts handle + * [enh] Allow to show the diff between conf in service_regen_conf + * [enh] Allow to list pending conf in service_regen_conf + * [enh] Add a dry-run option for service_regen_conf + * [enh] Update services.yml in yunohost conf_regen and update its content + * [enh] Add a yunopaste script to paste data to YunoHost Haste server + * [enh] Catch boolean in is_true method of app.py + * [enh] Implement the intersection of package version Specifier class + * [enh] Implement the union of package version Specifier class + * [enh] Implement the intersection of package version SpecifierSet class + * [enh] Prevent non-updated multi-instances apps installation (close #126) + * [enh] Force new MySQL password set if it's unknown at regen-conf + * [fix] Restore MySQL password for root user (bugfix #194) + * [fix] Restore current_host and use only one backup path for it + * [fix] Use SSL certificate of main domain in Dovecot and Postfix conf + * [fix] multi_instance manifest key is generally a string + * [fix] Call regen-conf only once passing a list in domain_add/remove + * [fix] Remove useless `email_legacy` conf_regen hook + * [fix] Import moulinette after dev env check in bin/yunohost{-api,} + * [fix] Skip hidden and temp files in hook_list + * [deb] Add etckeeper package in Recommends (wip #280) + * [deb] Enable yunohost-firewall on service restart at postinst + * [doc] Be more verbose when reset the MySQL root password + * [doc] Add documentation to contains methods of Specifier/SpecifierSet + + -- Jérôme Lebleu Tue, 26 Apr 2016 16:26:20 +0200 + +yunohost (2.3.12.1) testing; urgency=low + + * [deb] Rely on dh_installinit to restart yunohost-firewall after upgrade + * [deb] Add Install section to yunohost-firewall.service + + -- Jérôme Lebleu Sat, 09 Apr 2016 17:22:40 +0200 + +yunohost (2.3.12) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Use new rspamd configuration system to override metrics + * [enh] Allow to set script execution directory in hook_exec + * [enh] Add a ynh_user_list helper + * [enh] Call app remove script if installation fails + * [fix] Move imports at the top in yunohost and yunohost-api + * [fix] Use rspamd local.d folder to allow users to override the defaults + * [fix] Execute backup/restore app scripts from the backup dir (bugfix #139) + * [fix] Regenerate SSOwat conf after apps restoration + * [fix] Move imports at the top in backup.py + * [fix] Check if the package is actually installed in equivs helper + * [fix] Improve control file management in equivs helper + * [fix] Remove ending comma in backup.py + * [fix] Call yunohost commands with --quiet in setting helpers + * [fix] Check for tty in root_handlers before remove it in bin/yunohost + * [fix] Use dyndns.yunohost.org instead of dynhost.yunohost.org + * [fix] Set found private key and don't validate it in dyndns_update + * [fix] Update first registered domain with DynDNS instead of current_host + * [i18n] Rename app_requirements_failed err named variable + * [i18n] Update translations from Weblate + + [ opi ] + * [enh] Better message during service regenconf. + * [enh] Display hook path on error message. + * [enh] Use named arguments when calling m18n in service.py + * [enh] Use named arguments with m18n. + * [enh] Use named arguments for user_unknown string. + + -- Jérôme Lebleu Sat, 09 Apr 2016 12:13:10 +0200 + +moulinette-yunohost (2.2.4) stable; urgency=low + + [ Jérôme Lebleu ] + * [fix] Update first registered domain with DynDNS instead of current_host + * [fix] Set found private key and don't validate it in dyndns_update + * [fix] Use dyndns.yunohost.org instead of dynhost.yunohost.org + + [ opi ] + * [fix] Catch ConnectionError from requests package + + -- Jérôme Lebleu Sun, 27 Mar 2016 16:30:42 +0200 + +yunohost (2.3.11.2) testing; urgency=low + + * [fix] Don't fail dnsmasq regen if IPv4/6 cannot be retrieved + + -- Jérôme Lebleu Wed, 23 Mar 2016 14:57:22 +0100 + +yunohost (2.3.11.1) testing; urgency=low + + * [deb] Include sysvinit services and files in the package, thanks to + nthykier and pabs from #debian-mentor + + -- Jérôme Lebleu Wed, 23 Mar 2016 12:38:56 +0100 + +yunohost (2.3.11) testing; urgency=low + + [ Laurent Peuch ] + * [mod] Explain how to start yunohost-firewall service + + [ Jérôme Lebleu ] + * [fix] Remove useless API routes for some actions + * [fix] Update API route for hook_callback action + * [deb] Attempt to improve services management in Debian packaging + * [deb] Add missing cron dependency + * [deb] Clean debian/control with cosmetic changes + * [deb] Fix helpers bash script installation + + [ Julien Malik ] + * [enh] Add helper for IP address validation + * [enh] move /usr/share/yunohost/apps/helpers to + /usr/share/yunohost/helpers since it became of more general use + * [enh] Remove unused checkupdate and upgrade scripts + * [fix] Validate IP addresses returned by ipX.yunohost.org + * [fix] fix lintian script-not-executable + * [deb] dh_python2 replaces shebang during build. Using the correct one + in source directly + + [ Moul ] + * [enh] Add '-a' argument's usage example for app_install + + [ opi ] + * [enh] Add diagnosis function. #39 + * [enh] Redirect most of 404 to maindomain.tld/yunohost/sso + * [enh] Add --installed and --with-backup to app_list action (wip #227) + * [enh] More explicit backup forbidden directory error message. + * [enh] Use dedicated app list domain. + * [fix] Use only dyndns.yunohost.org domain. + * [fix] Use plain text 502 error page. + * [fix] Cleaner Nginx redirection rules. Use permanent only when paths match. + + -- Jérôme Lebleu Wed, 23 Mar 2016 10:39:34 +0100 + +yunohost (2.3.10.2) testing; urgency=low + + * [fix] Workaround for the bad people who are not using IPv6 yet + + -- Julien Malik Wed, 09 Mar 2016 08:46:41 +0100 + +yunohost (2.3.10.1) testing; urgency=low + + * [fix] Oops, debian/install prevent subpackages installation + + -- Jérôme Lebleu Tue, 08 Mar 2016 23:55:28 +0100 + +yunohost (2.3.10) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Introduce new 'requirements' manifest key (close #113) + * [enh] Implement package version specifier and use it for min_version + * [enh] Use https to retrieve public IP address + * [enh] Use a common method to retrieve public IP address + * [enh] Rely on APT python library to retrieve packages version + * [fix] Use http to retrieve public IPv6 due to Let's Encrypt restriction + + [ Julien Malik ] + * [fix] rspamd/rmilter now uses redis-server instead of memcached + * [fix] do not output warnings when services are already + uninstalled/disabled (fix #215) + * [enh] remove useless '|| true'. set -e does not exit for complex commands + * [enh] slaptest outputs on stderr, so generates a WARNING. make it quiet + * [enh] first stop rspamd.service, then start rspamd.socket + * [fix] use ip6.yunohost.org service to retrieve public IPv6 + * [fix] Protect against empty files + + [ opi ] + * [enh] Add dummy DMARC support if DKIM already supported. #233 + * [fix] Remove Dovecot autocreate deprecated plugin. Fix #103 + * [fix] Catch ConnectionError from requests package + * [fix] Multiple exceptions syntax. + * [fix] Wrong command name. + + -- Jérôme Lebleu Tue, 08 Mar 2016 23:32:52 +0100 + +yunohost (2.3.9) testing; urgency=low + + [ Cédric Félizard ] + * [fix] Don't emit Nginx version + + [ Jérôme Lebleu ] + * [enh] Store backup size and check free space before restoring (bugfix #189) + * [enh] Allow to install a given git reference of an app + * [enh] Add the repository from where the app is defined in app_list + * [enh] Add DKIM DNS record in domain_dns_conf (close #198) + * [enh] Get completely rid of os.system calls in _fetch_app_from_git + * [enh] Attempt to improve readability of domain_dns_conf + * [enh] Replace msignals.display by logging + * [i18n] Use named arguments for remaining translations in app.py + * [fix] Clean tmp directory when restoration is cancelled + * [fix] Improve and fix app fetching other than from github + * [fix] Start socket and stop rspamd/rmilter services in conf_regen (bugfix #196) + * [fix] Restart the service if reloading fails in conf_regen (bugfix #195) + * [fix] Review how app settings are initialized and set + * [fix] Open port 1900 when enabling UPnP (fix #30) + * [fix] Remove useless raw argument in domain_list + * [fix] Regenerate Rmilter conf on domain addition for DKIM key + * [fix] Add an example for ynh_get_plain_key helper usage + * [fix] Keep 'avail' key - removed from glances - in disk fs monitoring + * [fix] Be less restricitve on network interfaces name in monitoring + + [ julienmalik ] + * [fix] access to /var/lib/metronome/ needs sudo permissions + * [fix] missing brackets for testing saferemove output + * [fix] misssing sudo when removing files in /etc/nginx + * [fix] Set 'app status file not found' log level to debug + * [fix] Do not raise if one app upgrade fails and regen SSOwat conf + + [ taziden ] + * [enh] hardening postfix tls configuration + + [ Moul ] + * [enh] add '-ttl' parameter to 'domain dns conf' command. + * [enh] also get ssowat version with '-v' argement. + + [ opi ] + * [enh] Replace msignals.display by logging in tools.py + * [enh] More descriptive names for XMPP services + + -- Jérôme Lebleu Wed, 02 Mar 2016 20:45:41 +0100 + +yunohost (2.3.8) testing; urgency=low + + [ Jérôme Lebleu ] + * [fix] Add yunohost-firewall to services.yml + * [fix] Handle empty app settings error when it's not correctly installed + + [ opi ] + * [fix] head opening tag may have attributes. + + [ zamentur ] + * [fix] Add backup/restore hooks for ynh_conf_currenthost + + -- Jérôme Lebleu Sat, 13 Feb 2016 19:28:51 +0100 + +yunohost (2.3.7) testing; urgency=low + + [ Laurent Peuch ] + * [enh] new command to generate DNS configuration for a given domain name + + [ Jérôme Lebleu ] + * [fix] Save LDAP database when switching to MDB (bugfix #169) + * [fix] Review LDAP backup and restore hooks + * [enh] Replace msignals.display by logging in backup category + * [enh] Add a ynh_app_setting_delete helper + * [enh] Update rmilter hook and dependencies for 1.7 release + * [enh] Set minimum uid and ignore local users in nslcd.conf + * [enh] Use a common function to retrieve app settings + * [enh] Check the slapd config file at first in conf_regen + * [fix] Validate arguments and app settings in app_map (bugfix #168) + * [fix] Replace udisks-glue by udisks2 and only suggest it + * [fix] Correct condition syntax in metronome conf_regen hook + * [fix] Allow false and 0 as non-empty values for an app argument + * [fix] Some improvements and fixes to actions related to app access + * [fix] Remove old services and add rmilter/rspamd + * [fix] Correct log file of yunohost-api in services.yml + * [i18n] Use named variables in app category translations + + -- Jérôme Lebleu Sun, 07 Feb 2016 18:56:13 +0100 + +yunohost (2.3.6) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Pass app id to scripts and remove hook_check action + * [enh] Rely only on app_id argument for multi-instances apps + * [enh] Add support for app argument 'type' defined in the manifest + * [enh] Integrate 'optional' key of arguments in app manifest + * [enh] Implement 'boolean' argument type support in app manifest + * [enh] Add ping util as recommended package + * [enh] Add a helper to check if a user exists on the system + * [enh] Provide bash helpers for packages manipulation (wip #97) + * [enh] Add ynh_package_update helper and call it in install_from_equivs + * [fix] Do not block while set main domain + * [fix] Add GRANT OPTION in ynh_mysql_create_db helper + * [fix] Validate app argument choice for input value too + * [fix] Log rotation is already handled by WatchedFileHandler (fixbug #137) + * [fix] Use rmilter as a socket-activated service + * [fix] Parse app arguments before creating app folder and settings + * [fix] Use INFO logging level if app setting is not found + * [fix] Split service_configuration_conflict translation key (fixbug #136) + * [fix] Set default value of boolean argument type to false if unset + * [fix] Remove useless SPF setting in Postfix configuration (fixbug #150) + * [fix] Add procmail to packages dependencies + * [i18n] Review translations and keys related to app arguments + + [ Sebastien Badia ] + * hooks: Use a more elegant grep command for mysql process check + + -- Jérôme Lebleu Sun, 17 Jan 2016 02:57:53 +0100 + +yunohost (2.3.5) testing; urgency=low + + [ opi ] + * [enh] Get app label for installed app in app list + * [enh] Short cache on handlebars templates + + [ Jérôme Lebleu ] + * [enh] Allow to pass the admin password as argument in the cli + * [enh] Add main domain GET route + * [enh] Provide bash helpers for MySQL databases and app settings (wip #97) + * [enh] Rename ynh_password bash helper to ynh_string_random + * [fix] Check app min_version with yunohost package (fixbug #113) + * [fix] Use --output-as instead of deprecated options + * [fix] Prevent error if unset variable is treated in utils helper + * [doc] Improve usage and add examples for user helpers + * [i18n] Update translations from Transifex belatedly + + -- Jérôme Lebleu Thu, 24 Dec 2015 10:55:36 +0100 + +yunohost (2.3.4) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Make use of call_async_output in hook_exec to get output in real time + * [fix] Display a more detailed message when yunohost-firewall is stopped + * [fix] Prevent insserv warning when using systemd at package postinst + * [fix] Log real exception string error in hook_callback + * [fix] Add yunohost-firewall.service but do not enable it + + [ julienmalik ] + * [fix] Log for rmilter instead of rspamd + * [fix] Do not exit at first service which can't be stopped + + -- Jérôme Lebleu Tue, 17 Nov 2015 11:10:42 +0100 + +yunohost (2.3.3) testing; urgency=low + + * [fix] Do not modify handlers with root_handlers in bin/yunohost + + -- Jérôme Lebleu Sun, 15 Nov 2015 15:00:04 +0100 + +yunohost (2.3.2) testing; urgency=low + + [ Jérôme Lebleu ] + * [fix] Do not rely on dh_installinit and restart service after upgrade + * [fix] Add tty in root handlers if debug is set in bin/yunohost + + [ kload ] + * [fix] Do not remove the global_script directory + * [fix] Unexpected warnings comming from stderr + * [enh] Warn the user about the waiting at the configuration generation + * [fix] Delayed upgrade of the package 'yunohost' + + -- Jérôme Lebleu Sun, 15 Nov 2015 14:03:39 +0100 + +yunohost (2.3.1) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Add logrotate configuration + * [enh] Allow to set default options for yunohost-api service + * [enh] Add bash completion for bin/yunohost + * [enh] Make use of new logging facilities in firewall, hook and service + * [enh] Refactor bin/yunohost and bin/yunohost-api to follow moulinette + changes and provide help for global arguments + * [enh] Split stdout/stderr wrapping in hook_exec and add a no_trace option + * [fix] Create home directory during login (fixbug #80) + * [fix] Keep compat with deprecated --plain and --json in the cli + * [fix] Do not restrict warning to tty in service_saferemove + * [fix] Enable yunohost-api systemd service manually + + [ kload ] + * [fix] Restart Dovecot at the end of Rspamd configuration script + * [fix] Translate regenconf messages in English and French + + -- Jérôme Lebleu Sun, 15 Nov 2015 00:23:27 +0100 + +yunohost (2.3.0) testing; urgency=low + + [ breaking changes ] + * Merge all packages into one + * Wheezy compatibility drop + + [ features ] + * Implement a regenconf command + * Implement local backup/restore functions + * Allow to filter which app to backup/restore + * Replace the email stack by Rspamd/Rmilter + * Create shallow clone to increase app installation time + * Add helper bash functions for apps developers + * Update app_info to show app installation status + * Implement an app_debug function + * IPv6 compatibility enhancement + + [ bugfixes ] + * Display YunoHost packages versions (fix #11) + * Allow empty app arguments in app_install + * Invalidate passwd at user creation/deletion (fix #70) + * Fix skipped_urls for each domain and #68 + * Correct logger object in backup_list (fix #75) + * 2nd installation of apps with a hooks directory + * Add netcat-openbsd dependency + * Ensure that arguments are passed to the hook as string + * Use SSL/TLS to fetch app list + * IPv6 record in DynDNS + * Use sudo to execute hook script + * Debian postinst script : only respond to configure + * Handle SSL generation better + * Ensure that the service yunohost-api is always running + * Sieve permission denied + * Do not enable yunohost-firewall service at install + * Open port 1900 when enabling UPnP (fixes #30) + + [ other ] + * Add AGPL license + * French translation using Weblate + + -- kload Tue, 03 Nov 2015 11:55:19 +0000 + +moulinette-yunohost (2.3.1) testing; urgency=low + + [ Julien Malik ] + * [fix] Indent postinst script uniformly + * [enh] postinst : only respond to configure + * [lintian] fix output-of-updaterc.d-not-redirected-to-dev-null yunohost-api postinst + * [lintian] fix postrm-contains-additional-updaterc.d-calls etc/init.d/yunohost-firewall + * [lintian] fix init.d-script-missing-lsb-description + * [lintian] fix init.d-script-does-not-implement-required-option etc/init.d/yunohost-api force-reload + * [lintian] fix init.d-script-does-not-implement-required-option etc/init.d/yunohost-firewall force-reload + * [lintian] fix executable-not-elf-or-script usr/lib/moulinette/yunohost/__init__.py + * [lintian] fix script-not-executable for backup/restore hooks + * [fix] remove copy-pasted comments + + [ Le Kload ] + * [fix] Ensure that arguments are passed to the hook as string + + [ opi ] + * [fix] Use SSL/TLS to fetch app list + + [ Jérôme Lebleu ] + * [fix] Replace bind9 by dnsmasq in services definition + + [ kload ] + * [fix] IPv6 record in DynDNS + + [ Jocelyn Delande ] + * [fix] fix UnboundLocalError on ConnectionError + * [enh] Determine the public IPv6 locally + + -- kload Sun, 27 Sep 2015 10:36:34 +0000 + +moulinette-yunohost (2.3.0) testing; urgency=low + + [ M5oul ] + * Add AGPL license + + [ ZeHiro ] + * [fix] Fix skipped_urls for each domain and #68 + + [ nahoj ] + * typo + + [ Jérôme Lebleu ] + * [fix] Correct logger object in backup_list (fix #75) + + [ zamentur ] + * [fix] 2nd installation of apps with a hooks directory + + [ Julien Malik ] + * Add helper bash functions for apps developers + + [ opi ] + * [enh] Add ynh_user_exists helper. + * [fix] Use one file for all user related helpers. + + [ Adrien Beudin ] + * [fix] add netcat-openbsd packages in depends + + -- Jérôme Lebleu Tue, 08 Sep 2015 14:19:28 +0200 + +moulinette-yunohost (2.2.3) stable; urgency=low + + * [fix] Catch proper exception in backup_list (fix #65) + * [fix] Display YunoHost packages versions (fix #11) + * [fix] Allow empty app arguments in app_install + * [fix] Invalidate passwd at user creation/deletion (fix #70) + * [fix] Add minimum moulinette version in debian/control + + -- Jérôme Lebleu Sat, 18 Jul 2015 16:42:59 +0200 + +moulinette-yunohost (2.2.2) stable; urgency=low + + * [fix] Avoid cd errors + + -- kload Tue, 02 Jun 2015 15:19:07 +0000 + +moulinette-yunohost (2.2.1) stable; urgency=low + + [ Jérôme Lebleu ] + * [fix] Retrieve apps settings in a safer way (fix #61) + * [enh] Add post_user_delete hook + + -- kload Sat, 23 May 2015 15:14:54 +0200 + +moulinette-yunohost (2.2.0) stable; urgency=low + + * Bumping version to 2.2.0 + + -- kload Fri, 08 May 2015 19:15:07 +0000 + +moulinette-yunohost (2.1.11) testing; urgency=low + + * [fix] Include ca-certificates in dependencies + + -- kload Fri, 08 May 2015 19:07:57 +0000 + +moulinette-yunohost (2.1.10) testing; urgency=low + + * [fix] Add python-apt to requirements and remove rubygems + + -- kload Fri, 08 May 2015 18:05:37 +0000 + +moulinette-yunohost (2.1.9) testing; urgency=low + + * [fix] Allow SSH port in TCP only + + -- kload Fri, 08 May 2015 16:13:07 +0000 + +moulinette-yunohost (2.1.8) testing; urgency=low + + * [fix] Allow old custom applications compatibility + * [fix] Mandatory protocol for backward compatibility + + -- kload Fri, 08 May 2015 00:52:50 +0000 + +moulinette-yunohost (2.1.7) testing; urgency=low + + * [fix] Keep username in user list for compatibility + + -- kload Tue, 05 May 2015 20:00:55 +0200 + +moulinette-yunohost (2.1.6) testing; urgency=low + + [ Adrien Beudin ] + * remove yunohost-firewall package + * fix depends of yunohost-firewall + + -- kload Tue, 05 May 2015 11:29:12 +0200 + +moulinette-yunohost (2.1.5) testing; urgency=low + + [ Adrien Beudin ] + * [fix] add bind9utils + + [ Julien VAUBOURG ] + * Disable DNS forwarding + + -- kload Mon, 04 May 2015 16:26:02 +0200 + +moulinette-yunohost (2.1.4) testing; urgency=low + + [ Adrien Beudin ] + * [fix] yunohost firewall init script + * [fix] add depends on yunohost-firewall + * [fix] add rules for yunohost-firewall init script + + -- kload Fri, 01 May 2015 21:15:30 +0000 + +moulinette-yunohost (2.1.3) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Add support for user mailbox size quota + * [enh] List users by username + * [fix] Adapt broken calls to user_list + * [fix] Allow empty users argument in app_add/removeaccess + + [ root ] + * [fix] show usage quota status + + [ Adrien Beudin ] + * [fix] show usage quota status + * Revert "[fix] Allow empty users argument in app_add/removeaccess" + * [fix] show usage quota status + * [enh] Add check STMP outgoing port + * [fix] Remove import subprocess + + [ Jérôme Lebleu ] + * Revert "Revert "[fix] Allow empty users argument in app_add/removeaccess"" + + [ Adrien Beudin ] + * [enh] Add MX check + Refactoring + * [fix] user quota + * [fix] check mx ID + * [fix] remove domain beudi + * [fix] network check + * [fix] readd yunohost-firewall init script + + -- kload Fri, 01 May 2015 15:06:37 +0000 + +moulinette-yunohost (2.1.2) testing; urgency=low + + [ Jérôme Lebleu ] + * [fix] Ooops, so much yolo kills yolo and actionsmap + * [fix] Consider new gTLDs in email and domain regex (fix #46) + * [fix] Open TCP port 587 for mail submission + * [i18n] Update translations from Transifex + * [i18n] Remove unused 'yunohost' translation key + * [i18n] Fix JSON syntax errors + * [fix] Catch ConnectionError from requests package + + [ zamentur ] + * [enh] Add app settings to redirect request + + [ Julien Malik ] + * [fix] concatenate CA certificate with domain certificate + * [fix] Block XMPP Bosh port 5290 + + -- Julien Malik Tue, 17 Mar 2015 16:44:04 +0100 + +moulinette-yunohost (2.1.1) testing; urgency=low + + * Bump version to 2.1.1 to bootstrap new build workflow + + -- Julien Malik Thu, 12 Feb 2015 13:32:37 +0100 + +moulinette-yunohost (2.0-rc~megusta33) test; urgency=low + + * Test build: [enh] Use dnsmasq + + -- Adrien Beudin Wed, 24 Dec 2014 17:04:29 +0100 + +moulinette-yunohost (2.0-rc~megusta32) test; urgency=low + + * Test build: [enh] Replace udiskie by udisks-glue + + -- Adrien Beudin Fri, 31 Oct 2014 19:36:18 +0100 + +moulinette-yunohost (2.0-rc~megusta31) test; urgency=low + + * Test build: [enh] Working backup and restore + + -- Adrien Beudin Sun, 26 Oct 2014 00:50:34 +0200 + +moulinette-yunohost (2.0-rc~megusta30) test; urgency=low + + * Test build: Fixes + + -- Adrien Beudin Sun, 26 Oct 2014 00:16:21 +0200 + +moulinette-yunohost (2.0-rc~megusta29) test; urgency=low + + * Test build: Restore function WIP + + -- Adrien Beudin Sat, 25 Oct 2014 23:23:25 +0200 + +moulinette-yunohost (2.0-rc~megusta28) test; urgency=low + + * Test build: typo + + -- Adrien Beudin Sat, 25 Oct 2014 20:41:10 +0200 + +moulinette-yunohost (2.0-rc~megusta27) test; urgency=low + + * Test build: typo + + -- Adrien Beudin Sat, 25 Oct 2014 20:09:26 +0200 + +moulinette-yunohost (2.0-rc~megusta26) test; urgency=low + + * Test build: typo + + -- Adrien Beudin Sat, 25 Oct 2014 19:38:54 +0200 + +moulinette-yunohost (2.0-rc~megusta25) test; urgency=low + + * Test build: Backup / restore WIP + + -- Adrien Beudin Sat, 25 Oct 2014 18:58:03 +0200 + +moulinette-yunohost (2.0-rc~megusta24) test; urgency=low + + * Test build: [enh] add firewall init script + + -- Adrien Beudin Tue, 16 Sep 2014 14:29:10 +0200 + +moulinette-yunohost (2.0-rc~megusta23) test; urgency=low + + * Test build: [enh] Add avahi daemon + + -- Adrien Beudin Tue, 16 Sep 2014 09:43:51 +0200 + +moulinette-yunohost (2.0-rc~megusta22) megusta; urgency=low + + * Production build: Bump version + + -- Adrien Beudin Thu, 31 Jul 2014 12:31:32 +0200 + +moulinette-yunohost (2.0-rc~megusta21) megusta; urgency=low + + * Production build: Bump version + + -- Adrien Beudin Thu, 31 Jul 2014 12:09:57 +0200 + +moulinette-yunohost (2.0-rc~megusta20) test; urgency=low + + * Test build: Update from git 31ef39e4e + + -- Adrien Beudin Mon, 28 Jul 2014 18:09:50 +0200 + +moulinette-yunohost (2.0-rc~megusta19) megusta; urgency=low + + * Production build: bump version + + -- Adrien Beudin Mon, 21 Jul 2014 16:23:15 +0200 + +moulinette-yunohost (2.0-rc~megusta18) megusta; urgency=low + + * Production build: Fix upgrade and various fixes + + -- Adrien Beudin Mon, 21 Jul 2014 16:16:57 +0200 + +moulinette-yunohost (2.0-rc~megusta17) test; urgency=low + + * Test build: Various fixes + + -- Adrien Beudin Mon, 21 Jul 2014 16:10:40 +0200 + +moulinette-yunohost (2.0-rc~megusta16) test; urgency=low + + * Test build: Update from git fed3e6f67 + + -- Adrien Beudin Fri, 18 Jul 2014 18:37:44 +0200 + +moulinette-yunohost (2.0-rc~megusta15) megusta; urgency=low + + * Production build: Update from git 496b4910159d + + -- Adrien Beudin Tue, 01 Jul 2014 19:08:29 +0200 + +moulinette-yunohost (2.0-rc~megusta14) test; urgency=low + + * Test build: Update from git 496b4910159d + + -- Adrien Beudin Tue, 01 Jul 2014 19:01:33 +0200 + +moulinette-yunohost (2.0-rc~megusta13) test; urgency=low + + * Test build: [fix] Init script + + -- Adrien Beudin Mon, 30 Jun 2014 17:52:52 +0200 + +moulinette-yunohost (2.0-rc~megusta12) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 28 Jun 2014 15:07:05 +0200 + +moulinette-yunohost (2.0-rc~megusta11) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 28 Jun 2014 10:59:30 +0200 + +moulinette-yunohost (2.0-rc~megusta10) test; urgency=low + + * Test build: [fix] Properly separate upnp and firewall + + -- Adrien Beudin Thu, 26 Jun 2014 12:40:47 +0200 + +moulinette-yunohost (2.0-rc~megusta9) test; urgency=low + + * Test build: [fix] API init script + + -- Adrien Beudin Wed, 25 Jun 2014 22:31:51 +0200 + +moulinette-yunohost (2.0-rc~megusta8) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Tue, 24 Jun 2014 13:56:53 +0200 + +moulinette-yunohost (2.0-rc~megusta7) megusta; urgency=low + + * Production build: [fix] copy firewall.yml file + + -- Adrien Beudin Sat, 14 Jun 2014 13:42:34 +0200 + +moulinette-yunohost (2.0-rc~megusta6) megusta; urgency=low + + * Production build: [fix] Wrong translation key in app module + + -- Adrien Beudin Thu, 12 Jun 2014 19:13:54 +0200 + +moulinette-yunohost (2.0-rc~megusta5) test; urgency=low + + * Test build: [fix] Add --no-websocket option to yunohost-api when + gevent segfault + + -- Adrien Beudin Thu, 12 Jun 2014 09:51:56 +0200 + +moulinette-yunohost (2.0-rc~megusta4) test; urgency=low + + * Test build: [fix] Add --no-websocket option to yunohost-api when + gevent segfault + + -- Adrien Beudin Thu, 12 Jun 2014 09:43:13 +0200 + +moulinette-yunohost (2.0-rc~megusta3) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Thu, 12 Jun 2014 09:28:32 +0200 + +moulinette-yunohost (2.0-rc~megusta2) megusta; urgency=low + + * Production build: Bump version + + -- Adrien Beudin Mon, 09 Jun 2014 01:43:09 +0200 + +moulinette-yunohost (2.0-rc~megusta1) test; urgency=low + + * Bump version + + -- Adrien Beudin Mon, 09 Jun 2014 00:49:09 +0200 + +moulinette-yunohost (2.0~megusta44) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 09 Jun 2014 00:49:09 +0200 + +moulinette-yunohost (2.0~megusta43) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Fri, 06 Jun 2014 12:24:33 +0200 + +moulinette-yunohost (2.0~megusta42) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 02 Jun 2014 22:10:12 +0200 + +moulinette-yunohost (2.0~megusta41) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 02 Jun 2014 11:29:54 +0200 + +moulinette-yunohost (2.0~megusta40) test; urgency=low + + * Test build: [fix] Remove --no-ldap argument while fetching applist + + -- Adrien Beudin Sun, 01 Jun 2014 21:44:08 +0200 + +moulinette-yunohost (2.0~megusta39) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 31 May 2014 11:37:40 +0200 + +moulinette-yunohost (2.0~megusta38) test; urgency=low + + * Test build: [fix] Move udsikie init script + + -- Adrien Beudin Fri, 30 May 2014 13:37:38 +0200 + +moulinette-yunohost (2.0~megusta37) test; urgency=low + + * Test build: [fix] dependencies + + -- Adrien Beudin Thu, 29 May 2014 21:34:45 +0200 + +moulinette-yunohost (2.0~megusta36) test; urgency=low + + * Test build: [fix] dependencies + + -- Adrien Beudin Thu, 29 May 2014 21:21:52 +0200 + +moulinette-yunohost (2.0~megusta35) test; urgency=low + + * Test build: [fix] Install udiskie via pip + + -- Adrien Beudin Thu, 29 May 2014 10:48:21 +0200 + +moulinette-yunohost (2.0~megusta34) test; urgency=low + + * Test build: [fix] Install udiskie via pip + + -- Adrien Beudin Thu, 29 May 2014 10:20:24 +0200 + +moulinette-yunohost (2.0~megusta33) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Wed, 28 May 2014 21:05:27 +0200 + +moulinette-yunohost (2.0~megusta32) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Wed, 28 May 2014 15:38:24 +0200 + +moulinette-yunohost (2.0~megusta31) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Tue, 27 May 2014 14:44:24 +0200 + +moulinette-yunohost (2.0~megusta30) test; urgency=low + + * Test build: [fix] Reload SSOwat conf at postinst + + -- Adrien Beudin Tue, 27 May 2014 13:54:53 +0200 + +moulinette-yunohost (2.0~megusta29) test; urgency=low + + * Test build: [fix] Reload SSOwat conf at postinst + + -- Adrien Beudin Tue, 27 May 2014 13:51:08 +0200 + +moulinette-yunohost (2.0~megusta28) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Tue, 27 May 2014 12:44:06 +0200 + +moulinette-yunohost (2.0~megusta27) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 26 May 2014 13:40:28 +0200 + +moulinette-yunohost (2.0~megusta26) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 24 May 2014 21:33:08 +0200 + +moulinette-yunohost (2.0~megusta25) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 19 May 2014 17:29:32 +0200 + +moulinette-yunohost (2.0~megusta24) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 19 May 2014 17:28:57 +0200 + +moulinette-yunohost (2.0~megusta23) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 19 May 2014 13:06:04 +0200 + +moulinette-yunohost (2.0~megusta22) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 19 May 2014 10:50:53 +0200 + +moulinette-yunohost (2.0~megusta21) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 19 May 2014 10:38:27 +0200 + +moulinette-yunohost (2.0~megusta20) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Mon, 19 May 2014 10:34:23 +0200 + +moulinette-yunohost (2.0~megusta19) test; urgency=low + + * Test build: [enh] Move init script in the moulinette-yunohost + package + + -- Adrien Beudin Sun, 18 May 2014 15:15:23 +0200 + +moulinette-yunohost (2.0~megusta18) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sun, 18 May 2014 12:11:17 +0200 + +moulinette-yunohost (2.0~megusta17) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sun, 18 May 2014 11:57:39 +0200 + +moulinette-yunohost (2.0~megusta16) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sun, 18 May 2014 11:14:48 +0200 + +moulinette-yunohost (2.0~megusta15) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 17 May 2014 22:30:18 +0200 + +moulinette-yunohost (2.0~megusta14) test; urgency=low + + * Test build: [fix] Check if firewall.yml is old + + -- Adrien Beudin Sat, 17 May 2014 22:21:48 +0200 + +moulinette-yunohost (2.0~megusta13) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 17 May 2014 22:10:52 +0200 + +moulinette-yunohost (2.0~megusta12) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 17 May 2014 21:58:31 +0200 + +moulinette-yunohost (2.0~megusta11) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 17 May 2014 00:35:13 +0200 + +moulinette-yunohost (2.0~megusta10) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Sat, 17 May 2014 00:16:12 +0200 + +moulinette-yunohost (2.0~megusta9) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Fri, 16 May 2014 23:19:51 +0200 + +moulinette-yunohost (2.0~megusta8) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Fri, 16 May 2014 21:46:50 +0200 + +moulinette-yunohost (2.0~megusta7) test; urgency=low + + * Test build: Remove cache after upgrade + + -- Adrien Beudin Fri, 16 May 2014 20:57:45 +0200 + +moulinette-yunohost (2.0~megusta6) test; urgency=low + + * Test build: Update from git + + -- Adrien Beudin Fri, 16 May 2014 20:05:08 +0200 + +moulinette-yunohost (2.0~megusta5) test; urgency=low + + * Test build: Update init script + + -- Adrien Beudin Fri, 16 May 2014 18:23:14 +0200 + +moulinette-yunohost (2.0~megusta4) test; urgency=low + + * Test build: actionSSSSS map + + -- Adrien Beudin Fri, 16 May 2014 18:05:45 +0200 + +moulinette-yunohost (2.0~megusta3) test; urgency=low + + * Test build: actionSSSSS map + + -- Adrien Beudin Fri, 16 May 2014 17:31:04 +0200 + +moulinette-yunohost (2.0~megusta2) test; urgency=low + + * Test build: Bump version + + -- Adrien Beudin Fri, 16 May 2014 16:28:45 +0200 + +moulinette-yunohost (2.0~megusta1) test; urgency=low + + * Test build: Add moulinette-yunohost package + + -- Adrien Beudin Fri, 16 May 2014 16:05:31 +0200 + +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 new file mode 100644 index 0000000..4303a17 --- /dev/null +++ b/debian/control @@ -0,0 +1,60 @@ +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 (>= 3.0) +Standards-Version: 3.9.6 +Homepage: https://yunohost.org/ + +Package: yunohost +Essential: yes +Architecture: all +Depends: python3-all (>= 3.11), + , moulinette (>= 12.1), ssowat (>= 12.0), + , python3-psutil, python3-requests, python3-dnspython, python3-openssl + , python3-miniupnpc, python3-dbus, python3-jinja2 (>= 3.0) + , python3-toml, python3-packaging, python3-publicsuffix2 + , python3-ldap, python3-zeroconf (>= 0.47), python3-lexicon, + , python3-cryptography, python3-jwt, python3-passlib, python3-magic + , python-is-python3, python3-pydantic, python3-email-validator + , python3-sortedcollections, python3-sdbus, python3-debian + , udisks2, udisks2-bcache, udisks2-btrfs, udisks2-lvm2, udisks2-zram + , smartmontools + , python3-zmq + , nginx, nginx-extras (>=1.22) + , apt, apt-transport-https, apt-utils, aptitude, dirmngr + , openssh-server, nftables, 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, libsasl2-modules + , dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam + , opendkim-tools, opendkim, postsrsd, procmail, mailutils + , acl + , git, curl, wget, cron, unzip, jq, bc, at, procps, j2cli + , lsb-release, haveged, fake-hwclock, lsof, whois + , xz-utils, zstd +Recommends: yunohost-admin (>= 12.1), yunohost-portal (>= 12.1) + , ntp, inetutils-ping | iputils-ping + , bash-completion, rsyslog + , unattended-upgrades + , libdbd-ldap-perl, libnet-dns-perl +Conflicts: iptables-persistent + , apache2 + , bind9 + , openresolv + , systemd-resolved + , nginx-extras (>= 1.23) + , openssl (>= 3.1) + , slapd (>= 2.6) + , dovecot-core (>= 1:2.4) + , fail2ban (>= 1.1) + , nftables (>= 1.1) +Description: manageable and configured self-hosting server + YunoHost aims to make self-hosting accessible to everyone. It configures + an email, Web and IM server alongside a LDAP base. It also provides + facilities to manage users, domains, apps and so. + . + This package contains YunoHost scripts and binaries to be used by the + moulinette. It allows one to manage the server with a command-line tool + and an API. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..59483b8 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,678 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://github.com/YunoHost/yunohost + +Files: * +Copyright: 2015 YUNOHOST.ORG +License: AGPL-3 + +License: AGPL-3 + OPA is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License, version 3, as published by + the Free Software Foundation. + . + OPA 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. + . + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + . + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + . + Preamble + . + The GNU Affero General Public License is a free, copyleft license for + software and other kinds of works, specifically designed to ensure + cooperation with the community in the case of network server software. + . + The licenses for most software and other practical works are designed + to take away your freedom to share and change the works. By contrast, + our General Public Licenses are intended to guarantee your freedom to + share and change all versions of a program--to make sure it remains free + software for all its users. + . + When we speak of free software, we are referring to freedom, not + price. Our General Public Licenses are designed to make sure that you + have the freedom to distribute copies of free software (and charge for + them if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs, and that you know you can do these things. + . + Developers that use our General Public Licenses protect your rights + with two steps: (1) assert copyright on the software, and (2) offer + you this License which gives you legal permission to copy, distribute + and/or modify the software. + . + A secondary benefit of defending all users' freedom is that + improvements made in alternate versions of the program, if they + receive widespread use, become available for other developers to + incorporate. Many developers of free software are heartened and + encouraged by the resulting cooperation. However, in the case of + software used on network servers, this result may fail to come about. + The GNU General Public License permits making a modified version and + letting the public access it on a server without ever releasing its + source code to the public. + . + The GNU Affero General Public License is designed specifically to + ensure that, in such cases, the modified source code becomes available + to the community. It requires the operator of a network server to + provide the source code of the modified version running there to the + users of that server. Therefore, public use of a modified version, on + a publicly accessible server, gives the public access to the source + code of the modified version. + . + An older license, called the Affero General Public License and + published by Affero, was designed to accomplish similar goals. This is + a different license, not a version of the Affero GPL, but Affero has + released a new version of the Affero GPL which permits relicensing under + this license. + . + The precise terms and conditions for copying, distribution and + modification follow. + . + TERMS AND CONDITIONS + . + 0. Definitions. + . + "This License" refers to version 3 of the GNU Affero General Public License. + . + "Copyright" also means copyright-like laws that apply to other kinds of + works, such as semiconductor masks. + . + "The Program" refers to any copyrightable work licensed under this + License. Each licensee is addressed as "you". "Licensees" and + "recipients" may be individuals or organizations. + . + To "modify" a work means to copy from or adapt all or part of the work + in a fashion requiring copyright permission, other than the making of an + exact copy. The resulting work is called a "modified version" of the + earlier work or a work "based on" the earlier work. + . + A "covered work" means either the unmodified Program or a work based + on the Program. + . + To "propagate" a work means to do anything with it that, without + permission, would make you directly or secondarily liable for + infringement under applicable copyright law, except executing it on a + computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the + public, and in some countries other activities as well. + . + To "convey" a work means any kind of propagation that enables other + parties to make or receive copies. Mere interaction with a user through + a computer network, with no transfer of a copy, is not conveying. + . + An interactive user interface displays "Appropriate Legal Notices" + to the extent that it includes a convenient and prominently visible + feature that (1) displays an appropriate copyright notice, and (2) + tells the user that there is no warranty for the work (except to the + extent that warranties are provided), that licensees may convey the + work under this License, and how to view a copy of this License. If + the interface presents a list of user commands or options, such as a + menu, a prominent item in the list meets this criterion. + . + 1. Source Code. + . + The "source code" for a work means the preferred form of the work + for making modifications to it. "Object code" means any non-source + form of a work. + . + A "Standard Interface" means an interface that either is an official + standard defined by a recognized standards body, or, in the case of + interfaces specified for a particular programming language, one that + is widely used among developers working in that language. + . + The "System Libraries" of an executable work include anything, other + than the work as a whole, that (a) is included in the normal form of + packaging a Major Component, but which is not part of that Major + Component, and (b) serves only to enable use of the work with that + Major Component, or to implement a Standard Interface for which an + implementation is available to the public in source code form. A + "Major Component", in this context, means a major essential component + (kernel, window system, and so on) of the specific operating system + (if any) on which the executable work runs, or a compiler used to + produce the work, or an object code interpreter used to run it. + . + The "Corresponding Source" for a work in object code form means all + the source code needed to generate, install, and (for an executable + work) run the object code and to modify the work, including scripts to + control those activities. However, it does not include the work's + System Libraries, or general-purpose tools or generally available free + programs which are used unmodified in performing those activities but + which are not part of the work. For example, Corresponding Source + includes interface definition files associated with source files for + the work, and the source code for shared libraries and dynamically + linked subprograms that the work is specifically designed to require, + such as by intimate data communication or control flow between those + subprograms and other parts of the work. + . + The Corresponding Source need not include anything that users + can regenerate automatically from other parts of the Corresponding + Source. + . + The Corresponding Source for a work in source code form is that + same work. + . + 2. Basic Permissions. + . + All rights granted under this License are granted for the term of + copyright on the Program, and are irrevocable provided the stated + conditions are met. This License explicitly affirms your unlimited + permission to run the unmodified Program. The output from running a + covered work is covered by this License only if the output, given its + content, constitutes a covered work. This License acknowledges your + rights of fair use or other equivalent, as provided by copyright law. + . + You may make, run and propagate covered works that you do not + convey, without conditions so long as your license otherwise remains + in force. You may convey covered works to others for the sole purpose + of having them make modifications exclusively for you, or provide you + with facilities for running those works, provided that you comply with + the terms of this License in conveying all material for which you do + not control copyright. Those thus making or running the covered works + for you must do so exclusively on your behalf, under your direction + and control, on terms that prohibit them from making any copies of + your copyrighted material outside their relationship with you. + . + Conveying under any other circumstances is permitted solely under + the conditions stated below. Sublicensing is not allowed; section 10 + makes it unnecessary. + . + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + . + No covered work shall be deemed part of an effective technological + measure under any applicable law fulfilling obligations under article + 11 of the WIPO copyright treaty adopted on 20 December 1996, or + similar laws prohibiting or restricting circumvention of such + measures. + . + When you convey a covered work, you waive any legal power to forbid + circumvention of technological measures to the extent such circumvention + is effected by exercising rights under this License with respect to + the covered work, and you disclaim any intention to limit operation or + modification of the work as a means of enforcing, against the work's + users, your or third parties' legal rights to forbid circumvention of + technological measures. + . + 4. Conveying Verbatim Copies. + . + You may convey verbatim copies of the Program's source code as you + receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice; + keep intact all notices stating that this License and any + non-permissive terms added in accord with section 7 apply to the code; + keep intact all notices of the absence of any warranty; and give all + recipients a copy of this License along with the Program. + . + You may charge any price or no price for each copy that you convey, + and you may offer support or warranty protection for a fee. + . + 5. Conveying Modified Source Versions. + . + You may convey a work based on the Program, or the modifications to + produce it from the Program, in the form of source code under the + terms of section 4, provided that you also meet all of these conditions: + . + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + . + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + . + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + . + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + . + A compilation of a covered work with other separate and independent + works, which are not by their nature extensions of the covered work, + and which are not combined with it such as to form a larger program, + in or on a volume of a storage or distribution medium, is called an + "aggregate" if the compilation and its resulting copyright are not + used to limit the access or legal rights of the compilation's users + beyond what the individual works permit. Inclusion of a covered work + in an aggregate does not cause this License to apply to the other + parts of the aggregate. + . + 6. Conveying Non-Source Forms. + . + You may convey a covered work in object code form under the terms + of sections 4 and 5, provided that you also convey the + machine-readable Corresponding Source under the terms of this License, + in one of these ways: + . + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + . + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + . + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + . + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + . + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + . + A separable portion of the object code, whose source code is excluded + from the Corresponding Source as a System Library, need not be + included in conveying the object code work. + . + A "User Product" is either (1) a "consumer product", which means any + tangible personal property which is normally used for personal, family, + or household purposes, or (2) anything designed or sold for incorporation + into a dwelling. In determining whether a product is a consumer product, + doubtful cases shall be resolved in favor of coverage. For a particular + product received by a particular user, "normally used" refers to a + typical or common use of that class of product, regardless of the status + of the particular user or of the way in which the particular user + actually uses, or expects or is expected to use, the product. A product + is a consumer product regardless of whether the product has substantial + commercial, industrial or non-consumer uses, unless such uses represent + the only significant mode of use of the product. + . + "Installation Information" for a User Product means any methods, + procedures, authorization keys, or other information required to install + and execute modified versions of a covered work in that User Product from + a modified version of its Corresponding Source. The information must + suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because + modification has been made. + . + If you convey an object code work under this section in, or with, or + specifically for use in, a User Product, and the conveying occurs as + part of a transaction in which the right of possession and use of the + User Product is transferred to the recipient in perpetuity or for a + fixed term (regardless of how the transaction is characterized), the + Corresponding Source conveyed under this section must be accompanied + by the Installation Information. But this requirement does not apply + if neither you nor any third party retains the ability to install + modified object code on the User Product (for example, the work has + been installed in ROM). + . + The requirement to provide Installation Information does not include a + requirement to continue to provide support service, warranty, or updates + for a work that has been modified or installed by the recipient, or for + the User Product in which it has been modified or installed. Access to a + network may be denied when the modification itself materially and + adversely affects the operation of the network or violates the rules and + protocols for communication across the network. + . + Corresponding Source conveyed, and Installation Information provided, + in accord with this section must be in a format that is publicly + documented (and with an implementation available to the public in + source code form), and must require no special password or key for + unpacking, reading or copying. + . + 7. Additional Terms. + . + "Additional permissions" are terms that supplement the terms of this + License by making exceptions from one or more of its conditions. + Additional permissions that are applicable to the entire Program shall + be treated as though they were included in this License, to the extent + that they are valid under applicable law. If additional permissions + apply only to part of the Program, that part may be used separately + under those permissions, but the entire Program remains governed by + this License without regard to the additional permissions. + . + When you convey a copy of a covered work, you may at your option + remove any additional permissions from that copy, or from any part of + it. (Additional permissions may be written to require their own + removal in certain cases when you modify the work.) You may place + additional permissions on material, added by you to a covered work, + for which you have or can give appropriate copyright permission. + . + Notwithstanding any other provision of this License, for material you + add to a covered work, you may (if authorized by the copyright holders of + that material) supplement the terms of this License with terms: + . + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + . + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + . + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + . + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + . + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + . + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + . + All other non-permissive additional terms are considered "further + restrictions" within the meaning of section 10. If the Program as you + received it, or any part of it, contains a notice stating that it is + governed by this License along with a term that is a further + restriction, you may remove that term. If a license document contains + a further restriction but permits relicensing or conveying under this + License, you may add to a covered work material governed by the terms + of that license document, provided that the further restriction does + not survive such relicensing or conveying. + . + If you add terms to a covered work in accord with this section, you + must place, in the relevant source files, a statement of the + additional terms that apply to those files, or a notice indicating + where to find the applicable terms. + . + Additional terms, permissive or non-permissive, may be stated in the + form of a separately written license, or stated as exceptions; + the above requirements apply either way. + . + 8. Termination. + . + You may not propagate or modify a covered work except as expressly + provided under this License. Any attempt otherwise to propagate or + modify it is void, and will automatically terminate your rights under + this License (including any patent licenses granted under the third + paragraph of section 11). + . + However, if you cease all violation of this License, then your + license from a particular copyright holder is reinstated (a) + provisionally, unless and until the copyright holder explicitly and + finally terminates your license, and (b) permanently, if the copyright + holder fails to notify you of the violation by some reasonable means + prior to 60 days after the cessation. + . + Moreover, your license from a particular copyright holder is + reinstated permanently if the copyright holder notifies you of the + violation by some reasonable means, this is the first time you have + received notice of violation of this License (for any work) from that + copyright holder, and you cure the violation prior to 30 days after + your receipt of the notice. + . + Termination of your rights under this section does not terminate the + licenses of parties who have received copies or rights from you under + this License. If your rights have been terminated and not permanently + reinstated, you do not qualify to receive new licenses for the same + material under section 10. + . + 9. Acceptance Not Required for Having Copies. + . + You are not required to accept this License in order to receive or + run a copy of the Program. Ancillary propagation of a covered work + occurring solely as a consequence of using peer-to-peer transmission + to receive a copy likewise does not require acceptance. However, + nothing other than this License grants you permission to propagate or + modify any covered work. These actions infringe copyright if you do + not accept this License. Therefore, by modifying or propagating a + covered work, you indicate your acceptance of this License to do so. + . + 10. Automatic Licensing of Downstream Recipients. + . + Each time you convey a covered work, the recipient automatically + receives a license from the original licensors, to run, modify and + propagate that work, subject to this License. You are not responsible + for enforcing compliance by third parties with this License. + . + An "entity transaction" is a transaction transferring control of an + organization, or substantially all assets of one, or subdividing an + organization, or merging organizations. If propagation of a covered + work results from an entity transaction, each party to that + transaction who receives a copy of the work also receives whatever + licenses to the work the party's predecessor in interest had or could + give under the previous paragraph, plus a right to possession of the + Corresponding Source of the work from the predecessor in interest, if + the predecessor has it or can get it with reasonable efforts. + . + You may not impose any further restrictions on the exercise of the + rights granted or affirmed under this License. For example, you may + not impose a license fee, royalty, or other charge for exercise of + rights granted under this License, and you may not initiate litigation + (including a cross-claim or counterclaim in a lawsuit) alleging that + any patent claim is infringed by making, using, selling, offering for + sale, or importing the Program or any portion of it. + . + 11. Patents. + . + A "contributor" is a copyright holder who authorizes use under this + License of the Program or a work on which the Program is based. The + work thus licensed is called the contributor's "contributor version". + . + A contributor's "essential patent claims" are all patent claims + owned or controlled by the contributor, whether already acquired or + hereafter acquired, that would be infringed by some manner, permitted + by this License, of making, using, or selling its contributor version, + but do not include claims that would be infringed only as a + consequence of further modification of the contributor version. For + purposes of this definition, "control" includes the right to grant + patent sublicenses in a manner consistent with the requirements of + this License. + . + Each contributor grants you a non-exclusive, worldwide, royalty-free + patent license under the contributor's essential patent claims, to + make, use, sell, offer for sale, import and otherwise run, modify and + propagate the contents of its contributor version. + . + In the following three paragraphs, a "patent license" is any express + agreement or commitment, however denominated, not to enforce a patent + (such as an express permission to practice a patent or covenant not to + sue for patent infringement). To "grant" such a patent license to a + party means to make such an agreement or commitment not to enforce a + patent against the party. + . + If you convey a covered work, knowingly relying on a patent license, + and the Corresponding Source of the work is not available for anyone + to copy, free of charge and under the terms of this License, through a + publicly available network server or other readily accessible means, + then you must either (1) cause the Corresponding Source to be so + available, or (2) arrange to deprive yourself of the benefit of the + patent license for this particular work, or (3) arrange, in a manner + consistent with the requirements of this License, to extend the patent + license to downstream recipients. "Knowingly relying" means you have + actual knowledge that, but for the patent license, your conveying the + covered work in a country, or your recipient's use of the covered work + in a country, would infringe one or more identifiable patents in that + country that you have reason to believe are valid. + . + If, pursuant to or in connection with a single transaction or + arrangement, you convey, or propagate by procuring conveyance of, a + covered work, and grant a patent license to some of the parties + receiving the covered work authorizing them to use, propagate, modify + or convey a specific copy of the covered work, then the patent license + you grant is automatically extended to all recipients of the covered + work and works based on it. + . + A patent license is "discriminatory" if it does not include within + the scope of its coverage, prohibits the exercise of, or is + conditioned on the non-exercise of one or more of the rights that are + specifically granted under this License. You may not convey a covered + work if you are a party to an arrangement with a third party that is + in the business of distributing software, under which you make payment + to the third party based on the extent of your activity of conveying + the work, and under which the third party grants, to any of the + parties who would receive the covered work from you, a discriminatory + patent license (a) in connection with copies of the covered work + conveyed by you (or copies made from those copies), or (b) primarily + for and in connection with specific products or compilations that + contain the covered work, unless you entered into that arrangement, + or that patent license was granted, prior to 28 March 2007. + . + Nothing in this License shall be construed as excluding or limiting + any implied license or other defenses to infringement that may + otherwise be available to you under applicable patent law. + . + 12. No Surrender of Others' Freedom. + . + If conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot convey a + covered work so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you may + not convey it at all. For example, if you agree to terms that obligate you + to collect a royalty for further conveying from those to whom you convey + the Program, the only way you could satisfy both those terms and this + License would be to refrain entirely from conveying the Program. + . + 13. Remote Network Interaction; Use with the GNU General Public License. + . + Notwithstanding any other provision of this License, if you modify the + Program, your modified version must prominently offer all users + interacting with it remotely through a computer network (if your version + supports such interaction) an opportunity to receive the Corresponding + Source of your version by providing access to the Corresponding Source + from a network server at no charge, through some standard or customary + means of facilitating copying of software. This Corresponding Source + shall include the Corresponding Source for any work covered by version 3 + of the GNU General Public License that is incorporated pursuant to the + following paragraph. + . + Notwithstanding any other provision of this License, you have + permission to link or combine any covered work with a work licensed + under version 3 of the GNU General Public License into a single + combined work, and to convey the resulting work. The terms of this + License will continue to apply to the part which is the covered work, + but the work with which it is combined will remain governed by version + 3 of the GNU General Public License. + . + 14. Revised Versions of this License. + . + The Free Software Foundation may publish revised and/or new versions of + the GNU Affero General Public License from time to time. Such new versions + will be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + . + Each version is given a distinguishing version number. If the + Program specifies that a certain numbered version of the GNU Affero General + Public License "or any later version" applies to it, you have the + option of following the terms and conditions either of that numbered + version or of any later version published by the Free Software + Foundation. If the Program does not specify a version number of the + GNU Affero General Public License, you may choose any version ever published + by the Free Software Foundation. + . + If the Program specifies that a proxy can decide which future + versions of the GNU Affero General Public License can be used, that proxy's + public statement of acceptance of a version permanently authorizes you + to choose that version for the Program. + . + Later license versions may give you additional or different + permissions. However, no additional obligations are imposed on any + author or copyright holder as a result of your choosing to follow a + later version. + . + 15. Disclaimer of Warranty. + . + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + . + 16. Limitation of Liability. + . + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES. + . + 17. Interpretation of Sections 15 and 16. + . + If the disclaimer of warranty and limitation of liability provided + above cannot be given local legal effect according to their terms, + reviewing courts shall apply local law that most closely approximates + an absolute waiver of all civil liability in connection with the + Program, unless a warranty or assumption of liability accompanies a + copy of the Program in return for a fee. + . + END OF TERMS AND CONDITIONS + . + How to Apply These Terms to Your New Programs + . + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + . + To do so, attach the following notices to the program. It is safest + to attach them to the start of each source file to most effectively + state the exclusion of warranty; and each file should have at least + the "copyright" line and a pointer to where the full notice is found. + . + + Copyright (C) + . + 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 . + . + Also add information on how to contact you by electronic and paper mail. + . + If your software can interact with users remotely through a computer + network, you should also make sure that it provides a way for users to + get its source. For example, if your program is a web application, its + interface could display a "Source" link that leads users to an archive + of the code. There are many ways you could offer source, and different + solutions will be better for different programs; see section 13 for the + specific requirements. + . + You should also get your employer (if you work as a programmer) or school, + if any, to sign a "copyright disclaimer" for the program, if necessary. + For more information on this, and how to apply and follow the GNU AGPL, see + . diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..aee8981 --- /dev/null +++ b/debian/install @@ -0,0 +1,10 @@ +bin/* /usr/bin/ +share/* /usr/share/yunohost/ +hooks/* /usr/share/yunohost/hooks/ +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/ +doc/zsh_completion.d/* /usr/share/zsh/vendor-completions/ +src/* /usr/lib/python3/dist-packages/yunohost/ diff --git a/debian/logrotate b/debian/logrotate new file mode 100644 index 0000000..4d1cc22 --- /dev/null +++ b/debian/logrotate @@ -0,0 +1,8 @@ +/var/log/yunohost/*.log { + weekly + rotate 4 + delaycompress + compress + notifempty + missingok +} diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..33d2e1a --- /dev/null +++ b/debian/postinst @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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 ... + if [ -d /etc/yunohost/apps/ ] && ls /etc/yunohost/apps/* >/dev/null 2>&1; then + echo "Sounds like /etc/yunohost/installed mysteriously disappeared ... You should probably contact the Yunohost support ..." + else + export YNH_SETTINGS='{}' + bash /usr/share/yunohost/hooks/conf_regen/01-yunohost init + bash /usr/share/yunohost/hooks/conf_regen/02-ssl init + bash /usr/share/yunohost/hooks/conf_regen/09-nslcd init + bash /usr/share/yunohost/hooks/conf_regen/46-nsswitch init + bash /usr/share/yunohost/hooks/conf_regen/43-dnsmasq init + bash /usr/share/yunohost/hooks/conf_regen/06-slapd init + bash /usr/share/yunohost/hooks/conf_regen/15-nginx init + bash /usr/share/yunohost/hooks/conf_regen/37-mdns init + fi + else + echo "Regenerating configuration, this might take a while..." + yunohost app ssowatconf + yunohost tools regen-conf --output-as none + + echo "Launching migrations..." + yunohost tools migrations run --auto + + echo "Re-diagnosing server health..." + if [[ -n "${YNH_SKIP_DIAGNOSIS_DURING_UPGRADE:-}" ]]; then + echo "(Skipping)" + else + yunohost diagnosis run --force + fi + + echo "Refreshing app catalog..." + 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 + then + if [[ "${YUNOHOST_API_RESTART_WILL_BE_HANDLED_BY_YUNOHOST:-}" != "yes" ]]; + then + systemctl restart yunohost-api + else + echo "(Delaying the restart of yunohost-api, this should automatically happen after the end of this upgrade)" + cat << EOF | at -M now >/dev/null 2>&1 +# Wait for apt / dpkg / yunohost to not be up anymore, hence the upgrade finished + +while pgrep -x apt || pgrep -x apt-get || pgrep dpkg || test -e /var/run/moulinette_yunohost.lock; +do + sleep 3 +done + +# Restart yunohost-api, though only if it wasnt already restarted by something else in the last 60 secs + +API_START_TIMESTAMP="\$(date --date="\$(systemctl show yunohost-api | grep ExecMainStartTimestamp= | awk -F= '{print \$2}')" +%s)" + +if [ "\$(( \$(date +%s) - \$API_START_TIMESTAMP ))" -ge 60 ]; +then + systemctl restart yunohost-api +fi +EOF + fi + fi +} + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +case "$1" in + configure) + do_configure + ;; + abort-upgrade | abort-remove | abort-deconfigure) ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 diff --git a/debian/postrm b/debian/postrm new file mode 100644 index 0000000..298df24 --- /dev/null +++ b/debian/postrm @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# See https://manpages.debian.org/testing/dpkg-dev/deb-postrm.5.en.html +# to understand when / how this script is called... + +set -e + +if [ "$1" = "purge" ]; then + rm -f /etc/yunohost/installed +fi + +if [ "$1" = "remove" ]; then + rm -f /etc/yunohost/installed +fi + +# Reset dpkg vendor to debian +# see https://wiki.debian.org/Derivatives/Guidelines#Vendor +rm -f /etc/dpkg/origins/default +ln -s /etc/dpkg/origins/debian /etc/dpkg/origins/default + +#DEBHELPER# + +exit 0 diff --git a/debian/prerm b/debian/prerm new file mode 100644 index 0000000..d974963 --- /dev/null +++ b/debian/prerm @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +set -e + +if [ -x /etc/init.d/yunohost-api ] && ! [ -d /run/systemd/system ]; then + invoke-rc.d yunohost-firewall stop || true +fi + +#DEBHELPER# + +exit 0 diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..b1ba333 --- /dev/null +++ b/debian/rules @@ -0,0 +1,15 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +%: + dh ${@} --with python3 + +override_dh_auto_build: + # Generate bash completion file + mkdir -p doc/bash_completion.d + python3 doc/generate_bash_completion.py -o doc/bash_completion.d/yunohost + # Generate zsh completion file + mkdir -p doc/zsh_completion.d + python3 doc/generate_zsh_completion.py -o doc/zsh_completion.d/_yunohost + # Generate man pages + python3 doc/generate_manpages.py --gzip --output doc/yunohost.8.gz diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/doc/api.html b/doc/api.html new file mode 100644 index 0000000..502d124 --- /dev/null +++ b/doc/api.html @@ -0,0 +1,42 @@ + + + + + + Swagger UI + + + + + + +
+ + + + + + + diff --git a/doc/bash_completion.sh.j2 b/doc/bash_completion.sh.j2 new file mode 100644 index 0000000..48e27f6 --- /dev/null +++ b/doc/bash_completion.sh.j2 @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# +# completion for yunohost +# automatically generated from the actionsmap +# + +_yunohost() +{ + local cur prev opts narg + COMPREPLY=() + + # the number of words already typed + narg=${#COMP_WORDS[@]} + + # the current word being typed + cur="${COMP_WORDS[COMP_CWORD]}" + + # If one is currently typing a category, + # match with categorys + if [[ $narg == 2 ]]; then + opts="{{ categories | join(" ") }}" + fi + + # If one already typed a category, + # match the actions or the subcategories of that category + if [[ $narg == 3 ]]; then + # the category typed + category="${COMP_WORDS[1]}" + {%- for category, catinfo in categories.items() %} + if [[ $category == "{{ category }}" ]]; then + opts="{{ (catinfo.actions + catinfo.subs.keys()|list) | join(" ") }}" + fi + {%- endfor %} + fi + + # If one already typed an action or a subcategory, + # match the actions of that subcategory + if [[ $narg == 4 ]]; then + # the category typed + category="${COMP_WORDS[1]}" + + # the action or the subcategory typed + action_or_subcategory="${COMP_WORDS[2]}" + + {%- for category, catinfo in categories.items() %} + {%- if catinfo.subs %} + if [[ $category == "{{ category }}" ]]; then + {%- for sub, subinfo in catinfo.subs.items() %} + if [[ $action_or_subcategory == "{{ sub }}" ]]; then + opts="{{ subinfo | join(" ") }}" + fi + {%- endfor %} + fi + {%- endif -%} + {%- endfor %} + fi + + # If no options were found propose --help + if [ -z "$opts" ]; then + prev="${COMP_WORDS[COMP_CWORD-1]}" + + if [[ $prev != "--help" ]]; then + opts=( --help ) + fi + fi + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} + +complete -F _yunohost yunohost diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py new file mode 100755 index 0000000..05bd389 --- /dev/null +++ b/doc/generate_api_doc.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Generate JSON specification files API +""" + +import json +import os +import sys + +import yaml + + +def main(): + with open("../share/actionsmap.yml") as f: + action_map = yaml.safe_load(f) + + # try: + # with open("/etc/yunohost/current_host", "r") as f: + # domain = f.readline().rstrip() + # except IOError: + # domain = requests.get("http://ip.yunohost.org").text + + with open("../debian/changelog") as f: + top_changelog = f.readline() + api_version = top_changelog[top_changelog.find("(") + 1 : top_changelog.find(")")] + + csrf = { + "name": "X-Requested-With", + "in": "header", + "required": True, + "schema": {"type": "string", "default": "Swagger API"}, + } + + resource_list = { + "openapi": "3.0.3", + "info": { + "title": "YunoHost API", + "description": "This is the YunoHost API used on all YunoHost instances. This API is essentially used by YunoHost Webadmin.", + "version": api_version, + }, + "servers": [ + { + "url": "https://{domain}/yunohost/api", + "variables": { + "domain": { + "default": "demo.yunohost.org", + "description": "Your yunohost domain", + } + }, + } + ], + "tags": [{"name": "public", "description": "Public route"}], + "paths": { + "/login": { + "post": { + "tags": ["public"], + "summary": "Logs in and returns the authentication cookie", + "parameters": [csrf], + "requestBody": { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "credentials": { + "type": "string", + "format": "password", + } + }, + "required": ["credentials"], + } + } + }, + }, + "security": [], + "responses": { + "200": { + "description": "Successfully login", + "headers": {"Set-Cookie": {"schema": {"type": "string"}}}, + } + }, + } + }, + "/installed": { + "get": { + "tags": ["public"], + "summary": "Test if the API is working", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Successfully working", + } + }, + } + }, + }, + } + + def convert_categories(categories, parent_category=""): + for category, category_params in categories.items(): + if parent_category: + category = f"{parent_category} {category}" + if "subcategory_help" in category_params: + category_params["category_help"] = category_params["subcategory_help"] + + if "category_help" not in category_params: + category_params["category_help"] = "" + resource_list["tags"].append( + {"name": category, "description": category_params["category_help"]} + ) + + for action, action_params in category_params["actions"].items(): + if "action_help" not in action_params: + action_params["action_help"] = "" + if "api" not in action_params: + continue + if not isinstance(action_params["api"], list): + action_params["api"] = [action_params["api"]] + + for i, api in enumerate(action_params["api"]): + print(api) + method, path = api.split(" ") + method = method.lower() + key_param = "" + if "{" in path: + key_param = path[path.find("{") + 1 : path.find("}")] + resource_list["paths"].setdefault(path, {}) + + notes = "" + + operationId = f"{category}_{action}" + if i > 0: + operationId += f"_{i}" + operation = { + "tags": [category], + "operationId": operationId, + "summary": action_params["action_help"], + "description": notes, + "responses": {"200": {"description": "successful operation"}}, + } + if action_params.get("deprecated"): + operation["deprecated"] = True + + operation["parameters"] = [] + if method == "post": + operation["parameters"] = [csrf] + + if "arguments" in action_params: + if method in ["put", "post", "patch"]: + operation["requestBody"] = { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": {}, + "required": [], + } + } + }, + } + for arg_name, arg_params in action_params["arguments"].items(): + if "help" not in arg_params: + arg_params["help"] = "" + param_type = "query" + allow_multiple = False + required = True + allowable_values = None + name = str(arg_name).replace("-", "_") + if name[0] == "_": + required = False + if "full" in arg_params: + name = arg_params["full"][2:] + else: + name = name[2:] + name = name.replace("-", "_") + + if "choices" in arg_params: + allowable_values = arg_params["choices"] + _type = "string" + if "type" in arg_params: + types = {"open": "file", "int": "int"} + _type = types[arg_params["type"]] + if ( + "action" in arg_params + and arg_params["action"] == "store_true" + ): + _type = "boolean" + + if "nargs" in arg_params: + if arg_params["nargs"] == "*": + allow_multiple = True + required = False + _type = "array" + if arg_params["nargs"] == "+": + allow_multiple = True + required = True + _type = "array" + if arg_params["nargs"] == "?": + allow_multiple = False + required = False + else: + allow_multiple = False + + if name == key_param: + param_type = "path" + required = True + allow_multiple = False + + if method in ["put", "post", "patch"]: + schema = operation["requestBody"]["content"][ + "multipart/form-data" + ]["schema"] + schema["properties"][name] = { + "type": _type, + "description": arg_params["help"], + } + if required: + schema["required"].append(name) + prop_schema = schema["properties"][name] + else: + parameters = { + "name": name, + "in": param_type, + "description": arg_params["help"], + "required": required, + "schema": { + "type": _type, + }, + "explode": allow_multiple, + } + prop_schema = parameters["schema"] + operation["parameters"].append(parameters) + + if allowable_values is not None: + prop_schema["enum"] = allowable_values + if "default" in arg_params: + prop_schema["default"] = arg_params["default"] + if arg_params.get("metavar") == "PASSWORD": + prop_schema["format"] = "password" + if arg_params.get("metavar") == "MAIL": + prop_schema["format"] = "mail" + # Those lines seems to slow swagger ui too much + # if 'pattern' in arg_params.get('extra', {}): + # prop_schema['pattern'] = arg_params['extra']['pattern'][0] + + resource_list["paths"][path][method.lower()] = operation + + # Includes subcategories + if "subcategories" in category_params: + convert_categories(category_params["subcategories"], category) + + del action_map["_global"] + convert_categories(action_map) + + openapi_json = json.dumps(resource_list) + # Save the OpenAPI json + with open(os.getcwd() + "/openapi.json", "w") as f: + f.write(openapi_json) + + openapi_js = f"var openapiJSON = {openapi_json}" + with open(os.getcwd() + "/openapi.js", "w") as f: + f.write(openapi_js) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/doc/generate_bash_completion.py b/doc/generate_bash_completion.py new file mode 100755 index 0000000..ab1a24f --- /dev/null +++ b/doc/generate_bash_completion.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Simple automated generation of a bash_completion file +for yunohost command from the actionsmap. + +Generates a bash completion file assuming the structure +`yunohost category action` +adds `--help` at the end if one presses [tab] again. + +author: Christophe Vuillot +""" + +import argparse +from pathlib import Path +from typing import Any + +import yaml +from jinja2 import Template + +YUNOHOST_SRCDIR = Path(__file__).resolve().parent.parent + + +def render(actions: dict[str, Any]) -> str: + template_file = YUNOHOST_SRCDIR / "doc" / "bash_completion.sh.j2" + template = Template( + template_file.read_text(), + comment_start_string="disabled because bash contains {#", + ) + + result = template.render( + categories=actions, + ) + return result + + +def get_actions() -> dict[str, Any]: + actionsmap = YUNOHOST_SRCDIR / "share" / "actionsmap.yml" + categories = yaml.safe_load(actionsmap.open()) + + fullmap: dict[str, Any] = {} + + for category, cat_info in categories.items(): + if category.startswith("_"): + continue + fullmap[category] = {} + fullmap[category]["actions"] = [] + fullmap[category]["subs"] = {} + + for action, _ in cat_info.get("actions", {}).items(): + fullmap[category]["actions"].append(action) + + for subcat, sub_info in cat_info.get("subcategories", {}).items(): + fullmap[category]["subs"][subcat] = list(sub_info["actions"].keys()) + + return fullmap + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output", "-o", type=Path, required=True) + args = parser.parse_args() + + actions = get_actions() + result = render(actions) + + args.output.write_text(result) + + +if __name__ == "__main__": + main() diff --git a/doc/generate_json_schema.py b/doc/generate_json_schema.py new file mode 100755 index 0000000..840abd5 --- /dev/null +++ b/doc/generate_json_schema.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from yunohost.utils.configpanel import ConfigPanelModel + +print(ConfigPanelModel.schema_json(indent=2)) diff --git a/doc/generate_manpages.py b/doc/generate_manpages.py new file mode 100755 index 0000000..3fc6b76 --- /dev/null +++ b/doc/generate_manpages.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Inspired by yunohost_completion.py (author: Christophe Vuillot) +======= + +This script generates man pages for yunohost. +Pages are stored in OUTPUT_DIR +""" + +import argparse +import gzip +import os +from collections import OrderedDict +from datetime import date + +import yaml +from jinja2 import Template + +base_path = os.path.split(os.path.realpath(__file__))[0] + +template = Template(open(os.path.join(base_path, "manpage.template")).read()) + + +THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, "../share/actionsmap.yml") + + +def ordered_yaml_load(stream): + class OrderedLoader(yaml.SafeLoader): + pass + + OrderedLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + lambda loader, node: OrderedDict(loader.construct_pairs(node)), + ) + return yaml.load(stream, OrderedLoader) + + +def main(): + parser = argparse.ArgumentParser( + description="generate yunohost manpage based on actionsmap.yml" + ) + parser.add_argument("-o", "--output", default="output/yunohost") + parser.add_argument("-z", "--gzip", action="store_true", default=False) + + args = parser.parse_args() + + if os.path.isdir(args.output): + if not os.path.exists(args.output): + os.makedirs(args.output) + + output_path = os.path.join(args.output, "yunohost") + else: + output_dir = os.path.split(args.output)[0] + + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir) + + output_path = args.output + + # man pages of "yunohost *" + with open(ACTIONSMAP_FILE, "r") as actionsmap: + # Getting the dictionary containning what actions are possible per domain + actionsmap = ordered_yaml_load(actionsmap) + + for i in list(actionsmap.keys()): + if i.startswith("_"): + del actionsmap[i] + + today = date.today() + + result = template.render( + month=today.strftime("%B"), + year=today.year, + categories=actionsmap, + str=str, + ) + + if not args.gzip: + with open(output_path, "w") as output: + output.write(result) + else: + with gzip.open(output_path, mode="w", compresslevel=9) as output: + output.write(result.encode()) + + +if __name__ == "__main__": + main() diff --git a/doc/generate_zsh_completion.py b/doc/generate_zsh_completion.py new file mode 100755 index 0000000..b5d8faa --- /dev/null +++ b/doc/generate_zsh_completion.py @@ -0,0 +1,738 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +"""Automated generation of a zsh_completion file for yunohost. + +Using the actionsmap yaml file and a jinja template. + +INSTALL: + This script creates a zsh completion file for yunohost. + To install, copy (and rename) the created file to: + - (Debian) `/usr/share/zsh/vendor-completions/_yunohost` + - (Fedora) `/usr/share/zsh/site-functions/_yunohost` + - (other distribution) `/usr/local/share/zsh/site-functions/_yunohost` + +DOCS: +- https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide +- http://zsh.sourceforge.net/Doc/Release/Completion-System.html#Completion-System + or `man zshcompsys` +- http://zsh.sourceforge.net/Guide/zshguide06.html + +MISC: +- http://zsh.sourceforge.net/Doc/Release/Parameters.html#Array-Parameters + +MISSING: +- use the extra:required:True pattern (similar to `nargs`?) +- In `yunohost.yml`, consider merging: + - metavar + - pattern + - autocomplete +- Make use of `type`, maybe using `_guard` +- Use `pattern`, maybe with `_guard`. This seems hard though, as ZSH has +its own globbing language... +Link about this globbing system: +http://zsh.sourceforge.net/Doc/Release/Expansion.html#Filename-Generation + +Notes: +- Command for debugging zsh: `unfunction _yunohost; autoload -U _yunohost` + +- Optimization: + - caching mecanism: invalidate the cache afer some commands? Hard, the + cache is local to user + - implement a zstyle switch, to change the cache validity period? + +AUTHORS: + - buzuck (Fol) + - kayou + - getzze + +""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +import yaml +from jinja2 import Template + +if TYPE_CHECKING: + from typing import NotRequired, TypedDict + + class Function(TypedDict): + """Details of a helper function.""" + + name: str + shell_call: NotRequired[str] + aggregated: NotRequired[str] + + class Case(TypedDict): + """Details of dynamic argument completion function.""" + + name: str + shell_call: str + + class Action(TypedDict): + """Command line action.""" + + name: str + help: str + arguments: list[str] + cases: list[Case] + + class Category(TypedDict): + """Command line category. + + Categories have different level: + - level 1: main category, e.g. `yunohost user` + - level 2: sub-category, e.g. `yunohost user group` + + Only categories of level 1 have a `subs` key. + + Reminder: + yunohost user group list --full --short + ^ ^ ^ ^ + (script) | category | subcategory | action | parameters + + """ + + name: str + help: str + level: int + actions: list[Action] + subs: NotRequired[dict[str, str]] + + +YUNOHOST_SRCDIR = Path(__file__).resolve().parent.parent + + +def get_actions_zsh( + ynh_map: dict[str, dict[str, Any]], +) -> tuple[list[Category], list[Function]]: + """Parse categories, subcategories and actions from an actionsmap yml file. + + Parameters + ---------- + ynh_map: dict[str, dict[str, Any]] + A dict loaded from an actionsmap yml file. + + Returns + ------- + tuple[list[Category], list[Function]] + A tuple of categories dict and helper functions dict. + + """ + categories: list[Category] = [] + functions: list[Function] = [] + + for category, cat_info in ynh_map.items(): + if category.startswith("_") or cat_info.get("hide_in_help", False): + continue + + cats, funcs = parse_category(category, cat_info) + + categories.extend(cats) + functions.extend(funcs) + + # Remove duplicates in functions + functions = [ + cast("Function", dict(t)) for t in {tuple(d.items()) for d in functions} + ] + + return categories, functions + + +def parse_category( + name: str, + info: dict[str, Any], +) -> tuple[list[Category], list[Function]]: + """Parse a Category (level 1) for its actions and subcategories. + + Reminder: + yunohost monitor info --cpu --ram + ^ ^ ^ ^ + (script) | category | action | parameters + + A Category may contain subcategories (of level 2), so a list of categories + (of level 1 and 2) is returned. + A category may need to define help functions that are needed to build + the list of options. The list of help functions is returned. + + Parameters + ---------- + name: str + the category name + info: dict[str, Any] + the information dict about the category + + Returns + ------- + tuple[list[Category], list[Function]] + A tuple of the list of category and subcategories dicts + and the list of category and subcategories helper functions. + + """ + cat: Category = { + "name": name, + "level": 1, + "help": _escape(info.get("category_help", "")), + "actions": [], + "subs": {}, + } + # Add the category first, the subcategories will be appended later + categories: list[Category] = [cat] + functions: list[Function] = [] + + # Parse actions (before subcategories) + actions = [] + for action, action_info in info.get("actions", {}).items(): + if action_info.get("hide_in_help", False): + continue + + act, funcs = parse_actions(action, action_info) + actions.append(act) + functions.extend(funcs) + cat["actions"] = actions + + # Parse subcategories + subs = {} + for subcategory, subcategory_info in info.get("subcategories", {}).items(): + if subcategory.startswith("_") or subcategory_info.get("hide_in_help", False): + continue + + help, subcategory_dict, funcs = parse_subcategory( # noqa: A001 + name, + subcategory, + subcategory_info, + ) + subs[subcategory] = help + functions.extend(funcs) + # Append subcategory below the category + categories.append(subcategory_dict) + # Add the list of subcategories to the category + cat["subs"] = subs + + return categories, functions + + +def parse_subcategory( + category: str, + name: str, + info: dict[str, Any], +) -> tuple[str, Category, list[Function]]: + """Parse a sub-category (level 2) for its actions. + + Reminder: + yunohost user group list --full --short + ^ ^ ^ ^ + (script) | category | subcategory | action | parameters + + A subcategory is treated as a Category (of level 2), with an 'actions' key, + but no 'subs' key. + The help text of the subcategory is needed to construct the 'subs' dict + of the parent category. + Like a level-1 category, subcategories may need to define help functions. + The list of help functions is returned. + + Parameters + ---------- + category: str + the name of the parent category + name: str + the subcategory name + info: dict[str, Any] + the information dict about the subcategory + + Returns + ------- + tuple[str, Category, list[Function]] + A tuple of the subcategory help text, the subcategory dict + and the list of subcategory helper functions. + + """ + full_name = f"{category}_{name}" + help = _escape(info.get("subcategory_help", "")) # noqa: A001 + + subcat: Category = {"name": full_name, "level": 2, "help": help, "actions": []} + functions: list[Function] = [] + + # Parse actions (before subcategories) + actions = [] + for action, action_info in info.get("actions", {}).items(): + if action_info.get("hide_in_help", False): + continue + + act, funcs = parse_actions(action, action_info) + actions.append(act) + functions.extend(funcs) + subcat["actions"] = actions + + return help, subcat, functions + + +def parse_actions( + name: str, + info: dict[str, Any], +) -> tuple[Action, list[Function]]: + """Parse an Action for it's help text and arguments. + + Returns + ------- + tuple[Action, list[Function]] + A tuple of the action dict and the list of action helper functions. + + """ + functions: list[Function] = [] + + # This is a counter, in case of position dependent paremeters (the ones not + # beginning with a `-`) + position = 0 + + arguments: list[str] = [] + cases: list[Case] = [] + + for _argument_name, argument_info in info.get("arguments", {}).items(): + # + # Forcing to str, as the yaml parser inteprets numbers as integers + # (eg.: `firewall allow... -4`) + argument_name = str(_argument_name) + case: Case | None = None + funcs: list[Function] = [] + + # + # This is an optional parameter, beginning with a `-` + if argument_name.startswith("-"): + full_argument, case, funcs = parse_argument_optional( + argument_name, + argument_info, + ) + # + # A parameter not beginning with `-` is considered mandatory. + else: + position += 1 + full_argument, case, funcs = parse_argument_mandatory( + argument_name, + argument_info, + position, + ) + + # If action is None, do not display the parameter + if not full_argument: + continue + + # If case is not None, add a case below the arguments list + if case: + cases.append(case) + + # Add helper functions + functions.extend(funcs) + + # Append argument + arguments.append(full_argument) + + help = _escape(info.get("action_help", "")) # noqa: A001 + action_dict: Action = { + "name": name, + "help": help, + "arguments": arguments, + "cases": cases, + } + return action_dict, functions + + +def parse_argument_mandatory( + name: str, + info: dict[str, Any], + position: int = 0, +) -> tuple[str, Case | None, list[Function]]: + """Parse a mandatory argument.""" + # + # Initializing the argument dict to make sure all fields are defined + # - id: identifier (`-n` or `--name`). If none (e.g. `ynh app install + # APP_NAME`), this field is the arguments position or cardinality (from + # `nargs`) + # - excludes: usually the argument itself. Only used for optional args + # - desc: the argument description + # - completion: the completion function name + # + arg = {"excludes": "", "spec": "", "desc": "", "mess": "", "action": "", "func": ""} + + # + # Generation of the completion hints + # + arg["action"], case, functions = parse_argument_action(name, info) + + # Hidden argument + if arg["action"] is None: + return ("", None, []) + + # This parameter may be used more than once, else we use the position counter + if info.get("nargs", "") in ["+", "*"]: + if info["nargs"] == "+": + arg["spec"] = f"'{{{position!s},*}}'" + else: # argument_details["nargs"] == "*": + arg["spec"] = "*" + else: + arg["spec"] = str(position) + arg["mess"] = info.get("help", name) + + # + # If defined, add the default value as a hint + if "default" in info: + arg["mess"] += f" (default: {info['default']})" + # Escape special character in the description + arg["mess"] = _escape(arg["mess"]) + + # ---- + # NOTE: a double colon marks for an optional argument: + # ::Username to update:__ynh_user_list + # ---- + placeholder = "'{}{}{}:{}:{}'" + + # Escape special character in the description + arg["desc"] = _escape(arg["desc"]) + argument = placeholder.format( + arg["excludes"], + arg["spec"], + arg["desc"], + arg["mess"], + arg["action"], + ) + return (argument, case, functions) + + +def parse_argument_optional( + name: str, + info: dict[str, Any], +) -> tuple[str, Case | None, list[Function]]: + """Parse an optional argument.""" + # + # Initializing the argument dict to make sure all fields are defined + # - id: identifier (`-n` or `--name`). If none (e.g. `ynh app install + # APP_NAME`), this field is the arguments position or cardinality (from + # `nargs`) + # - excludes: usually the argument itself. Only used for optional args + # - desc: the argument description + # - completion: the completion function name + # + arg = {"excludes": "", "spec": "", "desc": "", "mess": "", "action": "", "func": ""} + + # + # Generation of the completion hints + # + arg["action"], case, functions = parse_argument_action(name, info) + + # Hidden argument + if arg["action"] is None: + return ("", None, []) + + # `full` is the extended form of the argument (e.g.: -n is short for --number) + if "full" in info: + full_name = info["full"] + arg["mess"] = str(full_name).lstrip("-") + arg["spec"] = f"'{{{name},{full_name}}}'" + arg["excludes"] = f"({name} {full_name})" + else: + arg["mess"] = str(name).lstrip("-") + arg["spec"] = name + # Escape special character in the description + arg["mess"] = _escape(arg["mess"]) + + # The description of the parameter + # Getting the `help` field if any, else simply by using it's name + help = info.get("help", arg["mess"]) # noqa: A001 + arg["desc"] = f"[{help}]" + + has_action = True + # Add a pattern field to match multiple arguments + if info.get("nargs", "") in ["+", "*"]: + if arg["excludes"]: + # suppose that `arg["excludes"] = (-f --foo)` + arg["excludes"] = "(* " + arg["excludes"][1:] + else: + arg["excludes"] = "(*)" + arg["mess"] = "*:" + arg["mess"] + has_action = True + + # Options without arguments should skip the message and action fields + elif info.get("action", "").startswith("store_"): + has_action = False + + # Place holder for the parameters + placeholder = "'{}{}{}:{}:{}'" if has_action else "'{}{}{}'" + # Escape special character in the description + arg["desc"] = _escape(arg["desc"]) + argument = placeholder.format( + arg["excludes"], + arg["spec"], + arg["desc"], + arg["mess"], + arg["action"], + ) + return (argument, case, functions) + + +def parse_argument_action( # noqa: C901, PLR0911, PLR0912 + name: str, + info: dict[str, Any], +) -> tuple[str, Case | None, list[Function]]: + """Parse an argument action.""" + functions: list[Function] = [] + # + # Finds the completion function for the given argument, if defined. + # + # `functions` hold the elements needed to generate it. The + # actual creation of this function will be done by build_completion_functions(), + # called near the end of this script. + # `choices` and `autocomplete` should not be present at the same time + # (`choices` takes precedence) + + # + # A list of choices is defined + if "choices" in info: + all_choices = " ".join(info["choices"]) + action = f"({all_choices})" + return (action, None, functions) + + # + # Look for an autocompletion function, but it is not defined + if "extra" not in info or "autocomplete" not in info["extra"]: + return ("", None, functions) + + # + # An autocompletion function is defined + autocomplete = info["extra"]["autocomplete"] + + # + # Check if the argument should be hidden (API only) + if autocomplete.get("hide_in_help", False): + return ("", None, functions) + + # + # This is a combinaision of YunoHost and jq commands + # + if "ynh_selector" in autocomplete and "jq_selector" in autocomplete: + # + # Function dependent on previous arguments + # + if "depends" in autocomplete and autocomplete["depends"] == "previous": + # Create cases that depend on the previous argument. + # + # First, build the shell command that returns the completions + call = ( + f"sudo yunohost {autocomplete['ynh_selector']} " + f'"${{previous}}" --output-as json ' + f"| jq -cr '{autocomplete['jq_selector']}' | xargs" + ) + # If a cache is needed, wrap the call in the caching function + if autocomplete.get("use_cache", False): + call = '__get_ynh_cache YNH_{}_"${{previous}}" "{}"'.format( + _norm_name(autocomplete["ynh_selector"]), + # Remove the double-quote in "{previous}" + # because the whole cmd will be encased in quotes. + call.replace('"', ""), + ) + + function_name = f"->{name}" + case: Case = {"name": name, "shell_call": call} + return (function_name, case, functions) + + # Create this function's name + function_name = _remove_special_chars( + "__ynh_" + _norm_name(autocomplete["ynh_selector"]), + ) + # + # Add a helper function + # + # First, build the shell command that returns the completions + call = "sudo yunohost {} --output-as json | jq -cr '{}'".format( + autocomplete["ynh_selector"], + autocomplete["jq_selector"], + ) + # If a cache is needed, wrap the call in the caching function + if autocomplete.get("use_cache", False): + call = "__get_ynh_cache 'YNH_{}' \"{}\"".format( + _norm_name(autocomplete["ynh_selector"]), + call, + ) + # Lastly, save the content + func: Function = {"name": function_name, "shell_call": call} + functions.append(func) + return (function_name, None, functions) + + # + # The autocompletion is done by a grep + # + if "shell_call" in autocomplete: + # Create this function's name + function_name = _remove_special_chars( + "__ynh_" + _norm_name(autocomplete["shell_call"]), + ) + # + # Add a helper function + # + # First, build the shell command that returns the completions + call = autocomplete["shell_call"] + + # If a cache is needed, wrap the call in the caching function + # Note: not tested with grep, only with YunoHost's commands + if autocomplete.get("use_cache", False): + call = "__get_ynh_cache 'YNH_{}' \"{}\"".format( + _remove_special_chars(autocomplete["shell_call"]), + call, + ) + # Lastly, save the content + func = {"name": function_name, "shell_call": call} + functions.append(func) + return (function_name, None, functions) + + # + # This is a combinaision of two other completion functions + # + if "aggregate" in autocomplete: + # Create this function's name + function_name = "__ynh" + for subcall in autocomplete["aggregate"]: + if "ynh_selector" in subcall: + function_name += "_" + _norm_name(subcall["ynh_selector"]) + + # + # Add a helper function + aggregation = "" + for subcall in autocomplete["aggregate"]: + if "name" in subcall and "ynh_selector" in subcall: + aggregation += "\n'{}:{}:{}' \\".format( + subcall["name"], + subcall["name"], + _norm_name("__ynh_" + subcall["ynh_selector"]), + ) + # Lastly, save the content + func = {"name": function_name, "aggregated": aggregation} + functions.append(func) + return (function_name, None, functions) + + # + # The autocompletion is done by a ZSH function + # + if "zsh_completion" in autocomplete: + return (autocomplete["zsh_completion"], None, functions) + + # + # No autocompletion schema was defined + # + return ("", None, functions) + + +def render_zsh(categories: list[Category], functions: list[Function]) -> str: + """Render the jinja template with the parsed categories and helper functions.""" + template_file = YUNOHOST_SRCDIR / "doc" / "zsh_completion.j2" + template = Template( + template_file.read_text(), + keep_trailing_newline=True, + comment_start_string="disabled because bash contains {#", + ) + + return template.render( + categories=categories, + functions=functions, + ) + + +# +# Utility functions, mainly string manipulation +# + + +def _norm_name(string: str) -> str: + """Normalize a string to make it look like a function name. + + Apply the transformations: + - lowercase + - spaces replaced by underscores + - no dashs + + :param str string: the string to norm. + :return str: The normed string + """ + return ( + string.lower() + .replace(" ", "_") + .replace("-", "") + .replace("/", "_") + .replace(".", "_") + ) + + +def _escape(string: str) -> str: + r"""Escape any special character. + + Escape the characters: + - single quotes (') are put in a separate double quoted string ('"'"') + - colons (:) and other characters are preceded by a backslash (\:) + + :param str string: The string to escape + :return str: The escaped string + """ + return string.replace("'", "'\"'\"'").replace(":", r"\:") + + +def _remove_special_chars(string: str) -> str: + """Remove any character with a special meaning in ZSH. + + Example of characters to remove: + `$`, `{`, `(`, `[`, ... + + :param str string: The string to clean + :return str: The cleaned string + """ + # NOTE: this list may not be comprehensive and should be extended if needed + return re.sub(r'[- =\^+:\?\'"$(){}\[\]/\\\\]', "", string).replace(".", "") + + +# +# Get action map +# + + +def get_action_map() -> dict[str, Any]: + """Load the actionmap from a YAML file.""" + actionsmap = YUNOHOST_SRCDIR / "share" / "actionsmap.yml" + return cast("dict[str, Any]", yaml.safe_load(actionsmap.open())) + + +def main() -> None: + """Generate the completion file for Zsh.""" + parser = argparse.ArgumentParser() + parser.add_argument("--output", "-o", type=Path, required=True) + + args = parser.parse_args() + + yunohost_map = get_action_map() + + categories, functions = get_actions_zsh(yunohost_map) + result = render_zsh(categories, functions) + + args.output.write_text(result) + + +if __name__ == "__main__": + main() diff --git a/doc/manpage.template b/doc/manpage.template new file mode 100644 index 0000000..6736c6d --- /dev/null +++ b/doc/manpage.template @@ -0,0 +1,121 @@ +.TH YunoHost "1" "{{ month }} {{ year }}" "YunoHost Collectif" +.SH NAME +YunoHost \- yunohost server administration command + +.SH SYNOPSIS +yunohost \fI\,CATEGORY\/\fR \fI\,COMMAND\/\fR [\fI\,SUBCOMMAND\/\fR] [\fI\,ARGUMENTS\/\fR]... [\fI\,OPTIONS\/\fR]... + +{# generale command format #} +.SH DESCRIPTION +usage: yunohost +{{ '{' }}{{ ",".join(categories) }}{{ '}' }} +\&... +[\-h|\-\-help] [\-\-no\-cache] [\-\-output\-as {json,plain,none}] [\-\-debug] +[\-\-quiet] [\-\-timeout ==SUPPRESS==] [\-\-admin\-password PASSWORD] +[\-v|\-\-version] + +.SS "optional arguments:" +.TP +\fB\-h\fR, \fB\-\-help\fR +show this help message and exit + +.SS "categories:" +.IP +{{ '{' }}{{ ",".join(categories) }}{{ '}' }} +{% for name, value in categories.items() %} +.TP +{{ name }} +{{ value["category_help"] }} +{% endfor %} + +.SS "global arguments:" +.TP +\fB\-\-no\-cache\fR +Don't use actions map cache +.TP +\fB\-\-output\-as\fR {json,plain,none} +Output result in another format +.TP +\fB\-\-debug\fR +Log and print debug messages +.TP +\fB\-\-quiet\fR +Don't produce any output +.TP +\fB\-\-timeout\fR SECONDS +Number of seconds before this command will timeout +because it can't acquire the lock (meaning that +another command is currently running), by default +there is no timeout and the command will wait until it +can get the lock +.TP +\fB\-\-admin\-password\fR PASSWORD +The admin password to use to authenticate +.TP +\fB\-v\fR, \fB\-\-version\fR +Display YunoHost packages versions + +{# each categories #} +{% for name, value in categories.items() %} +.SH YUNOHOST {{ name.upper() }} +usage: yunohost {{ name }} {{ '{' }}{{ ",".join(value.get("actions", {}).keys()) }}{{ '}' }} +\&... +.SS "description:" +.IP +{{ value["category_help"] }} + +{# each command of each category #} +{% for action, action_value in value.get("actions", {}).items() %} +.SS "yunohost {{ name }} {{ action }} \ +{% for argument_name, argument_value in action_value.get("arguments", {}).items() %}\ +{% set required=(not str(argument_name).startswith("-")) or argument_value.get("extra", {}).get("required", False) %}\ +{% if not required %}[{% endif %}\ +\fI\,{{ argument_name }}\/\fR{% if argument_value.get("full") %}|\fI\,{{ argument_value["full"] }}\fR{% endif %}\ +{% if str(argument_name).startswith("-") and not argument_value.get("action") == "store_true" %} {{ (argument_value.get("full", argument_name)).lstrip("-") }}{% endif %}\ +{% if not required %}]{% endif %} \ +{% endfor %}" + +{# help of the command #} +{{ action_value["action_help"] }} + +{# arguments of the command #} +{% if "arguments" in action_value %} +{% for argument_name, argument_value in action_value["arguments"].items() %} +.TP +\fB{{ argument_name }}\fR{% if argument_value.get("full") %}, \fB{{ argument_value["full"] }}\fR{% endif %}\ +{% if str(argument_name).startswith("-") and not argument_value.get("action") == "store_true" %} \fI\,{{ (argument_value.get("full", argument_name)).lstrip("-") }}\fR {% if "default" in argument_value %}(default: {{ argument_value["default"] }}){% endif %}{% endif %} +{{ argument_value.get("help", "")}} +{% endfor %} + +{% endif %} +{% endfor %} + +{# each subcategory #} +{% for subcategory_name, subcategory in value.get("subcategories", {}).items() %} +{% for action, action_value in subcategory["actions"].items() %} +.SS "yunohost {{ name }} {{ subcategory_name }} {{ action }} \ +{% for argument_name, argument_value in action_value.get("arguments", {}).items() %}\ +{% set required=(not str(argument_name).startswith("-")) or argument_value.get("extra", {}).get("required", False) %}\ +{% if not required %}[{% endif %}\ +\fI\,{{ argument_name }}\/\fR{% if argument_value.get("full") %}|\fI\,{{ argument_value["full"] }}\fR{% endif %}\ +{% if str(argument_name).startswith("-") and not argument_value.get("action") == "store_true" %} {{ (argument_value.get("full", argument_name)).lstrip("-") }}{% endif %}\ +{% if not required %}]{% endif %} \ +{% endfor %}" + +{# help of the command #} +{{ action_value["action_help"] }} + +{# arguments of the command #} +{% if "arguments" in action_value %} +{% for argument_name, argument_value in action_value["arguments"].items() %} +.TP +\fB{{ argument_name }}\fR{% if argument_value.get("full") %}, \fB{{ argument_value["full"] }}\fR{% endif %}\ +{% if str(argument_name).startswith("-") and not argument_value.get("action") == "store_true" %} \fI\,{{ (argument_value.get("full", argument_name)).lstrip("-") }}\fR {% if "default" in argument_value %}(default: {{ argument_value["default"] }}){% endif %}{% endif %} +{{ argument_value.get("help", "")}} +{% endfor %} + +{% endif %} +{% endfor %} +{% endfor %} + +{% endfor %} diff --git a/doc/zsh_completion.j2 b/doc/zsh_completion.j2 new file mode 100644 index 0000000..a0da88d --- /dev/null +++ b/doc/zsh_completion.j2 @@ -0,0 +1,219 @@ +#compdef yunohost +# +# ----------------------------------------------------------------------------- +# Description +# ----------- +# Completion script for yunohost, automatically generated from the action map +# decribed by `yunohost.yml` +# ----------------------------------------------------------------------------- + +local state line curcontext + +# For debug purposes only +__log() { + echo $@ >> '/tmp/zsh-completion.log' +} + +# First argument: The name of the completion list +# 2nd argument: The command to get it +# (( $+functions[__get_ynh_cache] )) || +function __get_ynh_cache() { + # Checking a global cache policy is defined, + # and linkage to ynh-cache-policy + local update_policy completion_items + zstyle -s ":completion:${curcontext}:" cache-policy update_policy + if [[ -z "$update_policy" ]]; then + zstyle ":completion:${curcontext}:" cache-policy __yunohost_cache_policy + fi + # If the cache is invalid (too old), regenerate it + if _cache_invalid $1 || ! _retrieve_cache $1; then + completion_items=(`eval $2`) + _store_cache $1 completion_items + else + _retrieve_cache $1 + fi + echo $completion_items +} + +# (( $+functions[__yunohost_cache_policy] )) || +__yunohost_cache_policy(){ + local cache_file="$1" + # Rebuild if the yunohost executable is newer than cache + [[ "${commands[yunohost]}" -nt "${cache_file}" ]] && return + + # Rebuild if cache is more than a week old + local -a oldp + # oldp=( "$1"(mM+1) ) # month + # oldp=( "$1"(Nm+7) ) # 1 week + oldp=( "$1"(Nmd+1) ) # 1 day + (( $#oldp )) && return + return 1 +} + +# +# Routing function, used to go through $words and find the correct subfunction +# (Suggestions welcome to improve that design... =/ ) +# (( $+functions[__jump] )) || +function __jump() { + local cmd + + # Remember the subcommand name + if (( ${#@} == 0 )); then + local cmd=${words[2]} + else + cmd=$1 # < no more used? + fi + + # Set the context for the subcommand + ynhcommand="${ynhcommand}_${cmd}" + # Narrow the range of words we are looking at to exclude `yunohost` + (( CURRENT-- )) + shift words + # Run the completion for the subcommand + if ! _call_function ret ${ynhcommand#:*:}; then + _default && ret=0 + fi + return ret +} + +#----------------------------------------- +# Command +#----------------------------------------- +# +# Principal entry point with general options and list of commands +# (( $+functions[_yunohost] )) || +function _yunohost() { + local curcontext="${curcontext}" state line ret=1 + local mode + # `ynhcommand` is where `__jump` builds the name of the completion function + ynhcommand='_yunohost' + + typeset -ag common_options; common_options=( + '(-h --help)'{-h,--help}'[Show this help message and exit]:help:' + '--version[Display YunoHost packages versions]:version:' + '--output-as[Output result in another format]:output-as:(json plain none)' + '--debug[Log and print debug messages]' + '--quiet[Don'"'"'t produce any output]' + '--timeout[Number of seconds before this command will timeout because it can'"'"'t acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock]:timeout:' + ) + + if (( CURRENT > 2 )); then + __jump + else + local -a yunohost_categories; yunohost_categories=( + {%- for catinfo in categories %} + {%- if catinfo.help and catinfo.level == 1 %} + '{{ catinfo.name }}:{{ catinfo.help }}' + {%- endif %} + {%- endfor %} + ) + _describe -V -t yunohost-commands 'yunohost category' yunohost_categories "$@" + fi + + _arguments -s -C $common_options + # unset common_option +} + +#----------------------------------------- +# Subcommands +#----------------------------------------- + +{%- for catinfo in categories %} +{%- if catinfo.actions or catinfo.subs %} +#----------------------------------------- +# {{ catinfo.name }} +#----------------------------------------- + +# (( $+functions[_yunohost_{{ catinfo.name }}] )) || +function _yunohost_{{ catinfo.name }}() { + if (( CURRENT > 2 )); then + __jump + else + {%- if catinfo.actions %} + local -a yunohost_{{ catinfo.name }}; yunohost_{{ catinfo.name }}=( + {%- for actioninfo in catinfo.actions %} + {%- if actioninfo.help %} + '{{ actioninfo.name }}:{{ actioninfo.help }}' + {%- endif %} + {%- endfor %} + ) + _describe -V -t yunohost-{{ catinfo.name }} 'yunohost {{ catinfo.name }} category' yunohost_{{ catinfo.name }} "$@" + + {%- endif %} + {% if catinfo.subs %} + local -a yunohost_{{ catinfo.name }}_subcategories; yunohost_{{ catinfo.name }}_subcategories=( + {%- for subcat_name, subcat_desc in catinfo.subs.items() %} + {%- if subcat_desc %} + '{{ subcat_name }}:{{ subcat_desc }}' + {%- endif %} + {%- endfor %} + ) + _describe -V -t yunohost-{{ catinfo.name }}-subcategories 'yunohost {{ catinfo.name }} subcategories' yunohost_{{ catinfo.name }}_subcategories "$@" + {%- endif %} + fi +} + +{% if catinfo.actions %} +{% for actioninfo in catinfo.actions %} +# (( $+functions[_yunohost_{{ catinfo.name }}_{{ actioninfo.name }}] )) || +{%- if actioninfo.arguments %} +function _yunohost_{{ catinfo.name }}_{{ actioninfo.name }}() { + {%- if actioninfo.cases %} + local context state state_descr line + typeset -A opt_args + {% endif %} + _arguments -s -C \ + {%- for argumentinfo in actioninfo.arguments %} + {{ argumentinfo }} {% if loop.revindex != 1 %}\{% endif %} + {%- endfor %} + {%- if actioninfo.cases %} + + if (($CURRENT > 2)); then + case "$state" in + {%- for case in actioninfo.cases %} + {{ case.name }}) + local previous="$words[${CURRENT} - 1]" + local cmd_ret=$({{ case.shell_call }}) + if (( ${#cmd_ret} != 0 )); then + local -a cmd_list=("${(s/ /)cmd_ret}") + _values '{{ case.name }}' $cmd_list + fi + ;; + {%- endfor %} + esac + fi + return $? + {%- endif %} +} + +{%- else %} +function _yunohost_{{ catinfo.name }}_{{ actioninfo.name }}() { } + +{%- endif %} +{% endfor %} +{% endif %} +{%- endif %} +{%- endfor %} + +#----------------------------------------- +# Completion functions +#----------------------------------------- + +{% for funcinfo in functions %} +{%- if funcinfo.aggregated %} + +# (( $+functions[{{ funcinfo.name }}] )) || +function {{ funcinfo.name }}() { + _alternative \ + {{ funcinfo.aggregated }} +} +{%- elif funcinfo.shell_call %} + +# (( $+functions[{{ funcinfo.name }}] )) || +function {{ funcinfo.name }}() { + compadd "$@" -- ${(@)$({{ funcinfo.shell_call }})} +} +{%- endif %} +{% endfor %} + +_yunohost "$@" \ No newline at end of file diff --git a/helpers/helpers b/helpers/helpers new file mode 100644 index 0000000..a03733c --- /dev/null +++ b/helpers/helpers @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 | sort) + source $YNH_HELPERS_DIR/getopts + 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 0000000..b46282e --- /dev/null +++ b/helpers/helpers.v1.d/apps @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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/helpers.v1.d/apt b/helpers/helpers.v1.d/apt new file mode 100644 index 0000000..835d5a2 --- /dev/null +++ b/helpers/helpers.v1.d/apt @@ -0,0 +1,662 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Check if apt is free to use, or wait, until timeout. +# +# [internal] +# +# usage: ynh_wait_dpkg_free +# | exit: Return 1 if dpkg is broken +# +# Requires YunoHost version 3.3.1 or higher. +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_err "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." + set -o xtrace # set -x + return 1 + fi + done 9<<< "$(ls -1 $dpkg_dir)" + 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 +# +# example: ynh_package_is_installed --package=yunohost && echo "installed" +# +# usage: ynh_package_is_installed --package=name +# | arg: -p, --package= - the package name to check +# | ret: 0 if the package is installed, 1 else. +# +# Requires YunoHost version 2.2.4 or higher. +ynh_package_is_installed() { + # Declare an array to define the options of this helper. + local legacy_args=p + local -A args_array=([p]=package=) + local package + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + dpkg-query --show --showformat='${Status}' "$package" 2> /dev/null \ + | grep --count "ok installed" &> /dev/null +} + +# Get the version of an installed package +# +# 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 +# +# Requires YunoHost version 2.2.4 or higher. +ynh_package_version() { + # Declare an array to define the options of this helper. + local legacy_args=p + local -A args_array=([p]=package=) + local package + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if ynh_package_is_installed "$package"; then + dpkg-query --show --showformat='${Version}' "$package" 2> /dev/null + else + echo '' + fi +} + +# APT wrapper for non-interactive operation +# +# [internal] +# +# usage: ynh_apt update +# +# Requires YunoHost version 2.4.0.3 or higher. +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 $@ +} + +# Update package index files +# +# [internal] +# +# usage: ynh_package_update +# +# Requires YunoHost version 2.2.4 or higher. +ynh_package_update() { + ynh_apt update --error-on=any +} + +# Install package(s) +# +# [internal] +# +# usage: ynh_package_install name [name [...]] +# | arg: name - the package name to install +# +# Requires YunoHost version 2.2.4 or higher. +ynh_package_install() { + ynh_apt --no-remove --option Dpkg::Options::=--force-confdef \ + --option Dpkg::Options::=--force-confold install $@ +} + +# Remove package(s) +# +# [internal] +# +# usage: ynh_package_remove name [name [...]] +# | arg: name - the package name to remove +# +# Requires YunoHost version 2.2.4 or higher. +ynh_package_remove() { + ynh_apt remove $@ +} + +# Remove package(s) and their uneeded dependencies +# +# [internal] +# +# usage: ynh_package_autoremove name [name [...]] +# | arg: name - the package name to remove +# +# Requires YunoHost version 2.2.4 or higher. +ynh_package_autoremove() { + ynh_apt autoremove $@ +} + +# Purge package(s) and their uneeded dependencies +# +# [internal] +# +# usage: ynh_package_autopurge name [name [...]] +# | arg: name - the package name to autoremove and purge +# +# Requires YunoHost version 2.7.2 or higher. +ynh_package_autopurge() { + ynh_apt autoremove --purge $@ +} + +# Build and install a package from an equivs control file +# +# [internal] +# +# example: generate an empty control file with `equivs-control`, adjust its +# content and use helper to build and install the package: +# ynh_package_install_from_equivs /path/to/controlfile +# +# usage: ynh_package_install_from_equivs controlfile +# | arg: controlfile - path of the equivs control file +# +# Requires YunoHost version 2.2.4 or higher. +ynh_package_install_from_equivs() { + local controlfile=$1 + + # retrieve package information + local pkgname=$(grep '^Package: ' $controlfile | cut --delimiter=' ' --fields=2) # Retrieve the name of the debian package + local pkgversion=$(grep '^Version: ' $controlfile | cut --delimiter=' ' --fields=2) # And its version number + [[ -z "$pkgname" || -z "$pkgversion" ]] \ + && ynh_die --message="Invalid control file" # Check if this 2 variables aren't empty. + + # Update packages cache + ynh_package_update + + # Build and install the package + local TMPDIR=$(mktemp --directory) + 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}/${pkgname}/DEBIAN/control" + + # Install the fake package without its dependencies with dpkg --force-depends + if ! LC_ALL=C dpkg-deb --build "${TMPDIR}/${pkgname}" "${TMPDIR}/${pkgname}.deb" > "${TMPDIR}/dpkg_log" 2>&1; then + cat "${TMPDIR}/dpkg_log" >&2 + ynh_die --message="Unable to install dependencies" + fi + # Don't crash in case of error, because is nicely covered by the following line + LC_ALL=C dpkg --force-depends --install "${TMPDIR}/${pkgname}.deb" 2>&1 | tee "${TMPDIR}/dpkg_log" || true + + ynh_package_install --fix-broken \ + || { # If the installation failed + # (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process) + # Parse the list of problematic dependencies from dpkg's log ... + # (relevant lines look like: "foo-ynh-deps depends on bar; however:") + local problematic_dependencies="$(cat $TMPDIR/dpkg_log | grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' | tr '\n' ' ')" + # Fake an install of those dependencies to see the errors + # The sed command here is, Print only from 'Reading state info' to the end. + [[ -n "$problematic_dependencies" ]] && ynh_package_install $problematic_dependencies --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2 + ynh_die --message="Unable to install dependencies" + } + [[ -n "$TMPDIR" ]] && rm --recursive --force $TMPDIR # Remove the temp dir. + + # check if the package is actually installed + ynh_package_is_installed "$pkgname" +} + +YNH_INSTALL_APP_DEPENDENCIES_REPLACE="true" + +# Define and install dependencies with a equivs control file +# +# This helper can/should only be called once per app +# +# 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). +# +# Requires YunoHost version 2.6.4 or higher. +ynh_install_app_dependencies() { + local dependencies=$@ + # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below) + dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')" + local dependencies=${dependencies//|/ | } + + local version=$(ynh_read_manifest --manifest_key="version") + if [ -z "${version}" ] || [ "$version" == "null" ]; then + version="1.0" + fi + local dep_app=${app//_/-} # Replace all '_' by '-' + + # Handle specific versions + if [[ "$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="$(echo "$dependencies" | sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g')" + fi + + # Check for specific php dependencies which requires sury + # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" + # The (?<=php) syntax corresponds to lookbehind ;) + local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>|)' | sort -u) + + if [[ -n "$specific_php_version" ]]; then + # Cover a small edge case where a packager could have specified "php7.4-pwet php5-gni" which is confusing + [[ $(echo $specific_php_version | wc -l) -eq 1 ]] \ + || ynh_die --message="Inconsistent php versions in dependencies ... found : $specific_php_version" + + dependencies+=", php${specific_php_version}, php${specific_php_version}-fpm, php${specific_php_version}-common" + + local old_phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) + + # If the PHP version changed, remove the old fpm conf + if [ -n "$old_phpversion" ] && [ "$old_phpversion" != "$specific_php_version" ]; then + local old_php_fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) + local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf" + + if [[ -f "$old_php_finalphpconf" ]]; then + ynh_backup_if_checksum_is_different --file="$old_php_finalphpconf" + ynh_remove_fpm_config + fi + fi + # Store phpversion into the config of this app + ynh_app_setting_set --app=$app --key=phpversion --value=$specific_php_version + + # Set the default php version back as the default version for php-cli. + if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION; then + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + fi + elif grep --quiet 'php' <<< "$dependencies"; then + ynh_app_setting_set --app=$app --key=phpversion --value=$YNH_DEFAULT_PHP_VERSION + fi + + local psql_installed="$(ynh_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)" + + # The first time we run ynh_install_app_dependencies, we will replace the + # entire control file (This is in particular meant to cover the case of + # upgrade script where ynh_install_app_dependencies is called with this + # expected effect) Otherwise, any subsequent call will add dependencies + # to those already present in the equivs control file. + if [[ $YNH_INSTALL_APP_DEPENDENCIES_REPLACE == "true" ]]; then + YNH_INSTALL_APP_DEPENDENCIES_REPLACE="false" + else + local current_dependencies="" + if ynh_package_is_installed --package="${dep_app}-ynh-deps"; then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " + current_dependencies=${current_dependencies// | /|} + fi + dependencies="$current_dependencies, $dependencies" + fi + + cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build +Section: misc +Priority: optional +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 + + ynh_package_install_from_equivs /tmp/${dep_app}-ynh-deps.control \ + || ynh_die --message="Unable to install dependencies" # Install the fake package and its dependencies + rm /tmp/${dep_app}-ynh-deps.control + + # Trigger postgresql regenconf if we may have just installed postgresql + local psql_installed2="$(ynh_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)" + if [[ "$psql_installed" != "$psql_installed2" ]]; then + yunohost tools regen-conf postgresql + fi + +} + +# 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. +# +# Requires YunoHost version 3.8.1 or higher. +ynh_add_app_dependencies() { + # Declare an array to define the options of this helper. + local legacy_args=pr + local -A args_array=([p]=package= [r]=replace) + local package + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + ynh_print_warn --message="Packagers: ynh_add_app_dependencies is deprecated and is now only an alias to ynh_install_app_dependencies" + ynh_install_app_dependencies "${package}" +} + +# Remove fake package and its dependencies +# +# Dependencies will removed only if no other package need them. +# +# usage: ynh_remove_app_dependencies +# +# Requires YunoHost version 2.6.4 or higher. +ynh_remove_app_dependencies() { + local dep_app=${app//_/-} # Replace all '_' by '-' + + local current_dependencies="" + if ynh_package_is_installed --package="${dep_app}-ynh-deps"; then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " + current_dependencies=${current_dependencies// | /|} + fi + + # Edge case where the app dep may be on hold, + # cf https://forum.yunohost.org/t/migration-error-cause-of-ffsync/20675/4 + if apt-mark showhold | grep -q -w ${dep_app}-ynh-deps; then + apt-mark unhold ${dep_app}-ynh-deps + fi + + # Remove the fake package and its dependencies if they not still used. + # (except if dpkg doesn't know anything about the package, + # which should be symptomatic of a failed install, and we don't want bash to report an error) + if dpkg-query --show ${dep_app}-ynh-deps &> /dev/null; then + ynh_package_autopurge ${dep_app}-ynh-deps + fi +} + +# Install packages from an extra repository properly. +# +# usage: ynh_install_extra_app_dependencies --repo="repo" --package="dep1 dep2" [--key=key_url] [--name=name] +# | arg: -r, --repo= - Complete url of the extra repository. +# | arg: -p, --package= - The packages to install from this extra repository +# | arg: -k, --key= - url to get the public key. +# | arg: -n, --name= - Name for the files for this repo, $app as default value. +# +# Requires YunoHost version 3.8.1 or higher. +ynh_install_extra_app_dependencies() { + # Declare an array to define the options of this helper. + local legacy_args=rpkn + local -A args_array=([r]=repo= [p]=package= [k]=key= [n]=name=) + local repo + local package + local key + local name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + name="${name:-$app}" + key=${key:-} + + # Set a key only if asked + if [ -n "$key" ]; then + key="--key=$key" + fi + # Add an extra repository for those packages + ynh_install_extra_repo --repo="$repo" $key --priority=995 --name=$name + + # Install requested dependencies from this extra repository. + ynh_install_app_dependencies "$package" + + # Force to upgrade to the last version... + # Without doing apt install, an already installed dep is not upgraded + local apps_auto_installed="$(apt-mark showauto $package)" + ynh_package_install "$package" + [ -z "$apps_auto_installed" ] || apt-mark auto $apps_auto_installed + + # Remove this extra repository after packages are installed + ynh_remove_extra_repo --name=$name +} + +# Add an extra repository correctly, pin it and get the key. +# +# [internal] +# +# usage: ynh_install_extra_repo --repo="repo" [--key=key_url] [--priority=priority_value] [--name=name] [--append] +# | arg: -r, --repo= - Complete url of the extra repository. +# | arg: -k, --key= - url to get the public key. +# | arg: -p, --priority= - Priority for the pin +# | arg: -n, --name= - Name for the files for this repo, $app as default value. +# | arg: -a, --append - Do not overwrite existing files. +# +# Requires YunoHost version 3.8.1 or higher. +ynh_install_extra_repo() { + # Declare an array to define the options of this helper. + local legacy_args=rkpna + local -A args_array=([r]=repo= [k]=key= [p]=priority= [n]=name= [a]=append) + local repo + local key + local priority + local name + local append + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + name="${name:-$app}" + append=${append:-0} + key=${key:-} + priority=${priority:-} + + if [ $append -eq 1 ]; then + append="--append" + wget_append="tee --append" + else + append="" + wget_append="tee" + fi + + 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. + 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 + + # Add the repository into sources.list.d + 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 + local pin="${uri#*://}" + pin="${pin%%/*}" + # Set a priority only if asked + if [ -n "$priority" ]; then + priority="--priority=$priority" + fi + ynh_pin_repo --package="*" --pin="origin \"$pin\"" $priority --name="$name" $append + + # Get the public key for the repo + if [ -n "$key" ] && [[ "$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 + fi + + # Update the list of package with the new repo + ynh_package_update +} + +# Remove an extra repository and the assiociated configuration. +# +# [internal] +# +# usage: ynh_remove_extra_repo [--name=name] +# | arg: -n, --name= - Name for the files for this repo, $app as default value. +# +# Requires YunoHost version 3.8.1 or higher. +ynh_remove_extra_repo() { + # Declare an array to define the options of this helper. + local legacy_args=n + local -A args_array=([n]=name=) + local name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + name="${name:-$app}" + + ynh_secure_remove --file="/etc/apt/sources.list.d/$name.list" + # Sury pinning is managed by the regenconf in the core... + [[ "$name" == "extra_php_version" ]] || ynh_secure_remove "/etc/apt/preferences.d/$name" + if [ -e /etc/apt/trusted.gpg.d/$name.gpg ]; then + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.gpg" + fi + + # (Do we even create a .asc file anywhere ...?) + if [ -e /etc/apt/trusted.gpg.d/$name.asc ]; then + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.asc" + fi + + # Update the list of package to exclude the old repo + ynh_package_update +} + +# Add a repository. +# +# [internal] +# +# usage: ynh_add_repo --uri=uri --suite=suite --component=component [--name=name] [--append] +# | arg: -u, --uri= - Uri of the repository. +# | arg: -s, --suite= - Suite of the repository. +# | 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 +# ynh_add_repo --uri=http://forge.yunohost.org/debian/ --suite=stretch --component=stable +# +# Requires YunoHost version 3.8.1 or higher. +ynh_add_repo() { + # Declare an array to define the options of this helper. + 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 $trust $uri $suite $component" \ + | $append "/etc/apt/sources.list.d/$name.list" +} + +# Pin a repository. +# +# [internal] +# +# usage: ynh_pin_repo --package=packages --pin=pin_filter [--priority=priority_value] [--name=name] [--append] +# | arg: -p, --package= - Packages concerned by the pin. Or all, *. +# | arg: -i, --pin= - Filter for the pin. +# | arg: -p, --priority= - Priority for the pin +# | arg: -n, --name= - Name for the files for this repo, $app as default value. +# | arg: -a, --append - Do not overwrite existing files. +# +# See https://manpages.debian.org/stretch/apt/apt_preferences.5.en.html#How_APT_Interprets_Priorities for information about pinning. +# +# Requires YunoHost version 3.8.1 or higher. +ynh_pin_repo() { + # Declare an array to define the options of this helper. + local legacy_args=pirna + local -A args_array=([p]=package= [i]=pin= [r]=priority= [n]=name= [a]=append) + local package + local pin + local priority + local name + local append + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + package="${package:-*}" + priority=${priority:-50} + name="${name:-$app}" + append=${append:-0} + + if [ $append -eq 1 ]; then + append="tee --append" + else + append="tee" + fi + + # Sury pinning is managed by the regenconf in the core... + [[ "$name" != "extra_php_version" ]] || return 0 + + mkdir --parents "/etc/apt/preferences.d" + echo "Package: $package +Pin: $pin +Pin-Priority: $priority +" \ + | $append "/etc/apt/preferences.d/$name" +} diff --git a/helpers/helpers.v1.d/backup b/helpers/helpers.v1.d/backup new file mode 100644 index 0000000..c433ad4 --- /dev/null +++ b/helpers/helpers.v1.d/backup @@ -0,0 +1,522 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +CAN_BIND=${CAN_BIND:-1} + +# Add a file or a directory to the list of paths to backup +# +# usage: ynh_backup --src_path=src_path [--dest_path=dest_path] [--is_big] [--not_mandatory] +# | arg: -s, --src_path= - file or directory to bind or symlink or copy. it shouldn't be in the backup dir. +# | arg: -d, --dest_path= - destination file or directory inside the backup dir +# | arg: -b, --is_big - Indicate data are big (mail, video, image ...) +# | arg: -m, --not_mandatory - Indicate that if the file is missing, the backup can ignore it. +# +# This helper can be used both in a system backup hook, and in an app backup script +# +# `ynh_backup` writes `src_path` and the relative `dest_path` into a CSV file, and it +# creates the parent destination directory +# +# If `dest_path` is ended by a slash it complete this path with the basename of `src_path`. +# +# Example in the context of a wordpress app : +# ``` +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" +# # => This line will be added into CSV file +# # "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf/nginx.conf" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/nginx.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf/" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf" +# +# #Deprecated usages (maintained for retro-compatibility) +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "${backup_dir}/conf/nginx.conf" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/nginx.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "/conf/" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf" +# +# ``` +# +# How to use `--is_big`: +# +# `--is_big` is used to specify that this part of the backup can be quite huge. +# So, you don't want that your package does backup that part during ynh_backup_before_upgrade. +# In the same way, an user may doesn't want to backup this big part of the app for +# each of his backup. And so handle that part differently. +# +# As this part of your backup may not be done, your restore script has to handle it. +# In your restore script, use `--not_mandatory` with `ynh_restore_file` +# As well in your remove script, you should not remove those data ! Or an user may end up with +# a failed upgrade restoring an app without data anymore ! +# +# To have the benefit of `--is_big` while doing a backup, you can whether set the environement +# variable `BACKUP_CORE_ONLY` to 1 (`BACKUP_CORE_ONLY=1`) before the backup command. It will affect +# only that backup command. +# Or set the config `do_not_backup_data` to 1 into the `settings.yml` of the app. This will affect +# all backups for this app until the setting is removed. +# +# Requires YunoHost version 2.4.0 or higher. +# Requires YunoHost version 3.5.0 or higher for the argument `--not_mandatory` +ynh_backup() { + # TODO find a way to avoid injection by file strange naming ! + + # Declare an array to define the options of this helper. + local legacy_args=sdbm + local -A args_array=([s]=src_path= [d]=dest_path= [b]=is_big [m]=not_mandatory) + local src_path + local dest_path + local is_big + local not_mandatory + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + dest_path="${dest_path:-}" + is_big="${is_big:-0}" + not_mandatory="${not_mandatory:-0}" + + BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0} + test -n "${app:-}" && do_not_backup_data=$(ynh_app_setting_get --app=$app --key=do_not_backup_data) + + # If backing up core only (used by ynh_backup_before_upgrade), + # don't backup big data items + if [ $is_big -eq 1 ] && ([ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ]); then + if [ $BACKUP_CORE_ONLY -eq 1 ]; then + ynh_print_info --message="$src_path will not be saved, because 'BACKUP_CORE_ONLY' is set." + else + ynh_print_info --message="$src_path will not be saved, because 'do_not_backup_data' is set." + fi + return 0 + fi + + # ============================================================================== + # Format correctly source and destination paths + # ============================================================================== + # Be sure the source path is not empty + if [ ! -e "$src_path" ]; then + ynh_print_warn --message="Source path '${src_path}' does not exist" + if [ "$not_mandatory" == "0" ]; then + # This is a temporary fix for fail2ban config files missing after the migration to stretch. + if echo "${src_path}" | grep --quiet "/etc/fail2ban"; then + touch "${src_path}" + ynh_print_info --message="The missing file will be replaced by a dummy one for the backup !!!" + else + return 1 + fi + else + return 0 + fi + fi + + # Transform the source path as an absolute path + # If it's a dir remove the ending / + src_path=$(realpath "$src_path") + + # If there is no destination path, initialize it with the source path + # relative to "/". + # eg: src_path=/etc/yunohost -> dest_path=etc/yunohost + if [[ -z "$dest_path" ]]; then + dest_path="${src_path#/}" + + else + if [[ "${dest_path:0:1}" == "/" ]]; then + + # If the destination path is an absolute path, transform it as a path + # relative to the current working directory ($YNH_CWD) + # + # If it's an app backup script that run this helper, YNH_CWD is equal to + # $YNH_BACKUP_DIR/apps/APP_INSTANCE_NAME/backup/ + # + # If it's a system part backup script, YNH_CWD is equal to $YNH_BACKUP_DIR + dest_path="${dest_path#$YNH_CWD/}" + + # Case where $2 is an absolute dir but doesn't begin with $YNH_CWD + if [[ "${dest_path:0:1}" == "/" ]]; then + dest_path="${dest_path#/}" + fi + fi + + # Complete dest_path if ended by a / + if [[ "${dest_path: -1}" == "/" ]]; then + dest_path="${dest_path}/$(basename $src_path)" + fi + fi + + # Check if dest_path already exists in tmp archive + if [[ -e "${dest_path}" ]]; then + ynh_print_err --message="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}") +} + +# Restore all files that were previously backuped in a core backup script or app backup script +# +# usage: ynh_restore +# +# Requires YunoHost version 2.6.4 or higher. +ynh_restore() { + # Deduce the relative path of $YNH_CWD + local REL_DIR="${YNH_CWD#$YNH_BACKUP_DIR/}" + REL_DIR="${REL_DIR%/}/" + + # For each destination path begining by $REL_DIR + cat ${YNH_BACKUP_CSV} | tr --delete $'\r' | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR.*\"$" \ + | while read line; do + local ORIGIN_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\"\K.*(?=\",\".*\"$)") + local ARCHIVE_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\".*\",\"$REL_DIR\K.*(?=\"$)") + ynh_restore_file --origin_path="$ARCHIVE_PATH" --dest_path="$ORIGIN_PATH" + done +} + +# 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 +# +# usage: ynh_restore_file --origin_path=origin_path [--dest_path=dest_path] [--not_mandatory] +# | arg: -o, --origin_path= - Path where was located the file or the directory before to be backuped or relative path to $YNH_CWD where it is located in the backup archive +# | arg: -d, --dest_path= - Path where restore the file or the dir. If unspecified, the destination will be `ORIGIN_PATH` or if the `ORIGIN_PATH` doesn't exist in the archive, the destination will be searched into `backup.csv` +# | arg: -m, --not_mandatory - Indicate that if the file is missing, the restore process can ignore it. +# +# Use the registered path in backup_list by ynh_backup to restore the file at the right place. +# +# examples: +# ynh_restore_file -o "/etc/nginx/conf.d/$domain.d/$app.conf" +# # You can also use relative paths: +# ynh_restore_file -o "conf/nginx.conf" +# +# If `DEST_PATH` already exists and is lighter than 500 Mo, a backup will be made in +# `/var/cache/yunohost/appconfbackup/`. Otherwise, the existing file is removed. +# +# if `apps/$app/etc/nginx/conf.d/$domain.d/$app.conf` exists, restore it into +# `/etc/nginx/conf.d/$domain.d/$app.conf` +# if no, search for a match in the csv (eg: conf/nginx.conf) and restore it into +# `/etc/nginx/conf.d/$domain.d/$app.conf` +# +# Requires YunoHost version 2.6.4 or higher. +# Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory +ynh_restore_file() { + # Declare an array to define the options of this helper. + local legacy_args=odm + local -A args_array=([o]=origin_path= [d]=dest_path= [m]=not_mandatory) + local origin_path + local dest_path + local not_mandatory + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + origin_path="/${origin_path#/}" + # Default value for dest_path = /$origin_path + dest_path="${dest_path:-$origin_path}" + not_mandatory="${not_mandatory:-0}" + + local archive_path="$YNH_CWD${origin_path}" + # If archive_path doesn't exist, search for a corresponding path in CSV + if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]; then + if [ "$not_mandatory" == "0" ]; then + archive_path="$YNH_BACKUP_DIR/$(_get_archive_path \"$origin_path\")" + else + return 0 + fi + fi + + # Move the old directory if it already exists + if [[ -e "${dest_path}" ]]; then + # Check if the file/dir size is less than 500 Mo + if [[ $(du --summarize --bytes ${dest_path} | cut --delimiter="/" --fields=1) -le "500000000" ]]; then + local backup_file="/var/cache/yunohost/appconfbackup/${dest_path}.backup.$(date '+%Y%m%d.%H%M%S')" + mkdir --parents "$(dirname "$backup_file")" + mv "${dest_path}" "$backup_file" # Move the current file or directory + else + ynh_secure_remove --file=${dest_path} + fi + fi + + # Restore origin_path into dest_path + mkdir --parents $(dirname "$dest_path") + + # Do a copy if it's just a mounting point + if mountpoint --quiet $YNH_BACKUP_DIR; then + if [[ -d "${archive_path}" ]]; then + archive_path="${archive_path}/." + mkdir --parents "$dest_path" + fi + cp --archive "$archive_path" "${dest_path}" + # Do a move if YNH_BACKUP_DIR is already a copy + else + mv "$archive_path" "${dest_path}" + fi + + # Boring hack for nginx conf file mapped to php7.3 + # Note that there's no need to patch the fpm config because most php apps + # will call "ynh_add_fpm_config" during restore, effectively recreating the file from scratch + if [[ "${dest_path}" == "/etc/nginx/conf.d/"* ]] && grep 'php7.3.*sock' "${dest_path}"; then + sed -i 's/php7.3/php7.4/g' "${dest_path}" + fi +} + +# Calculate and store a file checksum into the app settings +# +# usage: ynh_store_file_checksum --file=file +# | arg: -f, --file= - The file on which the checksum will performed, then stored. +# +# $app should be defined when calling this helper +# +# Requires YunoHost version 2.6.4 or higher. +ynh_store_file_checksum() { + # Declare an array to define the options of this helper. + local legacy_args=f + local -A args_array=([f]=file= [u]=update_only) + local file + local update_only + update_only="${update_only:-0}" + + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + + # If update only, we don't save the new checksum if no old checksum exist + if [ $update_only -eq 1 ]; then + local checksum_value=$(ynh_app_setting_get --app=$app --key=$checksum_setting_name) + if [ -z "${checksum_value}" ]; then + unset backup_file_checksum + return 0 + fi + fi + + ynh_app_setting_set --app=$app --key=$checksum_setting_name --value=$(md5sum "$file" | cut --delimiter=' ' --fields=1) + + if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then + # Using a base64 is in fact more reversible than "replace / and space by _" ... So we can in fact obtain the original file path in an easy reliable way ... + local file_path_base64=$(echo "$file" | base64 -w0) + mkdir -p /var/cache/yunohost/appconfbackup/ + cat $file > /var/cache/yunohost/appconfbackup/original_${file_path_base64} + fi + + # If backup_file_checksum isn't empty, ynh_backup_if_checksum_is_different has made a backup + if [ -n "${backup_file_checksum-}" ]; then + # Print the diff between the previous file and the new one. + # 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 +} + +# Verify the checksum and backup the file if it's different +# +# usage: ynh_backup_if_checksum_is_different --file=file +# | arg: -f, --file= - The file on which the checksum test will be perfomed. +# | ret: the name of a backup file, or nothing +# +# This helper is primarily meant to allow to easily backup personalised/manually +# modified config files. +# +# Requires YunoHost version 2.6.4 or higher. +ynh_backup_if_checksum_is_different() { + # Declare an array to define the options of this helper. + local legacy_args=f + local -A args_array=([f]=file=) + local file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + local checksum_value=$(ynh_app_setting_get --app=$app --key=$checksum_setting_name) + # backup_file_checksum isn't declare as local, so it can be reuse by ynh_store_file_checksum + backup_file_checksum="" + if [ -n "$checksum_value" ]; then # Proceed only if a value was stored into the app settings + if [ -e $file ] && ! echo "$checksum_value $file" | md5sum --check --status; then # If the checksum is now different + + backup_file_checksum="/var/cache/yunohost/appconfbackup/$file.backup.$(date '+%Y%m%d.%H%M%S')" + mkdir --parents "$(dirname "$backup_file_checksum")" + cp --archive "$file" "$backup_file_checksum" # Backup the current file + ynh_print_warn "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file_checksum" + echo "$backup_file_checksum" # Return the name of the backup file + if [ ${PACKAGE_CHECK_EXEC:-0} -eq 1 ]; then + local file_path_base64=$(echo "$file" | base64 -w0) + if test -e /var/cache/yunohost/appconfbackup/original_${file_path_base64}; then + ynh_print_warn "Diff with the original file:" + diff --report-identical-files --unified --color=always /var/cache/yunohost/appconfbackup/original_${file_path_base64} $file >&2 || true + fi + fi + fi + fi +} + +# Delete a file checksum from the app settings +# +# usage: ynh_delete_file_checksum --file=file +# | arg: -f, --file= - The file for which the checksum will be deleted +# +# $app should be defined when calling this helper +# +# Requires YunoHost version 3.3.1 or higher. +ynh_delete_file_checksum() { + # Declare an array to define the options of this helper. + local legacy_args=f + local -A args_array=([f]=file=) + local file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + ynh_app_setting_delete --app=$app --key=$checksum_setting_name +} + +# Checks a backup archive exists +# +# [internal] +# +ynh_backup_archive_exists() { + yunohost backup list --output-as json --quiet \ + | jq -e --arg archive "$1" '.archives | index($archive)' > /dev/null +} + +# Make a backup in case of failed upgrade +# +# [packagingv1] +# +# usage: ynh_backup_before_upgrade +# +# Usage in a package script: +# ``` +# ynh_backup_before_upgrade +# ynh_clean_setup () { +# ynh_restore_upgradebackup +# } +# ynh_abort_if_errors +# ``` +# +# Requires YunoHost version 2.7.2 or higher. +ynh_backup_before_upgrade() { + if [ ! -e "/etc/yunohost/apps/$app/scripts/backup" ]; then + ynh_print_warn --message="This app doesn't have any backup script." + return + fi + backup_number=1 + local old_backup_number=2 + local app_bck=${app//_/-} # Replace all '_' by '-' + NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} + + if [ "$NO_BACKUP_UPGRADE" -eq 0 ]; then + # Check if a backup already exists with the prefix 1 + if ynh_backup_archive_exists "$app_bck-pre-upgrade1"; then + # Prefix becomes 2 to preserve the previous backup + backup_number=2 + old_backup_number=1 + fi + + # Create backup + BACKUP_CORE_ONLY=1 yunohost backup create --apps $app --name $app_bck-pre-upgrade$backup_number --debug + if [ "$?" -eq 0 ]; then + # If the backup succeeded, remove the previous backup + if ynh_backup_archive_exists "$app_bck-pre-upgrade$old_backup_number"; then + # Remove the previous backup only if it exists + yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null + fi + else + ynh_die --message="Backup failed, the upgrade process was aborted." + fi + else + ynh_print_warn --message="\$NO_BACKUP_UPGRADE is set, backup will be avoided. Be careful, this upgrade is going to be operated without a security backup" + fi +} + +# Restore a previous backup if the upgrade process failed +# +# [packagingv1] +# +# usage: ynh_restore_upgradebackup +# +# Usage in a package script: +# ``` +# ynh_backup_before_upgrade +# ynh_clean_setup () { +# ynh_restore_upgradebackup +# } +# ynh_abort_if_errors +# ``` +# +# Requires YunoHost version 2.7.2 or higher. +ynh_restore_upgradebackup() { + ynh_print_err --message="Upgrade failed." + local app_bck=${app//_/-} # Replace all '_' by '-' + + NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} + + if [ "$NO_BACKUP_UPGRADE" -eq 0 ]; then + # Check if an existing backup can be found before removing and restoring the application. + if ynh_backup_archive_exists "$app_bck-pre-upgrade$backup_number"; then + # Remove the application then restore it + yunohost app remove $app + # Restore the backup + yunohost backup restore $app_bck-pre-upgrade$backup_number --apps $app --force --debug + if [[ -d /etc/yunohost/apps/$app ]]; then + ynh_die --message="The app was restored to the way it was before the failed upgrade." + else + ynh_die --message="Uhoh ... Yunohost failed to restore the app to the way it was before the failed upgrade :|" + fi + fi + else + ynh_print_warn --message="\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !" + fi +} diff --git a/helpers/helpers.v1.d/composer b/helpers/helpers.v1.d/composer new file mode 100644 index 0000000..4876bae --- /dev/null +++ b/helpers/helpers.v1.d/composer @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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/helpers.v1.d/config b/helpers/helpers.v1.d/config new file mode 100644 index 0000000..0e18594 --- /dev/null +++ b/helpers/helpers.v1.d/config @@ -0,0 +1,321 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +_ynh_app_config_get_one() { + local short_setting="$1" + local type="$2" + local bind="$3" + local getter="get__${short_setting}" + # Get value from getter if exists + if type -t $getter 2> /dev/null | grep -q '^function$' 2> /dev/null; then + old[$short_setting]="$($getter)" + formats[${short_setting}]="yaml" + + elif [[ "$bind" == *"("* ]] && type -t "get__${bind%%(*}" 2> /dev/null | grep -q '^function$' 2> /dev/null; then + old[$short_setting]="$("get__${bind%%(*}" $short_setting $type $bind)" + formats[${short_setting}]="yaml" + + elif [[ "$bind" == "null" ]]; then + old[$short_setting]="YNH_NULL" + + # Get value from app settings or from another file + elif [[ "$type" == "file" ]]; then + if [[ "$bind" == "settings" ]]; then + ynh_die --message="File '${short_setting}' can't be stored in settings" + fi + old[$short_setting]="$(ls "$bind" 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="$app" --key="$short_setting")" + elif [[ "$bind" == *":"* ]]; then + ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" + else + old[$short_setting]="$(cat "$bind" 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)" + old[$short_setting]="$(ynh_read_var_in_file --file="${bind_file}" --key="${bind_key_}" --after="${bind_after}")" + + fi +} +_ynh_app_config_apply_one() { + local short_setting="$1" + local setter="set__${short_setting}" + local bind="${binds[$short_setting]}" + local type="${types[$short_setting]}" + if [ "${changed[$short_setting]}" == "true" ]; then + # Apply setter if exists + if type -t $setter 2> /dev/null | grep -q '^function$' 2> /dev/null; then + $setter + + elif [[ "$bind" == *"("* ]] && type -t "set__${bind%%(*}" 2> /dev/null | grep -q '^function$' 2> /dev/null; then + "set__${bind%%(*}" $short_setting $type $bind + + elif [[ "$bind" == "null" ]]; then + return + + # Save in a file + elif [[ "$type" == "file" ]]; then + if [[ "$bind" == "settings" ]]; then + ynh_die --message="File '${short_setting}' can't be stored in settings" + fi + local bind_file="$bind" + if [[ "${!short_setting}" == "" ]]; then + ynh_backup_if_checksum_is_different --file="$bind_file" + ynh_secure_remove --file="$bind_file" + ynh_delete_file_checksum --file="$bind_file" + ynh_print_info --message="File '$bind_file' removed" + else + ynh_backup_if_checksum_is_different --file="$bind_file" + if [[ "${!short_setting}" != "$bind_file" ]]; then + cp "${!short_setting}" "$bind_file" + fi + ynh_store_file_checksum --file="$bind_file" --update_only + ynh_print_info --message="File '$bind_file' overwritten with ${!short_setting}" + fi + + # Save value in app settings + elif [[ "$bind" == "settings" ]]; then + ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" + ynh_print_info --message="Configuration key '$short_setting' edited in app settings" + + # Save multiline text in a file + elif [[ "$type" == "text" ]]; then + if [[ "$bind" == *":"* ]]; then + ynh_die --message="For technical reasons, multiline text '${short_setting}' can't be stored automatically in a variable file, you have to create custom getter/setter" + fi + local bind_file="$bind" + ynh_backup_if_checksum_is_different --file="$bind_file" + echo "${!short_setting}" > "$bind_file" + ynh_store_file_checksum --file="$bind_file" --update_only + ynh_print_info --message="File '$bind_file' overwritten with the content provided in question '${short_setting}'" + + # Set value into a kind of key/value file + else + local bind_after="" + local bind_key_="$(echo "$bind" | cut -d: -f1)" + 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)" + + ynh_backup_if_checksum_is_different --file="$bind_file" + ynh_write_var_in_file --file="${bind_file}" --key="${bind_key_}" --value="${!short_setting}" --after="${bind_after}" + ynh_store_file_checksum --file="$bind_file" --update_only + + # We stored the info in settings in order to be able to upgrade the app + ynh_app_setting_set --app=$app --key=$short_setting --value="${!short_setting}" + ynh_print_info --message="Configuration key '$bind_key_' edited into $bind_file" + + fi + fi +} + +_ynh_app_config_get() { + for line in $YNH_APP_CONFIG_PANEL_OPTIONS_TYPES_AND_BINDS; 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 --message="Checking what changed in the new configuration..." --weight=1 + 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 --message="Nothing has changed" + exit 0 + fi + + # Run validation if something is changed + ynh_script_progression --message="Validating the new configuration..." --weight=1 + + 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 --message="Reading config panel description and current configuration..." + ynh_app_config_get + + ynh_app_config_validate + + ynh_script_progression --message="Applying the new configuration..." + ynh_app_config_apply + ynh_script_progression --message="Configuration of $app completed" --last + ;; + *) + ynh_app_action_run $1 + ;; + esac +} diff --git a/helpers/helpers.v1.d/fail2ban b/helpers/helpers.v1.d/fail2ban new file mode 100644 index 0000000..156c38d --- /dev/null +++ b/helpers/helpers.v1.d/fail2ban @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Create a dedicated fail2ban config (jail and filter conf files) +# +# usage 1: ynh_add_fail2ban_config --logpath=log_file --failregex=filter [--max_retry=max_retry] [--ports=ports] +# | arg: -l, --logpath= - Log file to be checked by fail2ban +# | arg: -r, --failregex= - Failregex to be looked for by fail2ban +# | 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 +# +# This will use a template in `../conf/f2b_jail.conf` and `../conf/f2b_filter.conf` +# See the documentation of `ynh_add_config` for a description of the template +# format and how placeholders are replaced with actual variables. +# +# Generally your template will look like that by example (for synapse): +# ``` +# f2b_jail.conf: +# [__APP__] +# enabled = true +# port = http,https +# filter = __APP__ +# logpath = /var/log/__APP__/logfile.log +# maxretry = 3 +# ``` +# ``` +# 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 = +# ``` +# +# ##### 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 +# 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 +# ``` +# +# Requires YunoHost version 4.1.0 or higher. +ynh_add_fail2ban_config() { + # Declare an array to define the options of this helper. + local legacy_args=lrmptv + local -A args_array=([l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template) + local logpath + local failregex + local max_retry + local ports + local use_template + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + max_retry=${max_retry:-3} + ports=${ports:-http,https} + use_template="${use_template:-0}" + + 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." + + echo " +[__APP__] +enabled = true +port = __PORTS__ +filter = __APP__ +logpath = __LOGPATH__ +maxretry = __MAX_RETRY__ +" > "$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_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" + + # 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 + ynh_print_err --message="Fail2ban failed to load the jail for $app" + ynh_print_warn --message="${fail2ban_error#*WARNING}" + fi +} + +# Remove the dedicated fail2ban config (jail and filter conf files) +# +# usage: ynh_remove_fail2ban_config +# +# Requires YunoHost version 3.5.0 or higher. +ynh_remove_fail2ban_config() { + ynh_secure_remove --file="/etc/fail2ban/jail.d/$app.conf" + ynh_secure_remove --file="/etc/fail2ban/filter.d/$app.conf" + ynh_systemd_action --service_name=fail2ban --action=reload +} diff --git a/helpers/helpers.v1.d/getopts b/helpers/helpers.v1.d/getopts new file mode 100644 index 0000000..30c5ff5 --- /dev/null +++ b/helpers/helpers.v1.d/getopts @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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. +# +# To keep a retrocompatibility, a package can still call a helper, using getopts, with positional arguments. +# The "legacy mode" will manage the positional arguments and fill the variable in the same order than they are given in $args_array. +# e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2. +# +# Requires YunoHost version 3.2.2 or higher. +ynh_handle_getopts_args() { + # 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 [ $# -ne 0 ]; then + # 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 --message="Invalid argument: -${OPTARG:-}" + elif [ "$parameter" = ":" ]; then + ynh_die --message="-$OPTARG 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 --message="Argument \"${all_args[0]}\" not valid! Did you use a single \"-\" instead of two?" + fi + # Reduce the value of shift, because the option has been removed manually + shift_value=$((shift_value - 1)) + 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 + } + + # LEGACY MODE + # Check if there's getopts arguments + if [ "${arguments[0]:0:1}" != "-" ]; then + # If not, enter in legacy mode and manage the arguments as positionnal ones.. + # Dot not echo, to prevent to go through a helper output. But print only in the log. + local i + for i in $(seq 0 $((${#arguments[@]} - 1))); do + # Try to use legacy_args as a list of option_flag of the array args_array + # Otherwise, fallback to getopts_parameters to get the option_flag. But an associative arrays isn't always sorted in the correct order... + # Remove all ':' in getopts_parameters + getopts_parameters=${legacy_args:-${getopts_parameters//:/}} + # Get the option_flag from getopts_parameters, by using the option_flag according to the position of the argument. + option_flag=${getopts_parameters:$i:1} + if [ -z "$option_flag" ]; then + ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored." + continue + fi + # Use the long option, corresponding to the option_flag, as a variable + # (e.g. for [u]=user, 'user' will be used as a variable) + # Also, remove '=' at the end of the long option + # The variable name will be stored in 'option_var' + local option_var="${args_array[$option_flag]%=}" + + # Store each value given as argument in the corresponding variable + # The values will be stored in the same order than $args_array + eval ${option_var}+='"${arguments[$i]}"' + done + unset legacy_args + else + # END LEGACY MODE + # Call parse_arg and pass the modified list of args as an array of arguments. + parse_arg "${arguments[@]}" + fi + fi + eval "$xtrace_enable" +} diff --git a/helpers/helpers.v1.d/go b/helpers/helpers.v1.d/go new file mode 100644 index 0000000..b5ad58e --- /dev/null +++ b/helpers/helpers.v1.d/go @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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 + HOME=$install_dir 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/helpers.v1.d/hardware b/helpers/helpers.v1.d/hardware new file mode 100644 index 0000000..17d072d --- /dev/null +++ b/helpers/helpers.v1.d/hardware @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 +# | arg: -s, --ignore_swap - Ignore swap, consider only real RAM +# | arg: -o, --only_swap - Ignore real RAM, consider only swap +# | ret: the amount of free ram, in MB (MegaBytes) +# +# Requires YunoHost version 3.8.1 or higher. +ynh_get_ram() { + # Declare an array to define the options of this helper. + local legacy_args=ftso + local -A args_array=([f]=free [t]=total [s]=ignore_swap [o]=only_swap) + local free + local total + local ignore_swap + local only_swap + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + ignore_swap=${ignore_swap:-0} + only_swap=${only_swap:-0} + free=${free:-0} + total=${total:-0} + + if [ $free -eq $total ]; then + ynh_print_warn --message="You have to choose --free or --total when using ynh_get_ram" + ram=0 + # Use the total amount of ram + elif [ $free -eq 1 ]; then + local free_ram=$(LC_ALL=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') + local free_swap=$(LC_ALL=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') + local free_ram_swap=$((free_ram + free_swap)) + + # Use the total amount of free ram + local ram=$free_ram_swap + if [ $ignore_swap -eq 1 ]; then + # Use only the amount of free ram + ram=$free_ram + elif [ $only_swap -eq 1 ]; then + # Use only the amount of free swap + ram=$free_swap + fi + elif [ $total -eq 1 ]; then + local total_ram=$(LC_ALL=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') + local total_swap=$(LC_ALL=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') + local total_ram_swap=$((total_ram + total_swap)) + + local ram=$total_ram_swap + if [ $ignore_swap -eq 1 ]; then + # Use only the amount of free ram + ram=$total_ram + elif [ $only_swap -eq 1 ]; then + # Use only the amount of free swap + ram=$total_swap + fi + fi + + echo $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 +# | arg: -t, --total - Count total RAM+swap +# | arg: -s, --ignore_swap - Ignore swap, consider only real RAM +# | arg: -o, --only_swap - Ignore real RAM, consider only swap +# | ret: 1 if the ram is under the requirement, 0 otherwise. +# +# Requires YunoHost version 3.8.1 or higher. +ynh_require_ram() { + # Declare an array to define the options of this helper. + local legacy_args=rftso + local -A args_array=([r]=required= [f]=free [t]=total [s]=ignore_swap [o]=only_swap) + local required + local free + local total + local ignore_swap + local only_swap + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + # Dunno if that's the right way to do, but that's some black magic to be able to + # forward the bool args to ynh_get_ram easily? + # If the variable $free is not empty, set it to '--free' + free=${free:+--free} + total=${total:+--total} + ignore_swap=${ignore_swap:+--ignore_swap} + only_swap=${only_swap:+--only_swap} + + local ram=$(ynh_get_ram $free $total $ignore_swap $only_swap) + + if [ $ram -lt $required ]; then + return 1 + else + return 0 + fi +} diff --git a/helpers/helpers.v1.d/logging b/helpers/helpers.v1.d/logging new file mode 100644 index 0000000..4dfadbd --- /dev/null +++ b/helpers/helpers.v1.d/logging @@ -0,0 +1,360 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Print a message to stderr and exit +# +# usage: ynh_die --message=MSG [--ret_code=RETCODE] +# | arg: -m, --message= - Message to display +# | arg: -c, --ret_code= - Exit code to exit with +# +# Requires YunoHost version 2.4.0 or higher. +ynh_die() { + # Declare an array to define the options of this helper. + local legacy_args=mc + local -A args_array=([m]=message= [c]=ret_code=) + local message + local ret_code + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + ret_code=${ret_code:-1} + + echo "$message" 1>&2 + exit "$ret_code" +} + +# Display a message in the 'INFO' logging category +# +# usage: ynh_print_info --message="Some message" +# | arg: -m, --message= - Message to display +# +# Requires YunoHost version 3.2.0 or higher. +ynh_print_info() { + # Declare an array to define the options of this helper. + local legacy_args=m + local -A args_array=([m]=message=) + local message + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + echo "$message" >&$YNH_STDINFO +} + +# Main printer, just in case in the future we have to change anything about that. +# +# [internal] +# +# Requires YunoHost version 3.2.0 or higher. +ynh_print_log() { + echo -e "${1}" +} + +# Print a warning on stderr +# +# usage: ynh_print_warn --message="Text to print" +# | arg: -m, --message= - The text to print +# +# Requires YunoHost version 3.2.0 or higher. +ynh_print_warn() { + # Declare an array to define the options of this helper. + local legacy_args=m + local -A args_array=([m]=message=) + local message + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + ynh_print_log "${message}" >&2 +} + +# Print an error on stderr +# +# usage: ynh_print_err --message="Text to print" +# | arg: -m, --message= - The text to print +# +# Requires YunoHost version 3.2.0 or higher. +ynh_print_err() { + # Declare an array to define the options of this helper. + local legacy_args=m + local -A args_array=([m]=message=) + local message + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + ynh_print_log "[Error] ${message}" >&2 +} + +# Execute a command and print the result as an error +# +# usage: ynh_exec_err your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_err +# +# Requires YunoHost version 3.2.0 or higher. +ynh_exec_err() { + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]]; then + ynh_print_err --message="$(eval $@)" + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + ynh_print_err --message="$("$@")" + fi +} + +# Execute a command and print the result as a warning +# +# usage: ynh_exec_warn your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_warn +# +# Requires YunoHost version 3.2.0 or higher. +ynh_exec_warn() { + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]]; then + ynh_print_warn --message="$(eval $@)" + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + ynh_print_warn --message="$("$@")" + fi +} + +# Execute a command and force the result to be printed on stdout +# +# usage: ynh_exec_warn_less your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_warn +# +# Requires YunoHost version 3.2.0 or higher. +ynh_exec_warn_less() { + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]]; then + eval $@ 2>&1 + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" 2>&1 + fi +} + +# Execute a command and redirect stdout in /dev/null +# +# usage: ynh_exec_quiet your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_warn +# +# Requires YunoHost version 3.2.0 or higher. +ynh_exec_quiet() { + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]]; then + eval $@ > /dev/null + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" > /dev/null + fi +} + +# Execute a command and redirect stdout and stderr in /dev/null +# +# usage: ynh_exec_quiet your command and args +# | arg: command - command to execute +# +# Note that you should NOT quote the command but only prefix it with ynh_exec_quiet +# +# Requires YunoHost version 3.2.0 or higher. +ynh_exec_fully_quiet() { + # Boring legacy handling for when people calls ynh_exec_* wrapping the command in quotes, + # (because in the past eval was used) ... + # we detect this by checking that there's no 2nd arg, and $1 contains a space + if [[ "$#" -eq 1 ]] && [[ "$1" == *" "* ]]; then + eval $@ > /dev/null 2>&1 + else + # Note that "$@" is used and not $@, c.f. https://unix.stackexchange.com/a/129077 + "$@" > /dev/null 2>&1 + fi +} + +# 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 +# +# [internal] +# +# WARNING: You should be careful with this helper, and never forget to use ynh_print_ON as soon as possible to restore the logging. +# +# Requires YunoHost version 3.2.0 or higher. +ynh_print_OFF() { + exec {BASH_XTRACEFD}> /dev/null +} + +# Restore the logging after ynh_print_OFF +# +# usage: ynh_print_ON +# +# [internal] +# +# Requires YunoHost version 3.2.0 or higher. +ynh_print_ON() { + exec {BASH_XTRACEFD}>&1 + # Print an echo only for the log, to be able to know that ynh_print_ON has been called. + echo ynh_print_ON > /dev/null +} + +# 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="...................." +# Define base_time when the file is sourced +base_time=$(date +%s) + +# Print a progress bar showing the progression of an app script +# +# usage: ynh_script_progression --message=message [--weight=weight] [--time] +# | arg: -m, --message= - The text to print +# | arg: -w, --weight= - The weight for this progression. This value is 1 by default. Use a bigger value for a longer part of the script. +# | arg: -t, --time - Print the execution time since the last call to this helper. Especially usefull to define weights. The execution time is given for the duration since the previous call. So the weight should be applied to this previous call. +# | arg: -l, --last - Use for the last call of the helper, to fill the progression bar. +# +# Requires YunoHost version 3.5.0 or higher. +ynh_script_progression() { + set +o xtrace # set +x + # Declare an array to define the options of this helper. + local legacy_args=mwtl + local -A args_array=([m]=message= [w]=weight= [t]=time [l]=last) + local message + local weight + local time + local last + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + # 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} + else + time=${time:-0} + fi + + last=${last:-0} + + # Get execution time since the last $base_time + local exec_time=$(($(date +%s) - $base_time)) + base_time=$(date +%s) + + # Compute $max_progression (if we didn't already) + if [ "$max_progression" = -1 ]; then + # Get the number of occurrences of 'ynh_script_progression' in the script. Except those are commented. + local helper_calls="$(grep --count "^[^#]*ynh_script_progression" $0)" + # Get the number of call with a weight value + local weight_calls=$(grep --perl-regexp --count "^[^#]*ynh_script_progression.*(--weight|-w )" $0) + + # Get the weight of each occurrences of 'ynh_script_progression' in the script using --weight + local weight_valuesA="$(grep --perl-regexp "^[^#]*ynh_script_progression.*--weight" $0 | sed 's/.*--weight[= ]\([[:digit:]]*\).*/\1/g')" + # Get the weight of each occurrences of 'ynh_script_progression' in the script using -w + local weight_valuesB="$(grep --perl-regexp "^[^#]*ynh_script_progression.*-w " $0 | sed 's/.*-w[= ]\([[:digit:]]*\).*/\1/g')" + # Each value will be on a different line. + # Remove each 'end of line' and replace it by a '+' to sum the values. + local weight_values=$(($(echo "$weight_valuesA" "$weight_valuesB" | grep -v -E '^\s*$' | tr '\n' '+' | sed 's/+$/+0/g'))) + + # max_progression is a total number of calls to this helper. + # Less the number of calls with a weight value. + # Plus the total of weight values + max_progression=$(($helper_calls - $weight_calls + $weight_values)) + 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=$weight + + # Reduce $increment_progression to the size of the scale + if [ $last -eq 0 ]; then + local effective_progression=$(($increment_progression * $progress_scale / $max_progression)) + # If last is specified, fill immediately the progression_bar + else + local effective_progression=$progress_scale + fi + + # Build $progression_bar from progress_string(0,1,2) according to $effective_progression and the weight of the current task + # expected_progression is the progression expected after the current task + local expected_progression="$((($increment_progression + $weight) * $progress_scale / $max_progression - $effective_progression))" + if [ $last -eq 1 ]; then + 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}" + + local print_exec_time="" + if [ $time -eq 1 ] && [ "$exec_time" -gt 10 ]; then + print_exec_time=" [$(bc <<< "scale=1; $exec_time / 60") minutes]" + fi + + ynh_print_info "[$progression_bar] > ${message}${print_exec_time}" + set -o xtrace # set -x +} + +# 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 +# +# Requires YunoHost version 3.6.0 or higher. +ynh_return() { + echo "$1" >> "$YNH_STDRETURN" +} diff --git a/helpers/helpers.v1.d/logrotate b/helpers/helpers.v1.d/logrotate new file mode 100644 index 0000000..263005a --- /dev/null +++ b/helpers/helpers.v1.d/logrotate @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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 0000000..1afa9d8 --- /dev/null +++ b/helpers/helpers.v1.d/mongodb @@ -0,0 +1,361 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 << EOF +$database +${command} +quit() +EOF + else + # If database is provided + if [ -n "$database" ]; then + database="$database" + else + database="" + fi + + mongosh --quiet $database --username $user --password $password --authenticationDatabase $authenticationdatabase --host $host --port $port --eval="$command" + fi +} + +# Drop a database +# +# [internal] +# +# If you intend to drop the database *and* the associated user, +# consider using ynh_mongo_remove_db instead. +# +# usage: ynh_mongo_drop_db --database=database +# | arg: -d, --database= - The database name to drop +# +# +ynh_mongo_drop_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 "$@" + + ynh_mongo_exec --database="$database" --command='db.runCommand({dropDatabase: 1})' +} + +# Dump a database +# +# example: ynh_mongo_dump_db --database=wekan > ./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/helpers.v1.d/multimedia b/helpers/helpers.v1.d/multimedia new file mode 100644 index 0000000..15acb64 --- /dev/null +++ b/helpers/helpers.v1.d/multimedia @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +readonly MEDIA_GROUP=multimedia +readonly MEDIA_DIRECTORY=/home/yunohost.multimedia + +# Initialize the multimedia directory system +# +# usage: ynh_multimedia_build_main_dir +# +# Requires YunoHost version 4.2 or higher. +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" + + # Disable logging to prevent leaking the user list (and anyway it's long and boring) + local xtrace_enable=$(set +o | grep xtrace) + set +o xtrace # set +x + + ## 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 + + # Re-enable logging + eval "$xtrace_enable" + + # 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: -s, --source_dir= - Source directory - The real directory which contains your medias. +# | arg: -d, --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. +# +# Requires YunoHost version 4.2 or higher. +ynh_multimedia_addfolder() { + + # Declare an array to define the options of this helper. + local legacy_args=sd + local -A args_array=([s]=source_dir= [d]=dest_dir=) + local source_dir + local dest_dir + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Ajout d'un lien symbolique vers le dossier à partager + ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir" + + ## Application des droits étendus sur le dossier ajouté + # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: + setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. + setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. + setfacl -RL -m m::rwx "$source_dir" +} + +# Allow an user to have an write authorisation in multimedia directories +# +# usage: ynh_multimedia_addaccess user_name +# +# | arg: -u, --user_name= - The name of the user which gain this access. +# +# Requires YunoHost version 4.2 or higher. +ynh_multimedia_addaccess() { + # Declare an array to define the options of this helper. + local legacy_args=u + declare -Ar args_array=([u]=user_name=) + local user_name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + groupadd -f multimedia + usermod -a -G multimedia $user_name +} diff --git a/helpers/helpers.v1.d/mysql b/helpers/helpers.v1.d/mysql new file mode 100644 index 0000000..0fa5c25 --- /dev/null +++ b/helpers/helpers.v1.d/mysql @@ -0,0 +1,286 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Open a connection as a user +# +# usage: ynh_mysql_connect_as --user=user --password=password [--database=database] +# | arg: -u, --user= - the user name to connect as +# | arg: -p, --password= - the user password +# | arg: -d, --database= - the database to connect to +# +# examples: +# ynh_mysql_connect_as --user="user" --password="pass" <<< "UPDATE ...;" +# ynh_mysql_connect_as --user="user" --password="pass" < /path/to/file.sql +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_connect_as() { + # Declare an array to define the options of this helper. + local legacy_args=upd + local -A args_array=([u]=user= [p]=password= [d]=database=) + local user + local password + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + database="${database:-}" + + mysql --user="$user" --password="$password" --batch "$database" +} + +# Execute a command as root user +# +# usage: ynh_mysql_execute_as_root --sql=sql [--database=database] +# | arg: -s, --sql= - the SQL command to execute +# | arg: -d, --database= - the database to connect to +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_execute_as_root() { + # Declare an array to define the options of this helper. + local legacy_args=sd + local -A args_array=([s]=sql= [d]=database=) + local sql + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + database="${database:-}" + + if [ -n "$database" ]; then + database="--database=$database" + fi + + mysql -B "$database" <<< "$sql" +} + +# Execute a command from a file as root user +# +# usage: ynh_mysql_execute_file_as_root --file=file [--database=database] +# | arg: -f, --file= - the file containing SQL commands +# | arg: -d, --database= - the database to connect to +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_execute_file_as_root() { + # Declare an array to define the options of this helper. + local legacy_args=fd + local -A args_array=([f]=file= [d]=database=) + local file + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + database="${database:-}" + + if [ -n "$database" ]; then + database="--database=$database" + fi + + mysql -B "$database" < "$file" +} + +# Create a database and grant optionnaly privilegies to a user +# +# [internal] +# +# 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 +# +# Requires YunoHost version 2.2.4 or higher. +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 + + ynh_mysql_execute_as_root --sql="$sql" +} + +# Drop a database +# +# [internal] +# +# 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 +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_drop_db() { + ynh_mysql_execute_as_root --sql="DROP DATABASE ${1};" +} + +# Dump a database +# +# usage: ynh_mysql_dump_db --database=database +# | arg: -d, --database= - the database name to dump +# | ret: The mysqldump output +# +# example: ynh_mysql_dump_db --database=roundcube > ./dump.sql +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_dump_db() { + # Declare an array to define the options of this helper. + local legacy_args=d + local -A args_array=([d]=database=) + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + mysqldump --single-transaction --skip-dump-date --routines "$database" +} + +# Create a user +# +# [internal] +# +# usage: ynh_mysql_create_user user pwd [host] +# | arg: user - the user name to create +# | arg: pwd - the password to identify user by +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_create_user() { + ynh_mysql_execute_as_root \ + --sql="CREATE USER '${1}'@'localhost' IDENTIFIED BY '${2}';" +} + +# 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. +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_user_exists() { + # Declare an array to define the options of this helper. + local legacy_args=u + local -A args_array=([u]=user=) + local user + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if [[ -z $(ynh_mysql_execute_as_root --sql="SELECT User from mysql.user WHERE User = '$user';") ]]; then + return 1 + else + return 0 + 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] +# +# usage: ynh_mysql_drop_user user +# | arg: user - the user name to drop +# +# Requires YunoHost version 2.2.4 or higher. +ynh_mysql_drop_user() { + ynh_mysql_execute_as_root --sql="DROP USER '${1}'@'localhost';" +} + +# 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 +# | 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 "`mysqlpwd`" into the app settings. +# +# Requires YunoHost version 2.6.4 or higher. +ynh_mysql_setup_db() { + # Declare an array to define the options of this helper. + local legacy_args=unp + local -A args_array=([u]=db_user= [n]=db_name= [p]=db_pwd=) + local db_user + local db_name + db_pwd="" + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Generate a random password + local new_db_pwd=$(ynh_string_random) + # If $db_pwd is not provided, use new_db_pwd instead for db_pwd + db_pwd="${db_pwd:-$new_db_pwd}" + + # Dirty patch for super-legacy apps + dpkg --list | grep -q "^ii mariadb-server" || { + ynh_print_warn "Packager: you called ynh_mysql_setup_db without declaring a dependency to mariadb-server. Please add it to your apt dependencies !" + ynh_apt install mariadb-server + } + + ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" + ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd +} + +# 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 +# +# Requires YunoHost version 2.6.4 or higher. +ynh_mysql_remove_db() { + # Declare an array to define the options of this helper. + local legacy_args=un + local -Ar args_array=([u]=db_user= [n]=db_name=) + local db_user + local db_name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if ynh_mysql_database_exists "$db_name"; then + ynh_mysql_drop_db $db_name + else + ynh_print_warn --message="Database $db_name not found" + fi + + # Remove mysql user if it exists + if ynh_mysql_user_exists --user=$db_user; then + ynh_mysql_drop_user $db_user + fi +} diff --git a/helpers/helpers.v1.d/network b/helpers/helpers.v1.d/network new file mode 100644 index 0000000..bb4cc34 --- /dev/null +++ b/helpers/helpers.v1.d/network @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 +# +# example: port=$(ynh_find_port --port=8080) +# +# Requires YunoHost version 2.6.4 or higher. +ynh_find_port() { + # Declare an array to define the options of this helper. + local legacy_args=p + local -A args_array=([p]=port=) + local port + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + test -n "$port" || ynh_die --message="The argument of ynh_find_port must be a valid port." + while ! ynh_port_available --port=$port; do + port=$((port + 1)) + done + echo $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. +# +# example: ynh_port_available --port=1234 || ynh_die --message="Port 1234 is needs to be available for this app" +# +# Requires YunoHost version 3.8.0 or higher. +ynh_port_available() { + # Declare an array to define the options of this helper. + local legacy_args=p + local -A args_array=([p]=port=) + local port + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Check if the port is free + if ss --numeric --listening --tcp --udp | awk '{print$5}' | grep --quiet --extended-regexp ":$port$"; then + return 1 + # This is to cover (most) case where an app is using a port yet ain't currently using it for some reason (typically service ain't up) + elif grep -q "port: '$port'" /etc/yunohost/apps/*/settings.yml; then + return 1 + else + return 0 + fi +} + +# Validate an IP address +# +# [internal] +# +# 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 +# +# Requires YunoHost version 2.2.4 or higher. +ynh_validate_ip() { + # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298 + + # Declare an array to define the options of this helper. + local legacy_args=fi + local -A args_array=([f]=family= [i]=ip_address=) + local family + local ip_address + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + [ "$family" == "4" ] || [ "$family" == "6" ] || return 1 + + python3 /dev/stdin << EOF +import socket +import sys +family = { "4" : socket.AF_INET, "6" : socket.AF_INET6 } +try: + socket.inet_pton(family["$family"], "$ip_address") +except socket.error: + sys.exit(1) +sys.exit(0) +EOF +} + +# Validate an IPv4 address +# +# usage: ynh_validate_ip4 --ip_address=ip_address +# | arg: -i, --ip_address= - the ipv4 address to check +# | ret: 0 for valid ipv4 addresses, 1 otherwise +# +# example: ynh_validate_ip4 111.222.333.444 +# +# Requires YunoHost version 2.2.4 or higher. +ynh_validate_ip4() { + # Declare an array to define the options of this helper. + local legacy_args=i + local -A args_array=([i]=ip_address=) + local ip_address + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + ynh_validate_ip --family=4 --ip_address=$ip_address +} + +# Validate an IPv6 address +# +# usage: ynh_validate_ip6 --ip_address=ip_address +# | arg: -i, --ip_address= - the ipv6 address to check +# | ret: 0 for valid ipv6 addresses, 1 otherwise +# +# example: ynh_validate_ip6 2000:dead:beef::1 +# +# Requires YunoHost version 2.2.4 or higher. +ynh_validate_ip6() { + # Declare an array to define the options of this helper. + local legacy_args=i + local -A args_array=([i]=ip_address=) + local ip_address + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + ynh_validate_ip --family=6 --ip_address=$ip_address +} diff --git a/helpers/helpers.v1.d/nginx b/helpers/helpers.v1.d/nginx new file mode 100644 index 0000000..30503ea --- /dev/null +++ b/helpers/helpers.v1.d/nginx @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Create a dedicated nginx config +# +# usage: ynh_add_nginx_config +# +# This will use a template in `../conf/nginx.conf` +# See the documentation of `ynh_add_config` for a description of the template +# format and how placeholders are replaced with actual variables. +# +# Additionally, ynh_add_nginx_config will replace: +# - `#sub_path_only` by empty string if `path_url` is not `'/'` +# - `#root_path_only` by empty string if `path_url` *is* `'/'` +# +# This allows to enable/disable specific behaviors dependenging on the install +# location +# +# Requires YunoHost version 4.1.0 or higher. +ynh_add_nginx_config() { + + local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" + + ynh_add_config --template="nginx.conf" --destination="$finalnginxconf" + + if [ "${path_url:-}" != "/" ]; then + ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$finalnginxconf" + else + ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$finalnginxconf" + fi + + # Delete REMOTE_USER mapping, it's already provided by + # /etc/nginx/fastcgi_params which all PHP apps include, and maps to the + # appropriate YNH_USER HTTP header instead of $remote_user + sed -i '/fastcgi_param\s*REMOTE_USER/d' "$finalnginxconf" + + ynh_store_file_checksum --file="$finalnginxconf" + + ynh_systemd_action --service_name=nginx --action=reload +} + +# Remove the dedicated nginx config +# +# usage: ynh_remove_nginx_config +# +# Requires YunoHost version 2.7.2 or higher. +ynh_remove_nginx_config() { + ynh_secure_remove --file="/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_systemd_action --service_name=nginx --action=reload +} + +# 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 + 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" + + # Regen the nginx conf + ynh_add_nginx_config +} diff --git a/helpers/helpers.v1.d/nodejs b/helpers/helpers.v1.d/nodejs new file mode 100644 index 0000000..764a6c9 --- /dev/null +++ b/helpers/helpers.v1.d/nodejs @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +n_install_dir="/opt/node_n" +node_version_path="$n_install_dir/n/versions/node" + +# Load the version of node for an app, and set variables. +# +# usage: ynh_use_nodejs +# +# `ynh_use_nodejs` has to be used in any app scripts before using node for the first time. +# This helper will provide alias and variables to use in your scripts. +# +# To use npm or node, use the alias `ynh_npm` and `ynh_node`. +# +# Those alias will use the correct version installed for the app. +# For example: use `ynh_npm install` instead of `npm install` +# +# With `sudo` or `ynh_exec_as`, use instead the fallback variables `$ynh_npm` and `$ynh_node` +# And propagate $PATH to sudo with $ynh_node_load_PATH +# Exemple: `ynh_exec_as $app $ynh_node_load_PATH $ynh_npm install` +# +# $PATH contains the path of the requested version of node. +# However, $PATH is duplicated into $node_PATH to outlast any manipulation of `$PATH` +# You can use the variable `$ynh_node_load_PATH` to quickly load your node version +# in $PATH for an usage into a separate script. +# Exemple: $ynh_node_load_PATH $final_path/script_that_use_npm.sh` +# +# +# Finally, to start a nodejs service with the correct version, 2 solutions +# Either the app is dependent of node or npm, but does not called it directly. +# In such situation, you need to load PATH : +# ``` +# Environment="__NODE_ENV_PATH__" +# ExecStart=__FINALPATH__/my_app +# ``` +# You will replace __NODE_ENV_PATH__ with $ynh_node_load_PATH. +# +# Or node start the app directly, then you don't need to load the PATH variable +# ``` +# ExecStart=__YNH_NODE__ my_app run +# ``` +# You will replace __YNH_NODE__ with $ynh_node +# +# +# 2 other variables are also available +# - $nodejs_path: The absolute path to node binaries for the chosen version. +# - $nodejs_version: Just the version number of node for this app. Stored as 'nodejs_version' in settings.yml. +# +# Requires YunoHost version 2.7.12 or higher. +ynh_use_nodejs() { + nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version) + + # Get the absolute path of this version of node + nodejs_path="$node_version_path/$nodejs_version/bin" + + # Allow alias to be used into bash script + shopt -s expand_aliases + + # Create an alias for the specific version of node and a variable as fallback + ynh_node="$nodejs_path/node" + alias ynh_node="$ynh_node" + # And npm + ynh_npm="$nodejs_path/npm" + alias ynh_npm="$ynh_npm" + + # Load the path of this version of node in $PATH + if [[ :$PATH: != *":$nodejs_path"* ]]; then + PATH="$nodejs_path:$PATH" + fi + node_PATH="$PATH" + # Create an alias to easily load the PATH + 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 +# +# 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). +# +# `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 +# +# Refer to `ynh_use_nodejs` for more information about available commands and variables +# +# Requires YunoHost version 2.7.12 or higher. +ynh_install_nodejs() { + # Use n, https://github.com/tj/n to manage the nodejs versions + + # Declare an array to define the options of this helper. + local legacy_args=n + local -A args_array=([n]=nodejs_version=) + local nodejs_version + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Create $n_install_dir + mkdir --parents "$n_install_dir" + + # Install the requested version of nodejs + if [[ $YNH_ARCH == "arm64" ]]; then + N_PREFIX=$n_install_dir "$YNH_HELPERS_DIR/vendor/n/n" $nodejs_version --arch=arm64 + else + N_PREFIX=$n_install_dir "$YNH_HELPERS_DIR/vendor/n/n" $nodejs_version + fi + + # Find the last "real" version for this major version of node. + final_nodejs_version=$(find "$n_install_dir/n/versions/node/$nodejs_version"* -maxdepth 0 | sort --version-sort | tail --lines=1) + final_nodejs_version=$(basename "$final_nodejs_version") + + # Store nodejs_version into the config of this app + nodejs_version="$final_nodejs_version" + ynh_app_setting_set --key=nodejs_version --value="$final_nodejs_version" + + ynh_use_nodejs +} + +# Remove the version of node used by the app. +# +# usage: ynh_remove_nodejs +# +# 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. +# +# Requires YunoHost version 2.7.12 or higher. +ynh_remove_nodejs() { + nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version) + + ynh_app_setting_delete --app=$app --key=nodejs_version + + # Garbage-collect unused versions + local installed_versions="$(N_PREFIX=/opt/node_n "$YNH_HELPERS_DIR/vendor/n/n" ls | awk -F/ '{print $2}')" + for version in $installed_versions; do + if ! grep -qr "^nodejs_version: '$nodejs_version'" /etc/yunohost/apps/*/settings.yml; then + N_PREFIX=$n_install_dir "$YNH_HELPERS_DIR/vendor/n/n" rm $nodejs_version + fi + done + + # If no other app uses n, remove n + if ! grep -qr "^nodejs_version:" /etc/yunohost/apps/*/settings.yml; then + ynh_safe_rm "$n_install_dir" + sed --in-place "/N_PREFIX/d" /root/.bashrc + fi +} diff --git a/helpers/helpers.v1.d/permission b/helpers/helpers.v1.d/permission new file mode 100644 index 0000000..6c44423 --- /dev/null +++ b/helpers/helpers.v1.d/permission @@ -0,0 +1,387 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 \ +# --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 \ +# --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 ]] [--show_tile=true|false] +# [--protected=true|false] +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -u, --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: -A, --additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden +# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true +# | arg: -a, --allowed= - (optional) A list of group/user to allow for the permission +# | arg: -t, --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: -P, --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'. +# +# 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 +# +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_create() { + # Declare an array to define the options of this helper. + local legacy_args=puAhaltP + local -A args_array=([p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [t]=show_tile= [P]=protected=) + local permission + local url + local additional_urls + local auth_header + local allowed + local show_tile + local protected + ynh_handle_getopts_args "$@" + url=${url:-} + additional_urls=${additional_urls:-} + auth_header=${auth_header:-} + allowed=${allowed:-} + 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 ${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 $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: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_delete() { + # Declare an array to define the options of this helper. + local legacy_args=p + 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: -p, --permission= - the permission to check +# | exit: Return 1 if the permission doesn't exist, 0 otherwise +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_exists() { + # Declare an array to define the options of this helper. + local legacy_args=p + 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: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +# | arg: -u, --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: -a, --add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. +# | arg: -r, --remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden +# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application +# | arg: -c, --clear_urls - (optional) Clean all urls (url and additional_urls) +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_url() { + # Declare an array to define the options of this helper. + local legacy_args=puarhc + local -A args_array=([p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls) + local 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" ...]] +# [--show_tile=true|false] [--protected=true|false] +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -a, --add= - the list of group or users to enable add to the permission +# | arg: -r, --remove= - the list of group or users to remove from the permission +# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO +# | arg: -P, --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. +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_update() { + # Declare an array to define the options of this helper. + local legacy_args=parltP + local -A args_array=([p]=permission= [a]=add= [r]=remove= [t]=show_tile= [P]=protected=) + local permission + local add + local remove + local show_tile + local protected + ynh_handle_getopts_args "$@" + add=${add:-} + remove=${remove:-} + show_tile=${show_tile:-} + protected=${protected:-} + + 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 + + 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 user_permission_update; user_permission_update('$app.$permission' $add $remove $show_tile $protected , 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: -p, --permission= - the permission to check +# | arg: -u, --user= - the user seek in the permission +# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise +# +# Requires YunoHost version 3.7.1 or higher. +ynh_permission_has_user() { + local legacy_args=pu + # Declare an array to define the options of this helper. + local -A args_array=([p]=permission= [u]=user=) + local permission + local user + # Manage arguments with getopts + 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 +} + +# Check if a legacy permissions exist +# +# usage: ynh_legacy_permissions_exists +# | exit: Return 1 if the permission doesn't exist, 0 otherwise +# +# Requires YunoHost version 4.1.2 or higher. +ynh_legacy_permissions_exists() { + for permission in "skipped" "unprotected" "protected"; do + if ynh_permission_exists --permission="legacy_${permission}_uris"; then + return 0 + fi + done + return 1 +} + +# Remove all legacy permissions +# +# usage: ynh_legacy_permissions_delete_all +# +# example: +# if ynh_legacy_permissions_exists +# then +# ynh_legacy_permissions_delete_all +# # You can recreate the required permissions here with ynh_permission_create +# fi +# Requires YunoHost version 4.1.2 or higher. +ynh_legacy_permissions_delete_all() { + for permission in "skipped" "unprotected" "protected"; do + if ynh_permission_exists --permission="legacy_${permission}_uris"; then + ynh_permission_delete --permission="legacy_${permission}_uris" + fi + done +} diff --git a/helpers/helpers.v1.d/php b/helpers/helpers.v1.d/php new file mode 100644 index 0000000..9db2675 --- /dev/null +++ b/helpers/helpers.v1.d/php @@ -0,0 +1,377 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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: ynh_add_fpm_config +# +# Case 1 (recommended) : your provided a snippet conf/extra_php-fpm.conf +# +# 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. +# N - Or you can specify a quantitative footprint as MB by pool (use watch -n0.5 ps -o user,cmd,%cpu,rss -u APP) +# +# 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. +# +# 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 +# 'pm.start_servers', 'pm.min_spare_servers' and 'pm.max_spare_servers' which are defined from the +# value of 'pm.max_children' +# NOTE: 'pm.max_children' can't exceed 4 times the number of processor's cores. +# +# The usage value will defined the way php will handle the children for the pool. +# A value set as 'low' will set the process manager to 'ondemand'. Children will start only if the +# service is used, otherwise no child will stay alive. This config gives the lower footprint when the +# service is idle. But will use more proc since it has to start a child as soon it's used. +# Set as 'medium', the process manager will be at dynamic. If the service is idle, a number of children +# equal to pm.min_spare_servers will stay alive. So the service can be quick to answer to any request. +# The number of children can grow if needed. The footprint can stay low if the service is idle, but +# not null. The impact on the proc is a little bit less than 'ondemand' as there's always a few +# children already available. +# Set as 'high', the process manager will be set at 'static'. There will be always as many children as +# 'pm.max_children', the footprint is important (but will be set as maximum a quarter of the maximum +# RAM) but the impact on the proc is lower. The service will be quick to answer as there's always many +# children ready to answer. +# +# Requires YunoHost version 4.1.0 or higher. +ynh_add_fpm_config() { + local _globalphpversion=${phpversion-:} + # Declare an array to define the options of this helper. + local legacy_args=vufg + local -A args_array=([v]=phpversion= [u]=usage= [f]=footprint= [g]=group=) + local group + local phpversion + local usage + local footprint + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + group=${group:-} + + # The default behaviour is to use the template. + local autogenconf=false + usage="${usage:-}" + footprint="${footprint:-}" + 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 + + # Set the default PHP-FPM version by default + if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then + phpversion="${phpversion:-$YNH_PHP_VERSION}" + else + phpversion="${phpversion:-$_globalphpversion}" + fi + + local old_phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) + + # If the PHP version changed, remove the old fpm conf + # (NB: This stuff is also handled by the apt helper, which is usually triggered before this helper) + if [ -n "$old_phpversion" ] && [ "$old_phpversion" != "$phpversion" ]; then + local old_php_fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) + local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf" + + if [[ -f "$old_php_finalphpconf" ]]; then + ynh_backup_if_checksum_is_different --file="$old_php_finalphpconf" + ynh_remove_fpm_config + fi + 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=phpversion --value=$phpversion + + 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 + [ -e "$phpfpm_path" ] || ynh_die --message="Unable to find template to configure PHP-FPM." + else + # Usage 2, generate a PHP-FPM config file with ynh_get_scalable_phpfpm + + # 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 = __PHPFPM_GROUP__ + +chdir = __INSTALL_DIR__ + +listen = /var/run/php/php__PHPVERSION__-fpm-__APP__.sock +listen.owner = www-data +listen.group = www-data + +pm = __PHP_PM__ +pm.max_children = __PHP_MAX_CHILDREN__ +pm.max_requests = 500 +request_terminate_timeout = 1d +" > "$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" + + elif [ "$php_pm" = "ondemand" ]; then + echo " +pm.process_idle_timeout = 10s +" >> "$phpfpm_path" + fi + + # Concatene the extra config. + if [ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]; then + cat $YNH_APP_BASEDIR/conf/extra_php-fpm.conf >> "$phpfpm_path" + fi + fi + + local finalphpconf="$fpm_config_dir/pool.d/$app.conf" + ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf" + + # 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 +# +# usage: ynh_remove_fpm_config +# +# Requires YunoHost version 2.7.2 or higher. +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) + # Get the version of PHP used by this app + local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) + + # Assume default PHP-FPM version by default + phpversion="${phpversion:-$YNH_DEFAULT_PHP_VERSION}" + + # Assume default PHP files if not set + if [ -z "$fpm_config_dir" ]; then + fpm_config_dir="/etc/php/$YNH_DEFAULT_PHP_VERSION/fpm" + fpm_service="php$YNH_DEFAULT_PHP_VERSION-fpm" + fi + + ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf" + ynh_systemd_action --service_name=$fpm_service --action=reload +} + +# Define the values to configure PHP-FPM +# +# [internal] +# +# usage: ynh_get_scalable_phpfpm --usage=usage --footprint=footprint [--print] +# | arg: -f, --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 +# +# | arg: -u, --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, --print - Print the result (intended for debug purpose only when packaging the app) +ynh_get_scalable_phpfpm() { + local legacy_args=ufp + # Declare an array to define the options of this helper. + local -A args_array=([u]=usage= [f]=footprint= [p]=print) + local usage + local footprint + local print + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + # Set all characters as lowercase + footprint=${footprint,,} + usage=${usage,,} + print=${print:-0} + + if [ "$footprint" = "low" ]; then + footprint=20 + elif [ "$footprint" = "medium" ]; then + footprint=35 + elif [ "$footprint" = "high" ]; then + footprint=50 + fi + + # Define the factor to determine min_spare_servers + # to avoid having too few children ready to start for heavy apps + if [ $footprint -le 20 ]; then + min_spare_servers_factor=8 + elif [ $footprint -le 35 ]; then + min_spare_servers_factor=5 + else + min_spare_servers_factor=3 + fi + + # Define the way the process manager handle child processes. + if [ "$usage" = "low" ]; then + php_pm=ondemand + elif [ "$usage" = "medium" ]; then + php_pm=dynamic + elif [ "$usage" = "high" ]; then + php_pm=static + else + ynh_die --message="Does not recognize '$usage' as an usage value." + fi + + # Get the total of RAM available, except swap. + local max_ram=$(ynh_get_ram --total --ignore_swap) + + at_least_one() { + # Do not allow value below 1 + if [ $1 -le 0 ]; then + echo 1 + else + echo $1 + fi + } + + # Define pm.max_children + # The value of pm.max_children is the total amount of ram divide by 2 and divide again by the footprint of a pool for this app. + # So if PHP-FPM start the maximum of children, it won't exceed half of the ram. + php_max_children=$(($max_ram / 2 / $footprint)) + # If process manager is set as static, use half less children. + # Used as static, there's always as many children as the value of pm.max_children + if [ "$php_pm" = "static" ]; then + php_max_children=$(($php_max_children / 2)) + fi + php_max_children=$(at_least_one $php_max_children) + + # To not overload the proc, limit the number of children to 4 times the number of cores. + local core_number=$(nproc) + local max_proc=$(($core_number * 4)) + if [ $php_max_children -gt $max_proc ]; then + php_max_children=$max_proc + fi + + # Get a potential forced value for php_max_children + local php_forced_max_children=$(ynh_app_setting_get --app=$app --key=php_forced_max_children) + if [ -n "$php_forced_max_children" ]; then + php_max_children=$php_forced_max_children + fi + + if [ "$php_pm" = "dynamic" ]; then + # Define pm.start_servers, pm.min_spare_servers and pm.max_spare_servers for a dynamic process manager + php_min_spare_servers=$(($php_max_children / $min_spare_servers_factor)) + php_min_spare_servers=$(at_least_one $php_min_spare_servers) + + php_max_spare_servers=$(($php_max_children / 2)) + php_max_spare_servers=$(at_least_one $php_max_spare_servers) + + php_start_servers=$(($php_min_spare_servers + ($php_max_spare_servers - $php_min_spare_servers) / 2)) + php_start_servers=$(at_least_one $php_start_servers) + else + php_min_spare_servers=0 + php_max_spare_servers=0 + php_start_servers=0 + fi + + if [ $print -eq 1 ]; then + ynh_print_warn --message="Footprint=${footprint}Mb by pool." + ynh_print_warn --message="Process manager=$php_pm" + ynh_print_warn --message="Max RAM=${max_ram}Mb" + if [ "$php_pm" != "static" ]; then + ynh_print_warn --message="\nMax estimated footprint=$(($php_max_children * $footprint))" + ynh_print_warn --message="Min estimated footprint=$(($php_min_spare_servers * $footprint))" + fi + if [ "$php_pm" = "dynamic" ]; then + ynh_print_warn --message="Estimated average footprint=$(($php_max_spare_servers * $footprint))" + elif [ "$php_pm" = "static" ]; then + ynh_print_warn --message="Estimated footprint=$(($php_max_children * $footprint))" + fi + ynh_print_warn --message="\nRaw php-fpm values:" + ynh_print_warn --message="pm.max_children = $php_max_children" + if [ "$php_pm" = "dynamic" ]; then + ynh_print_warn --message="pm.start_servers = $php_start_servers" + ynh_print_warn --message="pm.min_spare_servers = $php_min_spare_servers" + ynh_print_warn --message="pm.max_spare_servers = $php_max_spare_servers" + fi + fi +} diff --git a/helpers/helpers.v1.d/postgresql b/helpers/helpers.v1.d/postgresql new file mode 100644 index 0000000..c4d2326 --- /dev/null +++ b/helpers/helpers.v1.d/postgresql @@ -0,0 +1,327 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +PSQL_ROOT_PWD_FILE=/etc/yunohost/psql +PSQL_VERSION=15 + +# Open a connection as a user +# +# usage: ynh_psql_connect_as --user=user --password=password [--database=database] +# | arg: -u, --user= - the user name to connect as +# | arg: -p, --password= - the user password +# | arg: -d, --database= - the database to connect to +# +# examples: +# ynh_psql_connect_as 'user' 'pass' <<< "UPDATE ...;" +# ynh_psql_connect_as 'user' 'pass' < /path/to/file.sql +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_connect_as() { + # Declare an array to define the options of this helper. + local legacy_args=upd + local -A args_array=([u]=user= [p]=password= [d]=database=) + local user + local password + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + database="${database:-}" + + sudo --user=postgres PGUSER="$user" PGPASSWORD="$password" psql "$database" +} + +# Execute a command as root user +# +# usage: ynh_psql_execute_as_root --sql=sql [--database=database] +# | arg: -s, --sql= - the SQL command to execute +# | arg: -d, --database= - the database to connect to +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_execute_as_root() { + # Declare an array to define the options of this helper. + local legacy_args=sd + local -A args_array=([s]=sql= [d]=database=) + local sql + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + database="${database:-}" + + if [ -n "$database" ]; then + database="--database=$database" + fi + + ynh_psql_connect_as --user="postgres" --password="$(cat $PSQL_ROOT_PWD_FILE)" \ + $database <<< "$sql" +} + +# Execute a command from a file as root user +# +# usage: ynh_psql_execute_file_as_root --file=file [--database=database] +# | arg: -f, --file= - the file containing SQL commands +# | arg: -d, --database= - the database to connect to +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_execute_file_as_root() { + # Declare an array to define the options of this helper. + local legacy_args=fd + local -A args_array=([f]=file= [d]=database=) + local file + local database + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + database="${database:-}" + + if [ -n "$database" ]; then + database="--database=$database" + fi + + ynh_psql_connect_as --user="postgres" --password="$(cat $PSQL_ROOT_PWD_FILE)" \ + $database < "$file" +} + +# Create a database and grant optionnaly privilegies to a user +# +# [internal] +# +# usage: ynh_psql_create_db db [user] +# | arg: db - the database name to create +# | arg: user - the user to grant privilegies +# +# Requires YunoHost version 3.5.0 or higher. +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 + + ynh_psql_execute_as_root --sql="$sql" +} + +# Drop a database +# +# [internal] +# +# 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 +# +# Requires YunoHost version 3.5.0 or higher. +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 + ynh_psql_execute_as_root --sql="REVOKE CONNECT ON DATABASE $db FROM public;" --database="$db" + ynh_psql_execute_as_root --sql="SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '$db' AND pid <> pg_backend_pid();" --database="$db" + sudo --user=postgres dropdb $db +} + +# Dump a database +# +# usage: ynh_psql_dump_db --database=database +# | arg: -d, --database= - the database name to dump +# | ret: the psqldump output +# +# example: ynh_psql_dump_db 'roundcube' > ./dump.sql +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_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 "$@" + + sudo --user=postgres pg_dump "$database" +} + +# Create a user +# +# [internal] +# +# usage: ynh_psql_create_user user pwd +# | arg: user - the user name to create +# | arg: pwd - the password to identify user by +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_create_user() { + local user=$1 + local pwd=$2 + ynh_psql_execute_as_root --sql="CREATE USER $user WITH ENCRYPTED PASSWORD '$pwd'" +} + +# 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 +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_user_exists() { + # Declare an array to define the options of this helper. + local legacy_args=u + local -A args_array=([u]=user=) + local user + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if ! sudo --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT rolname FROM pg_roles WHERE rolname='$user';" | grep --quiet "$user"; then + return 1 + else + return 0 + fi +} + +# Check if a psql database exists +# +# usage: ynh_psql_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 +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_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 psql is not there, we cannot check the db + # though it could exists. + if ! command -v psql; then + ynh_print_err -m "PostgreSQL is not installed, impossible to check for db existence." + return 1 + elif ! sudo --user=postgres PGUSER="postgres" PGPASSWORD="$(cat $PSQL_ROOT_PWD_FILE)" psql -tAc "SELECT datname FROM pg_database WHERE datname='$database';" | grep --quiet "$database"; then + return 1 + else + return 0 + fi +} + +# Drop a user +# +# [internal] +# +# usage: ynh_psql_drop_user user +# | arg: user - the user name to drop +# +# Requires YunoHost version 3.5.0 or higher. +ynh_psql_drop_user() { + ynh_psql_execute_as_root --sql="DROP USER ${1};" +} + +# 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 +# | 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 "psqlpwd" into the app settings. +# +# Requires YunoHost version 2.7.13 or higher. +ynh_psql_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 "$@" + + if ! ynh_psql_user_exists --user=$db_user; then + 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}" + + ynh_psql_create_user "$db_user" "$db_pwd" + elif [ -z $db_pwd ]; then + ynh_die --message="The user $db_user exists, please provide his password" + fi + + ynh_psql_create_db "$db_name" "$db_user" # Create the database + ynh_app_setting_set --app=$app --key=psqlpwd --value=$db_pwd # Store the password in the app's config +} + +# 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 +# +# Requires YunoHost version 2.7.13 or higher. +ynh_psql_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_psql_database_exists --database=$db_name; then # Check if the database exists + ynh_psql_drop_db $db_name # Remove the database + else + ynh_print_warn --message="Database $db_name not found" + fi + + # Remove psql user if it exists + if ynh_psql_user_exists --user=$db_user; then + ynh_psql_drop_user $db_user + else + ynh_print_warn --message="User $db_user not found" + fi +} + +# Create a master password and set up global settings +# +# [internal] +# +# usage: ynh_psql_test_if_first_run +# +# It also make sure that postgresql is installed and running +# Please always call this script in install and restore scripts +# +# Requires YunoHost version 2.7.13 or higher. +ynh_psql_test_if_first_run() { + + # Make sure postgresql is indeed installed + dpkg --list | grep -q "^ii\s*postgresql-$PSQL_VERSION" || ynh_die --message="postgresql-$PSQL_VERSION is not installed !?" + + yunohost tools regen-conf postgresql +} diff --git a/helpers/helpers.v1.d/redis b/helpers/helpers.v1.d/redis new file mode 100644 index 0000000..bee9af9 --- /dev/null +++ b/helpers/helpers.v1.d/redis @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 0000000..c175b87 --- /dev/null +++ b/helpers/helpers.v1.d/ruby @@ -0,0 +1,309 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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 + 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 + 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 + 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 +} diff --git a/helpers/helpers.v1.d/setting b/helpers/helpers.v1.d/setting new file mode 100644 index 0000000..9903880 --- /dev/null +++ b/helpers/helpers.v1.d/setting @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Get an application setting +# +# usage: ynh_app_setting_get --app=app --key=key +# | arg: -a, --app= - the application id +# | arg: -k, --key= - the setting to get +# +# Requires YunoHost version 2.2.4 or higher. +ynh_app_setting_get() { + local _globalapp=${app-:} + # Declare an array to define the options of this helper. + local legacy_args=ak + local -A args_array=([a]=app= [k]=key=) + local app + local key + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + app="${app:-$_globalapp}" + + ynh_app_setting "get" "$app" "$key" +} + +# Set an application setting +# +# When choosing the setting key's name, note that including the following keywords will make the associated setting's value appear masked in the debug logs (cf. [related code](https://github.com/YunoHost/yunohost/blob/216210d5e97070b85c96ebb4548c6abf36987771/src/log.py#L571)): `pwd`, `pass`, `passwd`, `password`, `passphrase`, `secret\w*` (regex), `\w+key` (regex), `token`, `PASSPHRASE` +# This is meant to allow sharing the logs while preserving confidential data, but having this in mind is useful would you expect to see those values while debugging your scripts. +# +# usage: ynh_app_setting_set --app=app --key=key --value=value +# | arg: -a, --app= - the application id +# | arg: -k, --key= - the setting name to set +# | arg: -v, --value= - the setting value to set +# +# Requires YunoHost version 2.2.4 or higher. +ynh_app_setting_set() { + local _globalapp=${app-:} + # Declare an array to define the options of this helper. + local legacy_args=akv + local -A args_array=([a]=app= [k]=key= [v]=value=) + local app + local key + local value + # Manage arguments with getopts + 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 --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 +} + +# Delete an application setting +# +# usage: ynh_app_setting_delete --app=app --key=key +# | arg: -a, --app= - the application id +# | arg: -k, --key= - the setting to delete +# +# Requires YunoHost version 2.2.4 or higher. +ynh_app_setting_delete() { + local _globalapp=${app-:} + # Declare an array to define the options of this helper. + local legacy_args=ak + local -A args_array=([a]=app= [k]=key=) + local app + local key + # Manage arguments with getopts + 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 - << EOF +import os, yaml, sys +app, action = os.environ['APP'], os.environ['ACTION'].lower() +key, value = os.environ['KEY'], os.environ.get('VALUE', None) +setting_file = "/etc/yunohost/apps/%s/settings.yml" % app +assert os.path.exists(setting_file), "Setting file %s does not exists ?" % setting_file +with open(setting_file) as f: + settings = yaml.safe_load(f) +if action == "get": + if key in settings: + print(settings[key]) +else: + if action == "delete": + if key in settings: + del settings[key] + elif action == "set": + if key in ['redirected_urls', 'redirected_regex']: + value = yaml.safe_load(value) + settings[key] = value + else: + raise ValueError("action should either be get, set or delete") + with open(setting_file, "w") as f: + yaml.safe_dump(settings, f, default_flow_style=False) +EOF + eval "$xtrace_enable" +} + +# 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 +# +# example: ynh_webpath_available --domain=some.domain.tld --path_url=/coffee +# +# Requires YunoHost version 2.6.4 or higher. +ynh_webpath_available() { + # Declare an array to define the options of this helper. + local legacy_args=dp + local -A args_array=([d]=domain= [p]=path_url=) + local domain + local path_url + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + yunohost domain url-available $domain $path_url +} + +# 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 +# | arg: -p, --path_url= - the web path to be registered +# +# example: ynh_webpath_register --app=wordpress --domain=some.domain.tld --path_url=/coffee +# +# Requires YunoHost version 2.6.4 or higher. +ynh_webpath_register() { + # Declare an array to define the options of this helper. + local legacy_args=adp + local -A args_array=([a]=app= [d]=domain= [p]=path_url=) + local app + local domain + local path_url + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + yunohost app register-url $app $domain $path_url +} diff --git a/helpers/helpers.v1.d/sources b/helpers/helpers.v1.d/sources new file mode 100644 index 0000000..ad01005 --- /dev/null +++ b/helpers/helpers.v1.d/sources @@ -0,0 +1,304 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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/helpers.v1.d/string b/helpers/helpers.v1.d/string new file mode 100644 index 0000000..c70f29b --- /dev/null +++ b/helpers/helpers.v1.d/string @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Generate a random string +# +# usage: ynh_string_random [--length=string_length] +# | arg: -l, --length= - the string length to generate (default: 24) +# | arg: -f, --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) +# +# Requires YunoHost version 2.2.4 or higher. +ynh_string_random() { + # Declare an array to define the options of this helper. + local legacy_args=lf + local -A args_array=([l]=length= [f]=filter=) + local length + local filter + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + length=${length:-24} + filter=${filter:-'A-Za-z0-9'} + + tr --complement --delete "$filter" < /dev/urandom | head -c "$length" +} + +# Substitute/replace a string (or expression) by another in a file +# +# usage: ynh_replace_string --match_string=match_string --replace_string=replace_string --target_file=target_file +# | arg: -m, --match_string= - String to be searched and replaced in the file +# | arg: -r, --replace_string= - String that will replace matches +# | arg: -f, --target_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) +# +# Requires YunoHost version 2.6.4 or higher. +ynh_replace_string() { + # Declare an array to define the options of this helper. + local legacy_args=mrf + local -A args_array=([m]=match_string= [r]=replace_string= [f]=target_file=) + local match_string + local replace_string + local target_file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + set +o xtrace # set +x + + local delimit=$'\001' + # Escape the delimiter if it's in the string. + match_string=${match_string//${delimit}/"\\${delimit}"} + replace_string=${replace_string//${delimit}/"\\${delimit}"} + + set -o xtrace # set -x + sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$target_file" +} + +# Substitute/replace a special string by another in a file +# +# usage: ynh_replace_special_string --match_string=match_string --replace_string=replace_string --target_file=target_file +# | arg: -m, --match_string= - String to be searched and replaced in the file +# | arg: -r, --replace_string= - String that will replace matches +# | arg: -t, --target_file= - File in which the string will be replaced. +# +# This helper will use ynh_replace_string, but as you can use special +# characters, you can't use some regular expressions and sub-expressions. +# +# Requires YunoHost version 2.7.7 or higher. +ynh_replace_special_string() { + # Declare an array to define the options of this helper. + local legacy_args=mrf + local -A args_array=([m]=match_string= [r]=replace_string= [f]=target_file=) + local match_string + local replace_string + local target_file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Escape any backslash to preserve them as simple backslash. + match_string=${match_string//\\/"\\\\"} + replace_string=${replace_string//\\/"\\\\"} + + # Escape the & character, who has a special function in sed. + match_string=${match_string//&/"\&"} + replace_string=${replace_string//&/"\&"} + + ynh_replace_string --match_string="$match_string" --replace_string="$replace_string" --target_file="$target_file" +} + +# 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 +# +# example: dbname=$(ynh_sanitize_dbid $app) +# +# Underscorify the string (replace - and . by _) +# +# Requires YunoHost version 2.2.4 or higher. +ynh_sanitize_dbid() { + # Declare an array to define the options of this helper. + local legacy_args=n + local -A args_array=([n]=db_name=) + local db_name + # Manage arguments with getopts + 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 +# +# [internal] +# +# 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_url=path_to_normalize +# | arg: -p, --path_url= - URL path to normalize before using it +# +# Requires YunoHost version 2.6.4 or higher. +ynh_normalize_url_path() { + # Declare an array to define the options of this helper. + local legacy_args=p + local -A args_array=([p]=path_url=) + local path_url + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + test -n "$path_url" || ynh_die --message="ynh_normalize_url_path expect a URL path as first argument and received nothing." + if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a / + path_url="/$path_url" # Add / at begin of path variable + 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.v1.d/systemd b/helpers/helpers.v1.d/systemd new file mode 100644 index 0000000..b569f35 --- /dev/null +++ b/helpers/helpers.v1.d/systemd @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Create a dedicated systemd config +# +# usage: ynh_add_systemd_config [--service=service] [--template=template] +# | arg: -s, --service= - Service name (optionnal, `$app` by default) +# | arg: -t, --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_add_config` for a description of the template +# format and how placeholders are replaced with actual variables. +# +# Requires YunoHost version 4.1.0 or higher. +ynh_add_systemd_config() { + # Declare an array to define the options of this helper. + local legacy_args=stv + local -A args_array=([s]=service= [t]=template=) + local service + local template + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + service="${service:-$app}" + template="${template:-systemd.service}" + + ynh_add_config --template="$template" --destination="/etc/systemd/system/$service.service" + + systemctl enable $service --quiet + systemctl daemon-reload +} + +# Remove the dedicated systemd config +# +# usage: ynh_remove_systemd_config [--service=service] +# | arg: -s, --service= - Service name (optionnal, $app by default) +# +# Requires YunoHost version 2.7.2 or higher. +ynh_remove_systemd_config() { + # Declare an array to define the options of this helper. + local legacy_args=s + local -A args_array=([s]=service=) + local service + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + local service="${service:-$app}" + + local finalsystemdconf="/etc/systemd/system/$service.service" + if [ -e "$finalsystemdconf" ]; then + ynh_systemd_action --service_name=$service --action=stop + systemctl disable $service --quiet + ynh_secure_remove --file="$finalsystemdconf" + 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_systemd_action [--service_name=service_name] [--action=action] [ [--line_match="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ] +# | arg: -n, --service_name= - Name of the service to start. Default : `$app` +# | arg: -a, --action= - Action to perform with systemctl. Default: start +# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. +# | arg: -p, --log_path= - Log file - Path to the log file. Default : `/var/log/$app/$app.log` +# | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds. +# | arg: -e, --length= - Length of the error log displayed for debugging : Default : 20 +# +# Requires YunoHost version 3.5.0 or higher. +ynh_systemd_action() { + # Declare an array to define the options of this helper. + local legacy_args=nalpte + local -A args_array=([n]=service_name= [a]=action= [l]=line_match= [p]=log_path= [t]=timeout= [e]=length=) + local service_name + local action + local line_match + local length + local log_path + local timeout + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + service_name="${service_name:-$app}" + action=${action:-start} + line_match=${line_match:-} + length=${length:-20} + log_path="${log_path:-/var/log/$service_name/$service_name.log}" + timeout=${timeout:-300} + + # Manage case of service already stopped + if [ "$action" == "stop" ] && ! systemctl is-active --quiet $service_name; then + return 0 + fi + + # Start to read the log + if [[ -n "$line_match" ]]; 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_name --follow --since=-0 --quiet > "$templog" & + # Get the PID of the journalctl command + local pid_tail=$! + else + # Read the specified log file + tail --follow=name --retry --lines=0 "$log_path" > "$templog" 2>&1 & + # 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_name; then + # Show syslog for this service + ynh_exec_err journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service_name + # If a log is specified for this service, show also the content of this log + if [ -e "$log_path" ]; then + ynh_exec_err tail --lines=$length "$log_path" + fi + ynh_clean_check_starting + return 1 + fi + + # Start the timeout and try to find line_match + 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 + # For systemd services, we in fact dont rely on the templog, which for some reason is not reliable, but instead re-read journalctl every iteration, starting at the timestamp where we triggered the action + if journalctl --unit=$service_name --since="$time_start" --quiet --no-pager --no-hostname | grep --extended-regexp --quiet "$line_match"; then + ynh_print_info --message="The service $service_name has correctly executed the action ${action}." + break + fi + else + if grep --extended-regexp --quiet "$line_match" "$templog"; then + ynh_print_info --message="The service $service_name has correctly executed the action ${action}." + break + fi + fi + if [ $i -eq 30 ]; then + echo "(this may take some time)" >&2 + 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 + if [ $i -ge 3 ]; then + echo "" >&2 + fi + if [ $i -eq $timeout ]; then + ynh_print_warn --message="The service $service_name didn't fully executed the action ${action} before the timeout." + ynh_print_warn --message="Please find here an extract of the end of the log of the service $service_name:" + ynh_exec_warn journalctl --quiet --no-hostname --no-pager --lines=$length --unit=$service_name + if [ -e "$log_path" ]; then + ynh_print_warn --message="\-\-\-" + ynh_exec_warn tail --lines=$length "$log_path" + fi + fi + ynh_clean_check_starting + fi +} + +# Clean temporary process and file used by ynh_check_starting +# +# [internal] +# +# Requires YunoHost version 3.5.0 or higher. +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_secure_remove --file="$templog" 2>&1 + fi +} diff --git a/helpers/helpers.v1.d/systemuser b/helpers/helpers.v1.d/systemuser new file mode 100644 index 0000000..fa0a7d0 --- /dev/null +++ b/helpers/helpers.v1.d/systemuser @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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. +# +# Requires YunoHost version 2.2.4 or higher. +ynh_system_user_exists() { + # Declare an array to define the options of this helper. + local legacy_args=u + local -A args_array=([u]=username=) + local username + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + getent passwd "$username" &> /dev/null +} + +# 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. +# +# Requires YunoHost version 3.5.0.2 or higher. +ynh_system_group_exists() { + # Declare an array to define the options of this helper. + local legacy_args=g + local -A args_array=([g]=group=) + local group + # Manage arguments with getopts + 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: -u, --username= - Name of the system user that will be create +# | arg: -h, --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: -s, --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: -g, --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 +# ``` +# +# Requires YunoHost version 2.6.4 or higher. +ynh_system_user_create() { + # Declare an array to define the options of this helper. + local legacy_args=uhs + local -A args_array=([u]=username= [h]=home_dir= [s]=use_shell [g]=groups=) + local username + local home_dir + local use_shell + local groups + + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + use_shell="${use_shell:-0}" + home_dir="${home_dir:-}" + groups="${groups:-}" + + if ! ynh_system_user_exists "$username"; then # Check if the user exists on the system + # If the user doesn't exist + if [ -n "$home_dir" ]; then # If a home dir is mentioned + local user_home_dir="--home-dir $home_dir" + else + local user_home_dir="--no-create-home" + fi + if [ $use_shell -eq 1 ]; then # If we want a shell for the user + local shell="" # Use default shell + else + local shell="--shell /usr/sbin/nologin" + fi + useradd $user_home_dir --system --user-group $username $shell || ynh_die --message="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: -u, --username= - Name of the system user that will be create +# +# Requires YunoHost version 2.6.4 or higher. +ynh_system_user_delete() { + # Declare an array to define the options of this helper. + local legacy_args=u + local -A args_array=([u]=username=) + local username + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + # Check if the user exists on the system + if ynh_system_user_exists "$username"; then + deluser $username + else + ynh_print_warn --message="The user $username was not found" + fi + + # Check if the group exists on the system + if ynh_system_group_exists "$username"; then + delgroup $username + fi +} + +# Execute a command as another user +# +# usage: ynh_exec_as $USER COMMAND [ARG ...] +# +# Requires YunoHost version 4.1.7 or higher. +ynh_exec_as() { + local user=$1 + shift 1 + + if [[ $user = $(whoami) ]]; then + eval "$@" + else + sudo -u "$user" "$@" + fi +} diff --git a/helpers/helpers.v1.d/templating b/helpers/helpers.v1.d/templating new file mode 100644 index 0000000..ba9d2fc --- /dev/null +++ b/helpers/helpers.v1.d/templating @@ -0,0 +1,425 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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//&/\\&} + 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 0000000..2ecbde9 --- /dev/null +++ b/helpers/helpers.v1.d/utils @@ -0,0 +1,465 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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" > /dev/null # Skip print to standard output, the caller may process the output and then expect only the HTTP response. + # See https://github.com/YunoHost-Apps/piwigo_ynh/issues/168#issuecomment-3694238343 + 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" > /dev/null + 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 0000000..9c39cc9 --- /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/0-utils b/helpers/helpers.v2.1.d/0-utils new file mode 100644 index 0000000..e1be8c4 --- /dev/null +++ b/helpers/helpers.v2.1.d/0-utils @@ -0,0 +1,649 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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 env PATH=$PATH) +# +# usage: ynh_exec_as_app COMMAND [ARG ...] +ynh_exec_as_app() { + sudo -u "$app" env \ + PATH="$PATH" \ + COREPACK_ENABLE_DOWNLOAD_PROMPT="0" \ + NPM_CONFIG_UPDATE_NOTIFIER="false" \ + "$@" +} + +# 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() { + + # 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 '&' + ynh_local_curl_raw "$1" --data "${POST_data::-1}" + else + ynh_local_curl_raw "$1" + fi +} + +# More low-level curl wrapper, meant for more elaborate use case where people +# want to pass arbitrary headers and other curl options, not just POST data +# +# [internal] +# +# example: ynh_local_curl_raw "/install.php?installButton" --data "foo=$var1&bar=$var2" +ynh_local_curl_raw() { + # 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 + + # 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" > /dev/null # Skip print to standard output, the caller may process the output and then expect only the HTTP response. + # See https://github.com/YunoHost-Apps/piwigo_ynh/issues/168#issuecomment-3694238343 + fi + + # Curl the URL + shift + curl --silent --show-error --insecure --location --header "Host: $domain" --resolve "$domain:443:127.0.0.1" "$full_page_url" --cookie-jar "$cookiefile" --cookie "$cookiefile" "$@" + + if [[ $visitors_enabled == "no" ]]; then + ynh_permission_update --permission="main" --remove="visitors" > /dev/null + 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 - along with $data_dir via ynh_restore?) + 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 + elif [ "$target" == "${data_dir:-}" ] || [ "$target" == "/var/log/$app" ]; then + # Read the group from the data manifest resource + local group="$(ynh_read_manifest 'resources.data_dir.group' | sed 's/null//g' | sed "s/__APP__/$app/g" | cut -f1 -d:)" + chmod 750 "$target" + chown -R "$app:${group:-$app}" "$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 << EOF +import socket +import sys +family = { "4" : socket.AF_INET, "6" : socket.AF_INET6 } +try: + socket.inet_pton(family["$family"], "$ip_address") +except socket.error: + sys.exit(1) +sys.exit(0) +EOF +} + +# Get the total or free amount of RAM+swap on the system +# +# [packagingv1] +# +# usage: ynh_get_ram [--free|--total] +# | arg: --free - Count free RAM+swap +# | arg: --total - Count total RAM+swap +# | ret: the amount of free ram, in MB (MegaBytes) +ynh_get_ram() { + # ============ Argument parsing ============= + local -A args_array=([f]=free [t]=total) + local free + local total + ynh_handle_getopts_args "$@" + free=${free:-0} + total=${total:-0} + # =========================================== + + if [ "$free" -eq "$total" ]; then + ynh_print_warn "You have to choose --free or --total when using ynh_get_ram" + ram=0 + elif [ "$free" -eq 1 ]; then + local free_ram=$(LC_ALL=C vmstat --stats --unit M | grep "free memory" | awk '{print $1}') + local free_swap=$(LC_ALL=C vmstat --stats --unit M | grep "free swap" | awk '{print $1}') + local free_ram_swap=$((free_ram + free_swap)) + local ram=$free_ram_swap + elif [ "$total" -eq 1 ]; then + local total_ram=$(LC_ALL=C vmstat --stats --unit M | grep "total memory" | awk '{print $1}') + local total_swap=$(LC_ALL=C vmstat --stats --unit M | grep "total swap" | awk '{print $1}') + local total_ram_swap=$((total_ram + total_swap)) + local ram=$total_ram_swap + fi + + echo "$ram" +} + +# Check if the scripts are being run by the package_check in CI +# +# usage: ynh_in_ci_tests +# +# Return 0 if in CI, 1 otherwise +ynh_in_ci_tests() { + [ "${PACKAGE_CHECK_EXEC:-0}" -eq 1 ] +} + +# Retrieve a YunoHost user information +# +# usage: ynh_user_get_info --username=username --key=key +# | arg: --username= - the username to retrieve info from +# | arg: --key= - the key to retrieve +# | ret: the value associate to that key +# +# example: mail=$(ynh_user_get_info --username="toto" --key=mail) +ynh_user_get_info() { + # ============ Argument parsing ============= + local -A args_array=([u]=username= [k]=key=) + local username + local key + 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 +ynh_user_list() { + yunohost user list --output-as json --quiet | jq -r ".users | keys[]" +} + +# Spawn a Bash shell with the app environment loaded +# +# usage: ynh_spawn_app_shell +# +# examples: +# ynh_spawn_app_shell <<< 'echo "$USER"' +# ynh_spawn_app_shell < /tmp/some_script.bash +# +# 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() { + + # Force Bash to be used to run this helper + [[ $0 =~ \/?bash$ ]] || ynh_die "Please use Bash as shell" + + # Make sure the app is installed + test -d "/etc/yunohost/apps/$app" || ynh_die "$app is not an installed app ?!" + + # Make sure the app has its own user + id -u "$app" &> /dev/null || ynh_die "There is no \"$app\" system user" + + # Make sure the app has an install_dir setting + [ -n "${install_dir:-}" ] || ynh_die "$app has no install_dir setting!" + + # Export HOME variable + export HOME=$install_dir + + # Force `php` to its intended version + # We use `eval`+`export` since `alias` is not propagated to subshells, even with `export` + if [ -n "${php_version:-}" ]; then + eval "php() { php${php_version} ${phpflags:-} \"\$@\"; }" + export -f php + fi + + # Load the app's service name, or default to $app + service=${service:-$app} + if systemctl list-units -a | grep -q "$service.service"; then + # 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?}" + + # Source the EnvironmentFiles from the app's service + local -a env_files + mapfile -t env_files < <(systemctl show "$service.service" -p "EnvironmentFiles" --value | sed 's| (ignore_errors=\w*)||') + if [ ${#env_files[*]} -gt 0 ]; then + for file in "${env_files[@]}"; do + if [[ $file = /* ]]; then + # set -/+a enables and disables new variables being automatically exported. Needed when using `source`. + set -a + # shellcheck disable=SC1090,SC1091 + source "$file" + set +a + fi + done + fi + 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 + # shellcheck disable=SC1090,SC1091 + 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) + cd "${env_dir:-$install_dir}" + + # Spawn the app shell + su -s /bin/bash "$app" +} + +# Add swap +# +# usage: ynh_add_swap --size=SWAP in Mb +# | arg: -s, --size= - Amount of SWAP to add in Mb. +ynh_add_swap() { + if systemd-detect-virt --container --quiet; then + ynh_print_warn "You are inside a container/VM. swap will not be added, but that can cause troubles for the app $app. Please make sure you have enough RAM available." + return + fi + + # Declare an array to define the options of this helper. + declare -Ar args_array=([s]=size=) + local size + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local swap_max_size=$((size * 1024)) + + local free_space=$(df --output=avail / | sed 1d) + # Because we don't want to fill the disk with a swap file, divide by 2 the available space. + local usable_space=$((free_space / 2)) + + SD_CARD_CAN_SWAP=${SD_CARD_CAN_SWAP:-0} + + # Swap on SD card only if it's is specified + if ynh_is_main_device_a_sd_card && [ "$SD_CARD_CAN_SWAP" == "0" ]; then + ynh_print_warn "The main mountpoint of your system '/' is on an SD card, swap will not be added to prevent some damage of this one, but that can cause troubles for the app $app. If you still want activate the swap, you can relaunch the command preceded by 'SD_CARD_CAN_SWAP=1'" + return + fi + + # Compare the available space with the size of the swap. + # And set a acceptable size from the request + if [ $usable_space -ge $swap_max_size ]; then + local swap_size=$swap_max_size + elif [ $usable_space -ge $((swap_max_size / 2)) ]; then + local swap_size=$((swap_max_size / 2)) + elif [ $usable_space -ge $((swap_max_size / 3)) ]; then + local swap_size=$((swap_max_size / 3)) + elif [ $usable_space -ge $((swap_max_size / 4)) ]; then + local swap_size=$((swap_max_size / 4)) + else + echo "Not enough space left for a swap file" >&2 + local swap_size=0 + fi + + # If there's enough space for a swap, and no existing swap here + if [ $swap_size -ne 0 ] && [ ! -e "/swap_$app" ]; then + # Create file + truncate -s 0 "/swap_$app" + + # try to set the No_COW attribute on the swapfile with chattr (depending of the filesystem type) + chattr +C "/swap_$app" 2> /dev/null + + # Preallocate space for the swap file, fallocate may sometime not be used, use dd instead in this case + if ! fallocate -l ${swap_size}K "/swap_$app"; then + dd if=/dev/zero of="/swap_$app" bs=1024 count=${swap_size} + fi + chmod 0600 "/swap_$app" + # Create the swap + mkswap "/swap_$app" + # And activate it + swapon "/swap_$app" + # Then add an entry in fstab to load this swap at each boot. + echo -e "/swap_$app swap swap defaults 0 0 #Swap added by $app" >> /etc/fstab + fi +} + +# Delete swap +# +# usage: ynh_del_swap +ynh_del_swap() { + # If there a swap at this place + if [ -e "/swap_$app" ]; then + # Clean the fstab + sed -i "/#Swap added by $app/d" /etc/fstab + # Deactive the swap file + swapoff "/swap_$app" + # And remove it + rm "/swap_$app" + fi +} + +# Check if the device of the main mountpoint "/" is an SD card +# +# [internal] +# +# return 0 if it's an SD card, else 1 +ynh_is_main_device_a_sd_card() { + if [ "$(systemd-detect-virt)" != "none" ]; then + # Assume virtualization does not take place on SD card + return 1 + fi + + local main_device=$(lsblk --output PKNAME --noheadings "$(findmnt / --nofsroot --uniq --output source --noheadings --first-only)") + + if echo "$main_device" | grep --quiet "mmc" && [ "$(tail -n1 "/sys/block/$main_device/queue/rotational")" == "0" ]; then + return 0 + else + return 1 + fi +} + +# Check available space before creating a temp directory. +# +# usage: ynh_smart_mktemp --min_size="Min size" +# +# | arg: -s, --min_size= - Minimal size needed for the temporary directory, in Mb +ynh_smart_mktemp() { + # Declare an array to define the options of this helper. + declare -Ar args_array=([s]=min_size=) + local min_size + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + min_size="${min_size:-300}" + # Transform the minimum size from megabytes to kilobytes + min_size=$((min_size * 1024)) + + # Check if there's enough free space in a directory + is_there_enough_space() { + local free_space=$(df --output=avail "$1" | sed 1d) + test "$free_space" -ge $min_size + } + + if is_there_enough_space /tmp; then + local tmpdir=/tmp + elif is_there_enough_space /var; then + local tmpdir=/var + elif is_there_enough_space /; then + local tmpdir=/ + elif is_there_enough_space /home; then + local tmpdir=/home + else + ynh_die "Insufficient free space to continue..." + fi + + mktemp --directory --tmpdir="$tmpdir" +} + +# Setup/update a git clone, meant to be used internally to fetch technical tools like goenv, rbenv +# +# [internal] +# +function _ynh_git_clone() { + local url="$1" + local dest_dir="$2" + local branch=${3:-master} + + mkdir -p "$dest_dir" + pushd "$dest_dir" || return 1 + if ! [ -d "$dest_dir/.git" ]; then + git init -q + git remote add origin "$url" + else + git remote set-url origin "$url" + fi + git fetch -q --tags --prune origin "$branch" + git reset --hard origin/"$branch" + popd || return 1 +} diff --git a/helpers/helpers.v2.1.d/apt b/helpers/helpers.v2.1.d/apt new file mode 100644 index 0000000..3c5d803 --- /dev/null +++ b/helpers/helpers.v2.1.d/apt @@ -0,0 +1,380 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +YNH_APT_INSTALL_DEPENDENCIES_REPLACE="true" + +# Define and install dependencies with a equivs control file +# +# example : ynh_apt_install_dependencies dep1 dep2 "dep3|dep4|dep5" +# +# usage: ynh_apt_install_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 + # Otherwise force php_version back to the default version ... + # ... except we don't want to do this when appending the "extra" dependencies if they don't contain a php version ... + elif grep --quiet 'php' <<< "$dependencies" && [[ $YNH_APT_INSTALL_DEPENDENCIES_REPLACE == "true" ]]; 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_apt_install_dependencies, we will replace the + # entire control file (This is in particular meant to cover the case of + # upgrade script where ynh_apt_install_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" << EOF +Section: misc +Priority: optional +Package: ${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 + + _ynh_apt update --error-on=any + + _ynh_wait_dpkg_free + + # Install the fake package without its dependencies with dpkg --force-depends + if ! LC_ALL=C dpkg-deb --build "${TMPDIR}/${app_ynh_deps}" "${TMPDIR}/${app_ynh_deps}.deb" > "${TMPDIR}/dpkg_log" 2>&1; then + cat "${TMPDIR}/dpkg_log" >&2 + ynh_die "Unable to install dependencies" + fi + # Don't crash in case of error, because is nicely covered by the following line + LC_ALL=C dpkg --force-depends --install "${TMPDIR}/${app_ynh_deps}.deb" 2>&1 | tee "${TMPDIR}/dpkg_log" || true + + # 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 + mapfile -t problematic_dependencies < <(grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' "$TMPDIR/dpkg_log") + # Fake an install of those dependencies to see the errors + # The sed command here is, Print only from 'Reading state info' to the end. + if ((${#problematic_dependencies[@]} != 0)); then + _ynh_apt_install "${problematic_dependencies[@]}" --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2 + fi + ynh_die "Unable to install apt dependencies, it might be due to a conflict with another app - or you should check and share the previous log about what are the problematic 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 package into packages list + local packages + read -r -a packages <<< "$package" + + # 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 --error-on=any -o Dir::Etc::sourcelist="/etc/apt/sources.list.d/$app.list" + + # Force the cache to be reupdated on the next "apt update" (in + # ynh_apt_install_dependencies) because the previous command with + # ::sourcelist option makes apt forget about every other package for other, + # so we want to force the cache to be reupdated entirely + touch "/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 + mapfile -t apps_auto_installed < <(apt-mark showauto "${packages[@]}") + _ynh_apt_install "${packages[@]}" + if ((${#apps_auto_installed[@]} != 0)); then + apt-mark auto "${apps_auto_installed[@]}" + fi + + # 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 --error-on=any +} + +# ##################### +# 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 -r 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() { + + # Optimization when just calling apt update : check if the cache was + # already refreshed in the last 30 min, which should be enough and prevent + # unecessary traffic and annoying wait time during app dependency installs etc + if [[ "$*" == "update" ]]; then + # trick from https://stackoverflow.com/a/205710 + local aptcache="/var/cache/apt/pkgcache.bin" + sleep 1 + if [[ -e $aptcache ]] && [[ -n "$(find $aptcache -mmin -30)" ]] && [[ -z "$(find /etc/apt/ -newer $aptcache)" ]]; then + echo "apt cache was already updated in the last 30 minutes, skipping 'apt update'" + return + fi + fi + + _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 0000000..eb9eeb2 --- /dev/null +++ b/helpers/helpers.v2.1.d/backup @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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:-0}" -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 0 + 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 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 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 -r line; do + local ARCHIVE_PATH=$(echo "$line" | grep --only-matching --no-filename --perl-regexp "^\"\K.*(?=\",\"$REL_DIR.*\"$)") + 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 0000000..9ffee08 --- /dev/null +++ b/helpers/helpers.v2.1.d/composer @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 0000000..0203e4f --- /dev/null +++ b/helpers/helpers.v2.1.d/config @@ -0,0 +1,332 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +_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 "$bind" 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="$app" --key="$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 "$bind" 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)" + 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="$bind" + 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="$bind" + 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)" + + 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() { + for line in $YNH_APP_CONFIG_PANEL_OPTIONS_TYPES_AND_BINDS; do + # Split line into short_setting, type and bind + IFS='|' read -r 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 + local xtrace_enable=$(set +o | grep xtrace) + # Disable logging during this loop because that's a lot of noisy logs, and that's an additional layer to prevent leaking from leaking secrets heaurqg + set +o xtrace # set +x + 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 [ -n "${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 + eval "$xtrace_enable" + + 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 0000000..52bd958 --- /dev/null +++ b/helpers/helpers.v2.1.d/fail2ban @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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): +# +# ```toml +# f2b_jail.conf: +# [__APP__] +# enabled = true +# port = http,https +# filter = __APP__ +# logpath = /var/log/__APP__/logfile.log +# maxretry = 5 +# ``` +# +# ```toml +# 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 on [the official fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters). +# +# To validate your regex you can test with this command: +# +# ```bash +# 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" + + # Create the folder and logfile if they doesn't exist, + # as fail2ban require an existing logfile before configuration + local logdir=$(dirname "$logpath") + if [ ! -d "$logdir" ]; then + mkdir -p "$logdir" + # Make sure log folder's permissions are correct + chown "$app:$app" "$logdir" + chmod u=rwX,g=rX,o= "$logdir" + fi + + if [ ! -f "$logpath" ]; then + touch "$logpath" + # Make sure log file's permissions are correct + chown "$app:$app" "$logpath" + chmod u=rwX,g=rX,o= "$logpath" + fi + + 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 0000000..8338c35 --- /dev/null +++ b/helpers/helpers.v2.1.d/getopts @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 0000000..1097645 --- /dev/null +++ b/helpers/helpers.v2.1.d/go @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +readonly GOENV_ROOT="/opt/goenv" +export GOENV_ROOT + +_ynh_load_go_in_path_and_other_tweaks() { + + # Get the absolute path of this version of go + go_dir="$GOENV_ROOT/versions/$go_version/bin" + + if [ ! -e "$go_dir" ]; then + echo "Skipping loading go, because it doesn't seem to be provisioned yet. This is likely to happen during restore and other specific contexts." + return + fi + + # 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_ROOT/bin/goenv" local "$go_version" + popd +} + +# Auto-load Go path tweaks if this app uses the Go resource in the manifest +if [ -n "${go_version:-}" ] && (cat "$YNH_APP_BASEDIR/manifest.toml" | toml_to_json | jq -e ".resources.go" > /dev/null); then + _ynh_load_go_in_path_and_other_tweaks +fi + +# 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) +ynh_go_install() { + + [[ -n "${go_version:-}" ]] || ynh_die "\$go_version should be defined prior to calling ynh_go_install" + + _ynh_git_clone "https://github.com/syndbg/goenv" "$GOENV_ROOT" + _ynh_git_clone "https://github.com/momo-lab/xxenv-latest" "$GOENV_ROOT/plugins/xxenv-latest" + + # Enable caching + mkdir -p "${GOENV_ROOT}/cache" + # Create shims directory if needed + mkdir -p "${GOENV_ROOT}/shims" + + # Install the requested version of Go + local final_go_version=$(PATH=$GOENV_ROOT/bin:$PATH "$GOENV_ROOT/plugins/xxenv-latest/bin/goenv-latest" --print "$go_version") + go_version=$final_go_version + ynh_app_setting_set --app="$app" --key="go_version" --value="$go_version" + + ynh_print_info "Installing Go $go_version" + $GOENV_ROOT/bin/goenv install --quiet --skip-existing "$go_version" 2>&1 + + # Cleanup Go versions + _ynh_go_cleanup + + _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() { + # 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() { + + # Remove no more needed Go versions + local installed_go_versions=$($GOENV_ROOT/bin/goenv versions --bare --skip-aliases | grep -Ev '/') + for installed_go_version in $installed_go_versions; do + if ! grep -qE "^go_version: '?$installed_go_version'?" /etc/yunohost/apps/*/settings.yml; then + # ynh_print_info "Removing Go-$installed_go_version" + $GOENV_ROOT/bin/goenv uninstall --force "$installed_go_version" + fi + done + + # If no app uses Go anymore + if ! grep -q "^go_version:" /etc/yunohost/apps/*/settings.yml; then + # Remove goenv environment configuration + # ynh_print_info "Removing goenv" + ynh_safe_rm "$GOENV_ROOT" + ynh_safe_rm "/etc/profile.d/goenv.sh" + fi +} diff --git a/helpers/helpers.v2.1.d/logging b/helpers/helpers.v2.1.d/logging new file mode 100644 index 0000000..51dc44e --- /dev/null +++ b/helpers/helpers.v2.1.d/logging @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 0000000..b592888 --- /dev/null +++ b/helpers/helpers.v2.1.d/logrotate @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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 0000000..b76f353 --- /dev/null +++ b/helpers/helpers.v2.1.d/mongodb @@ -0,0 +1,289 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 << EOF +use $database +${command} +quit() +EOF + else + mongosh --quiet --eval="$command" + fi +} + +# Drop a database +# +# [internal] +# +# If you intend to drop the database *and* the associated user, +# consider using ynh_mongo_remove_db instead. +# +# usage: ynh_mongo_drop_db --database=database +# | arg: --database= - The database name to drop +# +# +ynh_mongo_drop_db() { + # ============ Argument parsing ============= + local -A args_array=([d]=database=) + local database + ynh_handle_getopts_args "$@" + # =========================================== + + ynh_mongo_exec --database="$database" --command='db.runCommand({dropDatabase: 1})' +} + +# Dump a database +# +# example: ynh_mongo_dump_db --database=wekan > ./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 0000000..34a85ad --- /dev/null +++ b/helpers/helpers.v2.1.d/multimedia @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +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" + + # Disable logging to prevent leaking the user list (and anyway it's long and boring) + local xtrace_enable=$(set +o | grep xtrace) + set +o xtrace # set +x + + ## 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 + + # Re-enable logging + eval "$xtrace_enable" + + # 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 0000000..632d38e --- /dev/null +++ b/helpers/helpers.v2.1.d/mysql @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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} + + local default_character_set=() + if [[ -n "$database" ]]; then + default_character_set=( + --default-character-set + "$(mysql -B "$database" <<< 'show variables like "character_set_database";' | tail -n1 | cut -f2)" + ) + fi + + mysql "${default_character_set[@]}" -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} + + local default_character_set=() + if [[ -n "$database" ]]; then + default_character_set=( + --default-character-set + "$(mysql -B "$database" <<< 'show variables like "character_set_database";' | tail -n1 | cut -f2)" + ) + fi + + mysqldump "${default_character_set[@]}" --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 0000000..92539cc --- /dev/null +++ b/helpers/helpers.v2.1.d/nginx @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 + + # Delete REMOTE_USER mapping, it's already provided by + # /etc/nginx/fastcgi_params which all PHP apps include, and maps to the + # appropriate YNH_USER HTTP header instead of $remote_user + sed -i '/fastcgi_param\s*REMOTE_USER/d' "$finalnginxconf" + + 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 0000000..fc40fc3 --- /dev/null +++ b/helpers/helpers.v2.1.d/nodejs @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +readonly N_PREFIX="/opt/node_n" +export N_PREFIX + +# [internal] +_ynh_load_nodejs_in_path_and_other_tweaks() { + + # Get the absolute path of this version of node + nodejs_dir="$N_PREFIX/n/versions/node/$nodejs_version/bin" + + if [ ! -e "$nodejs_dir" ]; then + echo "Skipping loading nodejs, because it doesn't seem to be provisioned yet. This is likely to happen during restore and other specific contexts." + return + fi + + # 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 + # shellcheck disable=SC2034 + path_with_nodejs="$PATH" + # shellcheck disable=SC2034 + 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 + export NPM_CONFIG_UPDATE_NOTIFIER=false +} + +# Auto-load Nodejs path tweaks if this app uses the nodejs resource in the manifest +if [ -n "${nodejs_version:-}" ] && (cat "$YNH_APP_BASEDIR/manifest.toml" | toml_to_json | jq -e ".resources.nodejs" > /dev/null); then + _ynh_load_nodejs_in_path_and_other_tweaks +fi + +# 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_PREFIX + mkdir --parents "$N_PREFIX" + + # Install the requested version of nodejs + if [[ $YNH_ARCH == "arm64" ]]; then + "$YNH_HELPERS_DIR/vendor/n/n" install "$nodejs_version" --arch=arm64 + else + "$YNH_HELPERS_DIR/vendor/n/n" install "$nodejs_version" + fi + + # Find the last "real" version for this major version of node. + final_nodejs_version=$(find "$N_PREFIX/n/versions/node/$nodejs_version"* -maxdepth 0 | sort --version-sort | tail --lines=1) + final_nodejs_version=$(basename "$final_nodejs_version") + + # Store nodejs_version into the config of this app + nodejs_version="$final_nodejs_version" + ynh_app_setting_set --key=nodejs_version --value="$final_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" + + ynh_app_setting_delete --key=nodejs_version + + # Garbage-collect unused versions + local installed_versions="$(N_PREFIX=/opt/node_n "$YNH_HELPERS_DIR/vendor/n/n" ls | awk -F/ '{print $2}')" + for version in $installed_versions; do + if ! grep -qE "^nodejs_version: '?$version'?" /etc/yunohost/apps/*/settings.yml; then + "$YNH_HELPERS_DIR/vendor/n/n" rm "$version" + fi + done + + # If no other app uses n, remove n + if ! grep -q "^nodejs_version:" /etc/yunohost/apps/*/settings.yml; then + ynh_safe_rm "$N_PREFIX" + 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 0000000..df2e1ed --- /dev/null +++ b/helpers/helpers.v2.1.d/permission @@ -0,0 +1,319 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 \ +# --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 \ +# --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 ]] [--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: --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= [t]=show_tile= [P]=protected=) + local permission + local url + local additional_urls + local auth_header + local allowed + local show_tile + local protected + ynh_handle_getopts_args "$@" + url=${url:-} + additional_urls=${additional_urls:-} + auth_header=${auth_header:-} + allowed=${allowed:-} + 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 "${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 $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 0000000..73ad6bc --- /dev/null +++ b/helpers/helpers.v2.1.d/php @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# (this is used in the apt helpers, big meh ...) +readonly YNH_DEFAULT_PHP_VERSION=8.2 + +# 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 0000000..1317e2f --- /dev/null +++ b/helpers/helpers.v2.1.d/postgresql @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# shellcheck disable=SC2034 +PSQL_ROOT_PWD_FILE=/etc/yunohost/psql +PSQL_VERSION=15 + +# 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 --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 --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 --user=postgres psql "$db" <<< "REVOKE CONNECT ON DATABASE $db FROM public;" + sudo --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 --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 --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 --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 --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 --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 --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 0000000..22765d8 --- /dev/null +++ b/helpers/helpers.v2.1.d/redis @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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" +} + +# Erase a redis database so it can be reused by other apps. +# +# 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 0000000..017563b --- /dev/null +++ b/helpers/helpers.v2.1.d/ruby @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +readonly RBENV_ROOT="/opt/rbenv" +export RBENV_ROOT + +_ynh_load_ruby_in_path_and_other_tweaks() { + + # Get the absolute path of this version of Ruby + ruby_dir="$RBENV_ROOT/versions/$app/bin" + + if [ ! -e "$ruby_dir" ]; then + echo "Skipping loading ruby, because it doesn't seem to be provisioned yet. This is likely to happen during restore and other specific contexts." + return + fi + + # 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_ROOT/bin/rbenv" local "$ruby_version" + popd +} + +# Auto-load Ruby path tweaks if this app uses the ruby resource in the manifest +if [ -n "${ruby_version:-}" ] && (cat "$YNH_APP_BASEDIR/manifest.toml" | toml_to_json | jq -e ".resources.ruby" > /dev/null); then + _ynh_load_ruby_in_path_and_other_tweaks +fi + +# 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`) +ynh_ruby_install() { + + [[ -n "${ruby_version:-}" ]] || ynh_die "\$ruby_version should be defined prior to calling ynh_ruby_install" + + # Install or update rbenv + _ynh_git_clone "https://github.com/rbenv/rbenv" "${RBENV_ROOT}" + _ynh_git_clone "https://github.com/rbenv/ruby-build" "${RBENV_ROOT}/plugins/ruby-build" + _ynh_git_clone "https://github.com/tpope/rbenv-aliases" "${RBENV_ROOT}/plugins/rbenv-aliase" + _ynh_git_clone "https://github.com/momo-lab/xxenv-latest" "${RBENV_ROOT}/plugins/xxenv-latest" + + mkdir -p "${RBENV_ROOT}/cache" + mkdir -p "${RBENV_ROOT}/shims" + + # Install the requested version of Ruby + local final_ruby_version=$("${RBENV_ROOT}/bin/rbenv" latest --print "$ruby_version") + ruby_version=${final_ruby_version:-$ruby_version} + ynh_app_setting_set --key=ruby_version --value="$ruby_version" + + for PACKAGE in gcc make libjemalloc-dev libffi-dev libyaml-dev zlib1g-dev; do + _ynh_apt_package_is_installed "$PACKAGE" || ynh_die "$PACKAGE is required to install Ruby" + done + + echo "Installing Ruby $final_ruby_version" + RUBY_CONFIGURE_OPTS="--disable-install-doc --with-jemalloc" MAKE_OPTS="-j2" "${RBENV_ROOT}/bin/rbenv" install --skip-existing "$ruby_version" 2>&1 + + # Recreate rbenv alias for this version + if "${RBENV_ROOT}/bin/rbenv" alias --list | grep --quiet "$app "; then + "${RBENV_ROOT}/bin/rbenv" alias "$app" --remove + fi + ${RBENV_ROOT}/bin/rbenv alias "$app" "$ruby_version" + + # Cleanup Ruby versions + _ynh_ruby_cleanup + + _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" + + "${RBENV_ROOT}/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() { + + # Remove no more needed Ruby versions + local installed_ruby_versions=$("${RBENV_ROOT}/bin/rbenv" versions --bare --skip-aliases | grep -Ev '/') + for installed_ruby_version in $installed_ruby_versions; do + if ! grep -qE "^ruby_version: '?$installed_ruby_version'?" /etc/yunohost/apps/*/settings.yml; then + ynh_print_info "Removing Ruby-$installed_ruby_version" + "$RBENV_ROOT/bin/rbenv" uninstall --force "$installed_ruby_version" + fi + done + + # If no app uses Ruby anymore + if ! grep -q "^ruby_version:" /etc/yunohost/apps/*/settings.yml; then + # Remove rbenv environment configuration + ynh_print_info "Removing rbenv" + ynh_safe_rm "$RBENV_ROOT" + ynh_safe_rm "/etc/profile.d/rbenv.sh" + fi +} diff --git a/helpers/helpers.v2.1.d/setting b/helpers/helpers.v2.1.d/setting new file mode 100644 index 0000000..f407575 --- /dev/null +++ b/helpers/helpers.v2.1.d/setting @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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 +# +# When choosing the setting key's name, note that including the following keywords will make the associated setting's value appear masked in the debug logs (cf. [related code](https://github.com/YunoHost/yunohost/blob/216210d5e97070b85c96ebb4548c6abf36987771/src/log.py#L571)): `pwd`, `pass`, `passwd`, `password`, `passphrase`, `secret\w*` (regex), `\w+key` (regex), `token`, `PASSPHRASE` +# This is meant to allow sharing the logs while preserving confidential data, but having this in mind is useful would you expect to see those values while debugging your scripts. +# +# 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: +# +# ```bash +# 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 - << EOF +import os, yaml, sys +app, action = os.environ['APP'], os.environ['ACTION'].lower() +key, value = os.environ['KEY'], os.environ.get('VALUE', None) +setting_file = "/etc/yunohost/apps/%s/settings.yml" % app +assert os.path.exists(setting_file), "Setting file %s does not exists ?" % setting_file +with open(setting_file) as f: + settings = yaml.safe_load(f) +if action == "get": + if key in settings: + print(settings[key]) +else: + if action == "delete": + if key in settings: + del settings[key] + elif action == "set": + settings[key] = value + else: + raise ValueError("action should either be get, set or delete") + with open(setting_file, "w") as f: + yaml.safe_dump(settings, f, default_flow_style=False) +EOF + eval "$xtrace_enable" +} + +# Legacy: auto-convert phpversion to php_version (for consistency with nodejs_version, ruby_version, ...) +# This has to be here and not in the "php" code file because ynh_app_setting_set/delete need to be defined @_@ +if [[ -n "${app:-}" ]] && [[ -n "${phpversion:-}" ]]; then + if [[ -z "${php_version:-}" ]]; then + php_version=$phpversion + ynh_app_setting_set --key=php_version --value="$php_version" + fi + ynh_app_setting_delete --key=phpversion + unset phpversion +fi diff --git a/helpers/helpers.v2.1.d/sources b/helpers/helpers.v2.1.d/sources new file mode 100644 index 0000000..5de6421 --- /dev/null +++ b/helpers/helpers.v2.1.d/sources @@ -0,0 +1,307 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Download, check integrity, uncompress and patch upstream sources +# +# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id] [--keep="file1 file2"] [--full_replace] +# | arg: --dest_dir= - Directory where to setup sources +# | arg: --source_id= - Name of the source, defaults to `main` (when the sources resource exists in manifest.toml) or (legacy) `app` otherwise +# | arg: --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: --full_replace= - Remove previous sources before installing new sources (can be 1 or 0, default to 0) +# +# 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 +# ``` +# +# (See also the resources documentation which may be more complete?) +# +# ##### Optional flags in the 'sources' resource +# +# ```text +# format = "tar.gz"/xz/bz2/tar # 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 (the default filename being the value of `source_id` arg and not the upstream basename). +# 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 `patches/${src_id}/*.patch` will be applied to `$dest_dir` +# - Apply sane default permissions (see _ynh_apply_default_permissions) +ynh_setup_source() { + # ============ Argument parsing ============= + 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 + ynh_handle_getopts_args "$@" + keep="${keep:-}" + full_replace="${full_replace:-0}" + source_id="${source_id:-main}" + # =========================================== + + # Make sure to keep composer.phar that may have been provisioned prior to this, + # otherwise it'll get deleted right after when using --full-replace? + if [ -e "$dest_dir/composer.phar" ]; then + keep+=" composer.phar" + fi + + local sources_json=$(ynh_read_manifest "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_format="$(jq -r ".format" <<< "$sources_json" | sed 's/^null$//')" + local src_in_subdir="$(jq -r ".in_subdir" <<< "$sources_json" | sed 's/^null$//')" + src_in_subdir=${src_in_subdir:-true} + 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\.zst$ ]]; then + src_format="tar.zst" + elif [[ "$src_url" =~ ^.*\.tar\.bz2$ ]]; then + src_format="tar.bz2" + elif [[ "$src_url" =~ ^.*\.tar$ ]]; then + src_format="tar" + elif [[ "$src_url" =~ ^.*\.xz$ ]]; then + src_format="xz" + elif [[ "$src_url" =~ ^.*\.zst$ ]]; then + src_format="zst" + elif [[ -z "$src_extract" ]]; then + src_extract="false" + fi + fi + + 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 + + # 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 + [ -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}" | sha256sum --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 "$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" + elif [[ "$src_format" == "xz" ]]; then + # XZ format + if [[ "$src_in_subdir" == "true" ]]; then + ynh_die "XZ format does not support stripping components. (You should set in_subdir = false for this source)" + fi + if [[ -n "$src_rename" ]]; then + unxz "$src_filename" -c > "$dest_dir/$src_rename" + else + ynh_die "XZ format requires renaming. (You should set rename = \"your_name\" for this source)" + fi + ynh_safe_rm "$src_filename" + elif [[ "$src_format" == "zst" ]]; then + # ZSTD format + if [[ "$src_in_subdir" == "true" ]]; then + ynh_die "ZSTD format does not support stripping components. (You should set in_subdir = false for this source)" + fi + if [[ -n "$src_rename" ]]; then + unzstd "$src_filename" -o "$dest_dir/$src_rename" -f + else + ynh_die "ZSTD format requires renaming. (You should set rename = \"your_name\" for this source)" + 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.zst|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 + if [ -d "$YNH_APP_BASEDIR/patches/$source_id" ]; then + local patches_folder=$(realpath "$YNH_APP_BASEDIR/patches/$source_id") + 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 0000000..e3f8b1a --- /dev/null +++ b/helpers/helpers.v2.1.d/string @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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'} + # =========================================== + + tr --complement --delete "$filter" < /dev/urandom | head -c "$length" +} + +# 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). In particular +# for back-references, as in sed without specific options, you will need to escape +# the parentheses of the capture group. +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 0000000..ee71bdd --- /dev/null +++ b/helpers/helpers.v2.1.d/systemd @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Create a dedicated systemd config +# +# usage: ynh_config_add_systemd [--mount=mount] [--service=service] [--template=templatename] +# | arg: --mount= - Mount name (optional) +# | arg: --service= - Service name (optional, `$app` by default) +# | arg: --template= - Name of template file (optional, 'systemd.service' by default, meaning `../conf/systemd.service` will be used as template) +# +# This will use the template `../conf/`. +# +# 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=([m]=mount= [s]=service= [t]=template=) + local mount + local service + local template + ynh_handle_getopts_args "$@" + mount="${mount:-}" + service="${service:-$app}" + template="${template:-systemd.service}" + # =========================================== + + if [[ -n "$mount" ]]; then + ynh_config_add --template="$template" --destination="/etc/systemd/system/$mount.mount" + systemctl enable "$mount.mount" --quiet + else + ynh_config_add --template="$template" --destination="/etc/systemd/system/$service.service" + systemctl enable "$service.service" --quiet + fi + systemctl daemon-reload +} + +# Remove the dedicated systemd config +# +# usage: ynh_config_remove_systemd service +# | arg: service - Service name (optional, $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.service" --quiet + ynh_safe_rm "/etc/systemd/system/$service.service" + systemctl daemon-reload + fi + if [ -e "/etc/systemd/system/$service.mount" ]; then + ynh_systemctl --service="$service" --action=stop + systemctl disable "$service.mount" --quiet + ynh_safe_rm "/etc/systemd/system/$service.mount" + systemctl daemon-reload + fi +} + +# Start (or other actions) a service, print a log in case of failure and optionaly 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: --mount= - Name of the mount. If set, service="$mount.mount" +# | 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`; `systemd` to listen on `journalctl --unit=$service` +# | 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= [m]=mount= [a]=action= [w]=wait_until= [p]=log_path= [t]=timeout= [e]=length=) + local service + local mount + local action + local wait_until + local length + local log_path + local timeout + ynh_handle_getopts_args "$@" + service="${service:-$app}" + mount="${mount:-}" + action=${action:-start} + wait_until=${wait_until:-} + length=${length:-20} + log_path="${log_path:-/var/log/$service/$service.log}" + # =========================================== + + if [ -n "$mount" ]; then + service="$mount.mount" + fi + + if ynh_in_ci_tests && [ "$length" -le 20 ]; then + # Use a shorter timeout than in production to save CI time (services should rarely take up more than 2 minutes to start with the CI hardware) + timeout=${timeout:-120} + # On CI, use length=100 because it's sometime hell to debug otherwise for super-long output + length=100 + else + # 300 is a conservative value, especially because it may take quite + # long for service to start on low-end hardware, and CI should already + # have validate that the service is expected to be working + timeout=${timeout:-300} + 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 execute 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 but reached timeout, then handle it as a failure + if [ "$action" == "reload" ] || [ "$action" == "start" ] || [ "$action" == "restart" ]; then + # NB: we don't check the actual 'active' status of the service, + # because if the service is on Restart=always, it may be in a starting loop where it's temporarily active... + _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 0000000..a8d4e9d --- /dev/null +++ b/helpers/helpers.v2.1.d/systemuser @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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) : +# +# ```bash +# ynh_system_user_create --username=nextcloud +# ``` +# +# Create a discourse user using /var/www/discourse as home directory and the default login shell : +# +# ```bash +# 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 0000000..40e8d57 --- /dev/null +++ b/helpers/helpers.v2.1.d/templating @@ -0,0 +1,424 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# 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_config_add --template=".env" --destination="$install_dir/.env" # (use the template file "conf/.env" from the app's package) +# ynh_config_add --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` +# +# For this to work, template tags must be in uppercase and variables names must be in lowercase (for instance `__MY_var__` or `$myVar` would not be replaced as expected). +# +# 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 [the official Jinja documentation](https://jinja.palletsprojects.com/en/3.1.x/templates/). +# +# Note that in YunoHost context, all variables are from shell variables and therefore are strings +# To help handling complex data Jinja engine are executed with theses additional filters: +# - from_json: load a string as Json and return an object +# - from_yaml: load a string as Yaml and return an object +# - from_toml: load a string as Toml and return an object +# - to_json: serialize to string an object to Json +# - to_yaml: serialize to string an object to Yaml +# - to_toml: serialize to string an object to Toml +# +# So by example, if you want to convert a json string `$my_json` to Toml, you can to this way: +# `{{ my_json | from_json | to_toml }}` +# Or you can iterate on a Json list `my_list='["a", "b", "c"]'` this way: +# ``` +# {% for i in my_list | from_json %} +# value {{ i }} +# {% endfor }} +# ``` +# +# which will result: +# ``` +# value a +# value b +# value c +# ``` +# +# ##### 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 + ( + # shellcheck disable=SC2046 + export $(compgen -v) + j2 "$template_path" -f env \ + --filters "$YNH_J2_FILTERS_FILE_PATH" \ + -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` +# +# For this to work, template tags must be in uppercase and variables names must be in lowercase (for instance `__MY_var__` or `$myVar` would not be replaced as expected). +# +# 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 -a uniques_vars + mapfile -t 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//&/\\&} + 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 +# +# ```text +# .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) +# +# This helpers replaces 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 works line by line, and is not made to replace a type by another. +# +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 + formated="false" + # Boolean + true_equiv="^(true|True|TRUE|on|yes|t|1)$" + false_equiv="^(false|False|FALSE|off|no|f|0)$" + if [[ "$value" =~ $true_equiv ]] || [[ "$value" =~ $false_equiv ]]; then + _ynh_format_boolean() { + if [[ "$expression" =~ ^$1|$2$ ]] && [[ "$formated" == "false" ]]; then + if [[ "$value" =~ $true_equiv ]]; then + value="$1" + formated="true" + elif [[ "$value" =~ $false_equiv ]]; then + value="$2" + formated="true" + fi + fi + } + _ynh_format_boolean true false + _ynh_format_boolean True False + _ynh_format_boolean TRUE FALSE + _ynh_format_boolean on off + _ynh_format_boolean yes no + _ynh_format_boolean t f + _ynh_format_boolean 1 0 + fi + + # Number + if [[ "$formated" == "false" ]] && [[ "$value" =~ ^[+-]?[0-9]+([.][0-9]+)?$ ]]; then + formated="true" + fi + + # String + if [[ "$formated" == "false" ]] && [[ "$value" == *"'"* || "$value" == *'"'* || "$value" =~ $comments || "$ext" =~ ^php|py|json|js$ ]]; then + value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"' + formated="true" + 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/vendor b/helpers/helpers.v2.1.d/vendor new file mode 120000 index 0000000..9c39cc9 --- /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 0000000..e2614c8 --- /dev/null +++ b/helpers/helpers.v2.d @@ -0,0 +1 @@ +helpers.v1.d \ No newline at end of file diff --git a/helpers/vendor/docker-image-extract/LICENSE b/helpers/vendor/docker-image-extract/LICENSE new file mode 100644 index 0000000..986360f --- /dev/null +++ b/helpers/vendor/docker-image-extract/LICENSE @@ -0,0 +1,19 @@ +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"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/helpers/vendor/docker-image-extract/README.md b/helpers/vendor/docker-image-extract/README.md new file mode 100644 index 0000000..4c4fa30 --- /dev/null +++ b/helpers/vendor/docker-image-extract/README.md @@ -0,0 +1 @@ +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 new file mode 100755 index 0000000..abb2189 --- /dev/null +++ b/helpers/vendor/docker-image-extract/docker-image-extract @@ -0,0 +1,288 @@ +#!/usr/bin/env bash + +# This script pulls and extracts all files from an image in Docker Hub. +# +# 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"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +PLATFORM_DEFAULT="linux/amd64" +PLATFORM="${PLATFORM_DEFAULT}" +OUT_DIR="./output" + +usage() { + echo "This script pulls and extracts all files from an image in Docker Hub." + echo + echo "$0 [OPTIONS...] IMAGE[:REF]" + echo + echo "IMAGE can be a community user image (like 'some-user/some-image') or a" + echo "Docker official image (like 'hello-world', which contains no '/')." + echo + echo "REF is either a tag name or a full SHA-256 image digest (with a 'sha256:' prefix)." + echo "The default ref is the 'latest' tag." + echo + echo "Options:" + echo + echo " -p PLATFORM Pull image for the specified platform (default: ${PLATFORM})" + echo " For a given image on Docker Hub, the 'Tags' tab lists the" + echo " platforms supported for that image." + echo " -o OUT_DIR Extract image to the specified output dir (default: ${OUT_DIR})" + echo " -h Show help with usage examples" +} + +usage_detailed() { + usage + echo + echo "Examples:" + echo + echo "# Pull and extract all files in the 'hello-world' image tagged 'latest'." + echo "\$ $0 hello-world:latest" + echo + echo "# Same as above; ref defaults to the 'latest' tag." + echo "\$ $0 hello-world" + echo + echo "# Pull the 'hello-world' image for the 'linux/arm64/v8' platform." + echo "\$ $0 -p linux/arm64/v8 hello-world" + echo + echo "# Pull an image by digest." + echo "\$ $0 hello-world:sha256:90659bf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cbc042" +} + +if [ $# -eq 0 ]; then + usage_detailed + exit 0 +fi + +while getopts ':ho:p:' opt; do + case $opt in + o) + OUT_DIR="${OPTARG}" + ;; + p) + PLATFORM="${OPTARG}" + ;; + h) + usage_detailed + exit 0 + ;; + \?) + echo "ERROR: Invalid option '-$OPTARG'." + echo + usage + exit 1 + ;; + \:) echo "ERROR: Argument required for option '-$OPTARG'." + echo + usage + exit 1 + ;; + esac +done +shift $(($OPTIND - 1)) + +if [ $# -eq 0 ]; then + echo "ERROR: Image to pull must be specified." + echo + usage + exit 1 +fi + +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 +} + +have_wget() { + command -v wget >/dev/null +} + +if ! have_curl && ! have_wget; then + echo "This script requires either curl or wget." + exit 1 +fi + +image_spec="$1" +image="${image_spec%%:*}" +if [ "${image#*/}" = "${image}" ]; then + # Docker official images are in the 'library' namespace. + image="library/${image}" +fi +ref="${image_spec#*:}" +if [ "${ref}" = "${image_spec}" ]; then + echo "Defaulting ref to tag 'latest'..." + ref=latest +fi + +# Split platform (OS/arch/variant) into separate variables. +# A platform specifier doesn't always include the `variant` component. +OLD_IFS="${IFS}" +IFS=/ read -r OS ARCH VARIANT <":"" (assumes key/val won't contain double quotes). + # The colon may have whitespace on either side. + grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]\+\"" | + # Extract just by deleting the last '"', and then greedily deleting + # everything up to '"'. + sed -e 's/"$//' -e 's/.*"//' +} + +# Fetch a URL to stdout. Up to two header arguments may be specified: +# +# fetch [name1: value1] [name2: value2] +# +fetch() { + if have_curl; then + if [ $# -eq 2 ]; then + set -- -H "$2" "$1" + elif [ $# -eq 3 ]; then + set -- -H "$2" -H "$3" "$1" + fi + curl -sSL "$@" + else + if [ $# -eq 2 ]; then + set -- --header "$2" "$1" + elif [ $# -eq 3 ]; then + set -- --header "$2" --header "$3" "$1" + fi + wget -qO- "$@" + fi +} + +# https://docs.docker.com/docker-hub/api/latest/#tag/repositories +manifest_list_url="https://hub.docker.com/v2/repositories/${image}/tags/${ref}" + +# If 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 '}'. + # 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. + # "architecture":"arm64","features":"","variant":"v8","digest":"sha256:054c85801c4cb41511b176eb0bf13a2c4bbd41611ddd70594ec3315e88813524","os":"linux","os_features":"","os_version":null,"size":828724,"status":"active","last_pulled":"2022-09-02T22:46:48.240632Z","last_pushed":"2022-09-02T00:42:45.69226Z" + # The image details are interspersed with lines of stray punctuation, + # so grep for an arbitrary string that must be in these lines. + grep architecture | + # Search for an image that matches the platform. + while read -r image; do + # Arch is probably most likely to be unique, so check that first. + arch="$(echo ${image} | extract 'architecture')" + if [ "${arch}" != "${ARCH}" ]; then continue; fi + + os="$(echo ${image} | extract 'os')" + if [ "${os}" != "${OS}" ]; then continue; fi + + variant="$(echo ${image} | extract 'variant')" + if [ "${variant}" = "${VARIANT}" ]; then + echo ${image} | extract 'digest' + break + fi + done) + + 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 +api_token_url="https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" + +# https://github.com/docker/distribution/blob/master/docs/spec/api.md#pulling-an-image-manifest +manifest_url="https://registry-1.docker.io/v2/${image}/manifests/${digest}" + +# https://github.com/docker/distribution/blob/master/docs/spec/api.md#pulling-a-layer +blobs_base_url="https://registry-1.docker.io/v2/${image}/blobs" + +echo "Getting API token..." +token=$(fetch "${api_token_url}" | extract 'token') +auth_header="Authorization: Bearer $token" + +# 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}" "${accept_header}" | + # Extract `digest` values only after the `layers` section appears. + sed -n '/"layers":/,$ p' | + extract 'digest') + +if [ -z "${layers}" ]; then + echo "No layers returned. Verify that the image and ref are valid." + exit 1 +fi + +mkdir -p "${OUT_DIR}" + +for layer in $layers; do + hash="${layer#sha256:}" + echo "Fetching and extracting layer ${hash}..." + fetch "${blobs_base_url}/${layer}" "${auth_header}" | gzip -d | tar -C "${OUT_DIR}" -xf - + # Ref: https://github.com/moby/moby/blob/master/image/spec/v1.2.md#creating-an-image-filesystem-changeset + # https://github.com/moby/moby/blob/master/pkg/archive/whiteouts.go + # Search for "whiteout" files to indicate files deleted in this layer. + OLD_IFS="${IFS}" + find "${OUT_DIR}" -name '.wh.*' | while IFS= read -r f; do + dir="${f%/*}" + wh_file="${f##*/}" + file="${wh_file#.wh.}" + # Delete both the whiteout file and the whited-out file. + rm -rf "${dir}/${wh_file}" "${dir}/${file}" + done + IFS="${OLD_IFS}" +done + +echo "Image contents extracted into ${OUT_DIR}." diff --git a/helpers/vendor/n/LICENSE b/helpers/vendor/n/LICENSE new file mode 100644 index 0000000..8e04e84 --- /dev/null +++ b/helpers/vendor/n/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/helpers/vendor/n/README.md b/helpers/vendor/n/README.md new file mode 100644 index 0000000..9a29a39 --- /dev/null +++ b/helpers/vendor/n/README.md @@ -0,0 +1 @@ +This is taken from https://github.com/tj/n/ diff --git a/helpers/vendor/n/n b/helpers/vendor/n/n new file mode 100755 index 0000000..6b22759 --- /dev/null +++ b/helpers/vendor/n/n @@ -0,0 +1,1753 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2155 +# Disabled "Declare and assign separately to avoid masking return values": https://github.com/koalaman/shellcheck/wiki/SC2155 + +# +# log +# + +log() { + printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" +} + +# +# verbose_log +# Can suppress with --quiet. +# Like log but to stderr rather than stdout, so can also be used from "display" routines. +# + +verbose_log() { + if [[ "${SHOW_VERBOSE_LOG}" == "true" ]]; then + >&2 printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}\n" "$1" "$2" + fi +} + +# +# Exit with the given +# + +abort() { + >&2 printf "\n ${SGR_RED}Error: %s${SGR_RESET}\n\n" "$*" && exit 1 +} + +# +# Synopsis: trace message ... +# Debugging output to stderr, not used in production code. +# + +function trace() { + >&2 printf "trace: %s\n" "$*" +} + +# +# Synopsis: echo_red message ... +# Highlight message in colour (on stdout). +# + +function echo_red() { + printf "${SGR_RED}%s${SGR_RESET}\n" "$*" +} + +# +# Synopsis: n_grep +# grep wrapper to ensure consistent grep options and circumvent aliases. +# + +function n_grep() { + GREP_OPTIONS='' command grep "$@" +} + +# +# Setup and state +# + +VERSION="10.2.0" + +N_PREFIX="${N_PREFIX-/usr/local}" +N_PREFIX=${N_PREFIX%/} +readonly N_PREFIX + +N_CACHE_PREFIX="${N_CACHE_PREFIX-${N_PREFIX}}" +N_CACHE_PREFIX=${N_CACHE_PREFIX%/} +CACHE_DIR="${N_CACHE_PREFIX}/n/versions" +readonly N_CACHE_PREFIX CACHE_DIR + +N_NODE_MIRROR=${N_NODE_MIRROR:-${NODE_MIRROR:-https://nodejs.org/dist}} +N_NODE_MIRROR=${N_NODE_MIRROR%/} +readonly N_NODE_MIRROR + +N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR:-https://nodejs.org/download} +N_NODE_DOWNLOAD_MIRROR=${N_NODE_DOWNLOAD_MIRROR%/} +readonly N_NODE_DOWNLOAD_MIRROR + +# Using xz instead of gzip is enabled by default, if xz compatibility checks pass. +# User may set N_USE_XZ to 0 to disable, or set to anything else to enable. +# May also be overridden by command line flags. + +# Normalise external values to true/false +if [[ "${N_USE_XZ}" = "0" ]]; then + N_USE_XZ="false" +elif [[ -n "${N_USE_XZ+defined}" ]]; then + N_USE_XZ="true" +fi +# Not setting to readonly. Overriden by CLI flags, and update_xz_settings_for_version. + +N_MAX_REMOTE_MATCHES=${N_MAX_REMOTE_MATCHES:-20} +# modified by update_mirror_settings_for_version +g_mirror_url=${N_NODE_MIRROR} +g_mirror_folder_name="node" + +# Options for curl and wget. +# Defining commands in variables is fraught (https://mywiki.wooledge.org/BashFAQ/050) +# but we can follow the simple case and store arguments in an array. + +GET_SHOWS_PROGRESS="false" +# --location to follow redirects +# --fail to avoid happily downloading error page from web server for 404 et al +# --show-error to show why failed (on stderr) +CURL_OPTIONS=( "--location" "--fail" "--show-error" ) +if [[ -t 1 ]]; then + CURL_OPTIONS+=( "--progress-bar" ) + command -v curl &> /dev/null && GET_SHOWS_PROGRESS="true" +else + CURL_OPTIONS+=( "--silent" ) +fi +WGET_OPTIONS=( "-q" "-O-" ) + +# Legacy support using unprefixed env. No longer documented in README. +if [ -n "$HTTP_USER" ];then + if [ -z "$HTTP_PASSWORD" ]; then + abort "Must specify HTTP_PASSWORD when supplying HTTP_USER" + fi + CURL_OPTIONS+=( "-u $HTTP_USER:$HTTP_PASSWORD" ) + WGET_OPTIONS+=( "--http-password=$HTTP_PASSWORD" + "--http-user=$HTTP_USER" ) +elif [ -n "$HTTP_PASSWORD" ]; then + abort "Must specify HTTP_USER when supplying HTTP_PASSWORD" +fi + +# Set by set_active_node +g_active_node= + +# set by various lookups to allow mixed logging and return value from function, especially for engine and node +g_target_node= + +DOWNLOAD=false # set to opt-out of activate (install), and opt-in to download (run, exec) +CLEANUP=false # remove cached download after install +ARCH="${N_ARCH-}" +SHOW_VERBOSE_LOG="true" +OFFLINE=false + +# ANSI escape codes +# https://en.wikipedia.org/wiki/ANSI_escape_code +# https://no-color.org +# https://bixense.com/clicolors + +USE_COLOR="true" +if [[ -n "${CLICOLOR_FORCE+defined}" && "${CLICOLOR_FORCE}" != "0" ]]; then + USE_COLOR="true" +elif [[ -n "${NO_COLOR+defined}" || "${CLICOLOR}" = "0" || ! -t 1 ]]; then + USE_COLOR="false" +fi +readonly USE_COLOR +# Select Graphic Rendition codes +if [[ "${USE_COLOR}" = "true" ]]; then + # KISS and use codes rather than tput, avoid dealing with missing tput or TERM. + readonly SGR_RESET="\033[0m" + readonly SGR_FAINT="\033[2m" + readonly SGR_RED="\033[31m" + readonly SGR_CYAN="\033[36m" +else + readonly SGR_RESET= + readonly SGR_FAINT= + readonly SGR_RED= + readonly SGR_CYAN= +fi + +# +# set_arch to override $(uname -a) +# + +set_arch() { + if test -n "$1"; then + ARCH="$1" + else + abort "missing -a|--arch value" + fi +} + +# +# Synopsis: set_insecure +# Globals modified: +# - CURL_OPTIONS +# - WGET_OPTIONS +# + +function set_insecure() { + CURL_OPTIONS+=( "--insecure" ) + WGET_OPTIONS+=( "--no-check-certificate" ) +} + +# +# Synposis: display_major_version numeric-version +# +display_major_version() { + local version=$1 + version="${version#v}" + version="${version%%.*}" + echo "${version}" +} + +display_masked_url() { + echo "$1" | sed -r 's/(https?:\/\/[^:]+):([^@]+)@/\1:****@/' +} + +# +# Synopsis: update_mirror_settings_for_version version +# e.g. means using download mirror and folder is nightly +# Globals modified: +# - g_mirror_url +# - g_mirror_folder_name +# + +function update_mirror_settings_for_version() { + if is_download_folder "$1" ; then + g_mirror_folder_name="$1" + g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" + elif is_download_version "$1"; then + [[ "$1" =~ ^([^/]+)/(.*) ]] + local remote_folder="${BASH_REMATCH[1]}" + g_mirror_folder_name="${remote_folder}" + g_mirror_url="${N_NODE_DOWNLOAD_MIRROR}/${g_mirror_folder_name}" + fi +} + +# +# Synopsis: update_xz_settings_for_version numeric-version +# Globals modified: +# - N_USE_XZ +# + +function update_xz_settings_for_version() { + # tarballs in xz format were available in later version of iojs, but KISS and only use xz from v4. + if [[ "${N_USE_XZ}" = "true" ]]; then + local major_version="$(display_major_version "$1")" + if [[ "${major_version}" -lt 4 ]]; then + N_USE_XZ="false" + fi + fi +} + +# +# Synopsis: update_arch_settings_for_version numeric-version +# Globals modified: +# - ARCH +# + +function update_arch_settings_for_version() { + local tarball_platform="$(display_tarball_platform)" + if [[ -z "${ARCH}" && "${tarball_platform}" = "darwin-arm64" ]]; then + # First native builds were for v16, but can use x64 in rosetta for older versions. + local major_version="$(display_major_version "$1")" + if [[ "${major_version}" -lt 16 ]]; then + ARCH=x64 + fi + fi +} + +# +# Synopsis: is_lts_codename version +# + +function is_lts_codename() { + # https://github.com/nodejs/Release/blob/master/CODENAMES.md + # e.g. argon, Boron + [[ "$1" =~ ^([Aa]rgon|[Bb]oron|[Cc]arbon|[Dd]ubnium|[Ee]rbium|[Ff]ermium|[Gg]allium|[Hh]ydrogen|[Ii]ron|[Jj]od|[Kk]rypton|[Ll]ithium)$ ]] +} + +# +# Synopsis: is_download_folder version +# + +function is_download_folder() { + # e.g. nightly + [[ "$1" =~ ^(next-nightly|nightly|rc|release|test|v8-canary)$ ]] +} + +# +# Synopsis: is_download_version version +# + +function is_download_version() { + # e.g. nightly/, nightly/latest, nightly/v11 + if [[ "$1" =~ ^([^/]+)/(.*) ]]; then + local remote_folder="${BASH_REMATCH[1]}" + is_download_folder "${remote_folder}" + return + fi + return 2 +} + +# +# Synopsis: is_numeric_version version +# + +function is_numeric_version() { + # e.g. 6, v7.1, 8.11.3 + [[ "$1" =~ ^[v]{0,1}[0-9]+(\.[0-9]+){0,2}$ ]] +} + +# +# Synopsis: is_exact_numeric_version version +# + +function is_exact_numeric_version() { + # e.g. 6, v7.1, 8.11.3 + [[ "$1" =~ ^[v]{0,1}[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +# +# Synopsis: is_node_support_version version +# Reference: https://github.com/nodejs/package-maintenance/issues/236#issue-474783582 +# + +function is_node_support_version() { + [[ "$1" =~ ^(active|lts_active|lts_latest|lts|current|supported)$ ]] +} + +# +# Synopsis: display_latest_node_support_alias version +# Map aliases onto existing n aliases, current and lts +# + +function display_latest_node_support_alias() { + case "$1" in + "active") printf "current" ;; + "lts_active") printf "lts" ;; + "lts_latest") printf "lts" ;; + "lts") printf "lts" ;; + "current") printf "current" ;; + "supported") printf "current" ;; + *) printf "unexpected-version" + esac +} + +# +# Functions used when showing versions installed +# + +enter_fullscreen() { + # Set cursor to be invisible + tput civis 2> /dev/null + # Save screen contents + tput smcup 2> /dev/null + stty -echo +} + +leave_fullscreen() { + # Set cursor to normal + tput cnorm 2> /dev/null + # Restore screen contents + tput rmcup 2> /dev/null + stty echo +} + +handle_sigint() { + leave_fullscreen + S="$?" + kill 0 + exit $S +} + +handle_sigtstp() { + leave_fullscreen + kill -s SIGSTOP $$ +} + +# +# Output usage information. +# + +display_help() { + cat <<-EOF + +Usage: n [options] [COMMAND] [args] + +Commands: + + n Display downloaded Node.js versions and install selection + n latest Install the latest Node.js release (downloading if necessary) + n lts Install the latest LTS Node.js release (downloading if necessary) + n Install Node.js (downloading if necessary) + n install Install Node.js (downloading if necessary) + n run [args ...] Execute downloaded Node.js with [args ...] + n which Output path for downloaded node + n exec [args...] Execute command with modified PATH, so downloaded node and npm first + n rm Remove the given downloaded version(s) + n prune Remove all downloaded versions except the installed version + n --latest Output the latest Node.js version available + n --lts Output the latest LTS Node.js version available + n ls Output downloaded versions + n ls-remote [version] Output matching versions available for download + n uninstall Remove the installed Node.js + n download Download Node.js into cache + +Options: + + -V, --version Output version of n + -h, --help Display help information + -p, --preserve Preserve npm and npx during install of Node.js + -q, --quiet Disable curl output. Disable log messages processing "auto" and "engine" labels. + -d, --download Download if necessary. Used with run/exec/which. + --cleanup Remove cached version after install + -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. + +Aliases: + + install: i + latest: current + ls: list + lsr: ls-remote + lts: stable + rm: - + run: use, as + which: bin + +Versions: + + Numeric version numbers can be complete or incomplete, with an optional leading 'v'. + Versions can also be specified by label, or codename, + and other downloadable releases by / + + 4.9.1, 8, v6.1 Numeric versions + lts Newest Long Term Support official release + latest, current Newest official release + auto Read version from file: .n-node-version, .node-version, .nvmrc, or package.json + engine Read version from package.json + boron, carbon Codenames for release streams + lts_latest Node.js support aliases + + and nightly, rc/10 et al + +EOF +} + +err_no_installed_print_help() { + display_help + abort "no downloaded versions yet, see above help for commands" +} + +# +# Synopsis: next_version_installed selected_version +# Output version after selected (which may be blank under some circumstances). +# + +function next_version_installed() { + display_cache_versions | n_grep "$1" -A 1 | tail -n 1 +} + +# +# Synopsis: prev_version_installed selected_version +# Output version before selected (which may be blank under some circumstances). +# + +function prev_version_installed() { + display_cache_versions | n_grep "$1" -B 1 | head -n 1 +} + +# +# Output n version. +# + +display_n_version() { + echo "$VERSION" && exit 0 +} + +# +# Synopsis: set_active_node +# Checks cached downloads for a binary matching the active node. +# Globals modified: +# - g_active_node +# + +function set_active_node() { + g_active_node= + local node_path="$(command -v node)" + if [[ -x "${node_path}" ]]; then + local installed_version=$(node --version) + installed_version=${installed_version#v} + for dir in "${CACHE_DIR}"/*/ ; do + local folder_name="${dir%/}" + folder_name="${folder_name##*/}" + if diff &> /dev/null \ + "${CACHE_DIR}/${folder_name}/${installed_version}/bin/node" \ + "${node_path}" ; then + g_active_node="${folder_name}/${installed_version}" + break + fi + done + fi +} + +# +# Display sorted versions directories paths. +# + +display_versions_paths() { + find "$CACHE_DIR" -maxdepth 2 -type d \ + | sed 's|'"$CACHE_DIR"'/||g' \ + | n_grep -E "/[0-9]+\.[0-9]+\.[0-9]+" \ + | sed 's|/|.|' \ + | sort -k 1,1 -k 2,2n -k 3,3n -k 4,4n -t . \ + | sed 's|\.|/|' +} + +# +# Display installed versions with +# + +display_versions_with_selected() { + local selected="$1" + echo + for version in $(display_versions_paths); do + if test "$version" = "$selected"; then + printf " ${SGR_CYAN}ο${SGR_RESET} %s\n" "$version" + else + printf " ${SGR_FAINT}%s${SGR_RESET}\n" "$version" + fi + done + echo + printf "Use up/down arrow keys to select a version, return key to install, d to delete, q to quit" +} + +# +# Synopsis: display_cache_versions +# + +function display_cache_versions() { + for folder_and_version in $(display_versions_paths); do + echo "${folder_and_version}" + done +} + +# +# Display current node --version and others installed. +# + +menu_select_cache_versions() { + enter_fullscreen + set_active_node + local selected="${g_active_node}" + + clear + display_versions_with_selected "${selected}" + + trap handle_sigint INT + trap handle_sigtstp SIGTSTP + + ESCAPE_SEQ=$'\033' + UP=$'A' + DOWN=$'B' + CTRL_P=$'\020' + CTRL_N=$'\016' + + while true; do + read -rsn 1 key + case "$key" in + "$ESCAPE_SEQ") + # Handle ESC sequences followed by other characters, i.e. arrow keys + read -rsn 1 -t 1 tmp + # See "[" if terminal in normal mode, and "0" in application mode + if [[ "$tmp" == "[" || "$tmp" == "O" ]]; then + read -rsn 1 -t 1 arrow + case "$arrow" in + "$UP") + clear + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + "$DOWN") + clear + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + esac + fi + ;; + "d") + if [[ -n "${selected}" ]]; then + clear + # Note: prev/next is constrained to min/max + local after_delete_selection="$(next_version_installed "${selected}")" + if [[ "${after_delete_selection}" == "${selected}" ]]; then + after_delete_selection="$(prev_version_installed "${selected}")" + fi + remove_versions "${selected}" + + if [[ "${after_delete_selection}" == "${selected}" ]]; then + clear + leave_fullscreen + echo "All downloaded versions have been deleted from cache." + exit + fi + + selected="${after_delete_selection}" + display_versions_with_selected "${selected}" + fi + ;; + # Vim or Emacs 'up' key + "k"|"$CTRL_P") + clear + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + # Vim or Emacs 'down' key + "j"|"$CTRL_N") + clear + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" + ;; + "q") + clear + leave_fullscreen + exit + ;; + "") + # enter key returns empty string + leave_fullscreen + [[ -n "${selected}" ]] && activate "${selected}" + exit + ;; + esac + done +} + +# +# Move up a line and erase. +# + +erase_line() { + printf "\033[1A\033[2K" +} + +# +# Disable PaX mprotect for +# + +disable_pax_mprotect() { + test -z "$1" && abort "binary required" + local binary="$1" + + # try to disable mprotect via XATTR_PAX header + local PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl-ng 2>&1)" + local PAXCTL_ERROR=1 + if [ -x "$PAXCTL" ]; then + $PAXCTL -l && $PAXCTL -m "$binary" >/dev/null 2>&1 + PAXCTL_ERROR="$?" + fi + + # try to disable mprotect via PT_PAX header + if [ "$PAXCTL_ERROR" != 0 ]; then + PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl 2>&1)" + if [ -x "$PAXCTL" ]; then + $PAXCTL -Cm "$binary" >/dev/null 2>&1 + fi + fi +} + +# +# clean_copy_folder +# + +clean_copy_folder() { + local source="$1" + local target="$2" + if [[ -d "${source}" ]]; then + rm -rf "${target}" + cp -fR "${source}" "${target}" + fi +} + +# +# Activate +# + +activate() { + local version="$1" + local dir="$CACHE_DIR/$version" + local original_node="$(command -v node)" + local installed_node="${N_PREFIX}/bin/node" + log "copying" "$version" + + + # Ideally we would just copy from cache to N_PREFIX, but there are some complications + # - various linux versions use symlinks for folders in /usr/local and also error when copy folder onto symlink + # - we have used cp for years, so keep using it for backwards compatibility (instead of say rsync) + # - we allow preserving npm + # - we want to be somewhat robust to changes in tarball contents, so use find instead of hard-code expected subfolders + # + # This code was purist and concise for a long time. + # Now twice as much code, but using same code path for all uses, and supporting more setups. + + # Copy lib before bin so symlink targets exist. + # lib + mkdir -p "$N_PREFIX/lib" + # Copy everything except node_modules. + find "$dir/lib" -mindepth 1 -maxdepth 1 \! -name node_modules -exec cp -fR "{}" "$N_PREFIX/lib" \; + if [[ -z "${N_PRESERVE_NPM}" ]]; then + mkdir -p "$N_PREFIX/lib/node_modules" + # Copy just npm, skipping possible added global modules after download. Clean copy to avoid version change problems. + clean_copy_folder "$dir/lib/node_modules/npm" "$N_PREFIX/lib/node_modules/npm" + fi + # Takes same steps for corepack (experimental in node 16.9.0) as for npm, to avoid version problems. + if [[ -e "$dir/lib/node_modules/corepack" && -z "${N_PRESERVE_COREPACK}" ]]; then + mkdir -p "$N_PREFIX/lib/node_modules" + clean_copy_folder "$dir/lib/node_modules/corepack" "$N_PREFIX/lib/node_modules/corepack" + fi + + # bin + mkdir -p "$N_PREFIX/bin" + # Remove old node to avoid potential problems with firewall getting confused on Darwin by overwrite. + rm -f "$N_PREFIX/bin/node" + # Copy bin items by hand, in case user has installed global npm modules into cache. + cp -f "$dir/bin/node" "$N_PREFIX/bin" + [[ -e "$dir/bin/node-waf" ]] && cp -f "$dir/bin/node-waf" "$N_PREFIX/bin" # v0.8.x + if [[ -z "${N_PRESERVE_COREPACK}" ]]; then + [[ -e "$dir/bin/corepack" ]] && cp -fR "$dir/bin/corepack" "$N_PREFIX/bin" # from 16.9.0 + fi + if [[ -z "${N_PRESERVE_NPM}" ]]; then + [[ -e "$dir/bin/npm" ]] && cp -fR "$dir/bin/npm" "$N_PREFIX/bin" + [[ -e "$dir/bin/npx" ]] && cp -fR "$dir/bin/npx" "$N_PREFIX/bin" + fi + + # include + mkdir -p "$N_PREFIX/include" + find "$dir/include" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/include" \; + + # share + mkdir -p "$N_PREFIX/share" + # Copy everything except man, at it is a symlink on some Linux (e.g. archlinux). + find "$dir/share" -mindepth 1 -maxdepth 1 \! -name man -exec cp -fR "{}" "$N_PREFIX/share" \; + mkdir -p "$N_PREFIX/share/man" + find "$dir/share/man" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/share/man" \; + + disable_pax_mprotect "${installed_node}" + + local active_node="$(command -v node)" + if [[ -e "${active_node}" && -e "${installed_node}" && "${active_node}" != "${installed_node}" ]]; then + # Installed and active are different which might be a PATH problem. List both to give user some clues. + log "installed" "$("${installed_node}" --version) to ${installed_node}" + log "active" "$("${active_node}" --version) at ${active_node}" + else + local npm_version_str="" + local installed_npm="${N_PREFIX}/bin/npm" + local active_npm="$(command -v npm)" + if [[ -z "${N_PRESERVE_NPM}" && -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" = "${installed_npm}" ]]; then + npm_version_str=" (with npm $(npm --version))" + fi + + log "installed" "$("${installed_node}" --version)${npm_version_str}" + + # Extra tips for changed location. + if [[ -e "${active_node}" && -e "${original_node}" && "${active_node}" != "${original_node}" ]]; then + printf '\nNote: the node command changed location and the old location may be remembered in your current shell.\n' + log old "${original_node}" + log new "${active_node}" + printf 'If "node --version" shows the old version then start a new shell, or reset the location hash with:\nhash -r (for bash, zsh, ash, dash, and ksh)\nrehash (for csh and tcsh)\n' + fi + fi + + if [[ "$CLEANUP" == "true" ]]; then + log "cleanup" "removing cached $version" + remove_versions "$version" + fi + +} + +# +# Install +# + +install() { + [[ -z "$1" ]] && abort "version required" + local version + get_latest_resolved_version "$1" || return 2 + version="${g_target_node}" + [[ -n "${version}" ]] || abort "no version found for '$1'" + update_mirror_settings_for_version "$1" + update_xz_settings_for_version "${version}" + update_arch_settings_for_version "${version}" + + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + + # Note: decompression flags ignored with default Darwin tar which autodetects. + if test "$N_USE_XZ" = "true"; then + local tarflag="-Jx" + else + local tarflag="-zx" + fi + + if test -d "$dir"; then + if [[ ! -e "$dir/n.lock" ]] ; then + if [[ "$DOWNLOAD" == "false" ]] ; then + activate "${g_mirror_folder_name}/${version}" + else + log downloaded "${g_mirror_folder_name}/${version} already in cache" + fi + exit + fi + fi + if [[ "$OFFLINE" == "true" ]]; then + abort "version unavailable offline" + fi + + if [[ "$DOWNLOAD" == "false" ]]; then + log installing "${g_mirror_folder_name}-v$version" + else + log download "${g_mirror_folder_name}-v$version" + fi + + local url="$(tarball_url "$version")" + is_ok "${url}" || abort "download preflight failed for '$version' ($(display_masked_url "${url}"))" + + log mkdir "$dir" + mkdir -p "$dir" || abort "sudo required (or change ownership, or define N_PREFIX)" + touch "$dir/n.lock" + + cd "${dir}" || abort "Failed to cd to ${dir}" + + log fetch "$(display_masked_url "${url}")" + do_get "${url}" | tar "$tarflag" --strip-components=1 --no-same-owner -f - + pipe_results=( "${PIPESTATUS[@]}" ) + if [[ "${pipe_results[0]}" -ne 0 ]]; then + abort "failed to download archive for $version" + fi + if [[ "${pipe_results[1]}" -ne 0 ]]; then + abort "failed to extract archive for $version" + fi + [ "$GET_SHOWS_PROGRESS" = "true" ] && erase_line + rm -f "$dir/n.lock" + + disable_pax_mprotect bin/node + + if [[ "$DOWNLOAD" == "false" ]]; then + activate "${g_mirror_folder_name}/$version" + fi +} + +# +# Be more silent. +# + +set_quiet() { + SHOW_VERBOSE_LOG="false" + command -v curl > /dev/null && CURL_OPTIONS+=( "--silent" ) && GET_SHOWS_PROGRESS="false" +} + +# +# Synopsis: do_get [option...] url +# Call curl or wget with combination of global and passed options. +# + +function do_get() { + if command -v curl &> /dev/null; then + curl "${CURL_OPTIONS[@]}" "$@" + elif command -v wget &> /dev/null; then + wget "${WGET_OPTIONS[@]}" "$@" + else + abort "curl or wget command required" + fi +} + +# +# Synopsis: do_get_index [option...] url +# Call curl or wget with combination of global and passed options, +# with options tweaked to be more suitable for getting index. +# + +function do_get_index() { + if command -v curl &> /dev/null; then + # --silent to suppress progress et al + curl --silent "${CURL_OPTIONS[@]}" "$@" + elif command -v wget &> /dev/null; then + wget "${WGET_OPTIONS[@]}" "$@" + else + abort "curl or wget command required" + fi +} + +# +# Synopsis: remove_versions version ... +# + +function remove_versions() { + [[ -z "$1" ]] && abort "version(s) required" + while [[ $# -ne 0 ]]; do + local version + get_latest_resolved_version "$1" || break + version="${g_target_node}" + if [[ -n "${version}" ]]; then + update_mirror_settings_for_version "$1" + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + if [[ -s "${dir}" ]]; then + rm -rf "${dir}" + else + echo "$1 (${version}) not in downloads cache" + fi + else + echo "No version found for '$1'" + fi + shift + done +} + +# +# Synopsis: prune_cache +# + +function prune_cache() { + set_active_node + + for folder_and_version in $(display_versions_paths); do + if [[ "${folder_and_version}" != "${g_active_node}" ]]; then + echo "${folder_and_version}" + rm -rf "${CACHE_DIR:?}/${folder_and_version}" + fi + done +} + +# +# Synopsis: find_cached_version version +# Finds cache directory for resolved version. +# Globals modified: +# - g_cached_version + +function find_cached_version() { + [[ -z "$1" ]] && abort "version required" + local version + get_latest_resolved_version "$1" || exit 1 + version="${g_target_node}" + [[ -n "${version}" ]] || abort "no version found for '$1'" + + update_mirror_settings_for_version "$1" + g_cached_version="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + if [[ ! -d "${g_cached_version}" && "${DOWNLOAD}" == "true" ]]; then + (install "${version}") + fi + [[ -d "${g_cached_version}" ]] || abort "'$1' (${version}) not in downloads cache" +} + + +# +# Synopsis: display_bin_path_for_version version +# + +function display_bin_path_for_version() { + find_cached_version "$1" + echo "${g_cached_version}/bin/node" +} + +# +# Synopsis: run_with_version version [args...] +# Run the given of node with [args ..] +# + +function run_with_version() { + find_cached_version "$1" + shift # remove version from parameters + exec "${g_cached_version}/bin/node" "$@" +} + +# +# Synopsis: exec_with_version command [args...] +# Modify the path to include and execute command. +# + +function exec_with_version() { + find_cached_version "$1" + shift # remove version from parameters + PATH="${g_cached_version}/bin:$PATH" exec "$@" +} + +# +# Synopsis: is_ok url +# Check the HEAD response of . +# + +function is_ok() { + # Note: both curl and wget can follow redirects, as present on some mirrors (e.g. https://npm.taobao.org/mirrors/node). + # The output is complicated with redirects, so keep it simple and use command status rather than parse output. + if command -v curl &> /dev/null; then + do_get --silent --head "$1" > /dev/null || return 1 + else + do_get --spider "$1" > /dev/null || return 1 + fi +} + +# +# Synopsis: can_use_xz +# Test system to see if xz decompression is supported by tar. +# + +function can_use_xz() { + # Be conservative and only enable if xz is likely to work. Unfortunately we can't directly query tar itself. + # For research, see https://github.com/shadowspawn/nvh/issues/8 + local uname_s="$(uname -s)" + if [[ "${uname_s}" = "Linux" ]] && command -v xz &> /dev/null ; then + # tar on linux is likely to support xz if it is available as a command + return 0 + elif [[ "${uname_s}" = "Darwin" ]]; then + local macos_version="$(sw_vers -productVersion)" + local macos_major_version="$(echo "${macos_version}" | cut -d '.' -f 1)" + local macos_minor_version="$(echo "${macos_version}" | cut -d '.' -f 2)" + if [[ "${macos_major_version}" -gt 10 || "${macos_minor_version}" -gt 8 ]]; then + # tar on recent Darwin has xz support built-in + return 0 + fi + fi + return 2 # not supported +} + +# +# Synopsis: display_tarball_platform +# + +function display_tarball_platform() { + # https://en.wikipedia.org/wiki/Uname + + local os="unexpected_os" + local uname_a="$(uname -a)" + case "${uname_a}" in + Linux*) os="linux" ;; + Darwin*) os="darwin" ;; + SunOS*) os="sunos" ;; + AIX*) os="aix" ;; + CYGWIN*) >&2 echo_red "Cygwin is not supported by n" ;; + MINGW*) >&2 echo_red "Git BASH (MSYS) is not supported by n" ;; + esac + + # architecture might already be known from (priority order): + # * --arch flag on the command line, + # * otherwise, from $N_ARCH + # * otherwise from version specific adjustment (if applicable, see update_arch_settings_for_version) + local arch="$ARCH" + if [[ -z "$arch" ]]; then + arch="unexpected_arch" + local uname_m="$(uname -m)" + case "${uname_m}" in + x86_64) arch=x64 ;; + i386 | i686) arch="x86" ;; + aarch64) arch=arm64 ;; + armv8l) arch=arm64 ;; # armv8l probably supports arm64, and there is no specific armv8l build so give it a go + *) + # e.g. armv6l, armv7l, arm64 + arch="${uname_m}" + ;; + esac + fi + + echo "${os}-${arch}" +} + +# +# Synopsis: display_compatible_file_field +# display for current platform, as per field in index.tab, which is different than actual download +# + +function display_compatible_file_field { + local compatible_file_field="$(display_tarball_platform)" + if [[ -z "${ARCH}" && "${compatible_file_field}" = "darwin-arm64" ]]; then + # Look for arm64 for native but also x64 for older versions which can run in rosetta. + # (Downside is will get an install error if install version above 16 with x64 and not arm64.) + compatible_file_field="osx-arm64-tar|osx-x64-tar" + elif [[ "${compatible_file_field}" =~ darwin-(.*) ]]; then + compatible_file_field="osx-${BASH_REMATCH[1]}-tar" + fi + echo "${compatible_file_field}" +} + +# +# Synopsis: tarball_url version +# + +function tarball_url() { + local version="$1" + local ext=gz + [ "$N_USE_XZ" = "true" ] && ext="xz" + echo "${g_mirror_url}/v${version}/node-v${version}-$(display_tarball_platform).tar.${ext}" +} + +# +# Synopsis: get_file_node_version filename +# Sets g_target_node +# + +function get_file_node_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + # read returns a non-zero status but does still work if there is no line ending + local version + <"${filepath}" read -r version + # trim possible trailing \d from a Windows created file + version="${version%%[[:space:]]}" + verbose_log "read" "${version}" + g_target_node="${version}" +} + +# +# Synopsis: get_package_engine_version\ +# Sets g_target_node +# + +function get_package_engine_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + local range + if command -v jq &> /dev/null; then + range="$(jq -r '.engines.node // ""' < "${filepath}")" + elif command -v node &> /dev/null; then + range="$(node -e "package = require('${filepath}'); if (package && package.engines && package.engines.node) console.log(package.engines.node)")" + else + abort "either jq or an active version of node is required to read 'engines' from package.json" + fi + verbose_log "read" "${range}" + [[ -n "${range}" ]] || return 2 + if [[ "*" == "${range}" ]]; then + verbose_log "target" "current" + g_target_node="current" + return + fi + + local version + if [[ "${range}" =~ ^([>~^=]|\>\=)?v?([0-9]+(\.[0-9]+){0,2})(.[xX*])?$ ]]; then + local operator="${BASH_REMATCH[1]}" + version="${BASH_REMATCH[2]}" + case "${operator}" in + '' | =) ;; + \> | \>=) version="current" ;; + \~) [[ "${version}" =~ ^([0-9]+\.[0-9]+)\.[0-9]+$ ]] && version="${BASH_REMATCH[1]}" ;; + ^) [[ "${version}" =~ ^([0-9]+) ]] && version="${BASH_REMATCH[1]}" ;; + esac + verbose_log "target" "${version}" + else + command -v npx &> /dev/null || abort "an active version of npx is required to use complex 'engine' ranges from package.json" + [[ "$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' ' ') + # Using semver@7 so works with older versions of node. + # shellcheck disable=SC2086 + version=$(npm_config_yes=true npx --quiet semver@7 -r "${range}" ${versions_one_line} | tail -n 1) + fi + g_target_node="${version}" +} + +# +# Synopsis: get_nvmrc_version +# Sets g_target_node +# + +function get_nvmrc_version() { + g_target_node= + local filepath="$1" + verbose_log "found" "${filepath}" + local version + <"${filepath}" read -r version + # remove trailing comment, after # + version="$(echo "${version}" | sed 's/[[:space:]]*#.*//')" + verbose_log "read" "${version}" + # Translate from nvm aliases + case "${version}" in + lts/\*) version="lts" ;; + lts/*) version="${version:4}" ;; + node) version="current" ;; + *) ;; + esac + g_target_node="${version}" +} + +# +# Synopsis: get_engine_version [error-message] +# Sets g_target_node +# + +function get_engine_version() { + g_target_node= + local error_message="${1-package.json not found}" + local parent + parent="${PWD}" + while [[ -n "${parent}" ]]; do + if [[ -e "${parent}/package.json" ]]; then + get_package_engine_version "${parent}/package.json" + else + parent=${parent%/*} + continue + fi + break + done + [[ -n "${parent}" ]] || abort "${error_message}" + [[ -n "${g_target_node}" ]] || abort "did not find supported version of node in 'engines' field of package.json" +} + +# +# Synopsis: get_auto_version +# Sets g_target_node +# + +function get_auto_version() { + g_target_node= + # Search for a version control file first + local parent + parent="${PWD}" + while [[ -n "${parent}" ]]; do + if [[ -e "${parent}/.n-node-version" ]]; then + get_file_node_version "${parent}/.n-node-version" + elif [[ -e "${parent}/.node-version" ]]; then + get_file_node_version "${parent}/.node-version" + elif [[ -e "${parent}/.nvmrc" ]]; then + get_nvmrc_version "${parent}/.nvmrc" + else + parent=${parent%/*} + continue + fi + break + done + # Fallback to package.json + [[ -n "${parent}" ]] || get_engine_version "no file found for auto version (.n-node-version, .node-version, .nvmrc, or package.json)" + [[ -n "${g_target_node}" ]] || abort "file found for auto did not contain target version of node" +} + +# +# Synopsis: get_latest_resolved_version version +# Sets g_target_node +# + +function get_latest_resolved_version() { + g_target_node= + local version=${1} + + # Transform some labels before processing further to allow fast-track for exact numeric versions. + if [[ "${version}" = "auto" ]]; then + get_auto_version || return 2 + version="${g_target_node}" + elif [[ "${version}" = "engine" ]]; then + get_engine_version || return 2 + version="${g_target_node}" + fi + + simple_version=${version#node/} # Only place supporting node/ [sic] + if is_exact_numeric_version "${simple_version}"; then + # Just numbers, already resolved, no need to lookup first. + simple_version="${simple_version#v}" + g_target_node="${simple_version}" + 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") + fi +} + +# +# Synopsis: display_remote_index +# index.tab reference: https://github.com/nodejs/nodejs-dist-indexer +# Index fields are: version date files npm v8 uv zlib openssl modules lts security +# KISS and just return fields we currently care about: version files lts +# + +display_remote_index() { + local index_url="${g_mirror_url}/index.tab" + # tail to remove header line + do_get_index "${index_url}" | tail -n +2 | cut -f 1,3,10 + if [[ "${PIPESTATUS[0]}" -ne 0 ]]; then + # Reminder: abort will only exit subshell, but consistent error display + abort "failed to download version index ($(display_masked_url "${index_url}"))" + fi +} + +# +# Synopsis: display_match_limit limit +# + +function display_match_limit(){ + if [[ "$1" -gt 1 && "$1" -lt 32000 ]]; then + echo "Listing remote... Displaying $1 matches (use --all to see all)." + fi +} + +# +# Synopsis: display_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 + get_auto_version || return 2 + version="${g_target_node}" + elif [[ "${version}" = "engine" ]]; then + 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 +# + +function display_remote_versions() { + local version="$1" + update_mirror_settings_for_version "${version}" + local match='.' + local match_count="${N_MAX_REMOTE_MATCHES}" + + # Transform some labels before processing further. + if is_node_support_version "${version}"; then + version="$(display_latest_node_support_alias "${version}")" + match_count=1 + elif [[ "${version}" = "auto" ]]; then + get_auto_version || return 2 + version="${g_target_node}" + elif [[ "${version}" = "engine" ]]; then + get_engine_version || return 2 + version="${g_target_node}" + fi + + if [[ -z "${version}" ]]; then + match='.' + elif [[ "${version}" = "lts" || "${version}" = "stable" ]]; then + match_count=1 + # Codename is last field, first one with a name is newest lts + match="${TAB_CHAR}[a-zA-Z]+\$" + elif [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match_count=1 + match='.' + elif is_numeric_version "${version}"; then + version="v${version#v}" + # Avoid restriction message if exact version + is_exact_numeric_version "${version}" && match_count=1 + # Quote any dots in version so they are literal for expression + match="${version//\./\.}" + # Avoid 1.2 matching 1.23 + match="^${match}[^0-9]" + elif is_lts_codename "${version}"; then + # Capitalise (could alternatively make grep case insensitive) + codename="$(echo "${version:0:1}" | tr '[:lower:]' '[:upper:]')${version:1}" + # Codename is last field + match="${TAB_CHAR}${codename}\$" + elif is_download_folder "${version}"; then + match='.' + elif is_download_version "${version}"; then + version="${version#"${g_mirror_folder_name}"/}" + if [[ "${version}" = "latest" || "${version}" = "current" ]]; then + match_count=1 + match='.' + else + version="v${version#v}" + match="${version//\./\.}" + match="^${match}" # prefix + if is_numeric_version "${version}"; then + # Exact numeric match + match="${match}[^0-9]" + fi + fi + else + abort "invalid version '$1'" + fi + display_match_limit "${match_count}" + + # Implementation notes: + # - using awk rather than head so do not close pipe early on curl + # - restrict search to compatible files as not always available, or not at same time + # - return status of curl command (i.e. PIPESTATUS[0]) + display_remote_index \ + | n_grep -E "$(display_compatible_file_field)" \ + | n_grep -E "${match}" \ + | awk "NR<=${match_count}" \ + | cut -f 1 \ + | n_grep -E -o '[^v].*' + return "${PIPESTATUS[0]}" +} + +# +# Synopsis: delete_with_echo target +# + +function delete_with_echo() { + if [[ -e "$1" ]]; then + echo "$1" + rm -rf "$1" + fi +} + +# +# Synopsis: uninstall_installed +# Uninstall the installed node and npm (leaving alone the cache), +# so undo install, and may expose possible system installed versions. +# + +uninstall_installed() { + # npm: https://docs.npmjs.com/misc/removing-npm + # rm -rf /usr/local/{lib/node{,/.npm,_modules},bin,share/man}/npm* + # node: https://stackabuse.com/how-to-uninstall-node-js-from-mac-osx/ + # Doing it by hand rather than scanning cache, so still works if cache deleted first. + # This covers tarballs for at least node 4 through 10. + + while true; do + read -r -p "Do you wish to delete node and npm from ${N_PREFIX}? " yn + case $yn in + [Yy]* ) break ;; + [Nn]* ) exit ;; + * ) echo "Please answer yes or no.";; + esac + done + + echo "" + echo "Uninstalling node and npm" + delete_with_echo "${N_PREFIX}/bin/node" + delete_with_echo "${N_PREFIX}/bin/npm" + delete_with_echo "${N_PREFIX}/bin/npx" + delete_with_echo "${N_PREFIX}/bin/corepack" + delete_with_echo "${N_PREFIX}/include/node" + delete_with_echo "${N_PREFIX}/lib/dtrace/node.d" + delete_with_echo "${N_PREFIX}/lib/node_modules/npm" + delete_with_echo "${N_PREFIX}/lib/node_modules/corepack" + delete_with_echo "${N_PREFIX}/share/doc/node" + delete_with_echo "${N_PREFIX}/share/man/man1/node.1" + delete_with_echo "${N_PREFIX}/share/systemtap/tapset/node.stp" +} + +# +# Synopsis: show_permission_suggestions +# + +function show_permission_suggestions() { + echo "Suggestions:" + echo "- run n with sudo, or" + 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 +} + +# +# Synopsis: show_diagnostics +# Show environment and check for common problems. +# + +function show_diagnostics() { + echo "This information is to help you diagnose issues, and useful when reporting an issue." + echo "Note: some output may contain passwords. Redact before sharing." + + printf "\n\nCOMMAND LOCATIONS AND VERSIONS\n" + + printf "\nbash\n" + command -v bash && bash --version + + printf "\nn\n" + command -v n && n --version + + printf "\nnode\n" + if command -v node &> /dev/null; then + node --version + node -e 'if (process.versions.v8) console.log("JavaScript engine: v8");' + + printf "\nnpm\n" + command -v npm && npm --version + fi + + printf "\ntar\n" + if command -v tar &> /dev/null; then + tar --version + else + echo_red "tar not found. Needed for extracting downloads." + fi + + printf "\ncurl or wget\n" + if command -v curl &> /dev/null; then + curl --version + elif command -v wget &> /dev/null; then + wget --version + else + echo_red "Neither curl nor wget found. Need one of them for downloads." + fi + + printf "\njq\n" + command -v jq && jq --version + + printf "\nuname\n" + uname -a + + printf "\n\nSETTINGS\n" + + printf "\nn\n" + echo "node mirror: $(display_masked_url "${N_NODE_MIRROR}")" + echo "node downloads mirror: $(display_masked_url "${N_NODE_DOWNLOAD_MIRROR}")" + echo "install destination: ${N_PREFIX}" + [[ -n "${N_PREFIX}" ]] && echo "PATH: ${PATH}" + [[ -n "$N_ARCH" ]] && echo "default arch: $N_ARCH" + echo "ls-remote max matches: ${N_MAX_REMOTE_MATCHES}" + [[ -n "${N_PRESERVE_NPM}" ]] && echo "installs preserve npm by default" + [[ -n "${N_PRESERVE_COREPACK}" ]] && echo "installs preserve corepack by default" + + printf "\nProxy\n" + # disable "var is referenced but not assigned": https://github.com/koalaman/shellcheck/wiki/SC2154 + # shellcheck disable=SC2154 + [[ -n "${http_proxy}" ]] && echo "http_proxy: ${http_proxy}" + # shellcheck disable=SC2154 + [[ -n "${https_proxy}" ]] && echo "https_proxy: ${https_proxy}" + if command -v curl &> /dev/null; then + # curl supports lower case and upper case! + # shellcheck disable=SC2154 + [[ -n "${all_proxy}" ]] && echo "all_proxy: ${all_proxy}" + [[ -n "${ALL_PROXY}" ]] && echo "ALL_PROXY: ${ALL_PROXY}" + [[ -n "${HTTP_PROXY}" ]] && echo "HTTP_PROXY: ${HTTP_PROXY}" + [[ -n "${HTTPS_PROXY}" ]] && echo "HTTPS_PROXY: ${HTTPS_PROXY}" + if [[ -e "${CURL_HOME}/.curlrc" ]]; then + echo "have \$CURL_HOME/.curlrc" + elif [[ -e "${HOME}/.curlrc" ]]; then + echo "have \$HOME/.curlrc" + fi + elif command -v wget &> /dev/null; then + if [[ -e "${WGETRC}" ]]; then + echo "have \$WGETRC" + elif [[ -e "${HOME}/.wgetrc" ]]; then + echo "have \$HOME/.wgetrc" + fi + fi + + printf "\n\nCHECKS\n" + + printf "\nChecking n install destination is in PATH...\n" + local install_bin="${N_PREFIX}/bin" + local path_wth_guards=":${PATH}:" + if [[ "${path_wth_guards}" =~ :${install_bin}/?: ]]; then + printf "good\n" + else + echo_red "'${install_bin}' is not in PATH" + fi + if command -v node &> /dev/null; then + printf "\nChecking n install destination priority in PATH...\n" + local node_dir="$(dirname "$(command -v node)")" + + local index=0 + local path_entry + local path_entries + local install_bin_index=0 + local node_index=999 + IFS=':' read -ra path_entries <<< "${PATH}" + for path_entry in "${path_entries[@]}"; do + (( index++ )) + [[ "${path_entry}" =~ ^${node_dir}/?$ ]] && node_index="${index}" + [[ "${path_entry}" =~ ^${install_bin}/?$ ]] && install_bin_index="${index}" + done + if [[ "${node_index}" -lt "${install_bin_index}" ]]; then + echo_red "There is a version of node installed which will be found in PATH before the n installed version." + else + printf "good\n" + fi + fi + + # Check npm too. Simpler check than for PATH and node, more like the runtime logging for active/installed node. + if [[ -z "${N_PRESERVE_NPM}" ]]; then + printf "\nChecking npm install destination...\n" + local installed_npm="${N_PREFIX}/bin/npm" + local active_npm="$(command -v npm)" + if [[ -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" != "${installed_npm}" ]]; then + echo_red "There is an active version of npm shadowing the version installed by n. Check order of entries in PATH." + log "installed" "${installed_npm}" + log "active" "${active_npm}" + else + printf "good\n" + fi + fi + + printf "\nChecking 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 + printf "\nChecking permissions for install folders...\n" + local install_writeable="true" + for subdir in bin lib include share; do + if [[ -e "${N_PREFIX}/${subdir}" && ! -w "${N_PREFIX}/${subdir}" ]]; then + install_writeable="false" + echo_red "You do not have write permission to: ${N_PREFIX}/${subdir}" + break + fi + 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}\"" + echo " sudo mkdir -p bin lib include share" + echo " sudo chown -R $(whoami) bin lib include share" + fi + fi + + printf "\nChecking mirror is reachable...\n" + if is_ok "${N_NODE_MIRROR}/"; then + printf "good\n" + else + echo_red "mirror not reachable" + printf "Showing failing command and output\n" + if command -v curl &> /dev/null; then + ( set -x; do_get --head "${N_NODE_MIRROR}/" ) + else + ( set -x; do_get --spider "${N_NODE_MIRROR}/" ) + printf "\n" + fi + fi +} + +# +# Handle arguments. +# + +# First pass. Process the options so they can come before or after commands, +# particularly for `n lsr --all` and `n install --arch x686` +# which feel pretty natural. + +unprocessed_args=() +positional_arg="false" + +while [[ $# -ne 0 ]]; do + case "$1" in + --all) N_MAX_REMOTE_MATCHES=32000 ;; + -V|--version) display_n_version ;; + -h|--help|help) display_help; exit ;; + -q|--quiet) set_quiet ;; + -d|--download) DOWNLOAD="true" ;; + --cleanup) CLEANUP="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="" ;; + --use-xz) N_USE_XZ="true" ;; + --no-use-xz) N_USE_XZ="false" ;; + --latest) display_remote_versions latest; exit ;; + --stable) display_remote_versions lts; exit ;; # [sic] old terminology + --lts) display_remote_versions lts; exit ;; + -a|--arch) shift; set_arch "$1";; # set arch and continue + exec|run|as|use) + unprocessed_args+=( "$1" ) + positional_arg="true" + ;; + *) + if [[ "${positional_arg}" == "true" ]]; then + unprocessed_args+=( "$@" ) + break + fi + unprocessed_args+=( "$1" ) + ;; + esac + shift +done + +if [[ -z "${N_USE_XZ+defined}" ]]; then + N_USE_XZ="true" # Default to using xz + can_use_xz || N_USE_XZ="false" +fi + +set -- "${unprocessed_args[@]}" + +if test $# -eq 0; then + test -z "$(display_versions_paths)" && err_no_installed_print_help + menu_select_cache_versions +else + case "$1" in + bin|which) display_bin_path_for_version "$2"; exit ;; + run|as|use) shift; run_with_version "$@"; exit ;; + exec) shift; exec_with_version "$@"; exit ;; + doctor) show_diagnostics; exit ;; + rm|-) shift; remove_versions "$@"; exit ;; + prune) prune_cache; exit ;; + latest) install latest; exit ;; + stable) install stable; exit ;; + lts) install lts; exit ;; + ls|list) display_versions_paths; exit ;; + lsr|ls-remote|list-remote) shift; display_remote_versions "$1"; exit ;; + uninstall) uninstall_installed; exit ;; + i|install) shift; install "$1"; exit ;; + download) shift; DOWNLOAD="true"; install "$1"; exit ;; + N_TEST_DISPLAY_LATEST_RESOLVED_VERSION) shift; get_latest_resolved_version "$1" > /dev/null || exit 2; echo "${g_target_node}"; exit ;; + *) install "$1"; exit ;; + esac +fi diff --git a/hooks/backup/05-conf_ldap b/hooks/backup/05-conf_ldap new file mode 100644 index 0000000..5bee433 --- /dev/null +++ b/hooks/backup/05-conf_ldap @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/conf/ldap" + +# Backup the configuration +ynh_backup "/etc/ldap/ldap.conf" "${backup_dir}/ldap.conf" +slapcat -b cn=config -l "${backup_dir}/cn=config.master.ldif" + +# Backup the database +slapcat -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" diff --git a/hooks/backup/17-data_home b/hooks/backup/17-data_home new file mode 100644 index 0000000..910c146 --- /dev/null +++ b/hooks/backup/17-data_home @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/data/home" + +# Backup user home +for f in $(find /home/* -type d -prune | awk -F/ '{print $NF}'); do + if [[ ! "$f" =~ ^yunohost|lost\+found ]]; then + if [ ! -e "/home/$f/.nobackup" ]; then + ynh_backup "/home/$f" "${backup_dir}/$f" 1 + fi + fi +done diff --git a/hooks/backup/18-data_multimedia b/hooks/backup/18-data_multimedia new file mode 100644 index 0000000..0e0960a --- /dev/null +++ b/hooks/backup/18-data_multimedia @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/data/multimedia" + +if [ ! -e "/home/yunohost.multimedia" ] || [ -e "/home/yunohost.multimedia/.nobackup" ]; then + exit 0 +fi + +# Backup multimedia directory +ynh_backup --src_path="/home/yunohost.multimedia" --dest_path="${backup_dir}" --is_big --not_mandatory diff --git a/hooks/backup/20-conf_ynh_settings b/hooks/backup/20-conf_ynh_settings new file mode 100644 index 0000000..999dafa --- /dev/null +++ b/hooks/backup/20-conf_ynh_settings @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/conf/ynh" + +# Backup the configuration +ynh_backup "/etc/yunohost/firewall.yml" "${backup_dir}/firewall.yml" +ynh_backup "/etc/yunohost/permissions.yml" "${backup_dir}/permissions.yml" +ynh_backup "/etc/yunohost/current_host" "${backup_dir}/current_host" +[ ! -d "/etc/yunohost/portal" ] || ynh_backup "/etc/yunohost/portal" "${backup_dir}/portal" +[ ! -d "/etc/yunohost/domains" ] || ynh_backup "/etc/yunohost/domains" "${backup_dir}/domains" +[ ! -e "/etc/yunohost/settings.yml" ] || ynh_backup "/etc/yunohost/settings.yml" "${backup_dir}/settings.yml" +[ ! -d "/etc/yunohost/dyndns" ] || ynh_backup "/etc/yunohost/dyndns" "${backup_dir}/dyndns" +[ ! -d "/etc/dkim" ] || ynh_backup "/etc/dkim" "${backup_dir}/dkim" diff --git a/hooks/backup/21-conf_ynh_certs b/hooks/backup/21-conf_ynh_certs new file mode 100644 index 0000000..130f65a --- /dev/null +++ b/hooks/backup/21-conf_ynh_certs @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/conf/ynh/certs" + +# Backup certificates +ynh_backup "/etc/yunohost/certs" "$backup_dir" diff --git a/hooks/backup/23-data_mail b/hooks/backup/23-data_mail new file mode 100644 index 0000000..01539ab --- /dev/null +++ b/hooks/backup/23-data_mail @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/data/mail" + +# Backup mails +ynh_backup /var/mail "$backup_dir" 1 diff --git a/hooks/backup/50-conf_manually_modified_files b/hooks/backup/50-conf_manually_modified_files new file mode 100644 index 0000000..8d1fea4 --- /dev/null +++ b/hooks/backup/50-conf_manually_modified_files @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +ynh_abort_if_errors +YNH_CWD="${YNH_BACKUP_DIR%/}/conf/manually_modified_files" +mkdir -p "$YNH_CWD" +cd "$YNH_CWD" + +yunohost tools shell -c "from yunohost.regenconf import manually_modified_files; print('\n'.join(manually_modified_files()))" > ./manually_modified_files_list + +ynh_backup --src_path="./manually_modified_files_list" + +for file in $(cat ./manually_modified_files_list); do + [[ -e $file ]] && ynh_backup --src_path="$file" +done + +ynh_backup --src_path="/etc/ssowat/conf.json.persistent" diff --git a/hooks/conf_regen/01-yunohost b/hooks/conf_regen/01-yunohost new file mode 100755 index 0000000..aae3a70 --- /dev/null +++ b/hooks/conf_regen/01-yunohost @@ -0,0 +1,400 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +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 + # We use "|| true" because some filesystem do not support ACL (such as NTFS ... for example when incus storage is on an NTFS drive in dir storage) + setfacl -m g:all_users:--- /var/www || true + setfacl -m g:all_users:--- /var/log/nginx || true + setfacl -m g:all_users:--- /etc/yunohost || true + setfacl -m g:all_users:--- /etc/ssowat || true + fi +} + +do_init_regen() { + + cd /usr/share/yunohost/conf/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 ]] \ + || echo "yunohost.org" > /etc/yunohost/current_host + + # copy default services and firewall + [[ -f /etc/yunohost/firewall.yml ]] \ + || cp firewall.yml /etc/yunohost/firewall.yml + + # allow users to access /media directory + [[ -d /etc/skel/media ]] \ + || (mkdir -p /media && ln -s /media /etc/skel/media) + + # 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 yunoprompt.service /etc/systemd/system/yunoprompt.service + + systemctl daemon-reload + + systemctl enable yunohost-api.service --quiet + systemctl start yunohost-api.service + + systemctl enable yunohost-portal-api.service --quiet + systemctl start yunohost-portal-api.service + + # Enable yunoprompt (in particular for installs from ISO where we want this to show on first boot instead of asking for a login/password) + systemctl enable yunoprompt --quiet + + # Yunohost-firewall is enabled only during postinstall, not init, not 100% sure why + + cp dpkg-origins /etc/dpkg/origins/yunohost + + # Change dpkg vendor + # see https://wiki.debian.org/Derivatives/Guidelines#Vendor + if readlink -f /etc/dpkg/origins/default | grep -q debian; then + rm -f /etc/dpkg/origins/default + ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default + fi +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/yunohost + + mkdir -p "$pending_dir/etc/systemd/system" + mkdir -p "$pending_dir/etc/cron.d/" + mkdir -p "$pending_dir/etc/cron.daily/" + + # add cron job for diagnosis to be ran at 7h and 19h + a random delay between + # 0 and 20min, meant to avoid every instances running their diagnosis at + # exactly the same time, which may overload the diagnosis server. + cat > "$pending_dir/etc/cron.d/yunohost-diagnosis" << EOF +SHELL=/bin/bash +0 7,19 * * * root : YunoHost Automatic Diagnosis; sleep \$((RANDOM\\%1200)); yunohost diagnosis run --email > /dev/null 2>/dev/null || echo "Running the automatic diagnosis failed miserably" +EOF + + # Cron job that upgrade the app list everyday + cat > "$pending_dir/etc/cron.daily/yunohost-fetch-apps-catalog" << EOF +#!/bin/bash +sleep \$((RANDOM%3600)); yunohost tools update apps > /dev/null +EOF + + # Cron job that renew lets encrypt certificates if there's any that needs renewal + cat > "$pending_dir/etc/cron.daily/yunohost-certificate-renew" << EOF +#!/bin/bash +yunohost domain cert renew --email +EOF + + # If we subscribed to a dyndns domain, add the corresponding cron + # - delay between 0 and 60 secs to spread the check over a 1 min window + # - do not run the command if some process already has the lock, to avoid queuing hundreds of commands... + if ls -l /etc/yunohost/dyndns/K*.key 2> /dev/null; then + cat > "$pending_dir/etc/cron.d/yunohost-dyndns" << EOF +SHELL=/bin/bash +# Every 10 minutes, +# - (sleep random 60 is here to spread requests over a 1-min window) +# - if ipv4/6.yunohost.org answers ping (basic check to validate that we're connected to the internet and yunohost infra aint down) +# - and if lock ain't already taken by another command +# - trigger yunohost dyndns update +*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); ! ping -q -W5 -c1 ipv4.yunohost.org >/dev/null 2>&1 || test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null +EOF + else + # (Delete cron if no dyndns domain found) + touch "$pending_dir/etc/cron.d/yunohost-dyndns" + fi + + # Skip ntp if inside a container (inspired from the conf of systemd-timesyncd) + if systemctl | grep -q 'ntp.service'; then + mkdir -p "$pending_dir/etc/systemd/system/ntp.service.d/" + cat > "$pending_dir/etc/systemd/system/ntp.service.d/ynh-override.conf" << EOF +[Unit] +ConditionCapability=CAP_SYS_TIME +ConditionVirtualization=!container +EOF + fi + + mkdir -p "$pending_dir/etc/systemd/system/nftables.service.d/" + cp yunohost-nftables-hooks-override.conf "$pending_dir/etc/systemd/system/nftables.service.d/yunohost-nftables-hooks.conf" + # Delete legacy conflict between yunohost and nftables + touch "$pending_dir/etc/systemd/system/nftables.service.d/ynh-override.conf" + + # Don't suspend computer on LidSwitch + mkdir -p "$pending_dir/etc/systemd/logind.conf.d/" + cat > "$pending_dir/etc/systemd/logind.conf.d/ynh-override.conf" << EOF +[Login] +HandleLidSwitch=ignore +HandleLidSwitchDocked=ignore +HandleLidSwitchExternalPower=ignore +EOF + + 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 yunoprompt.service "$pending_dir/etc/systemd/system/yunoprompt.service" + cp proc-hidepid.service "$pending_dir/etc/systemd/system/proc-hidepid.service" + # Delete legacy yunohost-firewall service + touch "$pending_dir/etc/systemd/system/yunohost-firewall.service" + + 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 "$pending_dir/etc/cron.daily/node_update" +} + +do_post_regen() { + regen_conf_files=$1 + + # Re-mkdir / apply permission to all basic folders etc + base_folder_and_perm_init + + # 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 + + # 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 + + 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 {} \; + + for USER in $(yunohost user list --quiet --output-as json | jq -r '.users | .[] | .username'); do + [ ! -e "/home/$USER" ] || setfacl -m g:all_users:--- "/home/$USER" + done + + # Misc configuration / state files + for file in /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql}; do + if [ -f "$file" ]; then + if [ "$file" != "mdns.yml" ]; then + chown root:root "$file" + fi + chmod 600 "$file" + fi + done + + # Create ssh.app and sftp.app groups if they don't exist yet + grep -q '^ssh.app:' /etc/group || groupadd ssh.app + grep -q '^sftp.app:' /etc/group || groupadd sftp.app + + # Propagates changes in systemd service config overrides + if systemctl | grep -q 'ntp.service'; then + [[ ! "$regen_conf_files" =~ "ntp.service.d/ynh-override.conf" ]] || { + systemctl daemon-reload + systemctl restart ntp + } + fi + + [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || { + systemctl daemon-reload + systemctl restart systemd-logind + } + [[ ! "$regen_conf_files" =~ "yunohost-api.service" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "yunohost-portal-api.service" ]] || systemctl daemon-reload + [[ ! "$regen_conf_files" =~ "nftables.service.d/yunohost-nftables-hooks.conf" ]] || systemctl daemon-reload + + if [[ "$regen_conf_files" =~ "yunoprompt.service" ]]; then + systemctl daemon-reload + action=$([[ -e /etc/systemd/system/yunoprompt.service ]] && echo 'enable' || echo 'disable') + systemctl "$action" yunoprompt --quiet --now + fi + if [[ "$regen_conf_files" =~ "proc-hidepid.service" ]]; then + systemctl daemon-reload + action=$([[ -e /etc/systemd/system/proc-hidepid.service ]] && echo 'enable' || echo 'disable') + systemctl "$action" proc-hidepid --quiet --now + fi + + 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; then + rm -f /etc/dpkg/origins/default + ln -s /etc/dpkg/origins/yunohost /etc/dpkg/origins/default + fi + + if test -e /etc/yunohost/installed && test -e /etc/profile.d/check_yunohost_is_installed.sh; then + rm /etc/profile.d/check_yunohost_is_installed.sh + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/02-ssl b/hooks/conf_regen/02-ssl new file mode 100755 index 0000000..13fc3d0 --- /dev/null +++ b/hooks/conf_regen/02-ssl @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +ssl_dir="/usr/share/yunohost/ssl" +template_dir="/usr/share/yunohost/conf/ssl" +ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem" +ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem" +ynh_key="/etc/yunohost/certs/yunohost.org/key.pem" + +regen_local_ca() { + + domain="$1" + + echo -e "\n# Creating local certification authority with domain=$domain\n" + + # create certs and SSL directories + mkdir -p "/etc/yunohost/certs/yunohost.org" + mkdir -p "${ssl_dir}/"{ca,certs,crl,newcerts} + + pushd ${ssl_dir} + + # (Update the serial so that it's specific to this very instance) + # N.B. : the weird RANDFILE thing comes from: + # https://stackoverflow.com/questions/94445/using-openssl-what-does-unable-to-write-random-state-mean + RANDFILE=.rnd openssl rand -hex 19 > serial + rm -f index.txt + touch index.txt + cp ${template_dir}/openssl.cnf openssl.ca.cnf + sed -i "s/yunohost.org/${domain}/g" openssl.ca.cnf + openssl req -x509 \ + -new \ + -config openssl.ca.cnf \ + -days 3650 \ + -out ca/cacert.pem \ + -keyout ca/cakey.pem \ + -nodes \ + -batch \ + -subj "/CN=${domain}/O=${domain%.*}" 2>&1 + + chmod 640 ca/cacert.pem + chmod 640 ca/cakey.pem + + cp ca/cacert.pem $ynh_ca + ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem + update-ca-certificates + + popd +} + +do_init_regen() { + LOGFILE=/tmp/yunohost-ssl-init + touch "$LOGFILE" + chown root:root "$LOGFILE" + chmod 640 "$LOGFILE" + + # Make sure this conf exists + mkdir -p ${ssl_dir}/{ca,certs,crl,newcerts} + install -D -m 644 ${template_dir}/openssl.cnf "${ssl_dir}/openssl.cnf" + + # create default certificates + if [[ ! -f "$ynh_ca" ]]; then + regen_local_ca yunohost.org >> "$LOGFILE" + fi + + if [[ ! -f "$ynh_crt" ]]; then + echo -e "\n# Creating initial key and certificate \n" >> "$LOGFILE" + + openssl req -new \ + -config "${ssl_dir}/openssl.cnf" \ + -out "${ssl_dir}/certs/yunohost_csr.pem" \ + -keyout "${ssl_dir}/certs/yunohost_key.pem" \ + -nodes -batch &>> $LOGFILE + + openssl ca \ + -config "${ssl_dir}/openssl.cnf" \ + -days 730 \ + -in "${ssl_dir}/certs/yunohost_csr.pem" \ + -out "${ssl_dir}/certs/yunohost_crt.pem" \ + -batch &>> $LOGFILE + + chmod 640 "${ssl_dir}/certs/yunohost_key.pem" + chmod 640 "${ssl_dir}/certs/yunohost_crt.pem" + + cp "${ssl_dir}/certs/yunohost_key.pem" "$ynh_key" + cp "${ssl_dir}/certs/yunohost_crt.pem" "$ynh_crt" + ln -sf "$ynh_crt" /etc/ssl/certs/yunohost_crt.pem + ln -sf "$ynh_key" /etc/ssl/private/yunohost_key.pem + fi + + chown -R root:ssl-cert /etc/yunohost/certs/yunohost.org/ + chmod o-rwx /etc/yunohost/certs/yunohost.org/ +} + +do_pre_regen() { + pending_dir=$1 + + install -D -m 644 $template_dir/openssl.cnf "${pending_dir}/${ssl_dir}/openssl.cnf" +} + +do_post_regen() { + current_local_ca_domain=$(openssl x509 -in $ynh_ca -text | tr ',' '\n' | grep Issuer | awk '{print $4}') + main_domain=$(cat /etc/yunohost/current_host) + + # Automigrate legacy folder + if [ -e /usr/share/yunohost/yunohost-config/ssl/yunoCA ]; then + mv /usr/share/yunohost/yunohost-config/ssl/yunoCA/* ${ssl_dir} + rm -rf /usr/share/yunohost/yunohost-config + # Overwrite openssl.cnf because it may still contain references to the old yunoCA dir + install -D -m 644 ${template_dir}/openssl.cnf "${ssl_dir}/openssl.cnf" + install -D -m 644 ${template_dir}/openssl.cnf "${ssl_dir}/openssl.ca.cnf" + sed -i "s/yunohost.org/${main_domain}/g" openssl.ca.cnf + fi + + mkdir -p ${ssl_dir}/{ca,certs,crl,newcerts} + chown root:root ${ssl_dir} + chmod 750 ${ssl_dir} + chmod -R o-rwx ${ssl_dir} + chmod o+x ${ssl_dir}/certs + chmod o+r ${ssl_dir}/certs/yunohost_crt.pem + + if [[ "$current_local_ca_domain" != "$main_domain" ]]; then + regen_local_ca "$main_domain" + # Idk how useful this is, but this was in the previous python code (domain.main_domain()) + ln -sf "/etc/yunohost/certs/$main_domain/crt.pem" /etc/ssl/certs/yunohost_crt.pem + ln -sf "/etc/yunohost/certs/$main_domain/key.pem" /etc/ssl/private/yunohost_key.pem + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/03-ssh b/hooks/conf_regen/03-ssh new file mode 100755 index 0000000..7abe687 --- /dev/null +++ b/hooks/conf_regen/03-ssh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +set -e + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + 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 + export ipv6_enabled + + ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" +} + +do_post_regen() { + regen_conf_files=$1 + + # If no file changed, there's nothing to do + + [[ -n "$regen_conf_files" ]] || return 0 + + # Enforce permissions for /etc/ssh/sshd_config + chown root:root "/etc/ssh/sshd_config" + chmod 644 "/etc/ssh/sshd_config" + + systemctl restart ssh +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/06-slapd b/hooks/conf_regen/06-slapd new file mode 100755 index 0000000..85d2a6a --- /dev/null +++ b/hooks/conf_regen/06-slapd @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +tmp_backup_dir_file="/root/slapd-backup-dir.txt" + +config="/usr/share/yunohost/conf/slapd/config.ldif" +db_init="/usr/share/yunohost/conf/slapd/db_init.ldif" + +do_init_regen() { + + do_pre_regen "" + + # Drop current existing slapd data + + rm -rf /var/backups/*.ldapdb + rm -rf /var/backups/slapd-* + + debconf-set-selections << EOF +slapd slapd/password1 password yunohost +slapd slapd/password2 password yunohost +slapd slapd/domain string yunohost.org +slapd shared/organization string yunohost.org +slapd slapd/allow_ldap_v2 boolean false +slapd slapd/invalid_config boolean true +slapd slapd/backend select MDB +slapd slapd/move_old_database boolean true +slapd slapd/no_configuration boolean false +slapd slapd/purge_database boolean false +EOF + + DEBIAN_FRONTEND=noninteractive dpkg-reconfigure slapd -u + + # Enforce permissions + chown -R openldap:openldap /etc/ldap/schema/ + usermod -aG ssl-cert openldap + + # (Re-)init data according to default ldap entries + echo ' Initializing LDAP with YunoHost DB structure' + + rm -rf /etc/ldap/slapd.d + mkdir -p /etc/ldap/slapd.d + slapadd -F /etc/ldap/slapd.d -b cn=config -l "$config" 2>&1 \ + | grep -v "none elapsed\|Closing DB" || true + chown -R openldap: /etc/ldap/slapd.d + + rm -rf /var/lib/ldap + mkdir -p /var/lib/ldap + slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org -l "$db_init" 2>&1 \ + | grep -v "none elapsed\|Closing DB" || true + chown -R openldap: /var/lib/ldap + + nscd -i group || true + nscd -i passwd || true + + systemctl restart slapd +} + +_regenerate_slapd_conf() { + + # Validate the new slapd config + # To do so, we have to use the .ldif to generate the config directory + # so we use a temporary directory slapd_new.d + rm -Rf /etc/ldap/slapd_new.d + mkdir /etc/ldap/slapd_new.d + slapadd -b cn=config -l "$config" -F /etc/ldap/slapd_new.d/ 2>&1 \ + | grep -v "none elapsed\|Closing DB" || true + # Actual validation (-Q is for quiet, -u is for dry-run) + slaptest -Q -u -F /etc/ldap/slapd_new.d + + # "Commit" / apply the new config (meaning we delete the old one and replace + # it with the new one) + rm -Rf /etc/ldap/slapd.d + mv /etc/ldap/slapd_new.d /etc/ldap/slapd.d + + chown -R openldap:openldap /etc/ldap/slapd.d/ +} + +do_pre_regen() { + pending_dir=$1 + + # remove temporary backup file + rm -f "$tmp_backup_dir_file" + + # Define if we need to migrate from hdb to mdb + if [ -e /etc/ldap/slapd.conf ]; then + curr_backend=$(grep '^database' /etc/ldap/slapd.conf 2> /dev/null | awk '{print $2}') + if [ "$curr_backend" != 'mdb' ] && [ -n "$curr_backend" ]; then + backup_dir="/var/backups/dc=yunohost,dc=org-${curr_backend}-$(date +%s)" + mkdir -p "$backup_dir" + slapcat -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" + echo "$backup_dir" > "$tmp_backup_dir_file" + fi + fi + + # create needed directories + ldap_dir="${pending_dir}/etc/ldap" + schema_dir="${ldap_dir}/schema" + mkdir -p "$ldap_dir" "$schema_dir" + + cd /usr/share/yunohost/conf/slapd + + # copy configuration files + cp -a ldap.conf "$ldap_dir" + cp -a sudo.ldif mailserver.ldif permission.ldif "$schema_dir" + + mkdir -p "$pending_dir/etc/systemd/system/slapd.service.d/" + cp systemd-override.conf "$pending_dir/etc/systemd/system/slapd.service.d/ynh-override.conf" + + install -D -m 644 slapd.default "${pending_dir}/etc/default/slapd" +} + +do_post_regen() { + regen_conf_files=$1 + + # fix some permissions + echo "Enforce permissions on ldap/slapd directories and certs ..." + # penldap user should be in the ssl-cert group to let it access the certificate for TLS + usermod -aG ssl-cert openldap + chown -R openldap:openldap /etc/ldap/schema/ + chown -R openldap:openldap /etc/ldap/slapd.d/ + + # Fix weird scenarios where /etc/sudo-ldap.conf doesn't exists (yet is supposed to be + # created by the sudo-ldap package) : https://github.com/YunoHost/issues/issues/2091 + if [ ! -e /etc/sudo-ldap.conf ]; then + ln -s /etc/ldap/ldap.conf /etc/sudo-ldap.conf + fi + + # If we changed the systemd ynh-override conf + if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/slapd.service.d/ynh-override.conf$"; then + systemctl daemon-reload + systemctl restart slapd + sleep 3 + fi + + # For some reason, old setups don't have the admins group defined... + if ! slapcat -H "ldap:///cn=admins,ou=groups,dc=yunohost,dc=org" | grep -q 'cn=admins,ou=groups,dc=yunohost,dc=org'; then + slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org <<< \ + "dn: cn=admins,ou=groups,dc=yunohost,dc=org +cn: admins +gidNumber: 4001 +memberUid: admin +objectClass: posixGroup +objectClass: top" + chown -R openldap: /var/lib/ldap + systemctl restart slapd + nscd -i group + fi + + if [ -z "$regen_conf_files" ] && [ "$FORCE" == "false" ]; then + exit 0 + fi + + # regenerate LDAP config directory from slapd.conf + echo "Regenerate LDAP config directory from config.ldif" + _regenerate_slapd_conf + + # If there's a backup, re-import its data + if [ -f "$tmp_backup_dir_file" ]; then + backup_dir=$(cat "$tmp_backup_dir_file") + if [[ -n "$backup_dir" && -f "${backup_dir}/dc=yunohost-dc=org.ldif" ]]; then + # regenerate LDAP config directory and import database as root + echo "Import the database using slapadd" + slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" + chown -R openldap:openldap /var/lib/ldap 2>&1 + fi + fi + + echo "Running slapdindex" + su openldap -s "/bin/bash" -c "/usr/sbin/slapindex" + + echo "Reloading slapd" + systemctl force-reload slapd +} + +if [[ "$1" == _regenerate_slapd_conf ]]; then + _regenerate_slapd_conf +else + "do_$1_regen" "$(echo "${*:2}" | xargs)" +fi diff --git a/hooks/conf_regen/09-nslcd b/hooks/conf_regen/09-nslcd new file mode 100755 index 0000000..de94ea0 --- /dev/null +++ b/hooks/conf_regen/09-nslcd @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +do_init_regen() { + do_pre_regen "" + systemctl restart nslcd +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/nslcd + + install -D -m 644 nslcd.conf "${pending_dir}/etc/nslcd.conf" +} + +do_post_regen() { + regen_conf_files=$1 + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart nslcd +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/10-apt b/hooks/conf_regen/10-apt new file mode 100755 index 0000000..017e8b4 --- /dev/null +++ b/hooks/conf_regen/10-apt @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +readonly YNH_DEFAULT_PHP_VERSION=8.2 + +do_pre_regen() { + pending_dir=$1 + + mkdir --parents "${pending_dir}/etc/apt/preferences.d" + + # Add sury + mkdir -p "$pending_dir/etc/apt/sources.list.d/" + echo "deb [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 " +Package: php-common +Pin: origin \"packages.sury.org\" +Pin-Priority: 500" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version" + + packages_to_refuse_from_sury="php php-* openssl libssl1.1 libssl-dev" + for package in $packages_to_refuse_from_sury; do + echo " +Package: $package +Pin: origin \"packages.sury.org\" +Pin-Priority: -1" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version" + done + + # 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 +# bind9. These 2 packages conflict with YunoHost. + +# Installing apache2 will break nginx and break the entire YunoHost ecosystem +# on your server, therefore don't remove those lines! + +# You have been warned. + +Package: apache2 +Pin: release * +Pin-Priority: -1 + +Package: apache2-bin +Pin: release * +Pin-Priority: -1 + +# Also bind9 will conflict with dnsmasq. +# Same story as for apache2. +# Don't install it, don't remove those lines. + +Package: bind9 +Pin: release * +Pin-Priority: -1 +" >> "${pending_dir}/etc/apt/preferences.d/ban_packages" + +} + +do_post_regen() { + # 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' || true)" + 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 ]]; then + wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor > "/etc/apt/trusted.gpg.d/extra_php_version.gpg" + fi + + # 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 + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/15-nginx b/hooks/conf_regen/15-nginx new file mode 100755 index 0000000..a02aae1 --- /dev/null +++ b/hooks/conf_regen/15-nginx @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -e + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +do_base_regen() { + + 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 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" + cp proxy_params_with_auth "$nginx_dir" + cp proxy_params_no_auth "$nginx_dir" + cp fastcgi_params_with_auth "$nginx_dir" + cp fastcgi_params_no_auth "$nginx_dir" + + ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" + ynh_render_template "yunohost_admin.conf" "${nginx_conf_dir}/yunohost_admin.conf" + ynh_render_template "yunohost_admin.conf.inc" "${nginx_conf_dir}/yunohost_admin.conf.inc" + ynh_render_template "yunohost_api.conf.inc" "${nginx_conf_dir}/yunohost_api.conf.inc" + + mkdir -p "$nginx_conf_dir/default.d/" + cp "redirect_to_admin.conf" "$nginx_conf_dir/default.d/" +} + +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 || { + nginx -t + exit 1 + } + systemctl restart nginx || { + journalctl --no-pager --lines=10 -u nginx >&2 + exit 1 + } + + exit 0 +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/nginx + + nginx_dir="${pending_dir}/etc/nginx" + nginx_conf_dir="${nginx_dir}/conf.d" + mkdir -p "$nginx_conf_dir" + + 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 + + # Support different strategy for security configurations + 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)" + export tls_passthrough_enabled="$(jq -r '.tls_passthrough_enabled' <<< "$YNH_SETTINGS" | int_to_bool)" + export tls_passthrough_list="$(jq -r '.tls_passthrough_list' <<< "$YNH_SETTINGS" | int_to_bool)" + + do_base_regen "${pending_dir}" + + local tls_passthrough_module="${pending_dir}/etc/nginx/modules-enabled/tls_passthrough.conf" + mkdir -p "${pending_dir}/etc/nginx/modules-enabled/" + + if [[ "$tls_passthrough_enabled" == "True" ]]; then + ynh_render_template "tls_passthrough.conf" "${tls_passthrough_module}" + for tls_passthrough_domain_and_ip in ${tls_passthrough_list//,/ }; do + export tls_passthrough_domain=$(echo "$tls_passthrough_domain_and_ip" | awk -F';' '{print $1}') + export tls_passthrough_ip=$(echo "$tls_passthrough_domain_and_ip" | awk -F';' '{print $2}') + export tls_passthrough_port=$(echo "$tls_passthrough_domain_and_ip" | awk -F';' '{print $3}') + ynh_render_template "tls_passthrough_server.conf" "${nginx_conf_dir}/${tls_passthrough_domain}.forward80.conf" + done + else + touch "${tls_passthrough_module}" + fi + + # "Touch" every known .conf file for every domain, + # meaning it should be removed by the regen conf + # - For real 'existing' domains, this file will be overwritten with an actual conf right after using ynh_render_template + # - For old domains, this will tell the regen conf that it is "to be deleted" + ls -1 /etc/nginx/conf.d \ + | awk '/^[^\.]+\.[^\.]+.*\.conf$/ { print $1 }' \ + | xargs --replace={} touch "${nginx_conf_dir}/{}" + + # add domain conf files + cert_status=$(yunohost domain cert status --json) + for domain in $YNH_DOMAINS; do + domain_conf_dir="${nginx_conf_dir}/${domain}.d" + mkdir -p "$domain_conf_dir" + mail_autoconfig_dir="${pending_dir}/var/www/.well-known/${domain}/autoconfig/mail/" + mkdir -p "$mail_autoconfig_dir" + + # NGINX server configuration + export domain + export domain_cert_ca=$(echo "$cert_status" \ + | jq ".certificates.\"$domain\".CA_type" \ + | tr -d '"') + if tr ' ' '\n' <<< "$YNH_DOMAINS_WITH_MAIL_IN_AND_OUT" | grep -q "^$domain$"; then + export mail_enabled="True" + else + export mail_enabled="False" + fi + + ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf" + if [ $mail_enabled == "True" ]; then + ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" + fi + + touch "${domain_conf_dir}/yunohost_local.conf" # Clean legacy conf files + + done + + # 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 mail-autoconfig files + autoconfig_files=$(ls -1 /var/www/.well-known/*/autoconfig/mail/config-v1.1.xml 2> /dev/null || true) + for file in $autoconfig_files; do + domain=$(basename "$(readlink -f "$(dirname "$file")/../..")") + [[ $YNH_DOMAINS =~ $domain ]] \ + || (mkdir -p "$(dirname "${pending_dir}/${file}")" && touch "${pending_dir}/${file}") + done + + # disable default site + mkdir -p "${nginx_dir}/sites-enabled" + touch "${nginx_dir}/sites-enabled/default" +} + +do_post_regen() { + regen_conf_files=$1 + + # Make sure fastcgi / PHP uses the YNH_USER auth header instead of $remote_user from the Authorization header + # shellcheck disable=SC2016 + sed -i 's/$remote_user;/$http_ynh_user if_not_empty;/g' /etc/nginx/fastcgi_params + + # Hotfix CVE-2026-42945 + # shellcheck disable=SC2016 + if ! grep -qE 'fastcgi_param\s+HTTP_HOST\s+\$host;' /etc/nginx/fastcgi_params 2> /dev/null; then + # shellcheck disable=SC2016 + echo 'fastcgi_param HTTP_HOST $host;' >> /etc/nginx/fastcgi_params + fi + + 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 + for domain in $YNH_DOMAINS; do + mkdir -p "/etc/nginx/conf.d/${domain}.d" + done + + if ! nginx -t 2> /dev/null; then + # Print issues to console and exit + nginx -t + exit 1 + fi + + # Only reload nginx if it's already running + if pgrep nginx; then + if ! systemctl reload nginx; then + journalctl --no-pager --lines=10 -u nginx >&2 + exit 1 + fi + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/19-postfix b/hooks/conf_regen/19-postfix new file mode 100755 index 0000000..e9db66f --- /dev/null +++ b/hooks/conf_regen/19-postfix @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -e + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/postfix + + postfix_dir="${pending_dir}/etc/postfix" + mkdir -p "$postfix_dir" + + default_dir="${pending_dir}/etc/default/" + mkdir -p "$default_dir" + + # install plain conf files + cp plain/* "$postfix_dir" + + # prepare main.cf conf file + main_domain=$(cat /etc/yunohost/current_host) + + # Support different strategy for security configurations + export compatibility="$(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="$(jq -r '.smtp_relay_enabled' <<< "$YNH_SETTINGS" | int_to_bool)" + if [ "${relay_enabled}" == "True" ]; then + 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" + chmod 750 "${postfix_dir}/sasl_passwd" + # Avoid "postmap: warning: removing zero-length database file" + chown postfix "${pending_dir}/etc/postfix" + chown postfix "${pending_dir}/etc/postfix/sasl_passwd" + + cat <<< "[${relay_host}]:${relay_port} ${relay_user}:${relay_password}" > "${postfix_dir}/sasl_passwd" + fi + export enable_blocklists="$(jq -r '.enable_blocklists' <<< "$YNH_SETTINGS" | int_to_bool)" + + # Use this postfix server as a backup MX + export backup_mx_domains="$(jq -r '.smtp_backup_mx_domains' <<< "$YNH_SETTINGS" | sed 's/^null$//g' | sed "s/,/ /g")" + export backup_mx_emails="$(jq -r '.smtp_backup_mx_emails_whitelisted' <<< "$YNH_SETTINGS" | sed "s/,/ /g")" + rm -f "${postfix_dir}/relay_recipients" + touch "${postfix_dir}/relay_recipients" + rm -f "${postfix_dir}/relay_recipients.db" + touch "${postfix_dir}/relay_recipients.db" + if [ -n "${backup_mx_domains}" ] && [ -n "${backup_mx_emails}" ]; then + for mail in ${backup_mx_emails}; do + echo "$mail OK" >> "${postfix_dir}/relay_recipients" + done + postmap "${postfix_dir}/relay_recipients" + fi + + export main_domain + export domain_list="$YNH_DOMAINS_WITH_MAIL_IN_AND_OUT" + ynh_render_template "main.cf" "${postfix_dir}/main.cf" + ynh_render_template "sni" "${postfix_dir}/sni" + + # Activate mailbox only on domains with mail_in features + # If mail_in is disabled for a domain, this allows to send + # mails on external mailbox using this domain + # See: https://forum.yunohost.org/t/how-to-keep-your-mailbox-outside-yunohost/4860 + echo "# This file is regenerated automatically" > "${postfix_dir}/virtual-mailbox-domains" + echo "# Please DO NOT edit manually ... changes will be overwritten!" >> "${postfix_dir}/virtual-mailbox-domains" + tr ' ' '\n' <<< "$YNH_DOMAINS_WITH_MAIL_IN" >> "${postfix_dir}/virtual-mailbox-domains" + + cat postsrsd \ + | sed "s/{{ main_domain }}/${main_domain}/g" \ + | sed "s/{{ domain_list }}/${domain_list}/g" \ + > "${default_dir}/postsrsd" + + # adapt it for IPv4-only hosts + 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' \ + "${postfix_dir}/main.cf" + sed -i \ + 's/inet_interfaces = all/&\ninet_protocols = ipv4/' \ + "${postfix_dir}/main.cf" + fi +} + +do_post_regen() { + regen_conf_files=$1 + + chown postfix /etc/postfix + + if [ -e /etc/postfix/sasl_passwd ]; then + chmod 750 /etc/postfix/sasl_passwd* + chown postfix:root /etc/postfix/sasl_passwd* + postmap /etc/postfix/sasl_passwd + fi + + if [ -e /etc/postfix/relay_recipients ]; then + chmod 750 /etc/postfix/relay_recipients* + chown postfix:root /etc/postfix/relay_recipients* + fi + + 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; } + +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot new file mode 100755 index 0000000..23928c9 --- /dev/null +++ b/hooks/conf_regen/25-dovecot @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/dovecot + + dovecot_dir="${pending_dir}/etc/dovecot" + mkdir -p "${dovecot_dir}/global_script" + + # copy simple conf files + cp dovecot-ldap.conf "${dovecot_dir}/dovecot-ldap.conf" + cp dovecot.sieve "${dovecot_dir}/global_script/dovecot.sieve" + + export pop3_enabled="$(jq -r '.pop3_enabled' <<< "$YNH_SETTINGS" | int_to_bool)" + export main_domain=$(cat /etc/yunohost/current_host) + export domain_list="$YNH_DOMAINS_WITH_MAIL_IN_AND_OUT" + + ynh_render_template "dovecot.conf" "${dovecot_dir}/dovecot.conf" + + # adapt it for IPv4-only hosts + if [ ! -f /proc/net/if_inet6 ]; then + sed -i \ + 's/^\(listen =\).*/\1 */' \ + "${dovecot_dir}/dovecot.conf" + fi + + mkdir -p "${dovecot_dir}/yunohost.d" + cp pre-ext.conf "${dovecot_dir}/yunohost.d" + cp post-ext.conf "${dovecot_dir}/yunohost.d" +} + +do_post_regen() { + regen_conf_files=$1 + + mkdir -p "/etc/dovecot/yunohost.d/pre-ext.d" + mkdir -p "/etc/dovecot/yunohost.d/post-ext.d" + + # create vmail user + id vmail > /dev/null 2>&1 \ + || { + 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 + + # fix permissions + chown -R vmail:mail /etc/dovecot/global_script + chmod 770 /etc/dovecot/global_script + chown root:mail /var/mail + chmod 1775 /var/mail + + 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 + [[ "$regen_conf_files" =~ dovecot\.sieve ]] && { + sievec /etc/dovecot/global_script/dovecot.sieve + chown -R vmail:mail /etc/dovecot/global_script + } + + systemctl restart dovecot +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/30-opendkim b/hooks/conf_regen/30-opendkim new file mode 100755 index 0000000..3f63534 --- /dev/null +++ b/hooks/conf_regen/30-opendkim @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +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 + for domain in $YNH_DOMAINS_WITH_MAIL_IN_AND_OUT; do + domain_key="/etc/dkim/${domain}.mail.key" + if [ ! -f "$domain_key" ]; then + opendkim-genkey --domain="$domain" \ + --selector=mail --directory=/etc/dkim -b 2048 + mv /etc/dkim/mail.private "$domain_key" + mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" + fi + + 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" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/34-mysql b/hooks/conf_regen/34-mysql new file mode 100755 index 0000000..4ae7ced --- /dev/null +++ b/hooks/conf_regen/34-mysql @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +if ! dpkg --list | grep -q '^ii\s*mariadb-server\s'; then + echo 'mysql/mariadb is not installed, skipping' + exit 0 +fi + +do_pre_regen() { + # Nothing to do + : +} + +do_post_regen() { + regen_conf_files=$1 + + if [[ ! -d /var/lib/mysql/mysql ]]; then + # dpkg-reconfigure will initialize mysql (if it ain't already) + # It enabled auth_socket for root, so no need to define any root password... + # c.f. : cat /var/lib/dpkg/info/mariadb-server-10.3.postinst | grep install_db -C3 + MYSQL_PKG="$(dpkg --list | sed -ne 's/^ii \(mariadb-server-[[:digit:].]\+\) .*$/\1/p')" + dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 + + if ! systemctl -q is-active mariadb.service; then + systemctl start mariadb + fi + sleep 5 + + if ! echo "" | mysql; then + echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" >&2 + fi + fi + + # mysql is supposed to be an alias to mariadb... but in some weird case is not + # c.f. https://forum.yunohost.org/t/mysql-ne-fonctionne-pas/11661 + # Playing with enable/disable allows to recreate the proper symlinks. + if [ ! -e /etc/systemd/system/mysql.service ]; then + systemctl stop mysql -q + systemctl disable mysql -q + systemctl disable mariadb -q + systemctl enable mariadb -q + if ! systemctl is-active mariadb -q; then + systemctl start mariadb + fi + fi + + if [[ -n "$regen_conf_files" ]]; then + systemctl restart mysql + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/35-postgresql b/hooks/conf_regen/35-postgresql new file mode 100755 index 0000000..f4878b4 --- /dev/null +++ b/hooks/conf_regen/35-postgresql @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +if ! dpkg --list | grep -q "^ii\s*postgresql-$PSQL_VERSION\s"; then + echo 'postgresql is not installed, skipping' + exit 0 +fi + +if [ ! -e "/etc/postgresql/$PSQL_VERSION" ]; then + ynh_die --message="It looks like postgresql was not properly configured ? /etc/postgresql/$PSQL_VERSION is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist" +fi + +do_pre_regen() { + # Nothing to do + : +} + +do_post_regen() { + #regen_conf_files=$1 + + # Make sure postgresql is started and enabled + # (N.B. : to check the active state, we check the cluster state because + # postgresql could be flagged as active even though the cluster is in + # failed state because of how the service is configured..) + if ! systemctl is-active "postgresql@$PSQL_VERSION-main" -q; then + ynh_systemd_action --service_name=postgresql --action=restart + fi + if ! systemctl is-enabled postgresql -q; then + systemctl enable postgresql --quiet + fi + + # If this is the very first time, we define the root password + # and configure a few things + if [ ! -f "$PSQL_ROOT_PWD_FILE" ] || [ ! -s "$PSQL_ROOT_PWD_FILE" ]; then + ynh_string_random > "$PSQL_ROOT_PWD_FILE" + fi + chown root:postgres "$PSQL_ROOT_PWD_FILE" + chmod 440 "$PSQL_ROOT_PWD_FILE" + + sudo --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$(cat "$PSQL_ROOT_PWD_FILE")'" postgres + + # force all user to connect to local databases using hashed passwords + # https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html#EXAMPLE-PG-HBA.CONF + # Note: we can't use peer since YunoHost create users with nologin + # See: https://github.com/YunoHost/yunohost/blob/unstable/data/helpers.d/user + local pg_hba=/etc/postgresql/$PSQL_VERSION/main/pg_hba.conf + ynh_replace_string --match_string="local\(\s*\)all\(\s*\)all\(\s*\)peer" --replace_string="local\1all\2all\3md5" --target_file="$pg_hba" + + ynh_systemd_action --service_name=postgresql --action=reload +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/37-mdns b/hooks/conf_regen/37-mdns new file mode 100755 index 0000000..3b41a59 --- /dev/null +++ b/hooks/conf_regen/37-mdns @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +_generate_config() { + echo "domains:" + # Add yunohost.local (only if yunohost.local ain't already in ynh_domains) + if ! echo "${YNH_DOMAINS:-}" | tr ' ' '\n' | grep -q --line-regexp 'yunohost.local'; then + echo " - yunohost.local" + fi + for domain in ${YNH_DOMAINS:-}; do + # Only keep .local domains (don't keep + [[ "$domain" =~ [^.]+\.[^.]+\.local$ ]] && echo "Subdomain $domain cannot be handled by Bonjour/Zeroconf/mDNS" >&2 + [[ "$domain" =~ ^[^.]+\.local$ ]] || continue + echo " - $domain" + done + if [[ -e /etc/yunohost/mdns.aliases ]]; then + for localalias in $(cat /etc/yunohost/mdns.aliases | grep -v "^ *$"); do + echo " - $localalias.local" + done + fi +} + +do_init_regen() { + do_pre_regen "" + do_post_regen /etc/systemd/system/yunomdns.service + systemctl enable yunomdns --quiet +} + +do_pre_regen() { + pending_dir="$1" + + cd /usr/share/yunohost/conf/mdns + mkdir -p "$pending_dir/etc/systemd/system/" + cp yunomdns.service "$pending_dir/etc/systemd/system/" + + if ! getent passwd mdns &> /dev/null; then + useradd --no-create-home --shell /usr/sbin/nologin --system --user-group mdns + fi + + mkdir -p "$pending_dir/etc/yunohost" + _generate_config > "$pending_dir/etc/yunohost/mdns.yml" +} + +do_post_regen() { + regen_conf_files="$1" + + chown mdns:mdns /etc/yunohost/mdns.yml + + # If we changed the systemd ynh-override conf + if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/yunomdns.service$"; then + systemctl daemon-reload + fi + + # Legacy stuff to enable the new yunomdns service on legacy systems + if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf; then + systemctl enable yunomdns --now --quiet + sleep 2 + fi + + if [[ -n "$regen_conf_files" ]]; then + systemctl restart yunomdns + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/40-nftables b/hooks/conf_regen/40-nftables new file mode 100644 index 0000000..d311a18 --- /dev/null +++ b/hooks/conf_regen/40-nftables @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -e + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +PY_LIST_PORTS_OF=" +import os +import yaml +file = os.environ['FILE'] +proto = os.environ['PROTO'] +data = yaml.safe_load(open(file, 'r')) +ports = [str(port) for port, info in data.get(proto, {}).items() if info['open']] +# Sane fallback in case for reason we cant find any TCP port opened which probably indicates there's an issue with the file, we don't want the server to just drop all the traffic +if not ports and proto == 'TCP': + ports = [22, 80, 443] +print(' '.join(ports)) +" + +do_pre_regen() { + pending_dir=$1 + + firewall_file="/etc/yunohost/firewall.yml" + + tcp_ports=$(FILE=$firewall_file PROTO=tcp python3 -c "$PY_LIST_PORTS_OF") + udp_ports=$(FILE=$firewall_file PROTO=udp python3 -c "$PY_LIST_PORTS_OF") + export tcp_ports udp_ports + + cd /usr/share/yunohost/conf/nftables + mkdir -p "${pending_dir}/etc/nftables.d" + cp nftables.conf "${pending_dir}/etc/nftables.conf" + ynh_render_template nftables.d/yunohost-firewall.tpl.conf "${pending_dir}/etc/nftables.d/yunohost-firewall.conf" +} + +do_post_regen() { + regen_conf_files=$1 + + if ls -l /etc/nftables.d/*.conf > /dev/null; then + chown root:root /etc/nftables.d/*.conf + chmod 644 /etc/nftables.d/*.conf + fi + + [[ -z "$regen_conf_files" ]] \ + || systemctl restart nftables +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/43-dnsmasq b/hooks/conf_regen/43-dnsmasq new file mode 100755 index 0000000..f790559 --- /dev/null +++ b/hooks/conf_regen/43-dnsmasq @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -e + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +do_init_regen() { + + cd /usr/share/yunohost/conf/dnsmasq + + if jq -re '.dns_custom_resolvers_enabled' <<< "$YNH_SETTINGS"; then + read -ra nameservers <<< "$(jq -r '.dns_custom_resolvers_list' <<< "$YNH_SETTINGS")" + for nameserver in "${nameservers[@]}"; do + echo "nameserver $nameserver" >> /etc/resolv.dnsmasq.conf + done + else + # Use a seed derived from the machine id and current month + # This way, the shuffle is random but should be stable accross the same month + # and make sure the regenconf is idempotent (at least during the same month) + # i.e. that it doesn't re-shuffle the file everytime we run the regenconf + SEED=$( ( + cat /etc/machine-id || true + date +%m%Y + ) | md5sum) + cat plain/resolv.dnsmasq.conf | grep "^nameserver" | shuf --random-source=<(echo "$SEED") > /etc/resolv.dnsmasq.conf + fi + chown root /etc/resolv.dnsmasq.conf + chmod 644 /etc/resolv.dnsmasq.conf + + cp plain/etcdefault /etc/default/dnsmasq + + export wireless_interfaces="" + ynh_render_template "dnsmasq.conf.tpl" "${pending_dir}/etc/dnsmasq.conf" + + # Remove / disable services likely to conflict with dnsmasq + for SERVICE in systemd-resolved bind9; do + systemctl is-enabled $SERVICE &> /dev/null && systemctl disable $SERVICE 2> /dev/null + systemctl is-active $SERVICE &> /dev/null && systemctl stop $SERVICE + done + + systemctl restart dnsmasq +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/dnsmasq + + # create directory for pending conf + dnsmasq_dir="${pending_dir}/etc/dnsmasq.d" + mkdir -p "$dnsmasq_dir" + etcdefault_dir="${pending_dir}/etc/default" + mkdir -p "$etcdefault_dir" + + # add default conf files + cp plain/etcdefault "${pending_dir}/etc/default/dnsmasq" + + # add resolver pool + if jq -re '.dns_custom_resolvers_enabled' <<< "$YNH_SETTINGS"; then + read -ra nameservers <<< "$(jq -r '.dns_custom_resolvers_list' <<< "$YNH_SETTINGS" | sed 's/,/ /g')" + for nameserver in "${nameservers[@]}"; do + echo "nameserver $nameserver" >> "${pending_dir}/etc/resolv.dnsmasq.conf" + done + else + # Use a seed derived from the machine id and current month + # This way, the shuffle is random but should be stable accross the same month + # and make sure the regenconf is idempotent (at least during the same month) + # i.e. that it doesn't re-shuffle the file everytime we run the regenconf + SEED=$( ( + cat /etc/machine-id || true + date +%m%Y + ) | md5sum) + cat plain/resolv.dnsmasq.conf | grep "^nameserver" | shuf --random-source=<(echo "$SEED") > "${pending_dir}/etc/resolv.dnsmasq.conf" + fi + + # retrieve variables + ipv4=$(curl --max-time 10 -s -4 https://ipv4.yunohost.org 2> /dev/null || true) + ynh_validate_ip4 "$ipv4" || ipv4='127.0.0.1' + ipv6=$(curl --max-time 10 -s -6 https://ipv6.yunohost.org 2> /dev/null || true) + ynh_validate_ip6 "$ipv6" || ipv6='' + interfaces="$(ip -j addr show | jq -r '[.[].ifname]|join(" ")')" + wireless_interfaces="lo" + for dev in /sys/class/net/*; do + if [ -d "$dev/wireless" ] && grep -q "up" "$dev/operstate"; then + wireless_interfaces+=" $(basename "$dev")" + fi + done + + # General configuration + export wireless_interfaces + ynh_render_template "dnsmasq.conf.tpl" "${pending_dir}/etc/dnsmasq.conf" + + # add domain conf files + export interfaces + export ipv4 + export ipv6 + for domain in $YNH_DOMAINS; do + [[ ! $domain =~ \.local$ ]] || continue + export domain + + if tr ' ' '\n' <<< "$YNH_DOMAINS_WITH_MAIL_IN" | grep -q "^$domain$"; then + export mail_in="True" + else + export mail_in="False" + fi + ynh_render_template "domain.tpl" "${dnsmasq_dir}/${domain}" + done + + # We arbitrarily pick 'c' for spamhaus NS but there's a/b/c/e + SPAMHAUS_NS=c.gns.spamhaus.org + # We need to perform a dig request ... but dnsmasq may not be up yet, and we'll get an empty result + # It's not too dramatic because next time the regenconf is ran, dnsmasq should be up + # Nevertheless it's good to try to make sure that this doesn't happen to avoid weird stuff where + # the regenconf is not idempotent ... + # So if dnsmasq is not up, try to pick the first IPv4 resolver from the shuffled list + if systemctl --quiet is-active dnsmasq; then + RESOLVER_FOR_DIG="127.0.0.1" + else + RESOLVER_FOR_DIG=$(grep '^nameserver.*\.' /etc/resolv.dnsmasq.conf | head -n1 | awk '{print $2}') + fi + + cat << EOF > "${dnsmasq_dir}/spamhaus" +# Gotta force the usage of resolvers for spamhaus, +# Which will otherwise complain that we may be using an open resolver... +# cf https://www.spamhaus.com/resource-center/successfully-accessing-spamhauss-free-block-lists-using-a-public-dns/#yes-but-why-block-queries-from-public-recursive-name-servers +# We pick one of spamhaus' a/b/c/d/e nameservers, cf https://multirbl.valli.org/detail/zen.spamhaus.org.html +# And kind of hard-code the corresponding IPs because in practice dnsmasq doesn't allow to have a domain name for the resolver part of server= :| ... +# Fun-fact : did you know that AAAA is the name of IPv6 DNS records, but also the sound you make when debugging network and DNS??? #TheMoreYouKnow +EOF + for IP in $( ( + dig +short A $SPAMHAUS_NS "@$RESOLVER_FOR_DIG" 2> /dev/null || true + dig +short AAAA $SPAMHAUS_NS "@$RESOLVER_FOR_DIG" 2> /dev/null || true + ) | grep -v '^;' | sort); do + echo "server=/*.zen.spamhaus.org/$IP" >> "${dnsmasq_dir}/spamhaus" + done + + # remove old domain conf files + for conf_file in /etc/dnsmasq.d/*.*; do + domain=$(basename "$conf_file") + if [[ ! $YNH_DOMAINS =~ $domain ]] && [[ ! $domain =~ \.local$ ]] && [[ $domain != spamhaus ]]; then + touch "${dnsmasq_dir}/${domain}" + fi + done +} + +do_post_regen() { + regen_conf_files=$1 + + # Force permission (to cover some edge cases where root's umask is like 027 and then dnsmasq cant read this file) + chown 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 + if grep -q -E "^ *(domain|search)" /run/resolvconf/resolv.conf; then + if grep -q -E "^ *(domain|search)" /run/resolvconf/interface/*.dhclient 2> /dev/null; then + sed -E "s/^(domain|search)/#\1/g" -i /run/resolvconf/interface/*.dhclient + fi + + grep -q '^supersede domain-name "";' /etc/dhcp/dhclient.conf 2> /dev/null || echo 'supersede domain-name "";' >> /etc/dhcp/dhclient.conf + grep -q '^supersede domain-search "";' /etc/dhcp/dhclient.conf 2> /dev/null || echo 'supersede domain-search "";' >> /etc/dhcp/dhclient.conf + grep -q '^supersede search "";' /etc/dhcp/dhclient.conf 2> /dev/null || echo 'supersede search "";' >> /etc/dhcp/dhclient.conf + systemctl restart resolvconf + fi + + # Some stupid things like rabbitmq-server used by onlyoffice won't work if + # the *short* hostname doesn't exists in /etc/hosts -_- + short_hostname=$(hostname -s) + grep -q "127.0.0.1.*$short_hostname" /etc/hosts || echo -e "\n127.0.0.1\t$short_hostname" >> /etc/hosts + + [[ -n "$regen_conf_files" ]] || return 0 + + # Remove / disable services likely to conflict with dnsmasq + for SERVICE in systemd-resolved bind9; do + systemctl is-enabled $SERVICE &> /dev/null && systemctl disable $SERVICE 2> /dev/null + systemctl is-active $SERVICE &> /dev/null && systemctl stop $SERVICE + done + + systemctl restart dnsmasq +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/46-nsswitch b/hooks/conf_regen/46-nsswitch new file mode 100755 index 0000000..0c241a1 --- /dev/null +++ b/hooks/conf_regen/46-nsswitch @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +do_init_regen() { + do_pre_regen "" + systemctl restart unscd +} + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/nsswitch + + install -D -m 644 nsswitch.conf "$pending_dir/etc/nsswitch.conf" +} + +do_post_regen() { + regen_conf_files=$1 + + if [[ -n "$regen_conf_files" ]]; then + systemctl restart unscd + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/conf_regen/52-fail2ban b/hooks/conf_regen/52-fail2ban new file mode 100755 index 0000000..f79e609 --- /dev/null +++ b/hooks/conf_regen/52-fail2ban @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +do_pre_regen() { + pending_dir=$1 + + cd /usr/share/yunohost/conf/fail2ban + + fail2ban_dir="${pending_dir}/etc/fail2ban" + mkdir -p "${fail2ban_dir}/filter.d" + mkdir -p "${fail2ban_dir}/jail.d" + mkdir -p "${pending_dir}/etc/systemd/system/fail2ban.service.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" + cp systemd-override-bind-nftables.conf "${pending_dir}/etc/systemd/system/fail2ban.service.d/systemd-override-bind-nftables.conf" + + 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 + + if [[ -n "$regen_conf_files" ]]; then + systemctl reload fail2ban + fi +} + +"do_$1_regen" "$(echo "${*:2}" | xargs)" diff --git a/hooks/post_user_create/ynh_multimedia b/hooks/post_user_create/ynh_multimedia new file mode 100644 index 0000000..4f49300 --- /dev/null +++ b/hooks/post_user_create/ynh_multimedia @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +user=$1 + +readonly MEDIA_GROUP=multimedia +readonly MEDIA_DIRECTORY=/home/yunohost.multimedia + +# We only do this if multimedia directory is enabled (= the folder exists) +[ -e "$MEDIA_DIRECTORY" ] || exit 0 + +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 +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" + +## 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/$user" +# 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/$user" +# 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/$user" diff --git a/hooks/post_user_delete/ynh_multimedia b/hooks/post_user_delete/ynh_multimedia new file mode 100644 index 0000000..35f834a --- /dev/null +++ b/hooks/post_user_delete/ynh_multimedia @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +user=$1 +MEDIA_DIRECTORY=/home/yunohost.multimedia + +if [ -n "$user" ] && [ -e "$MEDIA_DIRECTORY/$user" ]; then + sudo rm -r "$MEDIA_DIRECTORY/$user" +fi diff --git a/hooks/restore/05-conf_ldap b/hooks/restore/05-conf_ldap new file mode 100644 index 0000000..2aa5bec --- /dev/null +++ b/hooks/restore/05-conf_ldap @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +backup_dir="${1}/conf/ldap" + +systemctl stop slapd + +# Create a directory for backup +TMPDIR="/tmp/$(date +%s)" +mkdir -p "$TMPDIR" + +die() { + state=$1 + error=$2 + + # Restore saved configuration and database + [[ $state -ge 1 ]] \ + && (rm -rf /etc/ldap/slapd.d \ + && mv "${TMPDIR}/slapd.d" /etc/ldap/slapd.d) + [[ $state -ge 2 ]] \ + && (rm -rf /var/lib/ldap \ + && mv "${TMPDIR}/ldap" /var/lib/ldap) + chown -R openldap: /etc/ldap/slapd.d /var/lib/ldap + + systemctl start slapd + rm -rf "$TMPDIR" + + # Print an error message and exit + printf "%s" "$error" 1>&2 + exit 1 +} + +# Restore the configuration +mv /etc/ldap/slapd.d "$TMPDIR" +mkdir -p /etc/ldap/slapd.d +cp -a "${backup_dir}/ldap.conf" /etc/ldap/ldap.conf +# Legacy thing but we need it to force the regen-conf in case of it exist +[ ! -e "${backup_dir}/slapd.conf" ] \ + || cp -a "${backup_dir}/slapd.conf" /etc/ldap/slapd.conf +slapadd -F /etc/ldap/slapd.d -b cn=config \ + -l "${backup_dir}/cn=config.master.ldif" \ + || die 1 "Unable to restore LDAP configuration" +chown -R openldap: /etc/ldap/slapd.d + +# Restore the database +mv /var/lib/ldap "$TMPDIR" +mkdir -p /var/lib/ldap +slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org \ + -l "${backup_dir}/dc=yunohost-dc=org.ldif" \ + || die 2 "Unable to restore LDAP database" +chown -R openldap: /var/lib/ldap + +systemctl start slapd +rm -rf "$TMPDIR" diff --git a/hooks/restore/17-data_home b/hooks/restore/17-data_home new file mode 100644 index 0000000..51ec8ed --- /dev/null +++ b/hooks/restore/17-data_home @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +backup_dir="$1/data/home" + +cp -a "$backup_dir/." /home diff --git a/hooks/restore/18-data_multimedia b/hooks/restore/18-data_multimedia new file mode 100644 index 0000000..af144ec --- /dev/null +++ b/hooks/restore/18-data_multimedia @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +backup_dir="data/multimedia" + +ynh_restore_file --origin_path="${backup_dir}" --dest_path="/home/yunohost.multimedia" --not_mandatory diff --git a/hooks/restore/20-conf_ynh_settings b/hooks/restore/20-conf_ynh_settings new file mode 100644 index 0000000..38de659 --- /dev/null +++ b/hooks/restore/20-conf_ynh_settings @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +backup_dir="$1/conf/ynh" + +cp -a "${backup_dir}/current_host" /etc/yunohost/current_host +cp -a "${backup_dir}/firewall.yml" /etc/yunohost/firewall.yml +cp -a "${backup_dir}/permissions.yml" /etc/yunohost/permissions.yml +[ ! -d "${backup_dir}/portal" ] || cp -a "${backup_dir}/portal" /etc/yunohost/portal +[ ! -d "${backup_dir}/domains" ] || cp -a "${backup_dir}/domains" /etc/yunohost/domains +[ ! -e "${backup_dir}/settings.yml" ] || cp -a "${backup_dir}/settings.yml" "/etc/yunohost/settings.yml" +[ ! -d "${backup_dir}/dyndns" ] || cp -raT "${backup_dir}/dyndns" "/etc/yunohost/dyndns" +[ ! -d "${backup_dir}/dkim" ] || cp -raT "${backup_dir}/dkim" "/etc/dkim" diff --git a/hooks/restore/21-conf_ynh_certs b/hooks/restore/21-conf_ynh_certs new file mode 100644 index 0000000..da523d6 --- /dev/null +++ b/hooks/restore/21-conf_ynh_certs @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +backup_dir="$1/conf/ynh/certs" + +mkdir -p /etc/yunohost/certs/ + +cp -a "$backup_dir/." /etc/yunohost/certs/ diff --git a/hooks/restore/23-data_mail b/hooks/restore/23-data_mail new file mode 100644 index 0000000..7f51222 --- /dev/null +++ b/hooks/restore/23-data_mail @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +backup_dir="$1/data/mail" + +cp -a "$backup_dir/." /var/mail/ || echo 'No mail found' +chown -R vmail:mail /var/mail/ diff --git a/hooks/restore/50-conf_manually_modified_files b/hooks/restore/50-conf_manually_modified_files new file mode 100644 index 0000000..e09894d --- /dev/null +++ b/hooks/restore/50-conf_manually_modified_files @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +# shellcheck source=../../helpers/helpers +source /usr/share/yunohost/helpers + +ynh_abort_if_errors +YNH_CWD="${YNH_BACKUP_DIR%/}/conf/manually_modified_files" +cd "$YNH_CWD" + +for file in $(cat ./manually_modified_files_list); do + ynh_restore_file --origin_path="$file" --not_mandatory +done + +ynh_restore_file --origin_path="/etc/ssowat/conf.json.persistent" --not_mandatory diff --git a/locales/ar.json b/locales/ar.json new file mode 100644 index 0000000..97f6da6 --- /dev/null +++ b/locales/ar.json @@ -0,0 +1,262 @@ +{ + "aborting": "إلغاء.", + "action_invalid": "إجراء غير صالح '{action}'", + "admin_password": "كلمة السر الإدارية", + "admins": "المدراء", + "all_users": "كافة مستخدمي واي يونوهوست", + "already_up_to_date": "كل شيء على ما يرام. ليس هناك ما يتطلّب تحديثًا.", + "app_action_broke_system": "يبدو أنّ هذا الإجراء أدّى إلى تحطيم هذه الخدمات المهمة: {services}", + "app_already_installed": "{app} تم تنصيبه مِن قبل", + "app_change_url_success": "تم تعديل الرابط التشعبي لتطبيق {app} إلى {domain}{path}", + "app_extraction_failed": "تعذر فك الضغط عن ملفات التنصيب", + "app_id_invalid": "مُعرّف التطبيق غير صالح", + "app_install_failed": "لا يمكن تنصيب {app}: {error}", + "app_install_files_invalid": "لا يمكن تنصيب هذه الملفات", + "app_manifest_install_ask_admin": "اختر مستخدمًا إداريًا لهذا التطبيق", + "app_manifest_install_ask_domain": "اختر اسم النطاق الذي ينبغي فيه تنصيب هذا التطبيق", + "app_manifest_install_ask_is_public": "هل يجب أن يكون هذا التطبيق ظاهرًا للزوار المجهولين؟", + "app_manifest_install_ask_password": "اختيار كلمة إدارية لهذا التطبيق", + "app_manifest_install_ask_path": "اختر مسار URL (بعد النطاق) حيث ينبغي تنصيب هذا التطبيق", + "app_not_correctly_installed": "يبدو أن التطبيق {app} لم يتم تنصيبه بشكل صحيح", + "app_not_installed": "إنّ التطبيق {app} غير مُنصَّب", + "app_not_properly_removed": "لم يتم حذف تطبيق {app} بشكلٍ جيّد", + "app_remove_after_failed_install": "جارٍ حذف التطبيق بعدما فشل تنصيبه…", + "app_removed": "تمت إزالة تطبيق {app}", + "app_requirements_checking": "جار فحص متطلبات تطبيق {app}…", + "app_sources_fetch_failed": "تعذر جلب ملفات المصدر ، هل عنوان URL صحيح؟", + "app_start_install": "جارٍ تثبيت {app}…", + "app_start_remove": "جارٍ حذف {app}…", + "app_start_restore": "جارٍ استرجاع {app}…", + "app_unknown": "برنامج مجهول", + "app_upgrade_app_name": "جارٍ تحديث {app}…", + "app_upgrade_failed": "تعذرت عملية تحديث {app}: {error}", + "app_upgrade_several_apps": "سوف يتم تحديث التطبيقات التالية: {apps}", + "app_upgrade_some_app_failed": "تعذرت عملية ترقية بعض التطبيقات", + "app_upgraded": "تم تحديث التطبيق {app}", + "apps_already_up_to_date": "كافة التطبيقات مُحدّثة", + "apps_catalog_update_success": "تم تحديث فهرس التطبيقات!", + "apps_catalog_updating": "جارٍ تحديث فهرس التطبيقات…", + "ask_admin_fullname": "الإسم الكامل للمدير", + "ask_admin_username": "اسم المستخدِم للمدير", + "ask_fullname": "الاسم الكامل (اللقب والاسم)", + "ask_main_domain": "النطاق الرئيسي", + "ask_new_admin_password": "كلمة السر الإدارية الجديدة", + "ask_new_domain": "نطاق جديد", + "ask_new_path": "مسار جديد", + "ask_password": "كلمة السر", + "ask_user_domain": "اسم النطاق الذي سيُستخدَم لعنوان بريد المستخدِم وكذا لحساب XMPP", + "backup_applying_method_copy": "جارٍ نسخ كافة الملفات المراد نسخها احتياطيا…", + "backup_applying_method_tar": "جارٍ إنشاء ملف TAR للنسخة الاحتياطية…", + "backup_archive_name_unknown": "أرشيف نسخ احتياطي محلي غير معروف باسم '{name}'", + "backup_archive_open_failed": "تعذر فتح النسخة الاحتياطية", + "backup_copying_to_organize_the_archive": "نسخ {size} ميغا بايت لتنظيم الأرشيف", + "backup_created": "تم إنشاء النسخة الإحتياطية: {name}", + "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", + "backup_mount_archive_for_restore": "جارٍ تهيئة النسخة الاحتياطية للاسترجاع…", + "backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية", + "cannot_open_file": "ليس بالإمكان فتح الملف {file} (السبب : {error})", + "cannot_write_file": "لا يمكن الكتابة في الملف {file} (السبب : {error})", + "certmanager_cert_install_failed": "أخفقت عملية تنصيب شهادة Let's Encrypt على {domains}", + "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق '{domain}'", + "certmanager_cert_install_success_selfsigned": "نجحت عملية تثبيت الشهادة الموقعة ذاتيا الخاصة بالنطاق '{domain}'", + "certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق '{domain}'", + "certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة", + "certmanager_no_cert_file": "تعذرت عملية قراءة ملف شهادة نطاق {domain} (الملف : {file})", + "corrupted_json": "قراءة ملف JSON مُشوّهة مِن {ressource} (السبب : {error})", + "corrupted_toml": "قراءة مُشوّهة لملف TOML مِن {ressource} (السبب : {error})", + "corrupted_yaml": "قراءة مُشوّهة لملف YAML مِن {ressource} (السبب : {error})", + "danger": "خطر:", + "diagnosis_apps_issue": "تم العثور على مشكلة في تطبيق {app}", + "diagnosis_basesystem_hardware": "بنية الخادم هي {virt} {arch}", + "diagnosis_basesystem_hardware_model": "طراز الخادم {model}", + "diagnosis_basesystem_host": "هذا الخادم يُشغّل ديبيان {debian_version}", + "diagnosis_basesystem_kernel": "هذا الخادم يُشغّل نواة لينكس {kernel_version}", + "diagnosis_basesystem_ynh_main_version": "هذا الخادم يُشغّل YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} الإصدار: {version} ({repo})", + "diagnosis_description_apps": "التطبيقات", + "diagnosis_description_basesystem": "النظام الأساسي", + "diagnosis_description_dnsrecords": "تسجيلات خدمة DNS", + "diagnosis_description_ip": "الإتصال بالإنترنت", + "diagnosis_description_mail": "البريد الإلكتروني", + "diagnosis_description_regenconf": "إعدادات النظام", + "diagnosis_description_services": "حالة الخدمات", + "diagnosis_description_systemresources": "موارد النظام", + "diagnosis_description_web": "الويب", + "diagnosis_dns_good_conf": "تم إعداد سجلات نظام أسماء النطاقات DNS بشكل صحيح للنطاق {domain} (category {category})", + "diagnosis_domain_expiration_error": "ستنتهي مدة صلاحية بعض النطاقات في القريب العاجل!", + "diagnosis_domain_expiration_warning": "ستنتهي مدة صلاحية بعض النطاقات قريبًا!", + "diagnosis_everything_ok": "كل شيء يبدو على ما يرام في {category}!", + "diagnosis_http_could_not_diagnose_details": "خطأ: {error}", + "diagnosis_ignored_issues": "(+ {nb_ignored} مشاكل تم تجاهلها)", + "diagnosis_ip_connected_ipv4": "الخادم مُتّصل بالإنترنت عبر IPv4!", + "diagnosis_ip_connected_ipv6": "الخادم مُتّصل بالإنترنت عبر IPv6!", + "diagnosis_ip_dnsresolution_working": "تحليل اسم النطاق يعمل!", + "diagnosis_ip_not_connected_at_all": "يبدو أنّ الخادم غير مُتّصل بتاتا بالإنترنت!؟", + "diagnosis_mail_blocklist_listed_by": "إن عنوان الـ IP الخاص بك أو نطاقك {item} مُدرَج ضمن قائمة سوداء على {blocklist_name}", + "diagnosis_mail_ehlo_ok": "يمكن الوصول إلى خادم بريد SMTP من الخارج وبالتالي فهو قادر على استقبال رسائل البريد الإلكتروني!", + "diagnosis_mail_outgoing_port_25_ok": "خادم بريد SMTP قادر على إرسال رسائل البريد الإلكتروني (منفذ البريد الصادر 25 غير محظور).", + "diagnosis_mail_queue_ok": "هناك {nb_pending} رسائل بريد إلكتروني معلقة في قوائم انتظار البريد", + "diagnosis_mail_queue_unavailable_details": "خطأ: {error}", + "diagnosis_ports_could_not_diagnose_details": "خطأ: {error}", + "diagnosis_ports_ok": "المنفذ {port} مفتوح ومتاح الوصول إليه مِن الخارج.", + "diagnosis_ports_unreachable": "المنفذ {port} غير متاح الوصول إليه مِن الخارج.", + "diagnosis_services_bad_status": "خدمة {service} {status} :(", + "diagnosis_services_running": "خدمة {service} شغّالة!", + "diagnosis_unknown_categories": "الفئات التالية غير معروفة: {categories}", + "disk_space_not_sufficient_install": "ليس هناك مساحة كافية لتنصيب هذا التطبيق", + "disk_space_not_sufficient_update": "ليس هناك مساحة كافية لتحديث هذا التطبيق", + "domain_cert_gen_failed": "لا يمكن إعادة توليد الشهادة", + "domain_config_api_protocol": "بروتوكول API", + "domain_config_auth_application_secret": "المفتاح السري للتطبيق", + "domain_config_auth_consumer_key": "مفتاح المستخدِم", + "domain_config_auth_entrypoint": "نقطة الدخول API", + "domain_config_auth_key": "مفتاح التوثيق", + "domain_config_cert_install": "تنصيب شهادة Let's Encrypt", + "domain_config_cert_issuer": "الهيئة الموثِّقة", + "domain_config_cert_renew": "تجديد شهادة Let's Encrypt", + "domain_config_cert_summary": "حالة الشهادة", + "domain_config_cert_summary_abouttoexpire": "مدة صلاحية الشهادة الحالية على وشك الإنتهاء ومِن المفتَرض أن يتم تجديدها تلقائيا قريبا.", + "domain_config_cert_summary_letsencrypt": "هنيئا! إنّك تستخدم الآن شهادة Let's Encrypt صالحة!", + "domain_config_cert_summary_ok": "حسنًا، يبدو أنّ الشهادة الحالية جيدة!", + "domain_config_cert_validity": "مدة الصلاحية", + "domain_config_default_app": "التطبيق الافتراضي", + "domain_config_default_app_help": "سيعاد توجيه الناس تلقائيا إلى هذا التطبيق عند فتح اسم النطاق هذا. وإذا لم يُحدَّد أي تطبيق، يعاد توجيه الناس إلى استمارة تسجيل الدخولفي بوابة المستخدمين.", + "domain_config_mail_in": "البريد الوارد", + "domain_config_mail_out": "البريد الخارج", + "domain_created": "تم إنشاء النطاق", + "domain_creation_failed": "تعذرت عملية إنشاء النطاق {domain}: {error}", + "domain_deleted": "تم حذف النطاق", + "domain_dns_push_success": "تم تحديث ادخالات سِجِلات نظام أسماء النطاقات!", + "domain_exists": "اسم النطاق موجود سلفًا", + "domain_unknown": "النطاق '{domain}' مجهول", + "domains_available": "النطاقات المتوفرة :", + "done": "تم", + "download_bad_status_code": "{url} أعاد رمز الحالة {code}", + "download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url}", + "download_timeout": "{url} استغرق مدة طويلة جدا للإستجابة، فتوقّف.", + "download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url} : {error}", + "downloading": "عملية التنزيل جارية…", + "dyndns_could_not_check_available": "لا يمكن التحقق مِن أنّ {domain} متوفر على {provider}.", + "dyndns_ip_updated": "لقد تم تحديث عنوان الإيبي الخاص بك على نظام أسماء النطاقات الديناميكي", + "dyndns_key_not_found": "لم يتم العثور على مفتاح DNS الخاص باسم النطاق هذا", + "dyndns_unavailable": "النطاق '{domain}' غير متوفر.", + "error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path}: {error}", + "error_removing": "خطأ أثناء عملية حذف {path}: {error}", + "error_writing_file": "طرأ هناك خطأ أثناء الكتابة في الملف {file}: {error}", + "extracting": "عملية فك الضغط جارية…", + "field_invalid": "الحقل غير صحيح : '{field}'", + "file_not_exist": "الملف غير موجود : '{path}'", + "global_settings_setting_admin_strength": "قوة الكلمة السرية الإدارية", + "global_settings_setting_backup_compress_tar_archives": "ضغط النُسخ الاحتياطية", + "global_settings_setting_pop3_enabled": "تفعيل POP3", + "global_settings_setting_root_password": "كلمة السر الجديدة لـ root", + "global_settings_setting_root_password_confirm": "كلمة السر الجديدة لـ root (تأكيد)", + "global_settings_setting_security_experimental_enabled": "ميزات أمان تجريبية", + "global_settings_setting_smtp_allow_ipv6": "سماح IPv6", + "global_settings_setting_ssh_password_authentication": "الاستيثاق بكلمة سرية", + "global_settings_setting_ssh_port": "منفذ SSH", + "global_settings_setting_user_strength": "قوة الكلمة السرية للمستخدم", + "global_settings_setting_webadmin_allowlist": "قائمة عناوين الإيبي المسموح لها النفاذ إلى واجهة الويب الإدارية", + "good_practices_about_user_password": "أنت الآن على وشك تحديد كلمة مرور مستخدم جديدة. يجب أن تتكون كلمة المرور من 8 أحرف على الأقل - أخذا بعين الإعتبار أنه من الممارسات الجيدة استخدام كلمة مرور أطول (أي عبارة مرور) و / أو مجموعة متنوعة من الأحرف (الأحرف الكبيرة والصغيرة والأرقام والأحرف الخاصة).", + "group_created": "تم إنشاء الفريق '{group}'", + "group_deleted": "تم حذف الفريق '{group}'", + "group_deletion_failed": "فشلت عملية حذف الفريق '{group}': {error}", + "group_unknown": "الفريق '{group}' مجهول", + "hook_name_unknown": "إسم الإجراء '{name}' غير معروف", + "installation_complete": "إكتملت عملية التنصيب", + "invalid_url": "فشل الاتصال بـ {url}… ربما تكون الخدمة معطلة ، أو أنك غير متصل بشكل صحيح بالإنترنت في IPv4 / IPv6.", + "log_app_change_url": "تعديل رابط تطبيق '{}'", + "log_app_install": "تنصيب تطبيق '{}'", + "log_app_makedefault": "تعيين '{}' كتطبيق افتراضي", + "log_app_remove": "حذف تطبيق '{}'", + "log_app_upgrade": "تحديث تطبيق '{}'", + "log_available_on_yunopaste": "هذا السجل متوفر الآن على {url}", + "log_backup_restore_app": "استرجاع '{}' مِن نسخة احتياطية", + "log_backup_restore_system": "استرجاع النظام مِن نسخة احتياطية", + "log_domain_add": "إضافة النطاق '{}' إلى إعدادات النظام", + "log_domain_main_domain": "جعل '{}' كنطاق أساسي", + "log_domain_remove": "حذف النطاق '{}' مِن إعدادات النظام", + "log_dyndns_subscribe": "تسجيل اسم نطاق واي يونوهوست فرعي '{}'", + "log_dyndns_update": "تحديث عنوان الإيبي ذي الصلة مع اسم النطاق الفرعي واي يونوهوست '{}'", + "log_letsencrypt_cert_install": "تنصيب شهادة Let’s Encrypt على النطاق '{}'", + "log_letsencrypt_cert_renew": "تجديد شهادة Let's Encrypt لـ '{}'", + "log_remove_on_failed_install": "حذف '{}' بعد فشل التنصيب", + "log_selfsigned_cert_install": "تنصيب شهادة موقَّعَة ذاتيا على اسم النطاق '{}'", + "log_tools_reboot": "إعادة تشغيل الخادم", + "log_tools_shutdown": "إطفاء الخادم", + "log_tools_upgrade": "تحديث حُزم ديبيان", + "log_user_create": "إضافة المستخدم '{}'", + "log_user_delete": "حذف المستخدم '{}'", + "log_user_update": "تحديث معلومات المستخدم '{}'", + "mail_unavailable": "عنوان البريد الإلكتروني هذا مخصص لفريق المدراء", + "mailbox_disabled": "صندوق البريد معطل للمستخدم {user}", + "main_domain_change_failed": "تعذّر تغيير النطاق الأساسي", + "main_domain_changed": "تم تغيير النطاق الأساسي", + "migration_ldap_migration_failed_trying_to_rollback": "فشِلَت الهجرة… محاولة استعادة الرجوع إلى النظام.", + "migration_ldap_rollback_success": "تمت العودة إلى حالة النظام الأصلي.", + "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", + "migrations_running_forward": "جارٍ تنفيذ الهجرة {id}…", + "migrations_skip_migration": "جارٍ تجاهل التهجير {id}…", + "migrations_success_forward": "اكتملت الهجرة {id}", + "password_confirmation_not_the_same": "كلمة المرور وتأكيدها غير متطابقان", + "password_too_long": "فضلا قم باختيار كلمة مرور طولها أقل مِن 127 حرفًا", + "password_too_simple_1": "يجب أن يكون طول الكلمة السرية على الأقل 8 حروف", + "password_too_simple_2": "يجب أن يكون طول كلمة المرور 8 حروف على الأقل وأن تحتوي على أرقام وحروف علوية ودنيا", + "password_too_simple_3": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل وأن تحتوي على أرقام و حروف كبيرة وصغيرة وأحرف خاصة", + "password_too_simple_4": "يجب أن تتكون كلمة المرور من 12 حرفًا على الأقل وأن تحتوي على أرقام وحروف كبيرة وصغيرة وأحرف خاصة", + "pattern_domain": "يتوجب أن يكون إسم نطاق صالح (مثل my-domain.org)", + "pattern_email": "يتوجب أن يكون عنوان بريد إلكتروني صالح (مثل someone@domain.org)", + "pattern_fullname": "يجب أن يكون اسماً كاملاً صالحاً (على الأقل 3 حروف)", + "pattern_password": "يتوجب أن تكون مكونة من 3 حروف على الأقل", + "pattern_password_app": "آسف، كلمات السر لا يمكن أن تحتوي على الحروف التالية: {forbidden_chars}", + "restore_extracting": "جارٍ فك الضغط عن الملفات اللازمة من النسخة الاحتياطية…", + "root_password_changed": "تم تغيير كلمة مرور الجذر", + "root_password_desynchronized": "تم تغيير كلمة مرور المدير ، لكن لم يتمكن YunoHost من نشرها على كلمة مرور الجذر!", + "server_reboot": "سيعاد تشغيل الخادوم", + "server_reboot_confirm": "سيعاد تشغيل الخادوم في الحين. هل أنت متأكد ؟ [{answers}]", + "server_shutdown": "سوف ينطفئ الخادوم", + "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers}]", + "service_add_failed": "تعذرت إضافة خدمة '{service}'", + "service_already_stopped": "إنّ خدمة '{service}' متوقفة مِن قبلُ", + "service_description_dnsmasq": "مُكلَّف بتحليل أسماء النطاقات (DNS)", + "service_description_mysql": "يقوم بتخزين بيانات التطبيقات (قواعد بيانات SQL)", + "service_description_nftables": "يُدير فتح وإغلاق منافذ الاتصال إلى الخدمات", + "service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك", + "service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية", + "service_description_slapd": "يخزّن المستخدمين والنطاقات والمعلومات المتعلقة بها", + "service_description_yunohost-api": "يقوم بإدارة التفاعلات ما بين واجهة الويب لواي يونوهوست و النظام", + "service_description_yunomdns": "يسمح لك بالوصول إلى خادمك الخاص باستخدام 'yunohost.local' في شبكتك المحلية", + "service_disabled": "لن يتم إطلاق خدمة '{service}' أثناء بداية تشغيل النظام بتاتا.", + "service_enabled": "سيتم الآن بدء تشغيل الخدمة '{service}' تلقائيًا أثناء تمهيد النظام.", + "service_reloaded": "تم إعادة تشغيل خدمة '{service}'", + "service_removed": "تمت إزالة خدمة '{service}'", + "service_restarted": "تم إعادة تشغيل خدمة '{service}'", + "service_started": "تم إطلاق تشغيل خدمة '{service}'", + "service_stopped": "تمّ إيقاف خدمة '{service}'", + "service_unknown": "الخدمة '{service}' غير معروفة", + "system_upgraded": "تمت عملية ترقية النظام", + "tools_upgrade": "تحديث حُزم النظام", + "unbackup_app": "لن يتم حفظ التطبيق '{app}'", + "unknown_error_reading_file": "طرأ هناك خطأ ما أثناء عملية قراءة الملف {file} (السبب: {error})", + "unknown_group": "الفريق '{group}' مجهول", + "unknown_user": "المستخدم '{user}' مجهول", + "unlimit": "دون تحديد الحصة", + "unrestore_app": "لن يتم استعادة التطبيق '{app}'", + "updating_apt_cache": "جارٍ جلب قائمة حُزم النظام المحدّثة المتوفرة…", + "upgrading_packages": "عملية ترقية الحُزم جارية…", + "upnp_disabled": "تم تعطيل UPnP", + "user_already_exists": "المستخدم '{user}' موجود مِن قَبل", + "user_created": "تم إنشاء المستخدم", + "user_deleted": "تم حذف المستخدم", + "user_deletion_failed": "لا يمكن حذف المستخدم", + "user_import_bad_line": "سطر غير صحيح {line}: {details}", + "user_import_success": "تم استيراد المستخدمين بنجاح", + "user_unknown": "المستخدم {user} مجهول", + "user_update_failed": "لا يمكن تحديث المستخدم {user}: {error}", + "user_updated": "تم تحديث معلومات المستخدم", + "visitors": "الزوار", + "yunohost_already_installed": "إنّ YunoHost مُنصّب مِن قَبل", + "yunohost_configured": "تم إعداد YunoHost الآن", + "yunohost_installing": "عملية تنصيب واي يونوهوست جارية…", + "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب بشكل جيد. فضلًا قم بتنفيذ الأمر 'yunohost tools postinstall'" +} diff --git a/locales/bn_BD.json b/locales/bn_BD.json new file mode 100644 index 0000000..b542512 --- /dev/null +++ b/locales/bn_BD.json @@ -0,0 +1,3 @@ +{ + "password_too_simple_1": "পাসওয়ার্ডটি কমপক্ষে 8 টি অক্ষরের দীর্ঘ হওয়া দরকার" +} diff --git a/locales/br.json b/locales/br.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/br.json @@ -0,0 +1 @@ +{} diff --git a/locales/ca.json b/locales/ca.json new file mode 100644 index 0000000..ce2b413 --- /dev/null +++ b/locales/ca.json @@ -0,0 +1,918 @@ +{ + "aborting": "Avortant.", + "action_invalid": "Acció '{action}' invàlida", + "additional_urls_already_added": "URL addicional «{url}» ja ha estat afegida per al permís «{permission}»", + "additional_urls_already_removed": "URL addicional «{url}» ja ha estat eliminada per al permís «{permission}»", + "admin_password": "Contrasenya d'administració", + "admins": "Administradors", + "all_users": "Tots els usuaris de YunoHost", + "already_up_to_date": "No hi ha res a fer. Tot està actualitzat.", + "app_action_broke_system": "Aquesta acció sembla haver trencat els següents serveis importants: {services}", + "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).", + "app_action_failed": "No s'ha pogut executar l'acció {action} per a l'aplicació {app}", + "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_arch_not_supported": "Aquesta aplicació només es pot instal·lar a les arquitectures {required}, però l'arquitectura del vostre servidor és {current}", + "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_change_url_failed": "No s'ha pogut canviar l'URL per a {app}: {error}", + "app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain}{path}'), no hi ha res per fer.", + "app_change_url_no_script": "L'aplicació '{app_name}' encara no permet modificar la URL. Potser s'ha d'actualitzar.", + "app_change_url_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_change_url_success": "La URL de {app} ara és {domain}{path}", + "app_config__core_name": "Tessel·les i permisos", + "app_config_permission_allowed": "Grups/comptes amb accés permès", + "app_config_permission_allowed_warn_protected": "Nota: aquest permís està «protegit» i, per tant, el grup «visitants» no es pot afegir/eliminar dels grups autoritzats.", + "app_config_permission_description": "Descripció", + "app_config_permission_description_help": "Això només és útil si esteu utilitzant el mode «descriptiu» del portal", + "app_config_permission_extraperm_section_name": "Permís '{perm}'", + "app_config_permission_label": "Etiqueta", + "app_config_permission_location": "Correspon a [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Logotip personalitzat per utilitzar", + "app_config_permission_logo_help": "Només s'admeten PNG", + "app_config_permission_show_tile": "Mostrar la tessel·la al portal", + "app_config_unable_to_apply": "No s'han pogut aplicar els valors del tauler de configuració.", + "app_config_unable_to_read": "No s'han pogut llegir els valors del tauler de configuració.", + "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}", + "app_extraction_failed": "No s'han pogut extreure els fitxers d'instal·lació", + "app_failed_to_download_asset": "No s'ha pogut baixar el recurs «{source_id}» ({url}) per a {app}: {out}", + "app_full_domain_unavailable": "Aquesta aplicació ha de ser instal·lada en el seu propi domini, però ja hi ha altres aplicacions instal·lades en el domini «{domain}». Podeu utilitzar un subdomini dedicat a aquesta aplicació.", + "app_id_invalid": "ID de l'aplicació incorrecte", + "app_install_failed": "No s'ha pogut instal·lar {app}: {error}", + "app_install_files_invalid": "Aquests fitxers no es poden instal·lar", + "app_install_script_failed": "Hi ha hagut un error en el script d'instal·lació de l'aplicació", + "app_location_unavailable": "Aquesta URL no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps}", + "app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' l'aplicació per defecte en el domini «{domain}», ja que ja és utilitzat per '{other_app}'", + "app_manifest_install_ask_admin": "Escolliu l'usuari administrador per aquesta aplicació", + "app_manifest_install_ask_domain": "Escolliu el domini en el que s'hauria d'instal·lar aquesta aplicació", + "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)", + "app_manifest_install_ask_init_main_permission": "Qui hauria de tenir accés a aquesta aplicació? (Això es pot canviar més endavant)", + "app_manifest_install_ask_is_public": "Aquesta aplicació hauria de ser visible per a visitants anònims?", + "app_manifest_install_ask_password": "Escolliu la contrasenya d'administració per 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_not_correctly_installed": "{app} sembla estar mal instal·lada", + "app_not_enough_disk": "Aquesta aplicació requereix {required} espai lliure.", + "app_not_enough_ram": "Aquesta aplicació requereix RAM {required} per instal·lar/actualitzar, però només {current} està disponible ara mateix.", + "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_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", + "app_remove_after_failed_install": "Eliminant l'aplicació després que hagi fallat la instal·lació…", + "app_removed": "{app} ha estat desinstal·lada", + "app_requirements_checking": "Verificació dels requisits per a {app}…", + "app_resource_failed": "No s'ha pogut subministrar, desaprovisionar o actualitzar recursos per a {app}: {error}", + "app_restore_failed": "No s'ha pogut restaurar {app}: {error}", + "app_restore_script_failed": "S'ha produït un error en el script de restauració de l'aplicació", + "app_sources_fetch_failed": "No s'han pogut carregar els fitxers font, l'URL és correcta?", + "app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per «{app}»…", + "app_start_install": "instal·lant {app}…", + "app_start_remove": "Eliminant {app}…", + "app_start_restore": "Recuperant {app}…", + "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_bad_quality": "Aquesta aplicació està actualment marcada com a incorrecta al catàleg d'aplicacions de YunoHost. Pot ser un problema temporal mentre els mantenidors intenten solucionar-lo. Mentrestant, l'actualització d'aquesta aplicació està desactivada.", + "app_upgrade_broke_the_system": "L'actualització de {app} sembla haver funcionat, però ha deixat el sistema en un estat defectuós i, per tant, es considera un error.", + "app_upgrade_cli_bad_quality": "S'estan ometent les actualitzacions per a {app} perquè actualment està marcada com a trencada al catàleg d'aplicacions de YunoHost.", + "app_upgrade_cli_up_to_date": "{app} ja està actualitzada ({current_version})", + "app_upgrade_cli_url_required": "{app} no és al catàleg (ja no?) i, per tant, no es pot actualitzar automàticament. Hauries d'utilitzar `yunohost app upgrade {app}` per proporcionar l'URL del repositori utilitzant l'opció `-u`.", + "app_upgrade_cli_will_force_upgrade": "L'actualització de l'aplicació {app} serà forçada ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} s'actualitzarà de {current_version} a {new_version}", + "app_upgrade_continuing_with_other_apps": "No s'ha pogut actualitzar {app}, però es continua amb l'actualització d'altres aplicacions igualment (perquè s'ha utilitzat `--continue-on-failure`)", + "app_upgrade_fail_requirements": "Hi ha una nova versió disponible per a aquesta aplicació ({new_version}), però no es compleixen alguns requisits:\n{failed_requirements}", + "app_upgrade_failed": "No s'ha pogut actualitzar {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "No s'ha pogut actualitzar l'aplicació '{app}' i ha deixat el sistema en un estat defectuós.", + "app_upgrade_script_failed": "Hi ha hagut un error en el script d'actualització de l'aplicació", + "app_upgrade_several_apps": "S'actualitzaran les següents aplicacions: {apps}", + "app_upgrade_some_app_failed": "No s'han pogut actualitzar algunes aplicacions", + "app_upgrade_specific_channel_msg": "Tingueu en compte que actualment esteu utilitzant `{channel}` com a font per a les actualitzacions. Assegureu-vos de consultar la discussió en curs [aquí]({pr_url}).", + "app_upgrade_up_to_date": "L'actualització forçada de l'aplicació (a la mateixa versió) de vegades pot ser útil per reconstruir l'aplicació i les configuracions.", + "app_upgrade_upgradable": "L'aplicació es pot actualitzar de la versió {current_version} a {new_version}", + "app_upgrade_url_required": "Aquesta aplicació (ja?) no existeix al catàleg, per tant, heu de gestionar les actualitzacions manualment.
Des de la línia d'ordres, podeu utilitzar `yunohost app upgrade ` i proporcionar l'URL del repositori mitjançant l'opció `-u`.", + "app_upgraded": "S'ha actualitzat {app}", + "app_yunohost_version_not_supported": "Aquesta aplicació requereix YunoHost >= {required} però la versió instal·lada actual és {current}.", + "apps_already_up_to_date": "Ja estan actualitzades totes les 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!", + "apps_catalog_updating": "S'està actualitzant el catàleg d'aplicacions…", + "apps_confirm_partial_upgrade": "Algunes aplicacions per a les quals s'ha sol·licitat una actualització no es poden actualitzar. Voleu continuar amb les altres igualment?", + "apps_no_target_can_be_upgraded": "No es pot actualitzar cap aplicació", + "apps_upgrade_cancelled": "Encara hi havia actualitzacions pendents per a diverses altres aplicacions, però la seva actualització s'ha cancel·lat (feu servir `--continue-on-failure` per continuar igualment): {apps}", + "ask_admin_fullname": "Nom complet de l'administrador", + "ask_admin_username": "Nom d'usuari de l'administrador", + "ask_dyndns_recovery_password": "Contrasenya de recuperació de DynDNS", + "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_explain_during_unsubscribe": "Introduïu la contrasenya de recuperació d'aquest domini DynDNS.", + "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.", + "ask_fullname": "Nom complet", + "ask_main_domain": "Domini principal", + "ask_new_admin_password": "Nova contrasenya d'administrador", + "ask_new_domain": "Nou domini", + "ask_new_path": "Nou camí", + "ask_password": "Contrasenya", + "ask_user_domain": "Domini a utilitzar per l'adreçar de correu electrònic", + "automatic_task": "Tasca automàtica", + "backup_abstract_method": "Encara està per implementar aquest mètode de còpia de seguretat", + "backup_actually_backuping": "Creant un arxiu de còpia de seguretat a partir dels fitxers recuperats…", + "backup_app_script_failed": "No s'han pogut recopilar els fitxers per fer una còpia de seguretat de l'aplicació {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_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_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}", + "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_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_before_upgrade_deleted_because_replaced_by_newer_backup": "La còpia de seguretat {name} s'ha suprimit perquè s'ha substituït per una còpia de seguretat més nova de {newname}", + "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_create_size_estimation": "L'arxiu tindrà aproximadament {size} de dades.", + "backup_created": "S'ha creat la còpia de seguretat: {name}", + "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: {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_no_file_collected": "No s'han pogut recopilar els fitxers per fer una còpia de seguretat", + "backup_no_uncompress_archive_dir": "El directori de l'arxiu descomprimit no existeix", + "backup_output_directory_forbidden": "Escolliu un directori de sortida different. Les còpies de seguretat no es poden crear ni dins els directoris /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ni dins els subdirectoris /home/yunohost.backup/archives", + "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_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.", + "backup_with_no_restore_script_for_app": "{app} no té un script de restauració, no podreu restaurar automàticament la còpia de seguretat d'aquesta aplicació.", + "cannot_open_file": "No s'ha pogut obrir el fitxer {file} (motiu: {error})", + "cannot_write_file": "No s'ha pogut escriure el fitxer {file} (motiu: {error})", + "certmanager_acme_not_configured_for_domain": "No s'ha pogut executar el ACME challenge pel domini {domain} en aquests moments ja que a la seva configuració de nginx li manca el codi corresponent… Assegureu-vos que la configuració nginx està actualitzada utilitzant «yunohost tools regen-conf nginx --dry-run --with-diff».", + "certmanager_attempt_to_renew_nonLE_cert": "El certificat pel domini «{domain}» no ha estat emès per Let's Encrypt. No es pot renovar automàticament!", + "certmanager_attempt_to_renew_valid_cert": "El certificat pel domini «{domain}» està a punt de caducar! (Utilitzeu --force si sabeu el que esteu fent)", + "certmanager_attempt_to_replace_valid_cert": "Esteu intentant sobreescriure un certificat correcte i vàlid pel domini {domain}! (Utilitzeu --force per ometre)", + "certmanager_cannot_read_cert": "S'ha produït un error al intentar obrir el certificat actual pel domini {domain} (arxiu: {file}), raó: {reason}", + "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_install_success": "S'ha instal·lat correctament un certificat Let's Encrypt pel domini «{domain}»", + "certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini «{domain}»", + "certmanager_cert_renew_failed": "La renovació del certificat de Let's Encrypt ha fallat per a {domains}", + "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_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": "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_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.)", + "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_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})", + "config_action_disabled": "No s'ha pogut executar l'acció «{action}» perquè està desactivada, assegureu-vos de complir les seves limitacions. ajuda: {help}", + "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_cant_set_value_on_section": "No podeu establir un sol valor en una secció sencera de configuració.", + "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_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}»).", + "config_no_panel": "No s'ha trobat cap tauler de configuració.", + "config_unknown_filter_key": "La clau de filtre «{filter_key}» és incorrecta.", + "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_insufficient_ram": "Aquesta aplicació requereix més RAM per instal·lar-se de la que hi ha disponible actualment. Fins i tot si aquesta aplicació pogués executar-se, el seu procés d'instal·lació/actualització requereix una gran quantitat de RAM, de manera que el servidor es podria bloquejar i fallar estrepitosament. Si voleu assumir aquest risc de totes maneres, escriviu '{answers}'", + "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}]", + "confirm_tos_acknowledgement": "He llegit i entenc les Condicions dels serveis [{answers}]", + "corrupted_json": "JSON corrupte llegit des de {ressource} (motiu: {error})", + "corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource} (motiu: {error})", + "corrupted_yaml": "YAML corrupte llegit des de {ressource} (motiu: {error})", + "danger": "Perill:", + "diagnosis_apps_allgood": "Totes les aplicacions instal·lades respecten les pràctiques bàsiques d'empaquetament", + "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_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_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_apps_issue": "S'ha trobat un problema per a l'aplicació {app}", + "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.", + "diagnosis_apps_outdated_packaging_format": "Aquesta aplicació utilitza un format d'empaquetatge obsolet i aviat deixarà de ser compatible amb YunoHost. Hauries de considerar actualitzar-la.", + "diagnosis_apps_outdated_ynh_requirement": "La versió instal·lada d'aquesta aplicació només requereix yunohost >= 2.x, 3.x ó 4.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.", + "diagnosis_apps_security_issue_error": "L'aplicació {app} es troba actualment a la versió «{current_version}», que és vulnerable a un problema de seguretat IMPORTANT: {title}. Es recomana actualitzar-la COM MÉS AVIAT POSSIBLE a la versió «{fixed_in_version}». Més informació: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "L'aplicació {app} es troba actualment a la versió «{current_version}», que és vulnerable a un problema de seguretat moderat: {title}. Es recomana actualitzar-la a «{fixed_in_version}». Més informació: {more_infos_list}", + "diagnosis_backports_in_sources_list": "Sembla que apt (el gestor de paquets) està configurat per utilitzar el repositori backports. A menys de saber el que esteu fent, recomanem fortament no instal·lar paquets de backports, ja que poder causar inestabilitats o conflictes en el sistema.", + "diagnosis_basesystem_hardware": "L'arquitectura del maquinari del servidor és {virt} {arch}", + "diagnosis_basesystem_hardware_model": "El model del servidor és {model}", + "diagnosis_basesystem_host": "El servidor funciona amb Debian {debian_version}", + "diagnosis_basesystem_kernel": "El servidor funciona amb el nucli de Linux {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Esteu utilitzant versions inconsistents dels paquets de YunoHost… probablement a causa d'una actualització fallida o parcial.", + "diagnosis_basesystem_ynh_main_version": "El servidor funciona amb YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} versió: {version}({repo})", + "diagnosis_cache_still_valid": "(La memòria cau encara és vàlida pel diagnòstic de {category}. No es tornar a diagnosticar de moment!)", + "diagnosis_cant_run_because_of_dep": "No es pot fer el diagnòstic per {category} mentre hi ha problemes importants relacionats amb {dep}.", + "diagnosis_description_apps": "Aplicacions", + "diagnosis_description_basesystem": "Sistema de base", + "diagnosis_description_dnsrecords": "Registres DNS", + "diagnosis_description_ip": "Connectivitat a Internet", + "diagnosis_description_mail": "Correu electrònic", + "diagnosis_description_ports": "Exposició dels ports", + "diagnosis_description_regenconf": "Configuració del sistema", + "diagnosis_description_services": "Verificació de l'estat dels serveis", + "diagnosis_description_systemresources": "Recursos del sistema", + "diagnosis_description_web": "Web", + "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_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_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_dns_bad_conf": "Alguns registres DNS són incorrectes o no existeixen pel domini {domain} (categoria {category})", + "diagnosis_dns_discrepancy": "La configuració DNS següent sembla que no segueix la configuració recomanada:
Tipus: {type}
Nom: {name}
Valor actual: {current}
Valor esperat: {content}", + "diagnosis_dns_good_conf": "Els registres DNS han estat correctament configurats pel domini {domain} (categoria {category})", + "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: {content}", + "diagnosis_dns_point_to_doc": "Consulteu la documentació a https://doc.yunohost.org/dns_config si necessiteu ajuda per configurar els registres DNS.", + "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.", + "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Alguns dominis expiraran EN BREUS!", + "diagnosis_domain_expiration_not_found": "No s'ha pogut comprovar la data d'expiració d'alguns dominis", + "diagnosis_domain_expiration_not_found_details": "La informació WHOIS pel domini {domain} sembla que no conté informació sobre la data d'expiració?", + "diagnosis_domain_expiration_success": "Els vostres dominis estan registrats i no expiraran properament.", + "diagnosis_domain_expiration_warning": "Alguns dominis expiraran properament!", + "diagnosis_domain_expires_in": "{domain} expirarà en {days} dies.", + "diagnosis_domain_not_found_details": "El domini {domain} no existeix en la base de dades WHOIS o ha expirat!", + "diagnosis_everything_ok": "Tot sembla correcte per {category}!", + "diagnosis_failed": "No s'han pogut obtenir els resultats del diagnòstic per la categoria «{category}»: {error}", + "diagnosis_failed_for_category": "Ha fallat el diagnòstic per la categoria «{category}»: {error}", + "diagnosis_found_errors": "S'ha trobat problema(es) important(s) {errors} relacionats amb {category}!", + "diagnosis_found_errors_and_warnings": "S'ha trobat problema(es) important(s) {errors} (i avis(os) {warnings}) relacionats amb {category}!", + "diagnosis_found_warnings": "S'han trobat ítems {warnings} que es podrien millorar per {category}.", + "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://doc.yunohost.org/security.", + "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_connection_error": "Error de connexió: no s'ha pogut connectar amb el domini demanat, segurament és inaccessible.", + "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}", + "diagnosis_http_hairpinning_issue": "Sembla que la vostra xarxa no té el hairpinning activat.", + "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://doc.yunohost.org/dns_local_network", + "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_http_ok": "El domini {domain} és accessible per mitjà de HTTP des de fora de la xarxa local.", + "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_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_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_unreachable": "Sembla que el domini {domain} no és accessible a través de HTTP des de fora de la xarxa local.", + "diagnosis_ignore_already_filtered": "(Ja hi ha un filtre de diagnòstic {category} amb aquests criteris)", + "diagnosis_ignore_criteria_error": "Els criteris han de tenir la forma clau=valor (p. ex., domini=yolo.test)", + "diagnosis_ignore_filter_added": "S'ha afegit un filtre de diagnòstic {category}", + "diagnosis_ignore_filter_removed": "S'ha eliminat un filtre de diagnòstic {category}", + "diagnosis_ignore_missing_criteria": "Heu de proporcionar almenys un criteri que sigui la categoria de diagnòstic a ignorar", + "diagnosis_ignore_no_filter_found": "(No hi ha cap filtre de diagnòstic {category} amb aquests criteris per eliminar)", + "diagnosis_ignore_no_issue_found": "No s'ha trobat cap problema que coincideixi amb els criteris indicats.", + "diagnosis_ignored_issues": "(+ {nb_ignored} problema(es) ignorat(s))", + "diagnosis_ip_broken_dnsresolution": "La resolució de nom de domini falla per algun motiu… Està el tallafocs bloquejant les peticions DNS?", + "diagnosis_ip_broken_resolvconf": "La resolució de nom de domini sembla caiguda en el servidor, podria estar relacionat amb el fet que /etc/resolv.conf no apunta cap a 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "El servidor està connectat a Internet amb IPv4!", + "diagnosis_ip_connected_ipv6": "El servidor està connectat a Internet amb IPv6!", + "diagnosis_ip_dnsresolution_working": "La resolució de nom de domini està funcionant!", + "diagnosis_ip_global": "IP global: {global}", + "diagnosis_ip_local": "IP local: {local}", + "diagnosis_ip_no_ipv4": "El servidor no té una IPv4 que funcioni.", + "diagnosis_ip_no_ipv6": "El servidor no té una IPv6 que funcioni.", + "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://doc.yunohost.org/ipv6. Si no podeu habilitar IPv6 o us sembla massa tècnic, podeu ignorar aquest avís sense problemes.", + "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://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "Sembla que el servidor no està connectat a internet!?", + "diagnosis_ip_weird_resolvconf": "La resolució DNS sembla estar funcionant, però sembla que esteu utilitzant un versió personalitzada de /etc/resolv.conf.", + "diagnosis_ip_weird_resolvconf_details": "El fitxer etc/resolv.conf hauria de ser un enllaç simbòlic cap a /etc/resolvconf/run/resolv.conf i que aquest apunti cap a 127.0.0.1 (dnsmasq). La configuració del «resolver» real s'hauria de fer a /etc/resolv.dnsmaq.conf.", + "diagnosis_mail_blocklist_listed_by": "La vostra IP o domini {item} està en una llista negra a {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Sembla que les IPs i el dominis d'aquest servidor no són en una llista negra", + "diagnosis_mail_blocklist_reason": "El motiu de ser a la llista negra és: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Sembla que el motiu esmenta «resolució oberta“.
Això normalment significa que el vostre servidor no utilitza el seu DNS local, sinó un de públic i obert.
Comproveu el contingut de /etc/resolv.conf, hauria de contenir nameserver 127.0.0.1.
Com que aquest fitxer normalment es genera automàticament, no l'editeu manualment. Comproveu la configuració de DHCP o la configuració de VPN si n'utilitzeu una, o si heu utilitzat una imatge de Debian feta per, per exemple, un proveïdor de VPS, busqueu una configuració de cloudinit.
Us convidem als canals de suport de YunoHost per obtenir ajuda sobre aquest problema.
El motiu literal de la llista negra és: {reason}", + "diagnosis_mail_blocklist_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 {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Un servei no SMTP a respost en el port 25 amb IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Podria ser que sigui per culpa d'una altra màquina responent en lloc del servidor.", + "diagnosis_mail_ehlo_could_not_diagnose": "No s'ha pogut diagnosticar si el servidor de correu electrònic postfix és accessible des de l'exterior amb IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", + "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!", + "diagnosis_mail_ehlo_unreachable": "El servidor de correu electrònic SMTP no és accessible des de l'exterior amb IPv{ipversion}. No podrà rebre correus electrònics.", + "diagnosis_mail_ehlo_unreachable_details": "No s'ha pogut establir una connexió amb el vostre servidor en el port 25 amb IPv{ipversion}. Sembla que el servidor no és accessible.
1. La causa més comú per aquest problema és que el port 25 no està correctament redireccionat cap al vostre servidor.
2. També us hauríeu d'assegurar que el servei postfix estigui funcionant.
3. En configuracions més complexes: assegureu-vos que no hi hagi cap tallafoc ni reverse-proxy interferint.", + "diagnosis_mail_ehlo_wrong": "Un servidor de correu electrònic SMTP diferent respon amb IPv{ipversion}. És probable que el vostre servidor no pugui rebre correus electrònics.", + "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_different_from_ehlo_domain": "El DNS invers no està correctament configurat amb IPv{ipversion}. Alguns correus electrònics poden no arribar al destinatari o ser marcats com correu brossa.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS invers actual: {rdns_domain}
Valor esperat: {ehlo_domain}", + "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_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://doc.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_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_ok": "S'ha configurat correctament el servidor DNS invers!", + "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_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_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://doc.yunohost.org/vpn_advantage
- També podeu considerar canviar-vos a un proveïdor més respectuós de la neutralitat de la xarxa", + "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_queue_ok": "{nb_pending} correus electrònics pendents en les cues de correu electrònic", + "diagnosis_mail_queue_too_big": "Hi ha massa correus electrònics pendents en la cua ({nb_pending} correus electrònics)", + "diagnosis_mail_queue_unavailable": "No s'ha pogut consultar el nombre de correus electrònics pendents en la cua", + "diagnosis_mail_queue_unavailable_details": "Error: {error}", + "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_no_cache": "Encara no hi ha memòria cau pel diagnòstic de la categoria «{category}»", + "diagnosis_package_installed_from_sury": "Alguns paquets del sistema s'han de tornar a versions anteriors", + "diagnosis_package_installed_from_sury_details": "Alguns paquets s'han instal·lat per equivocació des d'un repositori de tercers anomenat Sury. L'equip de YunoHost a millorat l'estratègia per a gestionar aquests paquets, però s'espera que algunes configuracions que han instal·lat aplicacions PHP7.3 a Stretch puguin tenir algunes inconsistències. Per a resoldre aquesta situació, hauríeu d'intentar executar la següent ordre: {cmd_to_fix}", + "diagnosis_package_security_issue_error": "El paquet de sistema «{package}» es troba actualment a la versió «{current_version}», que és vulnerable a un problema de seguretat IMPORTANT: {title}. Es recomana actualitzar-lo COM MÉS AVIAT POSSIBLE a la versió «{fixed_in_version}». Més informació: {more_infos_list}", + "diagnosis_package_security_issue_warning": "El paquet de sistema «{package}»es troba actualment a la versió «{current_version}», que és vulnerable a un problema de seguretat moderat: {title}. Es recomana actualitzar-lo a «{fixed_in_version}». Més informació: {more_infos_list}", + "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_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://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "És necessari exposar aquest port per a les funcions {category} (servei {service})", + "diagnosis_ports_ok": "El port {port} és accessible des de l'exterior.", + "diagnosis_ports_partially_unreachable": "El port {port} no és accessible des de l'exterior amb IPv{failed}.", + "diagnosis_ports_unreachable": "El port {port} no és accessible des de l'exterior.", + "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_ram_low": "El sistema només té {available} ({available_percent}%) de memòria RAM disponibles d'un total de {total}. Aneu amb compte.", + "diagnosis_ram_ok": "El sistema encara té {available} ({available_percent}%) de memòria RAM disponibles 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_regenconf_allgood": "Tots els fitxers de configuració estan en acord amb la configuració recomanada!", + "diagnosis_regenconf_manually_modified": "El fitxer de configuració {file} sembla haver estat modificat manualment.", + "diagnosis_regenconf_manually_modified_details": "No hauria de ser cap problema sempre i quan sapigueu el que esteu fent! YunoHost deixarà d'actualitzar aquest fitxer de manera automàtica… Però tingueu en compte que les actualitzacions de YunoHost podrien tenir canvis recomanats importants. Si voleu podeu mirar les diferències amb yunohost tools regen-conf {category} --dry-run --with-diff i forçar el restabliment de la configuració recomanada amb yunohost tools regen-conf {category} --force", + "diagnosis_rfkill_wifi": "La targeta Wi-Fi està desactivada i un avís del sistema pot impedir la instal·lació d'aplicacions", + "diagnosis_rfkill_wifi_details": "Aquest avís es col·la en moltes sortides d'ordres, trencant algunes aplicacions. Normalment cal especificar el vostre codi de país amb l'ordre sudo raspi-config. Aquest és l'error:
{rfkill_wifi_error}", + "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_security_vulnerable_to_meltdown": "Sembla que el sistema és vulnerable a la vulnerabilitat de seguretat crítica Meltdown", + "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_services_bad_status": "El servei {service} està {status} :(", + "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_services_conf_broken": "La configuració pel servei {service} està trencada!", + "diagnosis_services_running": "El servei {service} s'està executant!", + "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_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.", + "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.", + "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_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.", + "diagnosis_swap_ok": "El sistema té {total} de swap!", + "diagnosis_swap_tip": "Vigileu i tingueu en compte que els servidor està allotjant memòria d'intercanvi en una targeta SD o en l'emmagatzematge SSD, això pot reduir dràsticament l'esperança de vida del dispositiu.", + "diagnosis_unknown_categories": "Les categories següents són desconegudes: {categories}", + "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.", + "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ó bookworm. El fitxer de configuració corresponent hauria de ser /etc/apt/sources.list, o un fitxer a /etc/apt/sources.list.d/.", + "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.", + "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_install": "No queda prou espai al disc per instal·lar aquesta aplicació", + "disk_space_not_sufficient_update": "No queda prou espai al disc per actualitzar aquesta aplicació", + "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_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}».", + "domain_cert_gen_failed": "No s'ha pogut generar el certificat", + "domain_config_acme_eligible": "Elegibilitat (per a l') ACME", + "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_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_auth_key": "Clau d'autenticació", + "domain_config_auth_secret": "Secret d'autenticació", + "domain_config_auth_token": "Token d'autenticació", + "domain_config_cert_install": "Instal·la el certificat Let's Encrypt", + "domain_config_cert_issuer": "Autoritat de certificació", + "domain_config_cert_name": "Certificat", + "domain_config_cert_no_checks": "Ignorar les comprovacions de diagnòstic", + "domain_config_cert_renew": "Renova el certificat Let's Encrypt", + "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_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_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!", + "domain_config_cert_validity": "Validesa", + "domain_config_custom_css": "Full d'estil CSS personalitzat", + "domain_config_custom_css_help": "Això és per a administradors avançats que vulguin personalitzar l'aparença del portal", + "domain_config_default_app": "Aplicació per defecte", + "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.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Mostra la llista d'aplicacions públiques als visitants", + "domain_config_enable_public_apps_page_help": "Els visitants veuran una pàgina «aplicacions públiques» quan acabin al portal en lloc de només el formulari d'inici de sessió.", + "domain_config_feature_name": "Característiques", + "domain_config_mail_in": "Correus entrants", + "domain_config_mail_out": "Correus sortints", + "domain_config_portal_logo": "Logotip personalitzat", + "domain_config_portal_logo_help": "Accepta .svg, .png i .jpeg. Preferit un .svg monocrom amb fill: currentColor perquè el logotip s'adapti als temes.", + "domain_config_portal_name": "Personalització del portal", + "domain_config_portal_public_intro": "Introducció pública personalitzada", + "domain_config_portal_public_intro_help": "Podeu utilitzar HTML, els estils bàsics s'aplicaran als elements genèrics.", + "domain_config_portal_theme": "Tema de color predeterminat", + "domain_config_portal_theme_help": "Els usuaris poden triar-ne un altre a la seva configuració.", + "domain_config_portal_tile_theme": "Tema de mostra de fitxes d'aplicació", + "domain_config_portal_title": "Títol personalitzat", + "domain_config_portal_user_intro": "Introducció personalitzada de l'usuari", + "domain_config_portal_user_intro_help": "Podeu utilitzar HTML, els estils bàsics s'aplicaran als elements genèrics.", + "domain_config_search_engine": "URL del motor de cerca", + "domain_config_search_engine_help": "Aquesta és una característica opcional, que permet mostrar una barra de cerca al portal (per exemple, si voleu utilitzar el vostre portal YunoHost com a pàgina d'inici del vostre navegador). Hauria de ser un URL amb una cadena de consulta buida com ara `https://duckduckgo.com/?q=`, amb `q=` com a paràmetre de consulta buida de duckduckgo", + "domain_config_search_engine_name": "Nom del motor de cerca", + "domain_config_show_other_domains_apps": "Mostra les aplicacions d'altres dominis", + "domain_created": "S'ha creat el domini", + "domain_creation_failed": "No s'ha pogut crear el domini {domain}: {error}", + "domain_deleted": "S'ha eliminat el domini", + "domain_deletion_failed": "No s'ha pogut eliminar el domini {domain}: {error}", + "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_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_already_up_to_date": "Registres ja actualitzats, res a fer.", + "domain_dns_push_failed": "L'actualització dels registres DNS ha fallat estrepitosament.", + "domain_dns_push_failed_to_list": "No s'han pogut llistar les entrades actuals mitjançant l'API del registrador: {error}", + "domain_dns_push_managed_in_parent_domain": "La funció de configuració automàtica de DNS es gestiona al domini principal {parent_domain}.", + "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://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "Registres DNS parcialment actualitzats: s'han notificat alguns avisos/errors.", + "domain_dns_push_record_failed": "No s'ha pogut {action} el registre {type}/{name}: {error}", + "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://doc.yunohost.org/dns_config .", + "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://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (També podeu configurar manualment les vostres entrades DNS seguint la documentació a https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_use_auto": "Utilitzeu la funció DNS automàtica", + "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_dyndns_already_subscribed": "Ja us heu subscrit a un domini DynDNS", + "domain_exists": "El domini ja existeix", + "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_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_uninstall_app_first": "Aquestes aplicacions encara estan instal·lades en el vostre domini:\n{apps}\n\nDesinstal·leu-les utilitzant l'ordre «yunohost app remove id_de_lapplicació» o moveu-les a un altre domini amb «yunohost app change-url id_de_lapplicació» abans d'eliminar el domini", + "domain_unknown": "Domini «{domain}» desconegut", + "domains_available": "Dominis disponibles:", + "done": "Fet", + "download_bad_status_code": "{url} ha retornat el codi d'estat {code}", + "download_ssl_error": "Error SSL al connectar amb {url}", + "download_timeout": "{url} ha tardat massa en respondre, s'ha deixat d'esperar.", + "download_unknown_error": "Error al baixar dades des de {url}: {error}", + "downloading": "Descarregant…", + "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».", + "dpkg_lock_not_available": "No es pot utilitzar aquesta comanda en aquest moment ja que sembla que un altre programa està utilitzant el lock de dpkg (el gestor de paquets del sistema)", + "dyndns_could_not_check_available": "No s'ha pogut verificar la disponibilitat de {domain} a {provider}.", + "dyndns_domain_not_provided": "El proveïdor de DynDNS {provider} no pot oferir el domini {domain}.", + "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_not_found": "No s'ha trobat la clau DNS pel domini", + "dyndns_no_domain_registered": "No hi ha cap domini registrat amb DynDNS", + "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_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.", + "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}", + "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_success": "S'ha establert la contrasenya de recuperació!", + "dyndns_set_recovery_password_unknown_domain": "No s'ha pogut establir la contrasenya de recuperació: domini no registrat", + "dyndns_subscribe_failed": "No s'ha pogut subscriure el domini DynDNS: {error}", + "dyndns_subscribed": "Domini DynDNS subscrit", + "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.", + "dyndns_unavailable": "El domini {domain} no està disponible.", + "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", + "error_changing_file_permissions": "Error al canviar els permisos per {path}: {error}", + "error_removing": "Error al eliminar {path}: {error}", + "error_writing_file": "Error al escriure el fitxer {file}: {error}", + "extracting": "Extracció en curs…", + "field_invalid": "Camp incorrecte « {field} »", + "file_does_not_exist": "El camí {path} no existeix.", + "file_not_exist": "El fitxer no existeix: '{path}'", + "firewall_reload_failed": "No s'ha pogut tornar a carregar el tallafocs. Més informació en el registre.", + "firewall_reloaded": "S'ha tornat a carregar el tallafocs", + "global_settings_reset_success": "Restableix la configuració global", + "global_settings_setting_admin_strength": "Robustesa de la contrasenya d'administrador", + "global_settings_setting_admin_strength_help": "Aquests requisits només s'apliquen en inicialitzar o canviar la contrasenya", + "global_settings_setting_antispam_name": "Antispam", + "global_settings_setting_backup_compress_tar_archives": "Comprimir còpies de seguretat", + "global_settings_setting_backup_compress_tar_archives_help": "Comprimir els arxius (.tar.gz) en lloc d'arxius no comprimits (.tar) al crear noves còpies de seguretat. N.B.: activar aquesta opció permet fer arxius de còpia de seguretat més lleugers, però el procés inicial de còpia de seguretat serà significativament més llarg i més exigent a nivell de CPU.", + "global_settings_setting_backup_name": "Còpia de seguretat", + "global_settings_setting_dns_custom_resolvers_enabled": "Utilitza resolutors DNS personalitzats", + "global_settings_setting_dns_custom_resolvers_enabled_help": "Per defecte, YunoHost utilitza una llista de resolutors fiables ubicats a Europa. Els usuaris avançats poden voler especificar resolutors personalitzats.", + "global_settings_setting_dns_custom_resolvers_list": "Adreces dels resolutors personalitzats", + "global_settings_setting_dns_custom_resolvers_list_help": "Una llista d'almenys 2 servidors DNS per protocol IP en ús (IPv4/IPv6). Exemple: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "global_settings_setting_dns_exposure": "Versions IP a tenir en compte per a la configuració i el diagnòstic de DNS", + "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_email_name": "Correu electrònic", + "global_settings_setting_enable_blocklists": "Habilitar les llistes de bloqueig per al trànsit entrant", + "global_settings_setting_enable_blocklists_help": "Bloquejar els servidors llistats per spamcop.net, spamhaus.org i abuseat.org per evitar el correu brossa. Tanmateix, això pot causar problemes de lliurament per a alguns servidors de correu inofensius que poden ser llistats per aquests tercers, en aquest cas no es rebrà el correu enviat des d'aquests servidors.", + "global_settings_setting_experimental_name": "Experimental", + "global_settings_setting_misc_name": "Altres", + "global_settings_setting_network_name": "Xarxa", + "global_settings_setting_nginx_compatibility": "Compatibilitat NGINX", + "global_settings_setting_nginx_compatibility_help": "Solució de compromís entre compatibilitat i seguretat pel servidor web NGINX. Afecta els criptògrafs (i altres aspectes relacionats amb la seguretat)", + "global_settings_setting_nginx_name": "NGINX (servidor web)", + "global_settings_setting_nginx_redirect_to_https": "Força HTTPS", + "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_password_name": "Contrasenyes", + "global_settings_setting_passwordless_sudo": "Permet als administradors utilitzar «sudo» sense tornar a escriure les seves contrasenyes", + "global_settings_setting_pop3_enabled": "Activa POP3", + "global_settings_setting_pop3_enabled_help": "Habiliteu el protocol POP3 per al servidor de correu. POP3 és un protocol més antic per accedir a bústies de correu des de clients de correu electrònic i és més lleuger, però té menys funcions que IMAP (activat per defecte)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Permetre a les persones editar llur adreça de correu electrònic principal", + "global_settings_setting_portal_allow_edit_email_alias": "Permetre a les persones afegir, eliminar i editar àlies de correu", + "global_settings_setting_portal_allow_edit_email_alias_help": "Si està desactivat, han de demanar-ho a l'administració.", + "global_settings_setting_portal_allow_edit_email_forward": "Permetre a les persones afegir, eliminar i editar el reenviament de correu", + "global_settings_setting_portal_allow_edit_email_forward_help": "Si està desactivat, han de demanar-ho a l'administració.", + "global_settings_setting_portal_allow_edit_email_help": "Si està desactivat, han de demanar-ho a l'administració.", + "global_settings_setting_portal_name": "Portal", + "global_settings_setting_postfix_compatibility": "Compatibilitat Postfix", + "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_postfix_name": "Postfix (servidor de correu electrònic SMTP)", + "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_access_name": "Canvia la contrasenya de «root»", + "global_settings_setting_root_password": "Nova contrasenya de root", + "global_settings_setting_root_password_confirm": "Nova contrasenya de root (confirmeu)", + "global_settings_setting_security_experimental_enabled": "Característiques de seguretat experimentals", + "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_security_name": "Seguretat", + "global_settings_setting_smtp_allow_ipv6": "Permet IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Permet l'ús de IPv6 per rebre i enviar correus electrònics", + "global_settings_setting_smtp_backup_mx_domains": "Dominis per a actuar com a MX secundari", + "global_settings_setting_smtp_backup_mx_domains_help": "Permet que aquest servidor actuï com a domini MX *secundari* de recolzament per al domini indicat. Això vol dir que si no es pot accedir al MX principal del domini (per exemple a causa d'una interrupció), els correus s'enviaran a aquest servidor, que els conservarà durant un màxim de 20 dies i intentarà retransmetre'ls a la destinació real un cop torna a pujar. Es poden proporcionar diversos dominis, separats per comes.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Llista blanca de correus electrònics MX SMTP de recolzament", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Quan s'actua com a MX secundari, s'ha de proporcionar la llista exhaustiva d'adreces de correu electrònic dels destinataris permesos (en cas contrari, els correus seran rebutjats i descartats). Es poden proporcionar diverses entrades, separades per comes.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Activa la retransmissió SMTP", + "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_host": "Amfitrió de retransmissió SMTP", + "global_settings_setting_smtp_relay_password": "Contrasenya de retransmissió SMTP", + "global_settings_setting_smtp_relay_port": "Port de tramesa SMTP", + "global_settings_setting_smtp_relay_user": "Usuari de retransmissió SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilitat SSH", + "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_ssh_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Autenticació de contrasenya", + "global_settings_setting_ssh_password_authentication_help": "Permet l'autenticació de contrasenya per a SSH", + "global_settings_setting_ssh_port": "Port SSH", + "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.", + "global_settings_setting_tls_passthrough_enabled": "Habiliteu el reenviament basat en SNI / TLS-passthrough", + "global_settings_setting_tls_passthrough_enabled_help": "Aquesta és una característica avançada de proxy invers d'un domini sencer a una altra màquina *sense* desxifrar el trànsit. Útil quan voleu exposar diverses màquines darrere de la mateixa IP, però encara permeteu que cada màquina gestioni la terminació SSL.", + "global_settings_setting_tls_passthrough_explain": "Aquesta característica és AVANÇADA i EXPERIMENTAL i provocarà canvis importants en la configuració nginx d'aquest servidor. Si us plau, NO l'utilitzeu si no saps què estàs fent! En particular, heu de tenir en compte que fail2ban no es pot implementar al servidor intermediari (nftables no pot prohibir el trànsit maliciós ja que tots els paquets IP semblen procedents del servidor frontal). A més, de moment, la configuració nginx del servidor intermediari s'ha d'ajustar manualment per acceptar el `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "Llista de reenviaments", + "global_settings_setting_tls_passthrough_list_help": "Hauria de ser una llista de DOMAIN;DESTINATION;PORT, com ara domain.tld;192.168.1.42;443 o domain.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "Reenviament basat en SNI / TLS-passthrough", + "global_settings_setting_user_strength": "Robustesa de la contrasenya de l'usuari", + "global_settings_setting_user_strength_help": "Aquests requisits només s'apliquen en inicialitzar o canviar la contrasenya", + "global_settings_setting_webadmin_allowlist": "Llista d'IPs permeses de Webadmin", + "global_settings_setting_webadmin_allowlist_enabled": "Activa la lista d'IPs permeses de Webadmin", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permet que només algunes IPs accedeixin a Webadmin.", + "global_settings_setting_webadmin_allowlist_help": "Adreces IP permeses per accedir a Webadmin. La notació CIDR està permesa.", + "global_settings_setting_webadmin_name": "Webadmin", + "good_practices_about_admin_password": "Esteu a punt de definir una nova contrasenya d'administrador. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", + "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).", + "group_already_exist": "El grup {group} ja existeix", + "group_already_exist_on_system": "El grup {group} ja existeix en els grups del sistema", + "group_already_exist_on_system_but_removing_it": "El grup {group} ja existeix en els grups del sistema, però YunoHost l'eliminarà…", + "group_cannot_be_deleted": "El grup {group} no es pot eliminar manualment.", + "group_cannot_edit_all_users": "El grup «all_users» no es pot editar manualment. És un grup especial destinat a contenir els usuaris registrats a YunoHost", + "group_cannot_edit_primary_group": "El grup «{group}» no es pot editar manualment. És el grup principal destinat a contenir un usuari específic.", + "group_cannot_edit_visitors": "El grup «visitors» no es pot editar manualment. És un grup especial que representa els visitants anònims", + "group_cannot_remove_last_admin": "L'usuari '{user}' és l'únic usuari del grup 'admins' i no serà eliminat.", + "group_created": "S'ha creat el grup «{group}»", + "group_creation_failed": "No s'ha pogut crear el grup «{group}»: {error}", + "group_deleted": "S'ha eliminat el grup «{group}»", + "group_deletion_failed": "No s'ha pogut eliminar el grup «{group}»: {error}", + "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_no_change": "No hi ha res a canviar per al grup «{group}»", + "group_unknown": "Grup {group} desconegut", + "group_update_aliases": "S'estan actualitzant els àlies per al grup «{group}»", + "group_update_failed": "No s'ha pogut actualitzat el grup «{group}»: {error}", + "group_updated": "S'ha actualitzat el grup «{group}»", + "group_user_add": "L'usuari «{user}» s'afegirà al grup «{group}»", + "group_user_already_in_group": "L'usuari {user} ja està en el grup {group}", + "group_user_not_in_group": "L'usuari {user} no està en el grup {group}", + "group_user_remove": "L'usuari «{user}» s'eliminarà del grup «{group}»", + "hook_exec_failed": "No s'ha pogut executar el script: {path}", + "hook_exec_not_terminated": "El script no s'ha acabat correctament: {path}", + "hook_json_return_error": "No s'ha pogut llegir el retorn del script {path}. Error: {msg}. Contingut en brut: {raw_content}", + "hook_list_by_invalid": "Aquesta propietat no es pot utilitzar per llistar els hooks", + "hook_name_unknown": "Nom de script « {name} » desconegut", + "installation_complete": "Instal·lació completada", + "invalid_credentials": "La contrasenya o el nom d'usuari no són vàlids", + "invalid_number": "Ha de ser una xifra", + "invalid_password": "Contrasenya no vàlida", + "invalid_regex": "Regex no vàlid: «{regex}»", + "invalid_shell": "Shell no vàlid: {shell}", + "invalid_url": "No s'ha pogut connectar a {url}… pot ser que el servei estigui caigut, o que no hi hagi connexió a Internet amb IPv4/IPv6.", + "ldap_attribute_already_exists": "L'atribut LDAP «{attribute}» ja existeix amb el valor «{value}»", + "ldap_server_down": "No es pot arribar al servidor LDAP", + "ldap_server_is_down_restart_it": "El servei LDAP està inactiu, intenteu reiniciar-lo…", + "log_app_action_run": "Executa l'acció de l'aplicació «{}»", + "log_app_change_url": "Canvia l'URL de l'aplicació « {} »", + "log_app_config_set": "Aplica la configuració a l'aplicació «{}»", + "log_app_install": "Instal·la l'aplicació « {} »", + "log_app_makedefault": "Fes « {} » l'aplicació per defecte", + "log_app_remove": "Elimina l'aplicació « {} »", + "log_app_upgrade": "Actualitza l'aplicació « {} »", + "log_available_on_yunopaste": "Aquest registre està disponible via {url}", + "log_backup_create": "Crea un arxiu de còpia de seguretat", + "log_backup_restore_app": "Restaura « {} » a partir d'una còpia de seguretat", + "log_backup_restore_system": "Restaura el sistema a partir d'una còpia de seguretat", + "log_corrupted_md_file": "El fitxer de metadades YAML associat amb els registres està malmès: « {md_file} »\nError: {error}", + "log_diagnosis_run": "Executa el diagnòstic", + "log_does_exists": "No hi ha cap registre per l'operació amb el nom« {log} », utilitzeu « yunohost log list » per veure tots els registre d'operació disponibles", + "log_domain_add": "Afegeix el domini « {} »", + "log_domain_config_set": "Actualitza la configuració del domini «{}»", + "log_domain_dns_push": "Envia registres DNS per al domini «{}»", + "log_domain_main_domain": "Fes de « {} » el domini principal", + "log_domain_remove": "Elimina el domini « {} »", + "log_dyndns_subscribe": "Registra un subdomini YunoHost « {} »", + "log_dyndns_unsubscribe": "Cancel·lar el registre d'un subdomini de YunoHost «{}»", + "log_dyndns_update": "Actualitza la IP associada al subdomini YunoHost « {} »", + "log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log share {name} »", + "log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log show {name} »", + "log_letsencrypt_cert_install": "Instal·la un certificat Let's Encrypt al domini « {} »", + "log_letsencrypt_cert_renew": "Renova el certificat Let's Encrypt de « {} »", + "log_link_to_failed_log": "No s'ha pogut completar l'operació «{desc}». Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí", + "log_link_to_log": "El registre complet d'aquesta operació: «{desc}»", + "log_operation_unit_unclosed_properly": "L'operació no s'ha tancat de forma correcta", + "log_regen_conf": "Regenera la configuració del sistema « {} »", + "log_remove_on_failed_install": "Elimina « {} » després de que la instal·lació hagi fallat", + "log_resource_snippet": "Aprovisionament/desprovisionament/actualització d'un recurs", + "log_selfsigned_cert_install": "Instal·la el certificat autosignat al domini « {} »", + "log_settings_reset": "Restableix la configuració", + "log_settings_reset_all": "Restableix tots els paràmetres", + "log_settings_set": "Aplica la configuració", + "log_tools_migrations_migrate_forward": "Executa les migracions", + "log_tools_postinstall": "Fer la post instal·lació del servidor YunoHost", + "log_tools_reboot": "Reinicia el servidor", + "log_tools_shutdown": "Apaga el servidor", + "log_tools_update": "S'estan obtenint les actualitzacions del sistema disponibles i actualitzant el catàleg d'aplicacions", + "log_tools_upgrade": "Actualitza els paquets del sistema", + "log_user_create": "Afegeix l'usuari « {} »", + "log_user_delete": "Elimina l'usuari « {} »", + "log_user_group_create": "Crear grup «{}»", + "log_user_group_delete": "Eliminar grup «{}»", + "log_user_group_update": "Actualitzar grup «{}»", + "log_user_import": "Importa usuaris", + "log_user_update": "Actualitza la informació de l'usuari « {} »", + "mail_alias_remove_failed": "No s'han pogut eliminar els àlies del correu «{mail}»", + "mail_alias_unauthorized": "No esteu autoritzat per afegir àlies relacionats amb el domini '{domain}'", + "mail_already_exists": "L'adreça de correu «{mail}» ja existeix", + "mail_domain_unknown": "El domini «{domain}» de l'adreça de correu no és vàlid. Utilitzeu un domini administrat per aquest servidor.", + "mail_edit_operation_unauthorized": "No teniu permís per fer aquest canvi al vostre compte.", + "mail_forward_remove_failed": "No s'han pogut eliminar el reenviament de correu «{mail}»", + "mail_unavailable": "Aquesta adreça de correu està reservada per al grup d'administradors", + "mailbox_disabled": "La bústia de correu està desactivada per al usuari {user}", + "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", + "main_domain_change_failed": "No s'ha pogut canviar el domini principal", + "main_domain_changed": "S'ha canviat el domini principal", + "migration_0027_cleaning_up": "Netejar la memòria cau i els paquets ja no és útil…", + "migration_0027_delayed_api_restart": "L'API YunoHost es reiniciarà automàticament en 15 segons. És possible que no estigui disponible durant uns segons i després haureu d'iniciar sessió de nou.", + "migration_0027_general_warning": "Finalment, tingueu en compte que aquesta migració és **una operació delicada**. L'equip de YunoHost va fer tot el possible per revisar-lo i provar-lo, però la migració encara podria trencar parts del sistema o de les seves aplicacions.\n\nPer tant, es recomana:\n- **Feu còpies de seguretat** de qualsevol dada o aplicació crítica. Més informació a https://doc.yunohost.org/backup;\n- **Tingueu paciència** després d'iniciar la migració: depenent de la vostra connexió a Internet i el maquinari, pot trigar fins a una hora a actualitzar-se correctament;\n- **Arribeu a la comunitat** al fòrum si necessiteu ajuda per resoldre problemes.", + "migration_0027_main_upgrade": "S'està iniciant l'actualització principal…", + "migration_0027_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_0027_not_bullseye": "La distribució actual de Debian no és Bullseye! Si ja heu executat la migració Bullseye -> Bookworm, aquest error és simptomàtic del fet que el procediment de migració no ha tingut èxit al 100% (en cas contrari, YunoHost l'hauria marcat com a completat). Es recomana investigar què va passar amb l'equip d'assistència, que necessitarà el registre **complet** de la migració, que es pot trobar a Eines > Registres a Webadmin.", + "migration_0027_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_0027_patch_yunohost_conflicts": "S'està aplicant el pegat per solucionar el problema del conflicte…", + "migration_0027_patching_sources_list": "S'està aplicant pedaços al fitxer sources.lists…", + "migration_0027_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 marcats com a «funcionants». En conseqüència, no es pot garantir que encara funcionin després de l'actualització: {problematic_apps}", + "migration_0027_start": "S'està iniciant la migració a Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Alguna cosa va fallar durant l'actualització principal, sembla que el sistema encara està a Debian Bullseye.", + "migration_0027_system_not_fully_up_to_date": "El vostre sistema no està completament actualitzat. Realitzeu una actualització regular abans d'executar la migració a Bookworm.", + "migration_0027_yunohost_upgrade": "S'està iniciant l'actualització principal de YunoHost…", + "migration_not_enough_space": "Feu que hi hagi prou espai disponible a {path} per executar la migració.", + "migration_postgresql_previous_not_installed": "PostgreSQL no s'ha instal·lat al vostre sistema. Res a fer.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 està instal·lat, però no PostgreSQL 15!? Alguna cosa estranya podria haver passat al vostre sistema :(…", + "migration_python_venv_rebuild_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}`.", + "migration_python_venv_rebuild_disclaimer_base": "Després de l'actualització a Debian Bookworm, algunes aplicacions de Python s'han de reconstruir parcialment per convertir-se a la nova versió de Python distribuida amb Debian (en termes tècnics: cal recrear el que s'anomena 'virtualenv'). Mentrestant, és possible que aquestes aplicacions de Python no funcionin. 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_python_venv_rebuild_disclaimer_ignored": "Virtualenvs no es pot reconstruir automàticament per a aquestes aplicacions. Heu de forçar una actualització per a aquests, que es pot fer des de la línia d'ordres amb: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "S'intentarà reconstruir el virtualenv per a les aplicacions següents (NB: l'operació pot trigar una mica!): {rebuild_apps}", + "migration_python_venv_rebuild_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_python_venv_rebuild_in_progress": "Ara s'està intentant reconstruir el virtualenv de Python per a `{app}`", + "migration_0031_terms_of_services": "Aquesta migració és purament un missatge informatiu sobre el fet que el projecte YunoHost ara publica Condicions dels Serveis relacionades amb els serveis tècnics i comunitaris.", + "migration_0036_cleaning_up": "Netejant la memòria cau i els paquets que ja no són útils…", + "migration_0036_delayed_api_restart": "L'API de YunoHost es reiniciarà automàticament en 15 segons. És possible que no estigui disponible durant uns segons i, aleshores, haureu de tornar a iniciar la sessió.", + "migration_0036_general_warning": "Finalment, 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\nPer tant, us recomanem que:\n - **Feu còpies de seguretat** de qualsevol dada o aplicació crítica. Més informació a https://doc.yunohost.org/backup;\n - **Sigueu pacients** després d'iniciar la migració: depenent de la vostra connexió a Internet i del maquinari, pot trigar fins a una hora a actualitzar-se tot correctament;\n - **Poseu-vos en contacte amb la comunitat** al fòrum si necessiteu ajuda per resoldre problemes.", + "migration_0036_main_upgrade": "Iniciant l'actualització principal…", + "migration_0036_modified_files": "Tingueu en compte que els fitxers següents s'han modificat manualment i que és possible que s'hagin sobreescrit després de l'actualització:", + "migration_0036_not_bullseye": "La distribució actual de Debian no és Bookworm! Si ja heu executat la migració de Bookworm a Trixie, aquest error és simptomàtic del fet que el procediment de migració no ha estat 100% satisfactori (en cas contrari, YunoHost l'hauria marcat com a completat). Es recomana investigar què ha passat amb l'equip de suport, que necessitarà el registre **complet** de la migració, que es pot trobar a Eines > Registres a l'administrador web.", + "migration_0036_not_enough_free_space": "L'espai lliure és força baix a /var/! Hauries de tenir com a mínim 1 GB lliure per executar aquesta migració.", + "migration_0036_patch_yunohost_dpkg": "Aplicant un pegat a la base de dades dpkg per solucionar els problemes de conflicte…", + "migration_0036_patching_sources_list": "S'està pegat el fitxer sources.lists…", + "migration_0036_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 «en funcionament». En conseqüència, no es pot garantir que continuïn funcionant després de l'actualització:", + "migration_0036_start": "Iniciant la migració a Trixie…", + "migration_0036_still_on_bookworm_after_main_upgrade": "Alguna cosa ha anat malament durant l'actualització principal, sembla que el sistema encara està a Debian Bookworm.", + "migration_0036_system_not_fully_up_to_date": "El vostre sistema no està completament actualitzat. Si us plau, feu una actualització regular abans d'executar la migració a Trixie.", + "migration_0036_yunohost_upgrade": "Iniciant l'actualització del nucli de YunoHost…", + "migration_description_0027_migrate_to_bookworm": "Actualitzeu el sistema a Debian Bookworm i YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Suprimeix els antics permisos XMPP, Metronome és ara una aplicació", + "migration_description_0029_postgresql_13_to_15": "Migra bases de dades de PostgreSQL 13 a 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Repareu l'aplicació Python després de la migració de bookworm", + "migration_description_0031_terms_of_services": "Condicions dels serveis", + "migration_description_0032_firewall_config": "Migració del fitxer de configuració del tallafocs intern", + "migration_description_0033_rework_permission_infos": "Reelaborar la manera com s'emmagatzemen els permisos de les aplicacions", + "migration_description_0034_fix_missing_admins_aliases": "Corregir els àlies de correu que falten per al grup d'administració", + "migration_description_0035_fix_apps_nodejs_version": "Corregir les versions de nodejs a les configuracions de systemd de l'aplicació", + "migration_description_0036_migrate_to_trixie": "Actualitzar el sistema a Debian Trixie i YunoHost 13", + "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.", + "migration_ldap_rollback_success": "El sistema s'ha revertit.", + "migrations_already_ran": "Aquestes migracions ja s'han fet: {ids}", + "migrations_dependencies_not_satisfied": "Executeu aquestes migracions: «{dependencies_id}», abans la migració {id}.", + "migrations_exclusive_options": "«--auto», «--skip», i «--force-rerun» són opcions mútuament excloents.", + "migrations_failed_to_load_migration": "No s'ha pogut carregar la migració {id}: {error}", + "migrations_list_conflict_pending_done": "No es pot utilitzar «--previous» i «--done» al mateix temps.", + "migrations_loading_migration": "Carregant la migració {id}…", + "migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}", + "migrations_must_provide_explicit_targets": "Heu de proporcionar objectius explícits al utilitzar «--skip» o «--force-rerun»", + "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».", + "migrations_no_migrations_to_run": "No hi ha cap migració a fer", + "migrations_no_such_migration": "No hi ha cap migració anomenada «{id}»", + "migrations_not_pending_cant_skip": "Aquestes migracions no estan pendents, així que no poden ser omeses: {ids}", + "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_skip_migration": "Saltant migració {id}…", + "migrations_success_forward": "Migració {id} completada", + "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».", + "nftables_unavailable": "No podeu modificar les nftables aquí. O bé sou en un contenidor o bé el vostre nucli no és compatible amb aquesta opció", + "noninteractive_task": "Tasca no interactiva", + "not_enough_disk_space": "No hi ha prou espai en «{path}»", + "operation_interrupted": "S'ha interromput manualment l'operació?", + "other_available_options": "… i {n} altres opcions disponibles no es mostren", + "password_confirmation_not_the_same": "La contrasenya i la confirmació no coincideixen", + "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_long": "Si us plau, trieu una contrasenya de menys de 127 caràcters", + "password_too_simple_1": "La contrasenya ha de tenir un mínim de 8 caràcters", + "password_too_simple_2": "La contrasenya ha de tenir un mínim de 8 caràcters i ha de contenir dígits, majúscules i minúscules", + "password_too_simple_3": "La contrasenya ha de tenir un mínim de 8 caràcters i tenir dígits, majúscules, minúscules i caràcters especials", + "password_too_simple_4": "La contrasenya ha de tenir un mínim de 12 caràcters i tenir dígits, majúscules, minúscules i caràcters especials", + "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_email_forward": "Ha de ser una adreça de correu vàlida, s'accepta el símbol «+» (per exemple, algu+etiqueta@exemple.cat)", + "pattern_fullname": "Ha de ser un nom complet 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_password_app": "Les contrasenyes no poden de tenir els següents caràcters: {forbidden_chars}", + "pattern_port_or_range": "Ha de ser un número de port vàlid (i.e. 0-65535) o un interval de ports (ex. 100:200)", + "pattern_username": "Ha d'estar compost per caràcters alfanumèrics en minúscula i guió baix exclusivament", + "permission_already_allowed": "El grup «{group}» ja té el permís «{permission}» activat", + "permission_already_disallowed": "El grup «{group}» ja té el permís «{permission}» desactivat", + "permission_cannot_remove_main": "No es permet eliminar un permís principal", + "permission_cant_add_to_all_users": "El permís {permission} no es pot afegir a tots els usuaris.", + "permission_created": "S'ha creat el permís «{permission}»", + "permission_creation_failed": "No s'ha pogut crear el permís «{permission}»: {error}", + "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_deleted": "S'ha eliminat el permís «{permission}»", + "permission_deletion_failed": "No s'ha pogut eliminar el permís «{permission}»: {error}", + "permission_not_found": "No s'ha trobat el permís «{permission}»", + "permission_protected": "El permís {permission} està protegit. No podeu afegir o eliminar el grup visitants a o d'aquest permís.", + "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.", + "permission_update_failed": "No s'ha pogut actualitzar el permís «{permission}»: {error}", + "permission_updated": "S'ha actualitzat el permís «{permission}»", + "port_already_closed": "El port {port} ja està tancat", + "port_already_opened": "El port {port} ja està obert", + "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", + "pydantic_type_error": "Tipus no vàlid.", + "pydantic_type_error_none_not_allowed": "El valor és obligatori.", + "pydantic_type_error_str": "Tipus no vàlid, s'espera una cadena.", + "pydantic_value_error_color": "No és un color vàlid, el valor ha de ser un color amb nom o hexadecimal.", + "pydantic_value_error_const": "valor inesperat; triar entre {permitted}", + "pydantic_value_error_date": "Format de data no vàlid", + "pydantic_value_error_email": "El valor no és una adreça electrònica vàlida", + "pydantic_value_error_number_not_ge": "El valor ha de ser superior o igual a {limit_value}.", + "pydantic_value_error_number_not_le": "El valor ha de ser inferior o igual a {limit_value}.", + "pydantic_value_error_str_regex": "Cadena no vàlida; el valor no respecta el patró «{pattern}»", + "pydantic_value_error_time": "Format d'hora no vàlid", + "pydantic_value_error_url_extra": "L'URL no és vàlid, s'han trobat caràcters addicionals després de l'URL vàlid: «{extra}»", + "pydantic_value_error_url_host": "URL amfitrió no vàlid", + "pydantic_value_error_url_port": "URL port no vàlid, el port no pot superar 65535", + "pydantic_value_error_url_scheme": "L'esquema d'URL no és vàlid o falta", + "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_file_backed_up": "S'ha guardat una còpia de seguretat del fitxer de configuració «{conf}» a «{backup}»", + "regenconf_file_copy_failed": "No s'ha pogut copiar el nou fitxer de configuració «{new}» a «{conf}»", + "regenconf_file_kept_back": "S'espera que el fitxer de configuració «{conf}» sigui suprimit per regen-conf (categoria {category}) però s'ha mantingut.", + "regenconf_file_manually_modified": "El fitxer de configuració «{conf}» s'ha modificat manualment i no serà actualitzat", + "regenconf_file_manually_removed": "El fitxer de configuració «{conf}» s'ha suprimit manualment i no serà creat", + "regenconf_file_remove_failed": "No s'ha pogut eliminar el fitxer de configuració «{conf}»", + "regenconf_file_removed": "El fitxer de configuració «{conf}» ha estat suprimit", + "regenconf_file_updated": "El fitxer de configuració «{conf}» ha estat actualitzat", + "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", + "regenconf_now_managed_by_yunohost": "El fitxer de configuració «{conf}» serà gestionat per YunoHost a partir d'ara (categoria {category}).", + "regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»…", + "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}»", + "regex_incompatible_with_tile": "/!\\ Empaquetadors! El permís «{permission}» té «show_tile» definit a «true» i pertant no pot definir una URL regex com a URL principal", + "regex_with_only_domain": "No podeu utilitzar una expressió regular com a domini, només com a ruta", + "registrar_infos": "Informació del registrador", + "restore_already_installed_app": "Una aplicació amb la ID «{app}» ja està instal·lada", + "restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}", + "restore_backup_too_old": "Aquest arxiu de còpia de seguretat no es pot restaurar perquè prové d'una versió de YunoHost massa antiga.", + "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_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_system_part_failed": "No s'ha pogut restaurar la part «{part}» del sistema", + "root_password_changed": "la contrasenya de root s'ha canviat", + "root_password_desynchronized": "S'ha canviat la contrasenya d'administració, però YunoHost no ha pogut propagar-ho cap a la contrasenya root!", + "server_reboot": "Es reiniciarà el servidor", + "server_reboot_confirm": "Es reiniciarà el servidor immediatament, n'esteu segur? [{answers}]", + "server_shutdown": "S'aturarà el servidor", + "server_shutdown_confirm": "S'aturarà el servidor immediatament, n'esteu segur? [{answers}]", + "service_add_failed": "No s'ha pogut afegir el servei «{service}»", + "service_added": "S'ha afegit el servei «{service}»", + "service_already_started": "El servei «{service}» ja està funcionant", + "service_already_stopped": "Ja s'ha aturat el servei «{service}»", + "service_cmd_exec_failed": "No s'ha pogut executar l'ordre «{command}»", + "service_description_dnsmasq": "Gestiona la resolució del nom de domini (DNS)", + "service_description_dovecot": "Permet als clients de correu accedir/recuperar correus (via IMAP i POP3)", + "service_description_fail2ban": "Protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet", + "service_description_mysql": "Guarda les dades de les aplicacions (base de dades SQL)", + "service_description_nftables": "Gestiona els ports de connexió oberts i tancats als serveis", + "service_description_nginx": "Serveix o permet l'accés a totes les pàgines web allotjades en el servidor", + "service_description_opendkim": "Signa els correus electrònics de sortida mitjançant DKIM de manera que és menys probable que es marquin com a correu brossa", + "service_description_postfix": "Utilitzat per enviar i rebre correus", + "service_description_postgresql": "Emmagatzema dades de l'aplicació (base de dades SQL)", + "service_description_redis-server": "Una base de dades especialitzada per l'accés ràpid a dades, files d'espera i comunicació entre programes", + "service_description_slapd": "Guarda el usuaris, dominis i informació relacionada", + "service_description_ssh": "Permet la connexió remota al servidor via terminal (protocol SSH)", + "service_description_yunohost-api": "Gestiona les interaccions entre la interfície web de YunoHost i el sistema", + "service_description_yunohost-portal-api": "Gestiona les interaccions entre les diferents interfícies web del portal i el sistema", + "service_description_yunomdns": "Us permet arribar al vostre servidor mitjançant 'yunohost.local' a la vostra xarxa local", + "service_disable_failed": "No s'han pogut fer que el servei «{service}» no comenci a l'arrancada.", + "service_disabled": "El servei «{service}» ja no començarà al arrancar el sistema.", + "service_enable_failed": "No s'ha pogut fer que el servei «{service}» comenci automàticament a l'arrancada.", + "service_enabled": "El servei «{service}» començarà automàticament durant l'arrancada del sistema.", + "service_not_reloading_because_conf_broken": "No s'està tornant a carregar/reiniciar el servei «{name}» perquè la seva configuració està trencada: {errors}", + "service_reload_failed": "No s'ha pogut tornar a carregar el servei «{service}»", + "service_reload_or_restart_failed": "No s'ha pogut tornar a carregar o reiniciar el servei «{service}»", + "service_reloaded": "S'ha tornat a carregar el servei «{service}»", + "service_reloaded_or_restarted": "S'ha tornat a carregar o s'ha reiniciat el servei «{service}»", + "service_remove_failed": "No s'ha pogut eliminar el servei «{service}»", + "service_removed": "S'ha eliminat el servei «{service}»", + "service_restart_failed": "No s'ha pogut reiniciar el servei «{service}»", + "service_restarted": "S'ha reiniciat el servei «{service}»", + "service_start_failed": "No s'ha pogut iniciar el servei «{service}»", + "service_started": "S'ha iniciat el servei «{service}»", + "service_stop_failed": "No s'ha pogut aturar el servei «{service}»", + "service_stopped": "S'ha aturat el servei «{service}»", + "service_unknown": "Servei «{service}» desconegut", + "session_expired": "Sessió caducada", + "show_tile_cant_be_enabled_for_regex": "No podeu activar «show_title» ara, perquè la URL per al permís «{permission}» és una expressió regular", + "show_tile_cant_be_enabled_for_url_not_defined": "No podeu activar «show_title» ara, perquè primer s'ha de definir una URL per al permís «{permission}»", + "ssowat_conf_generated": "S'han regenerat les configuracions del SSO i del portal", + "system_upgraded": "S'ha actualitzat el sistema", + "system_username_exists": "El nom d'usuari ja existeix en la llista d'usuaris de sistema", + "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema)… Podeu intentar resoldre el problema connectant-vos amb SSH i executant `sudo apt install --fix-broken` i/o `sudo dpkg --configure -a`.", + "tools_upgrade": "Actualitzant paquets del sistema", + "tools_upgrade_failed": "No s'han pogut actualitzar els paquets: {packages_list}", + "tos_dyndns_acknowledgement": "Heu triat registrar un domini DynDNS que és un servei que ofereix el projecte YunoHost. Tenint en compte que els noms de domini són un aspecte important dels serveis digitals a llarg termini, us recordem que llegiu atentament les Condicions dels Serveis corresponents, en particular la secció relativa a aquests noms de domini gratuïts: .", + "tos_postinstall_acknowledgement": "El projecte YunoHost és un equip de voluntaris que han fet una causa comuna per crear un sistema operatiu gratuït per a servidors, anomenat YunoHost. El programari YunoHost es publica sota la llicència AGPLv3 (). En relació amb aquest programari, el projecte administra i posa a la seva disposició diversos serveis tècnics i comunitaris amb diferents finalitats. En utilitzar aquests serveis, accepteu estar subjecte a les Condicions dels Serveis següents: .", + "unable_authenticate": "No s'ha pogut autenticar la sessió", + "unbackup_app": "{app} no es guardarà", + "unexpected_error": "Hi ha hagut un error inesperat: {error}", + "unknown_error_reading_file": "Error desconegut al intentar llegir el fitxer {file} (motiu: {error})", + "unknown_group": "Grup '{group}' desconegut", + "unknown_main_domain_path": "Domini o ruta desconeguda per a «{app}». Heu d'especificar un domini i una ruta per a poder especificar una URL per al permís.", + "unknown_user": "Usuari '{user}' desconegut", + "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…", + "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", + "upnp_port_open_failed": "No s'ha pogut obrir el port UPnP", + "user_already_exists": "L'usuari «{user}» ja existeix", + "user_cannot_delete_last_admin": "El compte '{user}' és l'últim del grup d'administració i no s'eliminarà.", + "user_created": "S'ha creat l'usuari", + "user_creation_failed": "No s'ha pogut crear l'usuari {user}: {error}", + "user_deleted": "S'ha suprimit l'usuari", + "user_deletion_failed": "No s'ha pogut suprimir l'usuari {user}: {error}", + "user_home_creation_failed": "No s'ha pogut crear la carpeta personal «{home}» per l'usuari", + "user_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_cannot_edit_or_delete_admins": "No es pot editar o suprimir '{user}' mitjançant la importació perquè el compte és d'administració", + "user_import_failed": "L'operació d'importació dels usuaris ha fallat completament", + "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", + "user_import_success": "Els usuaris s'han importat correctament", + "user_unknown": "Usuari desconegut: {user}", + "user_update_failed": "No s'ha pogut actualitzar l'usuari {user}: {error}", + "user_updated": "S'ha canviat la informació de l'usuari", + "visitors": "Visitants", + "yunohost_already_installed": "YunoHost ja està instal·lat", + "yunohost_api": "API de YunoHost", + "yunohost_configured": "YunoHost està configurat", + "yunohost_installing": "Instal·lació de YunoHost…", + "yunohost_not_installed": "YunoHost no està instal·lat correctament. Executeu «yunohost tools postinstall»", + "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://doc.yunohost.org/admin." +} diff --git a/locales/ckb.json b/locales/ckb.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/ckb.json @@ -0,0 +1 @@ +{} diff --git a/locales/cs.json b/locales/cs.json new file mode 100644 index 0000000..5300445 --- /dev/null +++ b/locales/cs.json @@ -0,0 +1,221 @@ +{ + "aborting": "Ukončuji.", + "action_invalid": "Nesprávná akce '{action}'", + "additional_urls_already_added": "Další URL '{url}' již bylo přidáno pro oprávnění '{permission}'", + "additional_urls_already_removed": "Další URL '{url}' již bylo odebráno u oprávnění '{permission}'", + "admin_password": "Administrační heslo", + "admins": "Administrátoři", + "all_users": "Všichni uživatelé YunoHost", + "already_up_to_date": "Neprovedena žádná akce. Vše je již aktuální.", + "app_action_broke_system": "Zdá se, že tato akce rozbila následující důležité služby: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Pro běh této akce by měli být spuštěné následující služby: {services}. Zkuste je zrestartovat, případně zjistěte, proč neběží.", + "app_action_failed": "Selhala akce {action} pro aplikaci {app}", + "app_already_installed": "{app} je již nainstalován/a", + "app_already_installed_cant_change_url": "Tato aplikace je již nainstalována. URL nemůže být touto akcí změněna. Zkontrolujte `app changeurl` pokud je dostupné.", + "app_arch_not_supported": "Tato aplikace může být nainstalována na architekturách {required}, ale architektura vašeho serveru je {current}", + "app_argument_choice_invalid": "Vyberte jednu z možností '{choices}' pro argument'{name}'", + "app_argument_invalid": "Vyberte správnou hodnotu pro argument '{name}': {error}", + "app_change_url_failed": "Nemohla se změnit adresa URL pro {app}: {error}", + "app_change_url_identical_domains": "Stará a nová doména/url_cesta jsou totožné ('{domain}{path}'), nebudou provedeny žádné změny.", + "app_change_url_no_script": "Aplikace '{app_name}' nyní nepodporuje URL modifikace. Zkuste ji aktualizovat.", + "app_change_url_require_full_domain": "{app} nemůže být přesunuto na tuto novou URL adresu, protože vyžaduje celou doménu (tj. s cestou = /)", + "app_change_url_script_failed": "Uvnitř skriptu pro změnu URL došlo k chybě", + "app_change_url_success": "{app} URL je nyní {domain}{path}", + "app_config__core_name": "Konfigurace a operace", + "app_config_permission_allowed": "Skupiny/uživatelé s povolením k přístupu", + "app_config_permission_description": "Popis", + "app_config_permission_description_help": "Toto je pouze užitečné, pokud používáte 'deskriptivní' mód portálu", + "app_config_permission_extraperm_section_name": "Povolení '{perm}'", + "app_config_permission_label": "Štítek", + "app_config_permission_location": "Koresponduje k [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Vlastní logo na použití", + "app_config_permission_logo_help": "Pouze PNG jsou podporovány", + "app_extraction_failed": "Nelze rozbalit instalační soubory", + "app_full_domain_unavailable": "Tato aplikace musí být nainstalována na své vlastní doméně, ale jiné aplikace jsou již nainstalovány na doméně '{domain}'. Můžete použít poddoménu určenou pouze pro tuto aplikaci.", + "app_id_invalid": "Neplatné ID aplikace", + "app_install_failed": "Nelze instalovat {app}: {error}", + "app_install_files_invalid": "Tyto soubory nemohou být instalovány", + "app_install_script_failed": "Vyskytla se chyba uvnitř instalačního skriptu aplikace", + "app_manifest_install_ask_admin": "Vyberte administrátorského uživatele pro tuto aplikaci", + "app_manifest_install_ask_domain": "Vyberte doménu, kde by měla být tato aplikace nainstalována", + "app_manifest_install_ask_init_admin_permission": "Kdo by měl mít přístup k administrátorským funkcím této aplikace? (Toto může být později změněno)", + "app_manifest_install_ask_init_main_permission": "Kdo by měl mít přístup k této aplikaci? (Toto může být později změněno)", + "app_manifest_install_ask_is_public": "Měla by tato aplikace být dostupná pro anonymní návštěvníky?", + "app_manifest_install_ask_password": "Vyberte si administrátorské heslo pro tuto aplikaci", + "app_not_enough_disk": "Tato aplikace vyžaduje {required} volného místa.", + "app_removed": "Aplikace {app} odinstalována", + "app_requirements_checking": "Kontroluji požadavky pro {app}…", + "app_start_install": "Instaluji {app}…", + "app_start_remove": "Odstraňuji {app}…", + "app_start_restore": "Obnovuji {app}…", + "app_unknown": "Neznámá aplikace", + "cannot_open_file": "Nelze otevřít soubor/y {file} (reason: {error})", + "cannot_write_file": "Nelze zapsat soubor/y {file} (reason: {error})", + "corrupted_json": "Nepodařilo se načíst JSON {ressource} (reason: {error})", + "corrupted_toml": "Nepodařilo se načíst TOML z {ressource} (reason: {error})", + "corrupted_yaml": "Nepodařilo se načíst YAML z {ressource} (reason: {error})", + "download_bad_status_code": "{url} vrátil stavový kód {code}", + "download_ssl_error": "SSL chyba při spojení s {url}", + "download_timeout": "{url} příliš dlouho neodpovídá, akce přerušena.", + "download_unknown_error": "Chyba při stahování dat z {url}: {error}", + "error_changing_file_permissions": "Chyba při nastavování oprávnění pro {path}: {error}", + "error_removing": "Chyba při přesunu {path}: {error}", + "error_writing_file": "Chyba při zápisu souboru/ů {file}: {error}", + "file_not_exist": "Soubor neexistuje: '{path}'", + "global_settings_setting_admin_strength": "Požadavky na sílu hesla administrátora/ky", + "global_settings_setting_backup_compress_tar_archives_help": "Komprimovat nové zálohy (.tar.gz) namísto nekomprimovaných (.tar). Poznámka: povolení této volby znamená objemově menší soubory záloh, avšak zálohování bude trvat déle a bude více zatěžovat CPU.", + "global_settings_setting_postfix_compatibility_help": "Kompromis mezi kompatibilitou a bezpečností Postfix serveru. Ovlivní šifry a další související bezpečnostní nastavení", + "global_settings_setting_smtp_allow_ipv6_help": "Povolit použití IPv6 pro příjem a odesílání e-mailů", + "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 blocklistu (např. DUHL), nemůžete nastavit reverzní DNS záznam nebo tento server není přímo připojen do internetu a vy chcete použít jiný server k odesílání emailů.", + "global_settings_setting_smtp_relay_password": "SMTP relay heslo uživatele/hostitele", + "global_settings_setting_smtp_relay_port": "SMTP relay port", + "global_settings_setting_smtp_relay_user": "SMTP relay uživatelské jméno/účet", + "global_settings_setting_ssh_compatibility_help": "Kompromis mezi kompatibilitou a bezpečností SSH serveru. Ovlivní šifry a další související bezpečnostní nastavení. Viz https://infosec.mozilla.org/guidelines/openssh pro více informací.", + "global_settings_setting_ssh_port": "SSH port", + "global_settings_setting_user_strength": "Síla uživatelského hesla", + "good_practices_about_admin_password": "Nyní zvolte nové administrační heslo. Heslo by mělo být minimálně 8 znaků dlouhé, avšak je dobrou taktikou jej mít delší (např. použít více slov) a použít kombinaci znaků (velké, malé, čísla a speciílní znaky).", + "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).", + "group_already_exist": "Skupina {group} již existuje", + "group_already_exist_on_system": "Skupina {group} se již nalézá v systémových skupinách", + "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_cannot_be_deleted": "Skupina {group} nemůže být smazána.", + "group_cannot_edit_all_users": "Skupina 'all_users' nemůže být upravena. Jde o speciální skupinu obsahující všechny registrované uživatele na YunoHost", + "group_cannot_edit_primary_group": "Skupina '{group}' nemůže být upravena. Jde o primární skupinu obsahující pouze jednoho specifického uživatele.", + "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_created": "Skupina '{group}' vytvořena", + "group_creation_failed": "Nelze založit skupinu '{group}': {error}", + "group_deleted": "Skupina '{group}' smazána", + "group_deletion_failed": "Nelze smazat skupinu '{group}': {error}", + "group_unknown": "Neznámá skupina '{group}'", + "group_update_failed": "Nelze upravit skupinu '{group}': {error}", + "group_updated": "Skupina '{group}' upravena", + "group_user_already_in_group": "Uživatel {user} je již ve skupině {group}", + "invalid_url": "Nedá se připojit k {url}... možná je služba mimo provoz nebo nejste správně připojeni k internetu přes IPv4/IPv6.", + "migration_0027_yunohost_upgrade": "Spouštění aktualizace jádra YunoHost…", + "operation_interrupted": "Operace byla manuálně přerušena?", + "password_listed": "Toto heslo je jedním z nejpoužívanějších na světě. Zvolte si prosím něco jedinečnějšího.", + "password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé", + "password_too_simple_2": "Heslo musí být aspoň 8 znaků dlouhé a obsahovat číslici, velká a malá písmena", + "password_too_simple_3": "Heslo musí být aspoň 8 znaků dlouhé a obsahovat čísla, velká a malá písmena a speciální znaky", + "password_too_simple_4": "Heslo musí být aspoň 12 znaků dlouhé a obsahovat čísla, velká a malá písmena a speciální znaky", + "session_expired": "Sezení vypršelo", + "system_upgraded": "Systém aktualizován", + "system_username_exists": "Uživatelské jméno již existuje v seznamu systémových uživatelů", + "tools_upgrade": "Aktualizuji systémové balíčky", + "unknown_error_reading_file": "Vyskytla se neznámá chyba při čtení souboru/ů {file} (reason: {error})", + "unknown_group": "Neznámá '{group}' skupina", + "unknown_user": "Neznámý '{user}' uživatel", + "unrestore_app": "{app} nebude obnoveno", + "upgrading_packages": "Aktualizuji balíčky…", + "upnp_dev_not_found": "Nebylo nalezeno žádné UPnP zařízení", + "upnp_disabled": "UPnP vypnuto", + "upnp_enabled": "UPnP zapnuto", + "upnp_port_open_failed": "Port nemohl být otevřen přes UPnP", + "user_already_exists": "Uživatel '{user}' již existuje", + "user_cannot_delete_last_admin": "Uživatel '{user}' je posledním uživatelem ve skupině 'administrátoři' a nebude smazán.", + "user_created": "Uživatel vytvořen", + "user_creation_failed": "Nemohl být vytvořen uživatel {user}: {error}", + "user_deleted": "Uživatel smazán", + "user_deletion_failed": "Uživatel {user} nemohl být smazán: {error}", + "user_home_creation_failed": "Pro uživatele nemohla být vytvořena domovská složka '{home}'", + "user_import_success": "Uživatelé úspěšně importováni", + "user_unknown": "Neznámý uživatel: {user}", + "user_update_failed": "Nebylo možné aktualizovat uživatele {user}: {error}", + "user_updated": "Uživatelské informace změněny", + "visitors": "Návštěvníci", + "yunohost_already_installed": "YunoHost je již nainstalováno", + "yunohost_api": "YunoHost API", + "yunohost_configured": "YunoHost je nyní nakonfigurováno", + "yunohost_installing": "Instaluje se YunoHost…", + "yunohost_not_installed": "YunoHost není správně nainstalováno. Prosím spusťte 'yunohost tools postinstall'", + "domain_cert_gen_failed": "Nelze vygenerovat certifikát", + "domain_config_api_protocol": "API protokol", + "domain_config_auth_application_key": "Klíč aplikace", + "domain_config_auth_application_secret": "Tajný klíč aplikace", + "domain_config_cert_install": "Instalovat Let's Encrypt certifikát", + "domain_config_cert_issuer": "Certifikační autorita", + "domain_config_cert_name": "Certifikát", + "domain_config_cert_validity": "Platnost", + "domain_config_custom_css": "Vlastní CSS", + "domain_config_feature_name": "Vlastnosti", + "domain_config_mail_in": "Příchozí e-maily", + "domain_config_mail_out": "Odchozí e-maily", + "domain_config_portal_logo": "Volitelné logo", + "extracting": "Rozbaluji…", + "global_settings_reset_success": "Resetovat globální nastavení", + "global_settings_setting_backup_compress_tar_archives": "Komprimovat zálohy", + "global_settings_setting_email_name": "E-mail", + "global_settings_setting_nginx_redirect_to_https": "Vynutit HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Přesměrovat vždy HTTP požadavky na HTTPS (NEVYPÍNEJTE TOTO pokud opravdu nevíte, co to dělá!)", + "global_settings_setting_password_name": "Hesla", + "global_settings_setting_passwordless_sudo": "Povolit adminům použít 'sudo' bez znovu napsání jejich hesel", + "global_settings_setting_pop3_enabled": "Povolit POP3", + "global_settings_setting_pop3_enabled_help": "Povolit POP3 protokol pro poštovní server. POP3 je starší protokol pro přístup k poštovním schránkám z e-mailových klientů, které jsou nenáročné, ale mají méně funkcí než IMAP (ve výchozím stavu povoleno)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Povolit uživatelům upravit jejich hlavní e-mailovou adresu", + "global_settings_setting_portal_allow_edit_email_alias": "Povolit uživatelům přidat, odstranit a upravit poštovní aliasy", + "global_settings_setting_portal_allow_edit_email_alias_help": "Pokud je zakázáno, musejí se zeptat admina/ů, aby to pro ně vykonali.", + "global_settings_setting_portal_allow_edit_email_forward": "Povolit uživatelům přidat, odstranit a upravit přeposílání e-mailů", + "global_settings_setting_portal_allow_edit_email_forward_help": "Pokud je zakázáno, musejí se zeptat admina/ů, aby to pro ně vykonali.", + "global_settings_setting_portal_allow_edit_email_help": "Pokud je zakázáno, musejí se zeptat admina/ů, aby to pro ně vykonali.", + "global_settings_setting_portal_name": "Portál", + "global_settings_setting_postfix_compatibility": "Kompatibilita s postfixem", + "global_settings_setting_postfix_name": "Postfix (SMTP poštovní server)", + "global_settings_setting_root_access_name": "Změnit heslo uživatele root", + "global_settings_setting_root_password": "Nové heslo uživatele root", + "global_settings_setting_root_password_confirm": "Nové heslo uživatele root (potvrzení)", + "global_settings_setting_security_experimental_enabled": "Experimentální bezpečnostní funkce", + "global_settings_setting_security_name": "Bezpečnost", + "global_settings_setting_smtp_allow_ipv6": "Povolit IPv6", + "global_settings_setting_ssh_compatibility": "SSH kompatibilita", + "global_settings_setting_ssh_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Přihlášení heslem", + "global_settings_setting_ssh_password_authentication_help": "Povolit přihlášení heslem pro SSH", + "hook_exec_not_terminated": "Skript neskončil podle očekávání: {path}", + "installation_complete": "Instalace kompletní", + "invalid_credentials": "Neplatné heslo nebo uživatelské jméno", + "invalid_number": "Musí být číslem", + "invalid_password": "Neplatné heslo", + "log_tools_shutdown": "Vypnout váš server", + "log_tools_reboot": "Restartovat váš server", + "log_settings_reset": "Reset nastavení", + "log_settings_reset_all": "Reset všech nastavení", + "log_settings_set": "Aplikovat nastavení", + "log_tools_migrations_migrate_forward": "Spustit migrace", + "migration_description_0031_terms_of_services": "Podmínky služeb", + "migration_ldap_rollback_success": "Systém obnoven zpět.", + "migrations_already_ran": "Tyto migrace jsou již hotové: {ids}", + "migration_description_0029_postgresql_13_to_15": "Migrovat databáze z PostgreSQL 13 na 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Opravit Python aplikace po migraci Debian 12 Bookworm", + "ask_admin_username": "Uživatelské jméno administrátora/ky", + "ask_dyndns_recovery_password": "DynDNS heslo pro obnovení", + "app_upgrade_app_name": "Nyní aktualizuji {app}…", + "app_upgrade_some_app_failed": "Některé aplikace nemohou být aktualizovány", + "app_upgrade_several_apps": "Následující aplikace budou aktualizovány: {apps}", + "apps_already_up_to_date": "Všechny aplikace jsou aktuální", + "ask_main_domain": "Hlavní doména", + "ask_new_admin_password": "Nové heslo administrátora", + "ask_new_domain": "Nová doména", + "ask_new_path": "Nová cesta", + "ask_password": "Heslo", + "diagnosis_rfkill_wifi": "Wi-Fi karta je vypnutá a systémové varování může zabránit instalaci aplikací", + "diagnosis_ports_could_not_diagnose_details": "Chyba: {error}", + "diagnosis_mail_queue_unavailable_details": "Chyba: {error}", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Reverzní DNS záznam je nesprávně nakonfigurován pro IPv{ipversion}. Některým e-mailům může selhat doručení nebo mohou být označeny jako spam.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Současný DNS reverzní záznam: {rdns_domain}
Očekávaná hodnota: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "Pro IPv{ipversion} nebyl definován žádný DNS reverzní záznam. Některým e-mailům může selhat doručení nebo mohou být označeny jako spam.", + "diagnosis_mail_blocklist_reason": "Důvod zablokování je: {reason}", + "diagnosis_ip_dnsresolution_working": "Překlad jmen DNS je funkční!", + "diagnosis_ip_global": "Globální IP: {global}", + "diagnosis_ip_local": "Lokální IP: {local}", + "diagnosis_ip_no_ipv4": "Server nemá funkční IPv4.", + "diagnosis_ip_no_ipv6": "Server nemá funkční IPv6.", + "diagnosis_ip_connected_ipv4": "Server je připojen do internetu přes IPv4!", + "diagnosis_ip_connected_ipv6": "Server je připojen do internetu přes IPv6!", + "diagnosis_unknown_categories": "Následující kategorie jsou neznámé: {categories}", + "domain_config_auth_consumer_key": "Spotřebitelský klíč", + "domain_config_auth_entrypoint": "Vstupní bod API", + "domain_config_auth_key": "Autentizační klíč", + "domain_config_auth_secret": "Autentizační tajemství", + "backup_archive_open_failed": "Nelze otevřít archiv zálohy" +} diff --git a/locales/da.json b/locales/da.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/da.json @@ -0,0 +1 @@ +{} diff --git a/locales/de.json b/locales/de.json new file mode 100644 index 0000000..c444ff3 --- /dev/null +++ b/locales/de.json @@ -0,0 +1,845 @@ +{ + "aborting": "Breche ab.", + "action_invalid": "Ungültige Aktion '{action}'", + "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}'", + "admin_password": "Administrator-Passwort", + "admins": "Administratoren", + "all_users": "Alle YunoHost-Nutzer", + "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}", + "app_action_cannot_be_ran_because_required_services_down": "Diese erforderlichen Dienste sollten zur Durchführung dieser Aktion laufen: {services}. Versuchen Sie, sie neu zu starten, um fortzufahren (und möglicherweise zu untersuchen, warum sie nicht verfügbar sind).", + "app_action_failed": "Fehlgeschlagene Aktion {action} für Applikation {app}", + "app_already_installed": "{app} ist schon installiert", + "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_arch_not_supported": "Diese App kann nur auf bestimmten Architekturen {required} installiert werden, aber Ihre gegenwärtige Serverarchitektur ist {current}", + "app_argument_choice_invalid": "Wähle einen gültigen Wert für das Argument '{name}': '{value}' ist nicht unter den verfügbaren Auswahlmöglichkeiten ({choices})", + "app_argument_invalid": "Wähle einen gültigen Wert für das Argument '{name}': {error}", + "app_change_url_failed": "Kann die URL für {app} nicht ändern: {error}", + "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain} {path}'). Es gibt nichts zu tun.", + "app_change_url_no_script": "Die Applikation '{app_name}' unterstützt bisher keine URL-Modifikation. Vielleicht sollte sie aktualisiert werden.", + "app_change_url_require_full_domain": "{app} kann nicht auf diese neue URL verschoben werden, weil sie eine vollständige eigene Domäne benötigt (z.B. mit Pfad = /)", + "app_change_url_script_failed": "Es ist ein Fehler im URL-Änderungs-Script aufgetreten", + "app_change_url_success": "{app} URL ist nun {domain}{path}", + "app_config__core_name": "Einrichtung und Betrieb", + "app_config_permission_allowed": "Zugriffsberechtigte Gruppen/Konten", + "app_config_permission_allowed_warn_protected": "Hinweis: Diese Berechtigung ist „geschützt“, so dass die Gruppe „Besucher“ nicht zu den autorisierten Gruppen hinzugefügt oder entfernt werden kann.", + "app_config_permission_description": "Beschreibung", + "app_config_permission_extraperm_section_name": "Berechtigung '{perm}'", + "app_config_permission_logo": "Eigenes Logo", + "app_config_permission_logo_help": "Nur PNG wird unterstützt", + "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.", + "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}", + "app_extraction_failed": "Installationsdateien konnten nicht entpackt werden", + "app_failed_to_download_asset": "Konnte die Ressource '{source_id}' ({url}) für {app} nicht herunterladen: {out}", + "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_id_invalid": "Falsche Applikations-ID", + "app_install_failed": "Installation von {app} fehlgeschlagen: {error}", + "app_install_files_invalid": "Diese Dateien können nicht installiert werden", + "app_install_script_failed": "Im Installationsscript ist ein Fehler aufgetreten", + "app_location_unavailable": "Diese URL ist nicht verfügbar oder wird von einer installierten Applikation genutzt:\n{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", + "app_manifest_install_ask_admin": "Wähle einen Administrator für diese Applikation", + "app_manifest_install_ask_domain": "Wähle die Domäne, auf welcher die Applikation installiert werden soll", + "app_manifest_install_ask_init_admin_permission": "Wer soll Zugriff auf die administrativen Funktionen für diese App erhalten? (Dies kann später wieder geändert werden)", + "app_manifest_install_ask_init_main_permission": "Wer soll Zugriff auf diese App erhalten? (Dies kann später wieder geändert werden)", + "app_manifest_install_ask_is_public": "Soll diese Applikation für Gäste sichtbar sein?", + "app_manifest_install_ask_password": "Wähle ein Verwaltungspasswort für diese Applikation", + "app_manifest_install_ask_path": "Wähle den URL-Pfad (nach der Domäne), unter dem die Applikation installiert werden soll", + "app_not_correctly_installed": "{app} scheint nicht korrekt installiert zu sein", + "app_not_enough_disk": "Diese App benötigt {required} freien Speicherplatz.", + "app_not_enough_ram": "Diese App benötigt {required} RAM um installiert/aktualisiert zu werden, aber es sind aktuell nur {current} verfügbar.", + "app_not_installed": "{app} konnte nicht in der Liste installierter Apps gefunden werden: {all_apps}", + "app_not_properly_removed": "{app} wurde nicht ordnungsgemäß entfernt", + "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.", + "app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation…", + "app_removed": "{app} wurde entfernt", + "app_requirements_checking": "Überprüfe Voraussetzungen für {app}…", + "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_restore_failed": "Konnte {app} nicht wiederherstellen: {error}", + "app_restore_script_failed": "Im Wiederherstellungsskript der Applikation ist ein Fehler aufgetreten", + "app_sources_fetch_failed": "Quelldateien konnten nicht abgerufen werden, ist die URL korrekt?", + "app_start_backup": "Sammeln von Dateien, die für {app} gesichert werden sollen…", + "app_start_install": "{app} wird installiert…", + "app_start_remove": "{app} wird entfernt…", + "app_start_restore": "{app} wird wiederhergestellt…", + "app_unknown": "Unbekannte App", + "app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet", + "app_upgrade_app_name": "{app} wird jetzt aktualisiert…", + "app_upgrade_failed": "{app} konnte nicht aktualisiert werden: {error}", + "app_upgrade_script_failed": "Es ist ein Fehler im App-Upgrade-Skript aufgetreten", + "app_upgrade_several_apps": "Die folgenden Apps werden aktualisiert: {apps}", + "app_upgrade_some_app_failed": "Einige Applikationen können nicht aktualisiert werden", + "app_upgraded": "{app} aktualisiert", + "app_yunohost_version_not_supported": "Diese App setzt YunoHost >= {required} voraus aber die gegenwärtig installierte Version ist {current}", + "apps_already_up_to_date": "Alle Apps sind bereits aktuell", + "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!", + "apps_catalog_updating": "Aktualisierung des Applikationskatalogs…", + "ask_admin_fullname": "Vollständiger Name des Administrators", + "ask_admin_username": "Benutzername des Administrators", + "ask_dyndns_recovery_password": "DynDNS Wiederherstellungspasswort", + "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.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Bitte geben Sie das Wiederherstellungspasswort für Ihre DynDNS-Domain ein.", + "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_fullname": "Vollständiger Name (Vorname und Nachname)", + "ask_main_domain": "Hauptdomain", + "ask_new_admin_password": "Neues Verwaltungskennwort", + "ask_new_domain": "Neue Domain", + "ask_new_path": "Neuer Pfad", + "ask_password": "Passwort", + "ask_user_domain": "Domäne, welche für die E-Mail-Adresse und den XMPP-Account des Kontos verwendet werden soll", + "backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt", + "backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien…", + "backup_applying_method_copy": "Kopiere alle Dateien ins Backup…", + "backup_applying_method_custom": "Rufe die benutzerdefinierte Backup-Methode '{method}' auf…", + "backup_applying_method_tar": "Erstellen des Backup-tar Archives…", + "backup_archive_app_not_found": "{app} konnte in keiner Datensicherung gefunden werden", + "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path})", + "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).", + "backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}", + "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_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", + "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_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden", + "backup_cleaning_failed": "Temporäres Sicherungsverzeichnis konnte nicht geleert werden", + "backup_copying_to_organize_the_archive": "Kopieren von {size} MB, um das Archiv zu organisieren", + "backup_couldnt_bind": "{src} konnte nicht an {dest} angebunden werden.", + "backup_create_size_estimation": "Das Archiv wird etwa {size} an Daten enthalten.", + "backup_created": "Datensicherung vollständig: {name}", + "backup_creation_failed": "Konnte Backup-Archiv nicht erstellen", + "backup_csv_addition_failed": "Es konnten keine Dateien zur Sicherung in die CSV-Datei hinzugefügt werden", + "backup_csv_creation_failed": "Die zur Wiederherstellung erforderliche CSV-Datei kann nicht erstellt werden", + "backup_custom_backup_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Sicherung\" ein Fehler aufgetreten", + "backup_custom_mount_error": "Bei der benutzerdefinierten Sicherungsmethode ist beim Arbeitsschritt \"Einhängen/Verbinden\" ein Fehler aufgetreten", + "backup_delete_error": "Pfad '{path}' konnte nicht gelöscht werden", + "backup_deleted": "Backup gelöscht: {name}", + "backup_hook_unknown": "Der Datensicherungshook '{hook}' unbekannt", + "backup_method_copy_finished": "Sicherungskopie beendet", + "backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method}' beendet", + "backup_method_tar_finished": "Tar-Backup-Archiv erstellt", + "backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten…", + "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht", + "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_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_running_hooks": "Datensicherunghook wird ausgeführt…", + "backup_system_part_failed": "Der Systemteil '{part}' konnte nicht gesichert werden", + "backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden", + "backup_with_no_backup_script_for_app": "Die App {app} hat kein Sicherungsskript. Ignoriere es.", + "backup_with_no_restore_script_for_app": "{app} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.", + "cannot_open_file": "Datei {file} konnte nicht geöffnet werden (Ursache: {error})", + "cannot_write_file": "Kann Datei {file} nicht schreiben (reason: {error})", + "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_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_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_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain} zu öffnen (Datei: {file}), Grund: {reason}", + "certmanager_cert_install_failed": "Installation des Let's Encrypt-Zertifikat fehlgeschlagen für {domains}", + "certmanager_cert_install_failed_selfsigned": "Installation des selbst-signierten Zertifikats fehlgeschlagen für {domains}", + "certmanager_cert_install_success": "Let's-Encrypt-Zertifikat für die Domäne {domain} ist jetzt installiert", + "certmanager_cert_install_success_selfsigned": "Das selbstsignierte Zertifikat für die Domäne '{domain}' wurde erfolgreich installiert", + "certmanager_cert_renew_failed": "Erneuern des Let's Encrypt-Zertifikat fehlgeschlagen für {domains}", + "certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain} wurde erfolgreich erneuert", + "certmanager_cert_signing_failed": "Das neue Zertifikat konnte nicht signiert werden", + "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain} ist fehlgeschlagen…", + "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_domain_dns_ip_differs_from_public_ip": "Die DNS-Einträge der Domäne '{domain}' unterscheiden sich von der IP dieses Servers. Für weitere Informationen überprüfen Sie bitte die Kategorie \"DNS-Einträge\" (basic) in der Diagnose. Wenn Sie kürzlich Ihren A-Eintrag verändert haben, warten Sie bitte ein wenig, bis die Änderungen wirksam werden (es gibt Online-Checks für die DNS-Propagation). (Wenn Sie wissen, was Sie tun, können Sie '--no-checks' verwenden, um diese Überprüfung zu überspringen.)", + "certmanager_domain_http_not_working": "Es scheint, als ob die Domäne '{domain}' über HTTP nicht erreichbar ist. Bitte schauen Sie sich die 'Web'-Kategorie in der Diagnose an für weitere Informationen. (Wenn Sie wissen, was Sie tun, nutzen Sie '--no-checks' um die Überprüfung zu deaktivieren.)", + "certmanager_domain_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.)", + "certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit zu viele Zertifikate für dieselbe Domäne {domain} ausgestellt. Bitte versuche es später nochmal. Besuche https://letsencrypt.org/docs/rate-limits/ für mehr Informationen", + "certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain} (Datei: {file}) konnte nicht gelesen werden", + "certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file})", + "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file})", + "config_action_disabled": "Konnte die Aktion '{action}' nicht durchführen, weil sie deaktiviert ist. Stellen Sie sicher, dass sie ihre Einschränkungen einhält. Hilfe: {help}", + "config_action_failed": "Ausführung der Aktion '{action}' fehlgeschlagen: {error}", + "config_apply_failed": "Anwenden der neuen Konfiguration fehlgeschlagen: {error}", + "config_cant_set_value_on_section": "Sie können einen einzelnen Wert nicht auf einen gesamten Konfigurationsbereich anwenden.", + "config_forbidden_keyword": "Das Schlüsselwort '{keyword}' ist reserviert. Sie können kein Konfigurationspanel mit einer Frage erstellen, das diese ID verwendet.", + "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}').", + "config_no_panel": "Kein Konfigurationspanel gefunden.", + "config_unknown_filter_key": "Der Filterschlüssel '{filter_key}' ist inkorrekt.", + "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_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_warning": "Warnung: Diese Applikation funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single-Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers}] ", + "confirm_app_insufficient_ram": "Diese App braucht mehr RAM zur Installation, als derzeit verfügbar ist. 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}'", + "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}]", + "confirm_tos_acknowledgement": "Ich habe die Nutzungsbedingungen gelesen und verstanden [{answers}]", + "corrupted_json": "Beschädigtes JSON gelesen von {ressource} (reason: {error})", + "corrupted_toml": "Beschädigtes TOML gelesen von {ressource} (reason: {error})", + "corrupted_yaml": "Beschädigtes YAML gelesen von {ressource} (reason: {error})", + "danger": "Warnung:", + "diagnosis_apps_allgood": "Alle installierten Apps berücksichtigen die grundlegenden Paketierungspraktiken", + "diagnosis_apps_bad_quality": "Diese App ist im YunoHost-Applikationskatalog momentan als defekt gekennzeichnet. Es könnte sich dabei um einen vorübergehendes Problem handeln. Während der/die Betreuer:in versucht das Problem zu beheben, ist die Upgrade-Funktion für diese App gesperrt.", + "diagnosis_apps_broken": "Diese App ist im YunoHost-Applikationskatalog momentan als defekt gekennzeichnet. Es könnte sich dabei um einen vorübergehendes Problem handeln. Während der/die Betreuer:in versucht das Problem zu beheben, ist die Upgrade-Funktion für diese App gesperrt.", + "diagnosis_apps_deprecated_practices": "Die installierte Version dieser Applikation verwendet gewisse veraltete Paketierungspraktiken. Sie sollten sie wirklich aktualisieren.", + "diagnosis_apps_issue": "Ein Problem für die App {app} ist aufgetreten", + "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_packaging_format": "Diese App nutzt ein Paketformat, dass bald nicht mehr von YunoHost unterstützt wird. Denken Sie daran, die App rechtzeitig zu aktualisieren.", + "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_backports_in_sources_list": "Sie haben vermutlich apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie, was Sie tun.", + "diagnosis_basesystem_hardware": "Server Hardware Architektur ist {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", + "diagnosis_basesystem_host": "Server läuft unter Debian {debian_version}", + "diagnosis_basesystem_kernel": "Server läuft unter Linux-Kernel {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Sie verwenden inkonsistente Versionen der YunoHost-Pakete… wahrscheinlich wegen eines fehlgeschlagenen oder teilweisen Upgrades.", + "diagnosis_basesystem_ynh_main_version": "Server läuft YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} Version: {version} ({repo})", + "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_description_apps": "Applikationen", + "diagnosis_description_basesystem": "Grundsystem", + "diagnosis_description_dnsrecords": "DNS-Einträge", + "diagnosis_description_ip": "Internetkonnektivität", + "diagnosis_description_mail": "E-Mail", + "diagnosis_description_ports": "Geöffnete Ports", + "diagnosis_description_regenconf": "Systemkonfiguration", + "diagnosis_description_services": "Dienste-Status", + "diagnosis_description_systemresources": "Systemressourcen", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Der Speicher {mountpoint} (auf Gerät {device}) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von insgesamt {total}). Sei vorsichtig.", + "diagnosis_diskusage_ok": "Der Speicher {mountpoint} (auf Gerät {device}) hat immer noch {free} ({free_percent}%) freien Speicherplatz übrig(von insgesamt {total})!", + "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_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.", + "diagnosis_dns_bad_conf": "Einige DNS-Einträge für die Domäne {domain} fehlen oder sind nicht korrekt (Kategorie {category})", + "diagnosis_dns_discrepancy": "Der folgende DNS Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {content}", + "diagnosis_dns_good_conf": "DNS Einträge korrekt konfiguriert für die Domäne {domain} (Kategorie {category})", + "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: {content}", + "diagnosis_dns_point_to_doc": "Bitte schauen Sie in der Dokumentation unter https://doc.yunohost.org/dns_config nach, wenn Sie Hilfe bei der Konfiguration der DNS-Einträge benötigen.", + "diagnosis_dns_specialusedomain": "Die Domäne {domain} basiert auf einer Top-Level-Domain (TLD) für spezielle Zwecke wie .local oder .test und deshalb wird von ihr nicht erwartet, dass sie echte DNS-Einträge besitzt.", + "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_domain_expiration_error": "Einige Domänen werden SEHR BALD ablaufen!", + "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", + "diagnosis_domain_expiration_not_found_details": "Die WHOIS-Informationen für die Domäne {domain} scheinen keine Informationen über das Ablaufdatum zu enthalten. Stimmt das?", + "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", + "diagnosis_domain_expiration_warning": "Einige Domänen werden bald ablaufen!", + "diagnosis_domain_expires_in": "{domain} läuft in {days} Tagen ab.", + "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", + "diagnosis_everything_ok": "Alles sieht OK aus für {category}!", + "diagnosis_failed": "Kann Diagnose-Ergebnis für die Kategorie '{category}' nicht abrufen: {error}", + "diagnosis_failed_for_category": "Diagnose fehlgeschlagen für die Kategorie '{category}': {error}", + "diagnosis_found_errors": "{errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", + "diagnosis_found_errors_and_warnings": "{errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", + "diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.", + "diagnosis_high_number_auth_failures": "In letzter Zeit gab es eine verdächtig hohe Anzahl von Authentifizierungsfehlern. Stelle sicher, dass fail2ban läuft und korrekt konfiguriert ist, oder verwende einen benutzerdefinierten Port für SSH, wie unter https://doc.yunohost.org/security beschrieben.", + "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_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": "Konnte nicht diagnostizieren, ob die Domäne von aussen per IPv{ipversion} erreichbar ist.", + "diagnosis_http_could_not_diagnose_details": "Fehler: {error}", + "diagnosis_http_hairpinning_issue": "In deinem lokalen Netzwerk scheint Hairpinning nicht aktiviert zu sein.", + "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://doc.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_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_http_ok": "Die Domäne {domain} ist über HTTP von außerhalb des lokalen Netzwerks erreichbar.", + "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_special_use_tld": "Die Domäne {domain} basiert auf einer Top-Level-Domäne (TLD) für besondere Zwecke wie .local oder .test und wird daher voraussichtlich nicht außerhalb des lokalen Netzwerks zugänglich sein.", + "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 .", + "diagnosis_http_unreachable": "Die Domäne {domain} scheint von aussen per HTTP nicht erreichbar zu sein.", + "diagnosis_ignored_issues": "(+ {nb_ignored} ignorierte(s) Problem(e))", + "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_connected_ipv4": "Der Server ist mit dem Internet über IPv4 verbunden!", + "diagnosis_ip_connected_ipv6": "Der Server ist mit dem Internet über IPv6 verbunden!", + "diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!", + "diagnosis_ip_global": "Globale IP: {global}", + "diagnosis_ip_local": "Lokale IP: {local}", + "diagnosis_ip_no_ipv4": "Der Server hat kein funktionierendes IPv4.", + "diagnosis_ip_no_ipv6": "Der Server verfügt nicht über eine funktionierende IPv6-Adresse.", + "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://doc.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_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://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "Der Server scheint überhaupt nicht mit dem Internet verbunden zu sein!?", + "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber sei vorsichtig wenn du deine eigene /etc/resolv.conf verwendest.", + "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_mail_blocklist_listed_by": "Deine IP-Adresse oder Domäne {item} ist auf der Blacklist auf {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Die IP-Adressen und die Domänen, welche von diesem Server verwendet werden, scheinen nicht auf einer Blacklist zu sein", + "diagnosis_mail_blocklist_reason": "Der Grund für die Blacklist ist: {reason}", + "diagnosis_mail_blocklist_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 {blocklist_website} entfernt werden kann", + "diagnosis_mail_ehlo_bad_answer": "Ein nicht-SMTP-Dienst antwortete auf Port 25 per IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Das könnte daran liegen, dass anstelle Ihres Servers ein anderes Gerät antwortet.", + "diagnosis_mail_ehlo_could_not_diagnose": "Es war nicht möglich zu diagnostizieren, ob der Postfix-Mailserver von Aussen über IPv{ipversion} erreichbar ist.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Fehler: {error}", + "diagnosis_mail_ehlo_ok": "Der SMTP-Server ist von Aussen erreichbar und darum auch in der Lage E-Mails zu empfangen!", + "diagnosis_mail_ehlo_unreachable": "Der SMTP-Server ist von außen nicht erreichbar per IPv{ipversion}. Er wird nicht in der Lage sein E-Mails zu empfangen.", + "diagnosis_mail_ehlo_unreachable_details": "Konnte keine Verbindung zu deinem Server auf dem Port 25 herzustellen über IPv{ipversion}. Er scheint nicht erreichbar zu sein.
1. Das häufigste Problem ist, dass der Port 25 nicht richtig zu deinem Server weitergeleitet ist.
2. Du solltest auch sicherstellen, dass der Postfix-Dienst läuft.
3. In komplexeren Umgebungen: Stelle sicher, daß keine Firewall oder Reverse-Proxy stört.", + "diagnosis_mail_ehlo_wrong": "Ein anderer SMTP-Server antwortet auf IPv{ipversion}. Dein Server wird wahrscheinlich nicht in der Lage sein, E-Mails zu empfangen.", + "diagnosis_mail_ehlo_wrong_details": "Die vom Remote-Diagnose-Server per IPv{ipversion} empfangene EHLO weicht von der Domäne deines Servers ab.
Empfangene EHLO: {wrong_ehlo}
Erwartet: {right_ehlo}
Die geläufigste Ursache für dieses Problem ist, dass der Port 25 nicht korrekt auf deinem Server weitergeleitet wird. Du kannst zusätzlich auch prüfen, dass keine Firewall oder Reverse-Proxy stört.", + "diagnosis_mail_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_different_from_ehlo_domain_details": "Aktueller Reverse-DNS-Eintrag: {rdns_domain}
Erwarteter Wert: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "Kein Reverse-DNS-Eintrag ist definiert für IPv{ipversion}. Einige E-Mails könnten eventuell nicht zugestellt oder als Spam markiert werden.", + "diagnosis_mail_fcrdns_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://doc.yunohost.org/vpn_advantage
- Schließlich ist es auch möglich, zu einem anderen Provider 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 email.smtp.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_details": "Sie sollten zuerst versuchen, auf Ihrer Internet-Router-Oberfläche, in Ihrer Internet-Box oder auf Ihrer Hosting-Anbieter-Oberfläche den Reverse-DNS-Eintrag mit {ehlo_domain}zu konfigurieren. (Gewisse Hosting-Anbieter können möglicherweise verlangen, dass Sie dafür ein Support-Ticket erstellen).", + "diagnosis_mail_fcrdns_ok": "Dein Reverse-DNS-Eintrag ist korrekt konfiguriert!", + "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_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_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://doc.yunohost.org/vpn_advantage nach.
- Sie können auch in Betracht ziehen, zu einem netzneutralitätfreundlicheren Anbieter zu wechseln", + "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_queue_ok": "{nb_pending} anstehende E-Mails in der Warteschlange", + "diagnosis_mail_queue_too_big": "Zu viele anstehende Nachrichten in der Warteschlange ({nb_pending} emails)", + "diagnosis_mail_queue_unavailable": "Die Anzahl der anstehenden Nachrichten in der Warteschlange kann nicht abgefragt werden", + "diagnosis_mail_queue_unavailable_details": "Fehler: {error}", + "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_no_cache": "Kein Diagnose Cache aktuell für die Kategorie '{category}'", + "diagnosis_package_installed_from_sury": "Einige System-Pakete sollten gedowngradet werden", + "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}", + "diagnosis_ports_could_not_diagnose": "Konnte nicht diagnostizieren, ob die Ports von aussen per IPv{ipversion} erreichbar sind.", + "diagnosis_ports_could_not_diagnose_details": "Fehler: {error}", + "diagnosis_ports_forwarding_tip": "Um dieses Problem zu beheben, musst du höchstwahrscheinlich die Port-Weiterleitung auf deinem Internet-Router einrichten wie in https://doc.yunohost.org/admin/get_started/post_install/dns_config/ beschrieben", + "diagnosis_ports_needed_by": "Diesen Port zu öffnen ist nötig, um die Funktionalität des Typs {category} (service {service}) zu gewährleisten", + "diagnosis_ports_ok": "Port {port} ist von Aussen 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_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_ram_low": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total}). Sei vorsichtig.", + "diagnosis_ram_ok": "Das System hat noch {available} ({available_percent}%) RAM von {total} zur Verfügung.", + "diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})", + "diagnosis_regenconf_allgood": "Alle Konfigurationsdateien sind in Übereinstimmung mit der empfohlenen Konfiguration!", + "diagnosis_regenconf_manually_modified": "Die Konfigurationsdatei {file} scheint manuell verändert worden zu sein.", + "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_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.", + "diagnosis_security_vulnerable_to_meltdown": "Es scheint als ob Sie durch die kritische Meltdown-Verwundbarkeit verwundbar sind", + "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_services_bad_status": "Der Dienst {service} ist {status} :(", + "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_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!", + "diagnosis_services_running": "Dienst {service} läuft!", + "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_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.", + "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.", + "diagnosis_swap_none": "Das System hat gar keinen Swap. Du solltest überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.", + "diagnosis_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 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_unknown_categories": "Folgende Kategorien sind unbekannt: {categories}", + "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).", + "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.", + "disk_space_not_sufficient_install": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu installieren", + "disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktualisieren", + "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}", + "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_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", + "domain_config_acme_eligible": "Geeignet für ACME", + "domain_config_acme_eligible_explain": "Es scheint, als ob diese Domäne nicht bereit ist für ein Let's Encrypt-Zertifikat. Bitte überprüfen Sie Ihre DNS-Konfiguration und ob Ihr Server über HTTP erreichbar ist . Die Abschnitte 'DNS-Einträge' und 'Web' auf der Diagnose-Seite können Ihnen dabei helfen, zu verstehen, was falsch konfiguriert ist.", + "domain_config_api_protocol": "API-Protokoll", + "domain_config_auth_application_key": "Anwendungsschlüssel", + "domain_config_auth_application_secret": "Geheimer Anwendungsschlüssel", + "domain_config_auth_consumer_key": "Verbraucherschlüssel", + "domain_config_auth_entrypoint": "API-Einstiegspunkt", + "domain_config_auth_key": "Authentifizierungsschlüssel", + "domain_config_auth_secret": "Authentifizierungsgeheimnis", + "domain_config_auth_token": "Authentifizierungstoken", + "domain_config_cert_install": "Installation des Let's Encrypt-Zertifikats", + "domain_config_cert_issuer": "Zertifizierungsstelle", + "domain_config_cert_name": "Zertifikat", + "domain_config_cert_no_checks": "Tests und andere Diagnose-Überprüfungen ignorieren", + "domain_config_cert_renew": "Erneuern des Let's Encrypt-Zertifikats", + "domain_config_cert_renew_help": "Das Zertifikat wird automatisch während den letzten 15 Tagen seiner Gültigkeit erneuert. Sie können es manuell erneuern, wenn Sie möchten. (nicht empfohlen).", + "domain_config_cert_summary": "Zertifikats-Status", + "domain_config_cert_summary_abouttoexpire": "Das aktuelle Zertifikat läuft bald ab. Es sollte bald automatisch erneuert werden.", + "domain_config_cert_summary_expired": "ACHTUNG: Das aktuelle Zertifikat ist nicht gültig! HTTPS wird gar nicht funktionieren!", + "domain_config_cert_summary_letsencrypt": "Toll! Sie benutzen ein gültiges Let's Encrypt-Zertifikat!", + "domain_config_cert_summary_ok": "Gut, das aktuelle Zertifikat sieht gut aus!", + "domain_config_cert_summary_selfsigned": "WARNUNG: Aktuelles Zertifikat ist selbstssigniert. Browser werden neuen Besuchern eine furchteinflössende Warnung anzeigen!", + "domain_config_cert_validity": "Validität", + "domain_config_default_app": "Standard-Applikation", + "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_dns_name": "DNS", + "domain_config_feature_name": "Funktionen", + "domain_config_mail_in": "Eingehende E-Mails", + "domain_config_mail_out": "Ausgehende E-Mails", + "domain_config_portal_logo": "Eigenes Logo", + "domain_config_portal_name": "Portal anpassen", + "domain_config_portal_title": "Eigener Titel", + "domain_config_search_engine": "URL der Suchmaschine", + "domain_config_search_engine_name": "Name der Suchmaschine", + "domain_created": "Domäne erstellt", + "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_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.", + "domain_dns_conf_special_use_tld": "Diese Domäne basiert auf einer Top-Level-Domäne (TLD) für besondere Zwecke wie .local oder .test und wird daher vermutlich keine eigenen DNS-Einträge haben.", + "domain_dns_push_already_up_to_date": "Die Einträge sind auf dem neuesten Stand, es gibt nichts zu tun.", + "domain_dns_push_failed": "Die Aktualisierung der DNS-Einträge ist leider gescheitert.", + "domain_dns_push_failed_to_list": "Auflistung der aktuellen Einträge über die API des Registrars fehlgeschlagen: {error}", + "domain_dns_push_managed_in_parent_domain": "Die automatische DNS-Konfiguration wird von der übergeordneten Domäne {parent_domain} verwaltet.", + "domain_dns_push_not_applicable": "Die automatische DNS-Konfiguration ist nicht auf die Domäne {domain} anwendbar. Konfiguriere die DNS-Einträge manuell, wie unter https://doc.yunohost.org/dns_config beschrieben.", + "domain_dns_push_partial_failure": "DNS-Einträge teilweise aktualisiert: einige Warnungen/Fehler wurden gemeldet.", + "domain_dns_push_record_failed": "Fehler bei {action} Eintrag {type}/{name} : {error}", + "domain_dns_push_success": "DNS-Einträge aktualisiert!", + "domain_dns_pushing": "DNS-Einträge übertragen…", + "domain_dns_registrar_experimental": "Bislang wurde die Schnittstelle zur API von **{registrar}** noch nicht außreichend von der YunoHost-Community getestet und geprüft. Der Support ist **sehr experimentell** – sei vorsichtig!", + "domain_dns_registrar_managed_in_parent_domain": "Diese Domäne ist eine Unterdomäne von {parent_domain_link}. Die Konfiguration des DNS-Registrars sollte auf der Konfigurationsseite von {parent_domain} verwaltet werden.", + "domain_dns_registrar_not_supported": "YunoHost konnte den Registrar, der diese Domäne verwaltet, nicht automatisch erkennen. Du solltest die DNS-Einträge, wie unter https://doc.yunohost.org/dns_config beschrieben, manuell konfigurieren.", + "domain_dns_registrar_supported": "YunoHost hat automatisch erkannt, dass diese Domäne von dem Registrar **{registrar}** verwaltet wird. Wenn Du möchtest, konfiguriert YunoHost diese DNS-Zone automatisch, wenn Du die entsprechenden API-Zugangsdaten zur Verfügung stellst. Auf dieser Seite erfährst Du, wie Du deine API-Anmeldeinformationen erhältst: https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Du kannst deine DNS-Einträge auch, wie unter https://doc.yunohost.org/dns_config beschrieben, manuell konfigurieren)", + "domain_dns_registrar_use_auto": "Automatische DNS Funktion nutzen", + "domain_dns_registrar_yunohost": "Dies ist eine nohost.me / nohost.st / ynh.fr Domäne, ihre DNS-Konfiguration wird daher automatisch von YunoHost ohne weitere Konfiguration übernommen. (siehe Befehl 'yunohost dyndns update')", + "domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domäne registriert", + "domain_exists": "Die Domäne existiert bereits", + "domain_hostname_failed": "Neuer Hostname wurde nicht gesetzt. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", + "domain_registrar_is_not_configured": "Der DNS-Registrar ist noch nicht für die Domäne '{domain}' konfiguriert.", + "domain_remove_confirm_apps_removal": "Wenn du diese Domäne löschst, werden folgende Applikationen entfernt:\n{apps}\n\nBist du sicher? [{answers}]", + "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'", + "domain_unknown": "Domäne '{domain}' unbekannt", + "domains_available": "Verfügbare Domains:", + "done": "Erledigt", + "download_bad_status_code": "{url} lieferte folgende(n) Status Code(s) {code}", + "download_ssl_error": "SSL Fehler beim Verbinden zu {url}", + "download_timeout": "{url} brauchte zu lange zum Antworten, hab aufgegeben.", + "download_unknown_error": "Fehler beim Herunterladen von Daten von {url}: {error}", + "downloading": "Wird heruntergeladen…", + "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.", + "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", + "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain} auf {provider} verfügbar ist.", + "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider} kann die Domäne(n) {domain} nicht bereitstellen.", + "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_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden", + "dyndns_no_domain_registered": "Keine Domain mit DynDNS registriert", + "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_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.", + "dyndns_set_recovery_password_denied": "Konnte Wiederherstellungspasswort nicht einstellen: ungültiges Passwort", + "dyndns_set_recovery_password_failed": "Konnte Wiederherstellungspasswort nicht einstellen: {error}", + "dyndns_set_recovery_password_invalid_password": "Konnte Wiederherstellungspasswort nicht einstellen: Passwort ist nicht stark genug", + "dyndns_set_recovery_password_success": "Wiederherstellungspasswort eingestellt!", + "dyndns_set_recovery_password_unknown_domain": "Konnte Wiederherstellungspasswort nicht einstellen: Domäne nicht registriert", + "dyndns_subscribe_failed": "Konnte DynDNS-Domäne nicht registrieren: {error}", + "dyndns_subscribed": "DynDNS-Domäne registriert", + "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_unavailable": "Die Domäne {domain} ist nicht verfügbar.", + "dyndns_unsubscribe_already_unsubscribed": "Domäne ist bereits abgemeldet", + "dyndns_unsubscribe_denied": "Konnte Domäne nicht abmelden: ungültige Anmeldedaten", + "dyndns_unsubscribe_failed": "Konnte die DynDNS-Domäne nicht abmelden: {error}", + "dyndns_unsubscribed": "DynDNS-Domäne abgemeldet", + "error_changing_file_permissions": "Fehler beim Ändern der Berechtigungen für {path}: {error}", + "error_removing": "Fehler beim Entfernen {path}: {error}", + "error_writing_file": "Fehler beim Schreiben von Datei {file}: {error}", + "extracting": "Wird entpackt…", + "field_invalid": "Feld '{field}' ist unbekannt", + "file_does_not_exist": "Die Datei {path} existiert nicht.", + "file_not_exist": "Datei ist nicht vorhanden: '{path}'", + "firewall_reload_failed": "Firewall konnte nicht neu geladen werden. Mehr Informationen im Log.", + "firewall_reloaded": "Firewall neu geladen", + "global_settings_reset_success": "Reinitialisieren der globalen Einstellungen", + "global_settings_setting_admin_strength": "Stärke des Admin-Passworts", + "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_backup_compress_tar_archives_help": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.", + "global_settings_setting_backup_name": "Sicherung", + "global_settings_setting_dns_exposure": "Bei DNS-Konfiguration und -Diagnose zu berücksichtigende IP-Versionen", + "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_email_name": "E-Mail", + "global_settings_setting_misc_name": "Weitere Einstellungen", + "global_settings_setting_network_name": "Netzwerk", + "global_settings_setting_nginx_compatibility": "NGINX-Kompatibilität", + "global_settings_setting_nginx_compatibility_help": "Kompatibilitäts- vs. Sicherheits-Kompromiss für den Webserver NGINX. Betrifft die Ciphers (und andere sicherheitsrelevante Aspekte)", + "global_settings_setting_nginx_name": "NGINX (Web Server)", + "global_settings_setting_nginx_redirect_to_https": "HTTPS erzwingen", + "global_settings_setting_nginx_redirect_to_https_help": "HTTP-Anfragen standardmäßig auf HTTPs umleiten (NICHT AUSSCHALTEN, sofern Du nicht weißt was Du tust!)", + "global_settings_setting_passwordless_sudo": "Erlauben Sie Administratoren 'sudo' zu benützen, ohne das Passwort erneut einzugeben", + "global_settings_setting_pop3_enabled": "POP3 einschalten", + "global_settings_setting_pop3_enabled_help": "POP3-Protokoll für den Mail-Server aktivieren", + "global_settings_setting_postfix_compatibility": "Postfix-Kompatibilität", + "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_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_root_password": "Neues root-Passwort", + "global_settings_setting_root_password_confirm": "Neues root-Passwort (Bestätigung)", + "global_settings_setting_security_experimental_enabled": "Experimentelle Sicherheitsfunktionen", + "global_settings_setting_security_experimental_enabled_help": "Aktiviere experimentelle Sicherheitsfunktionen (nur aktivieren, wenn Du weißt was Du tust!)", + "global_settings_setting_smtp_allow_ipv6": "Autorisiere das IPv6", + "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": "Aktiviere das SMTP-Relais", + "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.", + "global_settings_setting_smtp_relay_host": "Adresse des SMTP-Relais", + "global_settings_setting_smtp_relay_password": "SMTP-Relais-Passwort", + "global_settings_setting_smtp_relay_port": "SMTP Relay Port", + "global_settings_setting_smtp_relay_user": "SMTP-Relais-Benutzeraccount", + "global_settings_setting_ssh_compatibility": "SSH-Kompatibilität", + "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": "Authentifizieren mit Passwort", + "global_settings_setting_ssh_password_authentication_help": "Passwort-Authentifizierung für SSH zulassen", + "global_settings_setting_ssh_port": "SSH-Port", + "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.", + "global_settings_setting_user_strength": "Stärke des Anmeldepassworts", + "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", + "global_settings_setting_webadmin_allowlist_enabled_help": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.", + "global_settings_setting_webadmin_allowlist_help": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. CIDR Notation ist erlaubt.", + "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.", + "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.", + "group_already_exist": "Die Gruppe {group} existiert bereits", + "group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen", + "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber YunoHost wird sie entfernen…", + "group_cannot_be_deleted": "Die Gruppe {group} kann nicht manuell entfernt werden.", + "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_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.", + "group_cannot_edit_visitors": "Die Gruppe \"Besucher\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe und repräsentiert anonyme Besucher", + "group_created": "Gruppe '{group}' angelegt", + "group_creation_failed": "Konnte Gruppe '{group}' nicht anlegen: {error}", + "group_deleted": "Gruppe '{group}' gelöscht", + "group_deletion_failed": "Konnte Gruppe '{group}' nicht löschen: {error}", + "group_mailalias_add": "Der E-Mail-Alias '{mail}' wird der Gruppe '{group}' hinzugefügt", + "group_mailalias_remove": "Der E-Mail-Alias '{mail}' wird von der Gruppe '{group}' entfernt", + "group_no_change": "Nichts zu ändern für die Gruppe '{group}'", + "group_unknown": "Die Gruppe '{group}' ist unbekannt", + "group_update_aliases": "Aktualisieren der Aliase für die Gruppe '{group}'", + "group_update_failed": "Kann Gruppe '{group}' nicht aktualisieren: {error}", + "group_updated": "Gruppe '{group}' erneuert", + "group_user_add": "Der Benutzer '{user}' wird der Gruppe '{group}' hinzugefügt werden", + "group_user_already_in_group": "Konto {user} ist bereits in der Gruppe {group}", + "group_user_not_in_group": "Konto {user} ist nicht in der Gruppe {group}", + "group_user_remove": "Der Benutzer '{user}' wird von der Gruppe '{group}' entfernt werden", + "hook_exec_failed": "Konnte Skript nicht ausführen: {path}", + "hook_exec_not_terminated": "Skript ist nicht normal beendet worden: {path}", + "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path} nicht lesen. Fehler: {msg}. Unformatierter Inhalt: {raw_content}", + "hook_list_by_invalid": "Dieser Wert kann nicht verwendet werden, um Hooks anzuzeigen", + "hook_name_unknown": "Hook '{name}' ist nicht bekannt", + "installation_complete": "Installation vollständig", + "invalid_credentials": "Ungültiges Passwort oder Benutzername", + "invalid_number": "Muss eine Zahl sein", + "invalid_regex": "Ungültige Regex:'{regex}'", + "invalid_shell": "Ungültiger Shell: {shell}", + "invalid_url": "Konnte keine Verbindung zu {url} herstellen… vielleicht ist der Dienst ausgefallen, oder Sie sind nicht richtig mit dem Internet in IPv4/IPv6 verbunden.", + "ldap_attribute_already_exists": "LDAP-Attribut '{attribute}' existiert bereits mit dem Wert '{value}'", + "ldap_server_down": "LDAP-Server kann nicht erreicht werden", + "ldap_server_is_down_restart_it": "Der LDAP-Dienst ist nicht erreichbar, versuche ihn neu zu starten…", + "log_app_action_run": "Führe Aktion der Applikation '{}' aus", + "log_app_change_url": "Ändere die URL der Applikation '{}'", + "log_app_config_set": "Konfiguration auf die Applikation '{}' anwenden", + "log_app_install": "Installiere die Applikation '{}'", + "log_app_makedefault": "Mache '{}' zur Standard-Applikation", + "log_app_remove": "Entferne die Applikation '{}'", + "log_app_upgrade": "Upgrade der Applikation '{}'", + "log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar", + "log_backup_create": "Erstelle ein Backup-Archiv", + "log_backup_restore_app": "Wiederherstellen von '{}' aus einem Sicherungsarchiv", + "log_backup_restore_system": "System aus einem Sicherungsarchiv wiederherstellen", + "log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''", + "log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende 'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen", + "log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration", + "log_domain_config_set": "Konfiguration für die Domäne '{}' aktualisieren", + "log_domain_dns_push": "DNS-Einträge für die Domäne '{}' übertragen", + "log_domain_main_domain": "Mache '{}' zur Hauptdomäne", + "log_domain_remove": "Domäne '{}' aus der Systemkonfiguration entfernen", + "log_dyndns_subscribe": "Für eine YunoHost-Subdomain registrieren '{}'", + "log_dyndns_unsubscribe": "Von einer YunoHost-Subdomain abmelden '{}'", + "log_dyndns_update": "Die IP, die mit der YunoHost-Subdomain '{}' verbunden ist, aktualisieren", + "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", + "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Das Let’s Encrypt auf der Domäne '{}' installieren", + "log_letsencrypt_cert_renew": "Erneuern des Let's Encrypt-Zeritifikates von '{}'", + "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", + "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", + "log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen", + "log_regen_conf": "Systemkonfiguration neu generieren '{}'", + "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation", + "log_resource_snippet": "Provisioning/Deprovisioning/Aktualisieren einer Ressource", + "log_selfsigned_cert_install": "Das selbstsignierte Zertifikat auf der Domäne '{}' installieren", + "log_settings_reset": "Einstellungen rücksetzen", + "log_settings_reset_all": "alle Parameter rücksetzen", + "log_settings_set": "Parameter anwenden", + "log_tools_migrations_migrate_forward": "Migrationen durchführen", + "log_tools_postinstall": "Post-Installation des YunoHost-Servers durchführen", + "log_tools_reboot": "Starten Sie Ihren Server neu", + "log_tools_shutdown": "Ihren Server herunterfahren", + "log_tools_upgrade": "Systempakete aktualisieren", + "log_user_create": "Füge Konto '{}' hinzu", + "log_user_delete": "Lösche Konto '{}'", + "log_user_group_create": "Erstelle Gruppe '{}'", + "log_user_group_delete": "Lösche Gruppe '{}'", + "log_user_group_update": "Aktualisiere Gruppe '{}'", + "log_user_import": "Konten importieren", + "log_user_update": "Aktualisiere Information für Konto '{}'", + "mail_alias_remove_failed": "Konnte E-Mail-Alias '{mail}' nicht entfernen", + "mail_domain_unknown": "Die Domäne '{domain}' dieser E-Mail-Adresse ist ungültig. Wähle bitte eine Domäne, welche durch diesen Server verwaltet wird.", + "mail_forward_remove_failed": "Die Weiterleitungs-E-Mail '{mail}' konnte nicht gelöscht werden", + "mail_unavailable": "Diese E-Mail-Adresse ist für die Administratoren-Gruppe reserviert", + "mailbox_disabled": "E-Mail für Konto {user} ist deaktiviert", + "mailbox_used_space_dovecot_down": "Der Dovecot-Mailbox-Dienst muss aktiv sein, wenn du den von der Mailbox belegten Speicher abrufen willst", + "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden", + "main_domain_changed": "Die Hauptdomain wurde geändert", + "migration_0027_patch_yunohost_conflicts": "Patch anwenden, um das Konfliktproblem zu umgehen…", + "migration_ldap_backup_before_migration": "Vor der eigentlichen Migration ein Backup der LDAP-Datenbank und der Applikations-Einstellungen erstellen.", + "migration_ldap_can_not_backup_before_migration": "Die Sicherung des Systems konnte nicht abgeschlossen werden, bevor die Migration fehlschlug. Fehler: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Migrieren war nicht möglich… Versuch, ein Rollback des Systems durchzuführen.", + "migration_ldap_rollback_success": "Das System wurde zurückgesetzt.", + "migrations_already_ran": "Diese Migrationen wurden bereits durchgeführt: {ids}", + "migrations_dependencies_not_satisfied": "Führe diese Migrationen aus: '{dependencies_id}', bevor du {id} migrierst.", + "migrations_exclusive_options": "'--auto', '--skip' und '--force-rerun' sind Optionen, die sich gegenseitig ausschliessen.", + "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_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_no_such_migration": "Es existiert keine Migration genannt '{id}'", + "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht ausstehend 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_running_forward": "Durchführen der Migrationen {id}…", + "migrations_skip_migration": "Überspringe Migrationen {id}…", + "migrations_success_forward": "Migration {id} abgeschlossen", + "migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehe zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führe 'yunohost tools migrations run' aus.", + "nftables_unavailable": "nftables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt", + "not_enough_disk_space": "Nicht genügend freier Speicherplatz unter '{path}'", + "operation_interrupted": "Wurde die Operation manuell unterbrochen?", + "other_available_options": "… und {n} weitere verfügbare Optionen, die nicht angezeigt werden", + "password_confirmation_not_the_same": "Das Passwort und die Bestätigung stimmen nicht überein", + "password_listed": "Dieses Passwort zählt zu den meistgenutzten Passwörtern der Welt. Bitte wähle ein anderes, einzigartigeres Passwort.", + "password_too_long": "Bitte wählen Sie ein Passwort aus, das weniger als 127 Zeichen hat", + "password_too_simple_1": "Das Passwort muss mindestens 8 Zeichen lang sein", + "password_too_simple_2": "Das Passwort muss mindestens 8 Zeichen lang sein und Gross- sowie Kleinbuchstaben enthalten", + "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten", + "password_too_simple_4": "Das Passwort muss mindestens 12 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten", + "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_email_forward": "Es muss sich um eine gültige E-Mail-Adresse handeln. Das Symbol '+' wird akzeptiert (zum Beispiel : maxmuster@beispiel.com oder maxmuster+yunohost@beispiel.com)", + "pattern_fullname": "Muss ein gültiger voller Name 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_password_app": "Entschuldige Bitte! Passwörter dürfen folgende Zeichen nicht enthalten: {forbidden_chars}", + "pattern_port_or_range": "Muss ein valider Port (z.B. 0-65535) oder ein Bereich (z.B. 100:200) sein", + "pattern_username": "Darf nur aus klein geschriebenen alphanumerischen Zeichen und Unterstrichen bestehen", + "permission_already_allowed": "Die Gruppe '{group}' hat die Berechtigung '{permission}' bereits erhalten", + "permission_already_disallowed": "Für die Gruppe '{group}' wurde die Berechtigung '{permission}' deaktiviert", + "permission_cannot_remove_main": "Entfernung einer Hauptberechtigung nicht genehmigt", + "permission_cant_add_to_all_users": "Die Berechtigung {permission} kann nicht für allen Konten hinzugefügt werden.", + "permission_created": "Berechtigung '{permission}' erstellt", + "permission_creation_failed": "Berechtigungserstellung nicht möglich '{permission}' : {error}", + "permission_currently_allowed_for_all_users": "Diese Berechtigung wird derzeit allen Konten zusätzlich zu anderen Gruppen erteilt. Möglicherweise möchtest du entweder die Berechtigung 'all_users' entfernen oder die anderen Gruppen entfernen, für die sie derzeit zulässig sind.", + "permission_deleted": "Berechtigung '{permission}' gelöscht", + "permission_deletion_failed": "Entfernung der Berechtigung nicht möglich '{permission}': {error}", + "permission_not_found": "Berechtigung '{permission}' nicht gefunden", + "permission_protected": "Die Berechtigung {permission} ist geschützt. Du kannst die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.", + "permission_require_account": "Berechtigung {permission} ist nur für Personen mit Konto sinnvoll und kann daher nicht für Gäste aktiviert werden.", + "permission_update_failed": "Die Berechtigung '{permission}' kann nicht aktualisiert werden : {error}", + "permission_updated": "Berechtigung '{permission}' aktualisiert", + "port_already_closed": "Der Port {port} wurde bereits geschlossen", + "port_already_opened": "Der Port {port} wird bereits benutzt", + "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Du wirst sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn du YunoHost trotz dieser Warnung installieren willst, wiederhole den Befehl mit --force-diskspace", + "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre…", + "regenconf_failed": "Konnte die Konfiguration für die Kategorie(n) {categories} nicht neu erstellen", + "regenconf_file_backed_up": "Die Konfigurationsdatei '{conf}' wurde unter '{backup}' gespeichert", + "regenconf_file_copy_failed": "Die neue Konfigurationsdatei '{new}' kann nicht nach '{conf}' kopiert werden", + "regenconf_file_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {category}) gelöscht werden, wurde aber beibehalten.", + "regenconf_file_manually_modified": "Die Konfigurationsdatei '{conf}' wurde manuell bearbeitet und wird nicht aktualisiert", + "regenconf_file_manually_removed": "Die Konfigurationsdatei '{conf}' wurde manuell gelöscht und wird nicht erstellt", + "regenconf_file_remove_failed": "Konnte die Konfigurationsdatei '{conf}' nicht entfernen", + "regenconf_file_removed": "Konfigurationsdatei '{conf}' entfernt", + "regenconf_file_updated": "Konfigurationsdatei '{conf}' aktualisiert", + "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.", + "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet.", + "regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an…", + "regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'", + "regenconf_updated": "Konfiguration aktualisiert für '{category}'", + "regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden", + "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", + "regex_with_only_domain": "Sie können regex nicht als Domain verwenden, sondern nur als Pfad", + "registrar_infos": "Registrar-Informationen (Herausgeber der Domainnamen/Domänennamen)", + "restore_already_installed_app": "Eine Applikation mit der ID '{app}' ist bereits installiert", + "restore_already_installed_apps": "Folgende Apps können nicht wiederhergestellt werden, weil sie schon installiert sind: {apps}", + "restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.", + "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öchten Sie die Wiederherstellung wirklich starten? [{answers}]", + "restore_extracting": "Auspacken der benötigten Dateien aus dem Archiv…", + "restore_failed": "System konnte nicht wiederhergestellt werden", + "restore_hook_unavailable": "Das Wiederherstellungsskript für '{part}' steht weder in Ihrem System noch im Archiv zur Verfügung", + "restore_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_not_enough_disk_space": "Nicht genug Speicher (Speicher: {free_space} B, benötigter Speicher: {needed_space} B, Sicherheitspuffer: {margin} B)", + "restore_nothings_done": "Nichts wurde wiederhergestellt", + "restore_removing_tmp_dir_failed": "Ein altes, temporäres Directory konnte nicht entfernt werden", + "restore_running_app_script": "App '{app}' wird wiederhergestellt…", + "restore_running_hooks": "Wiederherstellung wird gestartet…", + "restore_system_part_failed": "Die Systemteile '{part}' konnten nicht wiederhergestellt werden", + "root_password_changed": "Das root-Passwort wurde geändert", + "root_password_desynchronized": "Das Admin-Passwort wurde geändert, aber YunoHost konnte dies nicht auf das Root-Passwort übertragen!", + "server_reboot": "Der Server wird neu gestartet", + "server_reboot_confirm": "Der Server wird sofort neu gestartet. Sind Sie sicher? [{answers}]", + "server_shutdown": "Der Server wird heruntergefahren", + "server_shutdown_confirm": "Der Server wird sofort heruntergefahren, sind Sie sicher? [{answers}]", + "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", + "service_already_stopped": "Der Dienst '{service}' wurde bereits gestoppt", + "service_cmd_exec_failed": "Der Befehl '{command}' konnte nicht ausgeführt werden", + "service_description_dnsmasq": "Verwaltet die Auflösung des Domainnamens (DNS)", + "service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)", + "service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet", + "service_description_mysql": "Speichert die Applikationsdaten (SQL Datenbank)", + "service_description_nftables": "Verwaltet offene und geschlossene Ports zur Verbindung mit Diensten", + "service_description_nginx": "Stellt Daten aller Websiten auf dem Server bereit", + "service_description_postfix": "Wird benutzt, um E-Mails zu senden und zu empfangen", + "service_description_postgresql": "Speichert Applikations-Daten (SQL Datenbank)", + "service_description_redis-server": "Eine spezialisierte Datenbank für den schnellen Datenzugriff, die Aufgabenwarteschlange und die Kommunikation zwischen Programmen", + "service_description_slapd": "Speichert Konten, Domänen und verbundene Informationen", + "service_description_ssh": "Ermöglicht die Verbindung zu deinem Server über ein Terminal (SSH-Protokoll)", + "service_description_yunohost-api": "Verwaltet die Interaktionen zwischen der Weboberfläche von YunoHost und dem System", + "service_description_yunomdns": "Ermöglicht es Ihnen, den Server über 'yunohost.local' in Ihrem lokalen Netzwerk zu erreichen", + "service_disable_failed": "Der Start des Dienstes '{service}' beim Hochfahren konnte nicht verhindert werden.", + "service_disabled": "Der Dienst '{service}' wird beim Systemstart nicht mehr gestartet.", + "service_enable_failed": "Der Dienst '{service}' konnte beim Hochfahren nicht gestartet werden.", + "service_enabled": "Der Dienst '{service}' wird nun beim Hochfahren des Systems automatisch gestartet.", + "service_not_reloading_because_conf_broken": "Der Dienst '{name}' wird nicht neu geladen/gestartet, da seine Konfiguration fehlerhaft ist: {errors}", + "service_reload_failed": "Der Dienst '{service}' konnte nicht erneut geladen werden", + "service_reload_or_restart_failed": "Der Dienst '{service}' konnte nicht erneut geladen oder gestartet werden.", + "service_reloaded": "Der Dienst '{service}' wurde erneut geladen", + "service_reloaded_or_restarted": "Der Dienst '{service}' wurde erfolgreich neu geladen oder gestartet", + "service_remove_failed": "Konnte den Dienst '{service}' nicht entfernen", + "service_removed": "Der Dienst '{service}' wurde erfolgreich entfernt", + "service_restart_failed": "Der Dienst '{service}' konnte nicht erneut gestartet werden.", + "service_restarted": "Der Dienst '{service}' wurde neu gestartet", + "service_start_failed": "Der Dienst '{service}' konnte nicht gestartet werden", + "service_started": "Der Dienst '{service}' wurde erfolgreich gestartet", + "service_stop_failed": "Der Dienst '{service}' kann nicht beendet werden", + "service_stopped": "Der Dienst '{service}' wurde erfolgreich beendet", + "service_unknown": "Unbekannter Dienst '{service}'", + "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", + "ssowat_conf_generated": "SSOwat-Konfiguration neu generiert", + "system_upgraded": "System aktualisiert", + "system_username_exists": "Der Anmeldename existiert bereits in der Liste der System-Konten", + "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.", + "tools_upgrade": "Aktualisieren von Systempaketen", + "tools_upgrade_failed": "Pakete konnten nicht aktualisiert werden: {packages_list}", + "unbackup_app": "'{app}' wird nicht gespeichert werden", + "unexpected_error": "Ein unerwarteter Fehler ist aufgetreten {error}", + "unknown_error_reading_file": "Unbekannter Fehler beim Lesen der Datei {file} (reason: {error})", + "unknown_group": "Gruppe '{group}' ist unbekannt", + "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.", + "unknown_user": "Konto '{user}' ist unbekannt", + "unlimit": "Kein Kontingent", + "unrestore_app": "{app} wird nicht wiederhergestellt werden", + "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}", + "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}", + "updating_apt_cache": "Die Liste der verfügbaren Pakete wird aktualisiert…", + "upgrading_packages": "Pakete werden aktualisiert…", + "upnp_dev_not_found": "Es konnten keine UPnP Geräte gefunden werden", + "upnp_disabled": "UPnP deaktiviert", + "upnp_enabled": "UPnP aktiviert", + "upnp_port_open_failed": "Port konnte nicht via UPnP geöffnet werden", + "user_already_exists": "Das Konto '{user}' ist bereits vorhanden", + "user_created": "Konto erstellt", + "user_creation_failed": "Konto konnte nicht erstellt werden {user}: {error}", + "user_deleted": "Konto gelöscht", + "user_deletion_failed": "Konto konnte nicht gelöscht werden {user}: {error}", + "user_home_creation_failed": "Persönlicher Ordner '{home}' für dieses Konto konnte nicht erstellt werden", + "user_import_bad_file": "Deine CSV-Datei ist nicht korrekt formatiert und wird daher ignoriert, um einen möglichen Datenverlust zu vermeiden", + "user_import_bad_line": "Ungültige Zeile {line}: {details}", + "user_import_failed": "Der Import von Konten ist komplett fehlgeschlagen", + "user_import_missing_columns": "Die folgenden Spalten fehlen: {columns}", + "user_import_nothing_to_do": "Es muss kein Konto importiert werden", + "user_import_partial_failed": "Der Import von Konten ist teilweise fehlgeschlagen", + "user_import_success": "Konten erfolgreich importiert", + "user_unknown": "Unbekanntes Konto: {user}", + "user_update_failed": "Konto konnte nicht aktualisiert werden {user}: {error}", + "user_updated": "Kontoinformationen wurden aktualisiert", + "visitors": "Besucher", + "yunohost_already_installed": "YunoHost ist bereits installiert", + "yunohost_configured": "YunoHost ist nun konfiguriert", + "yunohost_installing": "YunoHost wird installiert…", + "yunohost_not_installed": "YunoHost ist nicht oder unvollständig installiert worden. Bitte 'yunohost tools postinstall' ausführen", + "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://doc.yunohost.org/admin.", + "app_config_permission_label": "Label", + "yunohost_api": "YunoHost API", + "app_upgrade_cli_will_upgrade": "{app} wird von {current_version} zu {new_version} aktualisiert", + "app_upgrade_broke_the_system": "Das {app}-Upgrade schien zwar funktioniert zu haben, hat das System jedoch in einem fehlerhaften Zustand zurückgelassen und gilt daher als fehlgeschlagen.", + "app_upgrade_cli_bad_quality": "Upgrades für {app} werden übersprungen, da die App derzeit im Anwendungskatalog von YunoHost als fehlerhaft gekennzeichnet ist.", + "app_upgrade_cli_up_to_date": "{app} ist bereits auf dem neusten Stand ({current_version})", + "app_upgrade_cli_will_force_upgrade": "Upgrade von {app} wird erzwungen ({current_version})", + "app_upgrade_continuing_with_other_apps": "Fehler beim Aktualisieren von {app}, das Upgrade der übrigen Apps wird jedoch fortgesetzt (da `--continue-on-failure` verwendet wurde)", + "app_upgrade_fail_requirements": "Für diese App ist eine neue Version verfügbar ({new_version}), jedoch sind einige Voraussetzungen nicht erfüllt:\n{failed_requirements}", + "apps_upgrade_cancelled": "Für mehrere andere Apps stehen noch Updates aus, deren Installation jedoch abgebrochen wurde (verwende `--continue-on-failure`, um trotzdem fortzufahren): {apps}", + "app_db_prompt_no_app_database": "Im Manifest dieser App scheint keine Datenbank definiert zu sein", + "app_db_prompt_type_not_supported": "Der Befehl unterstützt diesen Datenbanktyp nicht: {type}", + "app_upgrade_bad_quality": "Diese App ist derzeit im Anwendungskatalog von YunoHost als defekt gekennzeichnet. Möglicherweise handelt es sich um ein vorübergehendes Problem, während die Entwickler versuchen, den Fehler zu beheben. In der Zwischenzeit sind Updates für dieser App deaktiviert.", + "app_upgrade_cli_url_required": "{app} ist nicht (mehr?) im Katalog enthalten und kann daher nicht automatisch aktualisiert werden. Verwenden Sie den Befehl `yunohost app upgrade {app}`, um die URL des Repositorys mit der Option `-u` anzugeben.", + "app_upgrade_failed_and_broke_the_system": "Das Upgrade der App „{app}“ ist fehlgeschlagen, wodurch das System in einen fehlerhaften Zustand versetzt wurde.", + "app_upgrade_specific_channel_msg": "Beachte, dass derzeit `{channel}` als Quelle für Upgrades verwendet wird. Informiere dich unbedingt über die laufende Diskussion [here]({pr_url}).", + "app_upgrade_up_to_date": "Ein erzwungenes Update der App (auf dieselbe Version) kann manchmal nützlich sein, um die App und die Konfigurationen neu erstellen.", + "app_upgrade_upgradable": "Die App kann von Version {current_version} auf {new_version} aktualisiert werden", + "app_upgrade_url_required": "Diese App ist nicht (mehr?) im Katalog enthalten, daher muss das Upgrades manuell durchgeführt werden.
Über die Befehlszeile kann der Befehl `yunohost app upgrade ` verwendet und die URL des Repos mit der Option `-u` angeben werden.", + "apps_confirm_partial_upgrade": "Einige Apps, für die ein Upgrade angefordert wurde, können nicht aktualisiert werden. Trotzdem mit den übrigen fortfahren?", + "apps_no_target_can_be_upgraded": "Keine ausstehenden App-Upgrades", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "Sicherung {name} wurde gelöscht, da sie durch eine neuere Sicherung {newname} ersetzt wurde", + "backup_app_script_failed": "Fehler beim Auffinden der zu sichernden Dateien für {app}.", + "backup_no_file_collected": "Fehler beim Auffinden der zu sichernden Dateien", + "diagnosis_apps_security_issue_error": "Die App {app} verwendet derzeit die Version „{current_version}“, die von einer SCHWERWIEGENDEN Sicherheitslücke betroffen ist: {title}. Es wird dringend empfohlen, die App SO SCHNELL WIE MÖGLICH auf Version „{fixed_in_version}“ zu aktualisieren. Weitere Informationen: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "Die App {app} verwendet derzeit die Version „{current_version}“, die von einer moderaten Sicherheitslücke betroffen ist: {title}. Es wird empfohlen, die App auf Version „{fixed_in_version}“ zu aktualisieren. Weitere Informationen: {more_infos_list}", + "diagnosis_ignore_no_issue_found": "Es wurden keine Einträge gefunden, die den angegebenen Kriterien entsprechen.", + "diagnosis_package_security_issue_error": "Das Systempaket „{package}\" verwendet derzeit die Version „{current_version}“, die von einer SCHWERWIEGENDEN Sicherheitslücke betroffen ist: {title}. Es wird dringend empfohlen, es SO SCHNELL WIE MÖGLICH auf Version „{fixed_in_version}“ zu aktualisieren. Weitere Informationen: {more_infos_list}", + "diagnosis_package_security_issue_warning": "Das Systempaket „{package}\" verwendet derzeit die Version „{current_version}“, die von einer moderaten Sicherheitslücke betroffen ist: {title}. Es wird empfohlen, es auf Version „{fixed_in_version}“ zu aktualisieren. Weitere Informationen: {more_infos_list}", + "diagnosis_rfkill_wifi": "Die WLAN-Karte ist deaktiviert, und eine Systemwarnung könnte die Installation von Apps verhindern", + "global_settings_setting_enable_blocklists": "Blocklisten für eingehenden Datenverkehr aktivieren", + "global_settings_setting_enable_blocklists_help": "Server blockiert, die bei spamcop.net, spamhaus.org und abuseat.org gelistet sind, um Spam zu verhindern. Dies kann zu Zustellungsproblemen bei eigetlich harmlosen Mailservern führen, die möglicherweise von diesen Drittanbietern gelistet werden. E-Mails, die von diesen Servern gesendet werden, können dann nicht empfangen werden.", + "global_settings_setting_experimental_name": "Experimentell", + "global_settings_setting_password_name": "Passwörter", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Benutzern erlauben, ihre primäre E-Mail-Adresse zu bearbeiten", + "global_settings_setting_portal_allow_edit_email_alias": "Benutzern das Hinzufügen, Entfernen und Bearbeiten von E-Mail-Aliassen erlauben", + "global_settings_setting_portal_allow_edit_email_forward": "Benutzern das Hinzufügen, Entfernen und Bearbeiten von E-Mail-Weiterleitungen erlauben", + "global_settings_setting_postfix_name": "Postfix (SMTP-E-Mail-Server)", + "global_settings_setting_root_access_name": "Root-Passwort ändern", + "global_settings_setting_security_name": "Sicherheit", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_ssh_name": "SSH", + "global_settings_setting_tls_passthrough_enabled": "TLS-Passthrough / SNI-basierte Weiterleitung aktivieren", + "global_settings_setting_tls_passthrough_enabled_help": "Dies ist eine erweiterte Funktion, mit der sich eine gesamte Domain per Reverse Proxy an einen anderen Rechner weiterleiten lässt, *ohne* den Datenverkehr zu entschlüsseln. Das ist nützlich, wenn mehrere Rechner hinter derselben IP-Adresse erreichbar sein sollen, jeder Rechner die SSL/TLS-Terminierung jedoch selbst übernehmen soll.", + "global_settings_setting_tls_passthrough_list": "Liste der Weiterleitungen", + "global_settings_setting_tls_passthrough_list_help": "Dies sollte eine Liste aus DOMAIN;DESTINATION;PORT sein, beispielsweise domain.tld;192.168.1.42;443 oder domain.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "TLS-Passthrough / SNI-basierte Weiterleitung", + "global_settings_setting_webadmin_name": "Webadmin", + "group_cannot_remove_last_admin": "Der Benutzer „{user}“ ist der letzte Benutzer in der Gruppe „admins“ und wird nicht aus dieser entfernt.", + "invalid_password": "Ungültiges Passwort", + "log_diagnosis_run": "Diagnose starten", + "log_tools_update": "Verfügbare System-Updates abrufen und den App-Katalog aktualisieren", + "mail_alias_unauthorized": "Du bist nicht berechtigt, Aliasse für die Domain „{domain}“ hinzuzufügen", + "mail_already_exists": "Die E-Mail Adresse '{mail}' existiert bereits", + "mail_edit_operation_unauthorized": "Du bist nicht berechtigt, diese Änderung für dein Konto vorzunehmen.", + "migration_0027_cleaning_up": "Cache und nicht mehr benötigte Pakete bereinigen…", + "migration_0027_delayed_api_restart": "Die YunoHost-API wird in 15 Sekunden automatisch neu gestartet und ist möglicherweise für einige Sekunden nicht erreichbar. Anschließend musst du dich erneut anmelden.", + "migration_0027_main_upgrade": "Haupt-Upgrade wird gestartet…", + "migration_0027_modified_files": "Bitte beachte, dass die folgenden Dateien manuell geändert wurden und nach dem Upgrade möglicherweise überschrieben werden: {manually_modified_files}", + "migration_0027_not_enough_free_space": "In /var/ ist nur noch wenig freier Speicherplatz vorhanden! Für diese Migration sollten mindestens 1 GB freier Speicherplatz zur Verfügung stehen.", + "domain_config_custom_css_help": "Dies ist für erfahrene Administratoren, die das Erscheinungsbild des Portals anpassen möchten", + "domain_config_enable_public_apps_page": "Besuchern die Liste der öffentlichen Apps anzeigen", + "domain_config_enable_public_apps_page_help": "Wenn Besucher das Portal aufrufen, wird ihnen statt des reinen Anmeldeformulars eine Seite mit „öffentlichen Apps“ angezeigt.", + "domain_config_portal_public_intro": "Benutzerdefiniertes öffentliches Intro", + "global_settings_setting_dns_custom_resolvers_enabled_help": "Standardmäßig verwendet YunoHost eine Liste vertrauenswürdiger Resolver mit Standorten in Europa. Fortgeschrittene Nutzer möchten möglicherweise stattdessen eigene Resolver festlegen.", + "global_settings_setting_dns_custom_resolvers_enabled": "Benutzerdefinierte DNS-Resolver verwenden", + "global_settings_setting_dns_custom_resolvers_list": "Adressen benutzerdefinierter Resolver", + "global_settings_setting_dns_custom_resolvers_list_help": "Eine Liste mit mindestens 2 DNS-Resolvern pro verwendetem IP-Protokoll (IPv4/IPv6). Beispiel: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "global_settings_setting_portal_name": "Portal", + "migration_0027_start": "Die Migration zu Bookworm wird gestartet…", + "migration_python_venv_rebuild_failed": "Die Neuerstellung der Python-Virtual-Environment für {app} ist fehlgeschlagen. Solange dieses Problem nicht behoben ist, funktioniert die App möglicherweise nicht. Behebe das Problem, indem du das Upgrade dieser App mit dem Befehl `yunohost app upgrade --force {app}` erzwingst.", + "migration_python_venv_rebuild_in_progress": "Versuche, die Python-Virtual-Environment für `{app}` neu zu erstellen", + "migration_0036_cleaning_up": "Cache und nicht mehr benötigte Pakete bereinigen…", + "migration_0036_delayed_api_restart": "Die YunoHost-API wird in 15 Sekunden automatisch neu gestartet und ist möglicherweise für einige Sekunden nicht verfügbar. Anschließend musst du dich erneut anmelden.", + "migration_0036_main_upgrade": "Haupt-Upgrade wird gestartet…", + "migration_0036_apt_lists_file_still_exists": "Die veraltete Datei „{file}“ ist noch vorhanden, obwohl sie eigentlich nicht mehr vorhanden sein sollte. Sie wird in „{file}.legacy_bookworm“ umbenannt.", + "migration_0036_modified_files": "Bitte beachte, dass die folgenden Dateien manuell geändert wurden und nach dem Upgrade möglicherweise überschrieben werden:", + "migration_0036_not_enough_free_space": "In /var/ ist nur noch wenig freier Speicherplatz vorhanden! Für diese Migration sollten mindestens 1 GB freier Speicherplatz zur Verfügung stehen.", + "migration_0036_patch_yunohost_dpkg": "Anwenden eines Patches auf die dpkg-Datenbank, um Konflikte zu umgehen…", + "migration_0036_problematic_apps_warning": "Bitte beachte, dass die folgenden möglicherweise problematischen installierten Apps erkannt wurden. Es sieht so aus, als wären diese nicht über den YunoHost-App-Katalog installiert worden oder nicht als „funktionierend“ gekennzeichnet. Daher kann nicht garantiert werden, dass sie nach dem Upgrade weiterhin funktionieren:", + "migration_0037_upgrade_dkim_keys_failed": "Es konnte kein neuer 2048-Bit-Schlüssel für {domains} generiert werden.", + "migration_description_0027_migrate_to_bookworm": "Das System auf Debian Bookworm und YunoHost 12 aktualisieren", + "migration_description_0028_delete_legacy_xmpp_permission": "Entferne die alten XMPP-Berechtigungen, Metronome ist jetzt eine App", + "user_cannot_delete_last_admin": "Der Benutzer „{user}“ ist der letzte Benutzer in der Gruppe „admins“ und wird nicht gelöscht.", + "session_expired": "Die Sitzung ist abgelaufen", + "service_description_opendkim": "Signiert ausgehende E-Mails mit DKIM, damit sie weniger wahrscheinlich als Spam markiert werden", + "service_description_yunohost-portal-api": "Verwaltet die Interaktionen zwischen den verschiedenen Webschnittstellen des Portals und dem System", + "pydantic_value_error_url_scheme": "Ungültiges oder fehlendes URL-Schema", + "pydantic_value_error_url_port": "Ungültiger URL-Port; der Port darf den Wert 65535 nicht überschreiten", + "pydantic_value_error_url_host": "Ungültiger URL-Host", + "pydantic_value_error_url_extra": "Ungültige URL, zusätzliche Zeichen nach der gültigen URL gefunden: '{extra}'" +} diff --git a/locales/el.json b/locales/el.json new file mode 100644 index 0000000..acc6019 --- /dev/null +++ b/locales/el.json @@ -0,0 +1,12 @@ +{ + "aborting": "Ματαίωση.", + "action_invalid": "Μη έγκυρη ενέργεια '{action}'", + "admin_password": "Κωδικός διαχείρισης", + "admins": "Διαχειριστές", + "all_users": "Όλοι οι χρήστες YunoHost", + "already_up_to_date": "Δεν υπάρχει τίποτα να γίνει. Όλα είναι επικαιροποιημένα.", + "app_action_broke_system": "Αυτή η ενέργεια φαίνεται να έχει προκαλέσει προβλήματα σε αυτές τις σημαντικές υπηρεσίες: {services}", + "app_action_failed": "Αποτυχία εκτέλεσης ενέργειας {action} για την εφαρμογή {app}", + "app_already_installed": "Η φαρμογή {app} είναι ήδη εγκατεστημένη", + "password_too_simple_1": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 8 χαρακτήρες" +} diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..0b5a0c1 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,926 @@ +{ + "aborting": "Aborting.", + "action_invalid": "Invalid action '{action}'", + "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", + "already_up_to_date": "Nothing to do. Everything is already up-to-date.", + "app_action_broke_system": "This action seems to have broken these important services: {services}", + "app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).", + "app_action_failed": "Failed to run action {action} for app {app}", + "app_already_installed": "{app} is already installed", + "app_already_installed_cant_change_url": "This app is already installed. The URL cannot be changed just by this function. Check in `app changeurl` if it's available.", + "app_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_change_url_failed": "Could not change the url for {app}: {error}", + "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain}{path}'), nothing to do.", + "app_change_url_no_script": "The app '{app_name}' doesn't support URL modification yet. Maybe you should upgrade it.", + "app_change_url_require_full_domain": "{app} cannot be moved to this new URL because it requires a full domain (i.e. with path = /)", + "app_change_url_script_failed": "An error occured inside the change url script", + "app_change_url_success": "{app} URL is now {domain}{path}", + "app_config__core_name": "Tiles and permissions", + "app_config_permission_allowed": "Groups/users allowed to access", + "app_config_permission_allowed_warn_protected": "NB: this permission is 'protected' and therefore the 'visitors' group cannot be actually added/removed from the authorized groups.", + "app_config_permission_description": "Description", + "app_config_permission_description_help": "This is really only useful if you're using the 'descriptive' portal mode", + "app_config_permission_extraperm_section_name": "Permission '{perm}'", + "app_config_permission_label": "Label", + "app_config_permission_location": "Corresponds to [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Custom logo to use", + "app_config_permission_logo_help": "Only PNG are supported", + "app_config_permission_show_tile": "Display tile in portal", + "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 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_db_prompt_no_app_database": "This app does not appear to have a database declared in its manifest", + "app_db_prompt_type_not_supported": "The command does not support this type of database: {type}", + "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_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.", + "app_id_invalid": "Invalid app ID", + "app_install_failed": "Unable to install {app}: {error}", + "app_install_files_invalid": "These files cannot be installed", + "app_install_script_failed": "An error occurred inside the app installation script", + "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps}", + "app_make_default_location_already_used": "Unable to make '{app}' the default app on the domain, '{domain}' is already in use by '{other_app}'", + "app_manifest_install_ask_admin": "Choose an administrator user for this app", + "app_manifest_install_ask_domain": "Choose the domain where this app should be installed", + "app_manifest_install_ask_init_admin_permission": "Who should have access to admin features for this app? (This can later be changed)", + "app_manifest_install_ask_init_main_permission": "Who should have access to this app? (This can later be changed)", + "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?", + "app_manifest_install_ask_password": "Choose an administration password for this app", + "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed", + "app_not_correctly_installed": "{app} seems to be incorrectly installed", + "app_not_enough_disk": "This app requires {required} free space.", + "app_not_enough_ram": "This app requires {required} RAM to install/upgrade but only {current} is available right now.", + "app_not_installed": "Could not find {app} in the list of installed apps: {all_apps}", + "app_not_properly_removed": "{app} has not been properly removed", + "app_packaging_format_not_supported": "This app cannot be installed because its packaging format is not supported by your YunoHost version. You should probably consider upgrading your system.", + "app_remove_after_failed_install": "Removing the app after installation failure…", + "app_removed": "{app} uninstalled", + "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_unknown": "Unknown app", + "app_unsupported_remote_type": "Unsupported remote type used for the app", + "app_upgrade_app_name": "Now upgrading {app}…", + "app_upgrade_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", + "app_upgrade_broke_the_system": "{app} upgrade seemingly worked but left the system in a broken state and is therefore considered as a failure.", + "app_upgrade_cli_bad_quality": "Skipping upgrades for {app} because it is currently flagged as broken on YunoHost's application catalog.", + "app_upgrade_cli_up_to_date": "{app} is already up to date ({current_version})", + "app_upgrade_cli_url_required": "{app} is not in the catalog (anymore?) and therefore cannot be upgraded automatically. You should use `yunohost app upgrade {app}` to provide the url of the repo using the `-u` option.", + "app_upgrade_cli_will_force_upgrade": "{app} will be force-upgraded ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} will be upgraded from {current_version} to {new_version}", + "app_upgrade_continuing_with_other_apps": "Failed to upgrade {app}, but continuing with the upgrade of other apps anyway (because `--continue-on-failure` was used)", + "app_upgrade_fail_requirements": "A new version is available for this app ({new_version}), but some requirements are not met:\n{failed_requirements}", + "app_upgrade_failed": "Failed to upgrade {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "Failed to upgrade app '{app}', and it left the system in a broken state.", + "app_upgrade_script_failed": "An error occurred inside the app upgrade script", + "app_upgrade_several_apps": "The following apps will be upgraded: {apps}", + "app_upgrade_some_app_failed": "Some apps could not be upgraded", + "app_upgrade_specific_channel_msg": "Note that you're currently using `{channel}` as source for upgrades. Be sure to check the ongoing discussion [here]({pr_url}).", + "app_upgrade_up_to_date": "Force-upgrading the app (to the same version) can sometimes be useful to rebuild the app and configurations.", + "app_upgrade_upgradable": "The app can be upgraded from version {current_version} to {new_version}", + "app_upgrade_url_required": "This app is not in the catalog (anymore?), you must therefore manually take care of its upgrades.
From the command line, you can use `yunohost app upgrade ` and provide the url of the repo using the `-u` option.", + "app_upgraded": "{app} upgraded", + "app_yunohost_version_not_supported": "This app requires YunoHost >= {required} but current installed version is {current}.", + "apps_already_up_to_date": "All apps are already up-to-date", + "apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}", + "apps_catalog_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_confirm_partial_upgrade": "Some apps for which an upgrade was requested cannot be upgraded. Proceed with the others anyway?", + "apps_no_target_can_be_upgraded": "No apps can be upgraded", + "apps_upgrade_cancelled": "Upgrades were still pending for several other apps but their upgrade was cancelled (use `--continue-on-failure` to continue anyway): {apps}", + "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", + "automatic_task": "Automatic task", + "backup_abstract_method": "This backup method has yet to be implemented", + "backup_actually_backuping": "Creating a backup archive from the collected files…", + "backup_app_script_failed": "Failed to collect files to backed up for {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_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_corrupted": "It looks like the backup archive '{archive}' is corrupted : {error}", + "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", + "backup_archive_writing_error": "Could not add the files '{source}' (named in the archive '{dest}') to be backed up into the compressed archive '{archive}'", + "backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size}MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "Backup {name} was deleted because it is replaced by a newer backup {newname}", + "backup_cant_mount_uncompress_archive": "Could not mount the uncompressed archive as write protected", + "backup_cleaning_failed": "Could not clean up the temporary backup folder", + "backup_copying_to_organize_the_archive": "Copying {size}MB to organize the archive", + "backup_couldnt_bind": "Could not bind {src} to {dest}.", + "backup_create_size_estimation": "The archive will contain about {size} of data.", + "backup_created": "Backup created: {name}", + "backup_creation_failed": "Could not create the backup archive", + "backup_csv_addition_failed": "Could not add files to backup into the CSV file", + "backup_csv_creation_failed": "Could not create the CSV file needed for restoration", + "backup_custom_backup_error": "Custom backup method could not get past the 'backup' step", + "backup_custom_mount_error": "Custom backup method could not get past the 'mount' step", + "backup_delete_error": "Could not delete '{path}'", + "backup_deleted": "Backup deleted: {name}", + "backup_hook_unknown": "The backup hook '{hook}' is unknown", + "backup_method_copy_finished": "Backup copy finalized", + "backup_method_custom_finished": "Custom backup method '{method}' finished", + "backup_method_tar_finished": "TAR backup archive created", + "backup_mount_archive_for_restore": "Preparing archive for restoration…", + "backup_no_file_collected": "Failed to collect files to be backed up", + "backup_no_uncompress_archive_dir": "There is no such uncompressed archive directory", + "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", + "backup_output_directory_not_empty": "You should pick an empty output directory", + "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_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.", + "cannot_open_file": "Could not open file {file} (reason: {error})", + "cannot_write_file": "Could not write file {file} (reason: {error})", + "certmanager_acme_not_configured_for_domain": "The ACME challenge cannot be run for {domain} right now because its nginx conf lacks the corresponding code snippet… Please make sure that your nginx configuration is up to date using `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonLE_cert": "The certificate for the domain '{domain}' is not issued by Let's Encrypt. Cannot renew it automatically!", + "certmanager_attempt_to_renew_valid_cert": "The certificate for the domain '{domain}' is not about to expire! (You may use --force if you know what you're doing)", + "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain}! (Use --force to bypass)", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain} (file: {file}), reason: {reason}", + "certmanager_cert_install_failed": "Let's Encrypt certificate install failed for {domains}", + "certmanager_cert_install_failed_selfsigned": "Self-signed certificate install failed for {domains}", + "certmanager_cert_install_success": "Let's Encrypt certificate now installed for the domain '{domain}'", + "certmanager_cert_install_success_selfsigned": "Self-signed certificate now installed for the domain '{domain}'", + "certmanager_cert_renew_failed": "Let's Encrypt certificate renew failed for {domains}", + "certmanager_cert_renew_success": "Let's Encrypt certificate renewed for the domain '{domain}'", + "certmanager_cert_signing_failed": "Could not sign the new certificate", + "certmanager_certificate_fetching_or_enabling_failed": "Trying to use the new certificate for {domain} did not work…", + "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain} is not self-signed. Are you sure you want to replace it? (Use '--force' to do so.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS records for domain '{domain}' are different to this server's IP. Please check the 'DNS records' (basic) category in the diagnosis for more info. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_http_not_working": "Domain {domain} does not seem to be accessible through HTTP. Please check the 'Web' category in the diagnosis for more info. (If you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_domain_not_diagnosed_yet": "There is no diagnosis result for domain {domain} yet. Please re-run a diagnosis for categories 'DNS records' and 'Web' in the diagnosis section to check if the domain is ready for Let's Encrypt. (Or if you know what you are doing, use '--no-checks' to turn off these checks.)", + "certmanager_hit_rate_limit": "Too many certificates already issued for this exact set of domains {domain} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", + "certmanager_no_cert_file": "Could not read the certificate file for the domain {domain} (file: {file})", + "certmanager_self_ca_conf_file_not_found": "Could not find configuration file for self-signing authority (file: {file})", + "certmanager_unable_to_parse_self_CA_name": "Could not parse name of self-signing authority (file: {file})", + "config_action_disabled": "Could not run action '{action}' since it is disabled, make sure to meet its constraints. help: {help}", + "config_action_failed": "Failed to run action '{action}': {error}", + "config_apply_failed": "Applying the new configuration failed: {error}", + "config_cant_set_value_on_section": "You can't set a single value on an entire config section.", + "config_forbidden_keyword": "The keyword '{keyword}' is reserved, you can't create or use a config panel with a question with this id.", + "config_forbidden_readonly_type": "The type '{type}' can't be set as readonly, use another type to render this value (relevant arg id: '{id}').", + "config_no_panel": "No config panel found.", + "config_unknown_filter_key": "The filter key '{filter_key}' is incorrect.", + "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": "This app requires more RAM to install than currently available. 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}]", + "confirm_tos_acknowledgement": "I have read and understand the Terms of Services [{answers}]", + "corrupted_json": "Corrupted JSON read from {ressource}: {error}", + "corrupted_toml": "Corrupted TOML read from {ressource}: {error}", + "corrupted_yaml": "Corrupted YAML read from {ressource}: {error}", + "danger": "Danger:", + "diagnosis_apps_allgood": "All installed apps respect basic packaging practices", + "diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", + "diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.", + "diagnosis_apps_deprecated_practices": "This app's installed version still uses some very old, deprecated packaging practices. You should really consider upgrading it.", + "diagnosis_apps_issue": "An issue was found for app {app}", + "diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and was removed, you should consider uninstalling this app as it won't receive upgrades and may compromise the integrity and security of your system.", + "diagnosis_apps_outdated_packaging_format": "This app uses a deprecated packaging format and will soon be unsupported by YunoHost. You should really consider upgrading it.", + "diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x, 3.x or 4.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.", + "diagnosis_apps_security_issue_error": "Application {app} is currently in version '{current_version}', which is vulnerable to a MAJOR security issue: {title}. It is recommended to upgrade it AS SOON AS POSSIBLE to version '{fixed_in_version}'. More infos: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "Application {app} is currently in version '{current_version}', which is vulnerable to a moderate security issue: {title}. It is recommended to upgrade it to '{fixed_in_version}'. More infos: {more_infos_list}", + "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", + "diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Server model is {model}", + "diagnosis_basesystem_host": "Server is running Debian {debian_version}", + "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_main_version": "Server is running YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})", + "diagnosis_cache_still_valid": "(Cache still valid for {category} diagnosis. Won't re-diagnose it yet!)", + "diagnosis_cant_run_because_of_dep": "Can't run diagnosis for {category} while there are important issues related to {dep}.", + "diagnosis_description_apps": "Applications", + "diagnosis_description_basesystem": "Base system", + "diagnosis_description_dnsrecords": "DNS records", + "diagnosis_description_ip": "Internet connectivity", + "diagnosis_description_mail": "Email", + "diagnosis_description_ports": "Ports exposure", + "diagnosis_description_regenconf": "System configurations", + "diagnosis_description_services": "Services status check", + "diagnosis_description_systemresources": "System resources", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Storage {mountpoint} (on device {device}) has only {free} ({free_percent}%) space remaining (out of {total}). Be careful.", + "diagnosis_diskusage_ok": "Storage {mountpoint} (on device {device}) still has {free} ({free_percent}%) space left (out of {total})!", + "diagnosis_diskusage_verylow": "Storage {mountpoint} (on device {device}) has only {free} ({free_percent}%) space remaining (out of {total}). You should really consider cleaning up some space!", + "diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues --human-readable' from the command-line.", + "diagnosis_dns_bad_conf": "Some DNS records are missing or incorrect for domain {domain} (category {category})", + "diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {content}", + "diagnosis_dns_good_conf": "DNS records are correctly configured for domain {domain} (category {category})", + "diagnosis_dns_missing_record": "According to the recommended DNS configuration, you should add a DNS record with the following info.
Type: {type}
Name: {name}
Value: {content}", + "diagnosis_dns_point_to_doc": "Please check the documentation at https://doc.yunohost.org/dns_config if you need help configuring DNS records.", + "diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", + "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Some domains will expire VERY SOON!", + "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", + "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", + "diagnosis_domain_expiration_success": "Your domains are registered and not going to expire anytime soon.", + "diagnosis_domain_expiration_warning": "Some domains will expire soon!", + "diagnosis_domain_expires_in": "{domain} expires in {days} days.", + "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!", + "diagnosis_everything_ok": "Everything looks OK for {category}!", + "diagnosis_failed": "Failed to fetch diagnosis result for category '{category}': {error}", + "diagnosis_failed_for_category": "Diagnosis failed for category '{category}': {error}", + "diagnosis_found_errors": "Found {errors} significant issue(s) related to {category}!", + "diagnosis_found_errors_and_warnings": "Found {errors} significant issue(s) (and {warnings} warning(s)) related to {category}!", + "diagnosis_found_warnings": "Found {warnings} item(s) that could be improved for {category}.", + "diagnosis_high_number_auth_failures": "There's been a suspiciously high number of authentication failures recently. You may want to make sure that fail2ban is running and is correctly configured, or use a custom port for SSH as explained in https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "It looks like another machine (maybe your internet router) answered instead of your server.
1. The most common cause for this issue is that port 80 (and 443) are not correctly forwarded to your server.
2. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.", + "diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Error: {error}", + "diagnosis_http_hairpinning_issue": "Your local network does not seem to have hairpinning enabled.", + "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "This domain's nginx configuration appears to have been modified manually, and prevents YunoHost from diagnosing if it's reachable on HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference from the command line using yunohost tools regen-conf nginx --dry-run --with-diff and if you're ok with it, apply the changes with yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", + "diagnosis_http_partially_unreachable": "Domain {domain} appears unreachable through HTTP from outside the local network in IPv{failed}, though it works in IPv{passed}.", + "diagnosis_http_special_use_tld": "Domain {domain} is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to be exposed outside the local network.", + "diagnosis_http_timeout": "Timed-out while trying to contact your server from 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_resolvconf": "Domain name resolution seems to be broken on your server, which seems related to /etc/resolv.conf not pointing to 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "The server is connected to the Internet through IPv4!", + "diagnosis_ip_connected_ipv6": "The server is connected to the Internet through IPv6!", + "diagnosis_ip_dnsresolution_working": "Domain name resolution is working!", + "diagnosis_ip_global": "Global IP: {global}", + "diagnosis_ip_local": "Local IP: {local}", + "diagnosis_ip_no_ipv4": "The server does not have working IPv4.", + "diagnosis_ip_no_ipv6": "The server does not have working IPv6.", + "diagnosis_ip_no_ipv6_tip": "Having a working IPv6 is not mandatory for your server to work, but it is better for the health of the Internet as a whole. IPv6 should usually be automatically configured by the system or your provider if it's available. Otherwise, you might need to configure a few things manually as explained in the documentation here: https://doc.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://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "The server does not seem to be connected to the Internet at all!?", + "diagnosis_ip_weird_resolvconf": "DNS resolution seems to be working, but it looks like you're using a custom /etc/resolv.conf.", + "diagnosis_ip_weird_resolvconf_details": "The file /etc/resolv.conf should be a symlink to /etc/resolvconf/run/resolv.conf itself pointing to 127.0.0.1 (dnsmasq). If you want to manually configure DNS resolvers, please edit /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blocklist_listed_by": "Your IP or domain {item} is blocklisted on {blocklist_name}", + "diagnosis_mail_blocklist_ok": "The IPs and domains used by this server do not appear to be blocklisted", + "diagnosis_mail_blocklist_reason": "The blocklist reason is: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "It looks like the reason mentions 'open resolver'.
This usually means your server is not using its local DNS, but a public, open, one.
Check the contents of /etc/resolv.conf, it should contain nameserver 127.0.0.1.
Since this file is usually automatically generated, do not edit it manually. Check your DHCP settings, or your VPN settings if you are using one, or if you used a Debian image made by, for example, a VPS provider, look for a cloudinit configuration.
You are most welcome on the YunoHost support channels to get help on this issue.
The verbatim blacklist reason is: {reason}", + "diagnosis_mail_blocklist_website": "After identifying why you are listed and fixing it, feel free to ask for your IP or domain to be removed on {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "A non-SMTP service answered on port 25 on IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "It could be due to an another machine answering instead of your server.", + "diagnosis_mail_ehlo_could_not_diagnose": "Could not diagnose if postfix mail server is reachable from the outside in IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", + "diagnosis_mail_ehlo_ok": "The SMTP mail server is reachable from the outside and therefore is able to receive emails!", + "diagnosis_mail_ehlo_unreachable": "The SMTP mail server is unreachable from the outside on IPv{ipversion}. It won't be able to receive emails.", + "diagnosis_mail_ehlo_unreachable_details": "Could not open a connection on port 25 to your server in IPv{ipversion}. It appears to be unreachable.
1. The most common cause for this issue is that port 25 is not correctly forwarded to your server.
2. You should also make sure that service postfix is running.
3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_mail_ehlo_wrong": "A different SMTP mail server answers on IPv{ipversion}. Your server will probably not be able to receive emails.", + "diagnosis_mail_ehlo_wrong_details": "The EHLO received by the remote diagnoser in IPv{ipversion} is different from your server's domain.
Received EHLO: {wrong_ehlo}
Expected: {right_ehlo}
The most common cause for this issue is that port 25 is not correctly forwarded to your server. Alternatively, make sure that no firewall or reverse-proxy is interfering.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "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://doc.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://doc.yunohost.org/vpn_advantage
- You can also consider switching to a more net neutrality-friendly provider", + "diagnosis_mail_outgoing_port_25_ok": "The SMTP mail server is able to send emails (outgoing port 25 is not blocked).", + "diagnosis_mail_queue_ok": "{nb_pending} pending emails in the mail queues", + "diagnosis_mail_queue_too_big": "Too many pending emails in mail queue ({nb_pending} emails)", + "diagnosis_mail_queue_unavailable": "Can not consult number of pending emails in queue", + "diagnosis_mail_queue_unavailable_details": "Error: {error}", + "diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.", + "diagnosis_no_cache": "No diagnosis cache yet for category '{category}'", + "diagnosis_package_installed_from_sury": "Some system packages should be downgraded", + "diagnosis_package_installed_from_sury_details": "Some packages were inadvertendly installed from a third-party repository called Sury. The YunoHost team improved the strategy that handle these packages, but it's expected that some setups that installed PHP7.3 apps while still on Stretch have some remaining inconsistencies. To fix this situation, you should try running the following command: {cmd_to_fix}", + "diagnosis_package_security_issue_error": "System package '{package}' is currently in version '{current_version}', which is vulnerable to a MAJOR security issue: {title}. It is recommended to upgrade AS SOON AS POSSIBLE to version '{fixed_in_version}'. More infos: {more_infos_list}", + "diagnosis_package_security_issue_warning": "System package '{package}' is currently in version '{current_version}', which is vulnerable to a moderate security issue: {title}. It is recommended to upgrade it to '{fixed_in_version}'. More infos: {more_infos_list}", + "diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Error: {error}", + "diagnosis_ports_forwarding_tip": "To fix this issue, you most probably need to configure port forwarding on your internet router as described in https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Exposing this port is needed for {category} features (service {service})", + "diagnosis_ports_ok": "Port {port} is reachable from the outside.", + "diagnosis_ports_partially_unreachable": "Port {port} is not reachable from the outside in IPv{failed}.", + "diagnosis_ports_unreachable": "Port {port} is not reachable from the outside.", + "diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process consuming too much memory. Summary of the processes killed:\n{kills_summary}", + "diagnosis_ram_low": "The system has {available} ({available_percent}%) RAM available (out of {total}). Be careful.", + "diagnosis_ram_ok": "The system still has {available} ({available_percent}%) RAM available out of {total}.", + "diagnosis_ram_verylow": "The system has only {available} ({available_percent}%) RAM available! (out of {total})", + "diagnosis_regenconf_allgood": "All 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_rfkill_wifi": "The Wi-Fi card is disabled and a system warning might prevent app installations", + "diagnosis_rfkill_wifi_details": "This warning sneaks in many command outputs, breaking some apps. It is usually required to specify your country code with the command sudo raspi-config. Here is the error:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.", + "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly… It's recommended to have at least 16 GB for the root filesystem.", + "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown critical security vulnerability", + "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more info.", + "diagnosis_services_bad_status": "Service {service} is {status} :(", + "diagnosis_services_bad_status_tip": "You can try to restart the service, and if it doesn't work, have a look at the service logs in the webadmin (from the command line, you can do this with yunohost service restart {service} and yunohost service log {service}).", + "diagnosis_services_conf_broken": "Configuration is broken for service {service}!", + "diagnosis_services_running": "Service {service} is running!", + "diagnosis_sshd_config_inconsistent": "It looks like the SSH port was manually modified in /etc/ssh/sshd_config. Since YunoHost 4.2, a new global setting 'security.ssh.ssh_port' is available to avoid manually editing the configuration.", + "diagnosis_sshd_config_inconsistent_details": "Please run yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT to define the SSH port, and check yunohost tools regen-conf ssh --dry-run --with-diff and yunohost tools regen-conf ssh --force to reset your conf to the YunoHost recommendation.", + "diagnosis_sshd_config_insecure": "The SSH configuration appears to have been manually modified, and is insecure because it contains no 'AllowGroups' or 'AllowUsers' directive to limit access to authorized users.", + "diagnosis_swap_none": "The system has no swap at all. You should consider adding at least {recommended} of swap to avoid situations where the system runs out of memory.", + "diagnosis_swap_notsomuch": "The system has only {total} swap. You should consider having at least {recommended} to avoid situations where the system runs out of memory.", + "diagnosis_swap_ok": "The system has {total} of swap!", + "diagnosis_swap_tip": "Please be careful and aware that if the server is hosting swap on an SD card or SSD storage, it may drastically reduce the life expectancy of the device.", + "diagnosis_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 (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_remove_main": "You cannot remove '{domain}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n '; here is the list of candidate domains: {other_domains}", + "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add ', then set is as the main domain using 'yunohost domain main-domain -n ' and then you can remove the domain '{domain}' using 'yunohost domain remove {domain}'.", + "domain_cert_gen_failed": "Could not generate certificate", + "domain_config_acme_eligible": "ACME eligibility", + "domain_config_acme_eligible_explain": "This domain doesn't seem ready for a Let's Encrypt certificate. Please check your DNS configuration and HTTP server reachability. The 'DNS records' and 'Web' section in the diagnosis page can help you understand what is misconfigured.", + "domain_config_api_protocol": "API protocol", + "domain_config_auth_application_key": "Application key", + "domain_config_auth_application_secret": "Application secret key", + "domain_config_auth_consumer_key": "Consumer key", + "domain_config_auth_entrypoint": "API entry point", + "domain_config_auth_key": "Authentication key", + "domain_config_auth_secret": "Authentication secret", + "domain_config_auth_token": "Authentication token", + "domain_config_cert_install": "Install Let's Encrypt certificate", + "domain_config_cert_issuer": "Certification authority", + "domain_config_cert_name": "Certificate", + "domain_config_cert_no_checks": "Ignore diagnosis checks", + "domain_config_cert_renew": "Renew Let's Encrypt certificate", + "domain_config_cert_renew_help": "Certificate will be automatically renewed during the last 15 days of validity. You can manually renew it if you want to. (Not recommended).", + "domain_config_cert_summary": "Certificate status", + "domain_config_cert_summary_abouttoexpire": "Current certificate is about to expire. It should soon be renewed automatically.", + "domain_config_cert_summary_expired": "CRITICAL: Current certificate is not valid! HTTPS won't work at all!", + "domain_config_cert_summary_letsencrypt": "Great! You're using a valid Let's Encrypt certificate!", + "domain_config_cert_summary_ok": "Okay, current certificate looks good!", + "domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!", + "domain_config_cert_validity": "Validity", + "domain_config_custom_css": "Custom CSS stylesheet", + "domain_config_custom_css_help": "This is for advanced admins willing to customize the appearance of the portal", + "domain_config_default_app": "Default app", + "domain_config_default_app_help": "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the portal login form.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Show the list of public apps to visitors", + "domain_config_enable_public_apps_page_help": "Visitors will see a 'public apps' page when ending up on the portal instead of just the login form.", + "domain_config_feature_name": "Features", + "domain_config_mail_in": "Incoming emails", + "domain_config_mail_out": "Outgoing emails", + "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_name": "Portal customization", + "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 color theme", + "domain_config_portal_theme_help": "Users are allowed to choose another one in their settings.", + "domain_config_portal_tile_theme": "App tiles display theme", + "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": "This is an optional feature, allowing to display a search bar in the portal (for example if you like to use your YunoHost portal as your browser's home page). This should be an URL with an empty query string such as `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", + "domain_deletion_failed": "Unable to delete domain {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "This command shows you the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.", + "domain_dns_conf_special_use_tld": "This domain is based on a special-use top-level domain (TLD) such as .local or .test and is therefore not expected to have actual DNS records.", + "domain_dns_push_already_up_to_date": "Records already up to date, nothing to do.", + "domain_dns_push_failed": "Updating the DNS records failed miserably.", + "domain_dns_push_failed_to_list": "Failed to list current records using the registrar's API: {error}", + "domain_dns_push_managed_in_parent_domain": "The automatic DNS configuration feature is managed in the parent domain {parent_domain}.", + "domain_dns_push_not_applicable": "The automatic DNS configuration feature is not applicable to domain {domain}. You should manually configure your DNS records following the documentation at https://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "DNS records partially updated: some warnings/errors were reported.", + "domain_dns_push_record_failed": "Failed to {action} record {type}/{name} : {error}", + "domain_dns_push_success": "DNS records updated!", + "domain_dns_pushing": "Pushing DNS records…", + "domain_dns_registrar_experimental": "So far, the interface with **{registrar}**'s API has not been properly tested and reviewed by the YunoHost community. Support is **very experimental** - be careful!", + "domain_dns_registrar_managed_in_parent_domain": "This domain is a subdomain of {parent_domain_link}. DNS registrar configuration should be managed in {parent_domain}'s configuration panel.", + "domain_dns_registrar_not_supported": "YunoHost could not automatically detect the registrar handling this domain. You should manually configure your DNS records following the documentation at https://doc.yunohost.org/dns_config.", + "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://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (You can also manually configure your DNS records following the documentation at https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_use_auto": "Use automatic DNS feature", + "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_exists": "The domain already exists", + "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", + "domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.", + "domain_remove_confirm_apps_removal": "Removing this domain will remove those applications:\n{apps}\n\nAre you sure you want to do that? [{answers}]", + "domain_uninstall_app_first": "Those applications are still installed on your domain:\n{apps}\n\nPlease uninstall them using 'yunohost app remove the_app_id' or move them to another domain using 'yunohost app change-url the_app_id' before proceeding to domain removal", + "domain_unknown": "Domain '{domain}' unknown", + "domains_available": "Available domains:", + "done": "Done", + "download_bad_status_code": "{url} returned status code {code}", + "download_ssl_error": "SSL error when connecting to {url}", + "download_timeout": "{url} took too long to answer, gave up.", + "download_unknown_error": "Error when downloading data from {url}: {error}", + "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_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_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.", + "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", + "error_changing_file_permissions": "Error when changing permissions for {path}: {error}", + "error_removing": "Error when removing {path}: {error}", + "error_writing_file": "Error when writing file {file}: {error}", + "extracting": "Extracting…", + "field_invalid": "Invalid field '{field}'", + "file_does_not_exist": "The file {path} does not exist.", + "file_not_exist": "File does not exist: '{path}'", + "firewall_reload_failed": "Could not reload the firewall. More info in log.", + "firewall_reloaded": "Firewall reloaded", + "global_settings_reset_success": "Reset global settings", + "global_settings_setting_admin_strength": "Admin password strength requirements", + "global_settings_setting_admin_strength_help": "These requirements are only enforced when initializing or changing the password", + "global_settings_setting_antispam_name": "Antispam", + "global_settings_setting_backup_compress_tar_archives": "Compress backups", + "global_settings_setting_backup_compress_tar_archives_help": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", + "global_settings_setting_backup_name": "Backup", + "global_settings_setting_dns_custom_resolvers_enabled": "Use custom DNS resolvers", + "global_settings_setting_dns_custom_resolvers_enabled_help": "By default, YunoHost uses a list of trustworthy resolvers located in Europe. Advanced users may want to specify custom resolvers instead.", + "global_settings_setting_dns_custom_resolvers_list": "Custom resolvers' addresses", + "global_settings_setting_dns_custom_resolvers_list_help": "A list of at least 2 DNS resolvers per IP protocol in use (IPv4/IPv6). Example: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "global_settings_setting_dns_exposure": "IP versions to consider for DNS configuration and diagnosis", + "global_settings_setting_dns_exposure_help": "NB: This only affects the recommended DNS configuration and diagnosis checks. This does not affect system configurations.", + "global_settings_setting_email_name": "Email", + "global_settings_setting_enable_blocklists": "Enable blocklists for incoming traffic", + "global_settings_setting_enable_blocklists_help": "Blocks servers listed by spamcop.net, spamhaus.org and abuseat.org to prevent spam. However, this may cause delivery problems for some harmless mail servers which may be listed by those third parties, in which case mail sent from these servers won't be received.", + "global_settings_setting_experimental_name": "Experimental", + "global_settings_setting_misc_name": "Other", + "global_settings_setting_network_name": "Network", + "global_settings_setting_nginx_compatibility": "NGINX Compatibility", + "global_settings_setting_nginx_compatibility_help": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", + "global_settings_setting_nginx_name": "NGINX (web server)", + "global_settings_setting_nginx_redirect_to_https": "Force HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Redirect HTTP requests to HTTPs by default (DO NOT TURN OFF unless you really know what you're doing!)", + "global_settings_setting_password_name": "Passwords", + "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. 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_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Allow users to edit their main email address", + "global_settings_setting_portal_allow_edit_email_alias": "Allow users to add, remove, edit mail aliases", + "global_settings_setting_portal_allow_edit_email_alias_help": "If disabled, they need to ask admins to do it for them.", + "global_settings_setting_portal_allow_edit_email_forward": "Allow users to add, remove, edit mail forward", + "global_settings_setting_portal_allow_edit_email_forward_help": "If disabled, they need to ask admins to do it for them.", + "global_settings_setting_portal_allow_edit_email_help": "If disabled, they need to ask admins to do it for them.", + "global_settings_setting_portal_name": "Portal", + "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_postfix_name": "Postfix (SMTP email server)", + "global_settings_setting_root_access_explain": "On Linux systems, 'root' is the absolute admin. In YunoHost context, direct 'root' SSH login is by default disable - except from the local network of the server. Members of the 'admins' group can use the sudo command to act as root from the command line. However, it can be helpful to have a (robust) root password to debug the system if for some reason regular admins can not login anymore.", + "global_settings_setting_root_access_name": "Change root password", + "global_settings_setting_root_password": "New root password", + "global_settings_setting_root_password_confirm": "New root password (confirm)", + "global_settings_setting_security_experimental_enabled": "Experimental security features", + "global_settings_setting_security_experimental_enabled_help": "Enable experimental security features (don't enable this if you don't know what you're doing!)", + "global_settings_setting_security_name": "Security", + "global_settings_setting_smtp_allow_ipv6": "Allow IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Allow the use of IPv6 to receive and send mail", + "global_settings_setting_smtp_backup_mx_domains": "Domains to act as secondary MX for", + "global_settings_setting_smtp_backup_mx_domains_help": "Allow this server to act as a backup *secondary* MX domain for the listed domain. This means that if the main MX for the domain is not reachable (for example because of an outage), mails will still be sent to this server, which will keep them during a maximum of 20 days and try to relay them to the real destination once it goes back up. Several domains can be provided, separated by commas.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "SMTP backup MX emails whitelist", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "When acting as a secondary MX, the exhaustive list of allowed recipient's email addresses must be provided (otherwise mails will be refused and discarded). Several entries can be provided, separated by commas.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Enable SMTP relay", + "global_settings_setting_smtp_relay_enabled_help": "Enable the SMTP relay to use in order to send mail instead of this yunohost instance. Useful if you are in one of this situation: your 25 port is blocked by your ISP or VPS provider, you have a residential IP listed on DUHL, you are not able to configure reverse DNS or this server is not directly exposed on the internet and you want use an other one to send mails.", + "global_settings_setting_smtp_relay_host": "SMTP relay host", + "global_settings_setting_smtp_relay_password": "SMTP relay password", + "global_settings_setting_smtp_relay_port": "SMTP relay port", + "global_settings_setting_smtp_relay_user": "SMTP relay user", + "global_settings_setting_ssh_compatibility": "SSH Compatibility", + "global_settings_setting_ssh_compatibility_help": "Compatibility vs. security tradeoff for the SSH server. Affects the ciphers (and other security-related aspects). See https://infosec.mozilla.org/guidelines/openssh for more info.", + "global_settings_setting_ssh_name": "SSH", + "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_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_tls_passthrough_enabled": "Enable TLS-passthrough / SNI-based forwarding", + "global_settings_setting_tls_passthrough_enabled_help": "This is an advanced feature to reverse-proxy an entire domain to another machine *without* decrypting the traffic. Useful when you want to expose several machines behind the same IP but still allow each machine to handle the SSL termination.", + "global_settings_setting_tls_passthrough_explain": "This feature is ADVANCED and EXPERIMENTAL and will trigger major changes in the nginx configuration of this server. Please DO NOT use it if you don't know what you are doing! In particular, you must be aware that fail2ban cannot be implemented on the proxied server (nftables cannot ban malicious traffic as all IP packets appear as coming from the front server). In addition, for now the proxied server's nginx configuration needs to manually tweaked to accept the `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "List of forwarding", + "global_settings_setting_tls_passthrough_list_help": "Should be a list of DOMAIN;DESTINATION;PORT, such as domain.tld;192.168.1.42;443 or domain.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "TLS-passthrough / SNI-based forwarding", + "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. CIDR notation is allowed.", + "global_settings_setting_webadmin_name": "Webadmin", + "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).", + "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).", + "group_already_exist": "Group {group} already exists", + "group_already_exist_on_system": "Group {group} already exists in the system groups", + "group_already_exist_on_system_but_removing_it": "Group {group} already exists in the system groups, but YunoHost will remove it…", + "group_cannot_be_deleted": "The group {group} cannot be deleted manually.", + "group_cannot_edit_all_users": "The group 'all_users' cannot be edited manually. It is a special group meant to contain all users registered in YunoHost", + "group_cannot_edit_primary_group": "The group '{group}' cannot be edited manually. It is the primary group meant to contain only one specific user.", + "group_cannot_edit_visitors": "The group 'visitors' cannot be edited manually. It is a special group representing anonymous visitors", + "group_cannot_remove_last_admin": "The user '{user}' is the last user in the group 'admins' and will not be removed from it.", + "group_created": "Group '{group}' created", + "group_creation_failed": "Could not create the group '{group}': {error}", + "group_deleted": "Group '{group}' deleted", + "group_deletion_failed": "Could not delete the group '{group}': {error}", + "group_mailalias_add": "The email alias '{mail}' will be added to the group '{group}'", + "group_mailalias_remove": "The email alias '{mail}' will be removed from the group '{group}'", + "group_no_change": "Nothing to change for group '{group}'", + "group_unknown": "The group '{group}' is unknown", + "group_update_aliases": "Updating aliases for group '{group}'", + "group_update_failed": "Could not update the group '{group}': {error}", + "group_updated": "Group '{group}' updated", + "group_user_add": "The user '{user}' will be added to the group '{group}'", + "group_user_already_in_group": "User {user} is already in group {group}", + "group_user_not_in_group": "User {user} is not in group {group}", + "group_user_remove": "The user '{user}' will be removed from the group '{group}'", + "hook_exec_failed": "Could not run script: {path}", + "hook_exec_not_terminated": "Script did not finish properly: {path}", + "hook_json_return_error": "Could not read return from hook {path}. Error: {msg}. Raw content: {raw_content}", + "hook_list_by_invalid": "This property can not be used to list hooks", + "hook_name_unknown": "Unknown hook name '{name}'", + "installation_complete": "Installation completed", + "invalid_credentials": "Invalid password or username", + "invalid_number": "Must be a number", + "invalid_password": "Invalid password", + "invalid_regex": "Invalid regex:'{regex}'", + "invalid_shell": "Invalid shell: {shell}", + "invalid_url": "Failed to connect to {url}… maybe the service is down, or you are not properly connected to the Internet in IPv4/IPv6.", + "ldap_attribute_already_exists": "LDAP attribute '{attribute}' already exists with value '{value}'", + "ldap_server_down": "Unable to reach LDAP server", + "ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it…", + "log_app_action_run": "Run action of the '{}' app", + "log_app_change_url": "Change the URL of the '{}' app", + "log_app_config_set": "Apply config to the '{}' app", + "log_app_install": "Install the '{}' app", + "log_app_makedefault": "Make '{}' the default app", + "log_app_remove": "Remove the '{}' app", + "log_app_upgrade": "Upgrade the '{}' app", + "log_available_on_yunopaste": "This log is now available via {url}", + "log_backup_create": "Create a backup archive", + "log_backup_restore_app": "Restore '{}' from a backup archive", + "log_backup_restore_system": "Restore system from a backup archive", + "log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'", + "log_diagnosis_run": "Run diagnosis", + "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs", + "log_domain_add": "Add domain '{}'", + "log_domain_config_set": "Update configuration for domain '{}'", + "log_domain_dns_push": "Push DNS records for domain '{}'", + "log_domain_main_domain": "Make '{}' the main domain", + "log_domain_remove": "Remove domain '{}'", + "log_dyndns_subscribe": "Register YunoHost subdomain '{}'", + "log_dyndns_unsubscribe": "Unregister YunoHost subdomain '{}'", + "log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'", + "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", + "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Install a Let's Encrypt certificate on '{}' domain", + "log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate", + "log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help", + "log_link_to_log": "Full log of this operation: '{desc}'", + "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", + "log_regen_conf": "Regenerate system configurations '{}'", + "log_remove_on_failed_install": "Remove '{}' after a failed installation", + "log_resource_snippet": "Provisioning/deprovisioning/updating a resource", + "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain", + "log_settings_reset": "Reset setting", + "log_settings_reset_all": "Reset all settings", + "log_settings_set": "Apply settings", + "log_tools_migrations_migrate_forward": "Run migrations", + "log_tools_postinstall": "Postinstall your YunoHost server", + "log_tools_reboot": "Reboot your server", + "log_tools_shutdown": "Shutdown your server", + "log_tools_update": "Fetching available system updates and refreshing app catalog", + "log_tools_upgrade": "Upgrade system packages", + "log_user_create": "Add '{}' user", + "log_user_delete": "Delete '{}' user", + "log_user_group_create": "Create '{}' group", + "log_user_group_delete": "Delete '{}' group", + "log_user_group_update": "Update '{}' group", + "log_user_import": "Import users", + "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 address '{mail}' already exists", + "mail_domain_unknown": "Invalid e-mail address for domain '{domain}'. Please, use a domain administrated by this server.", + "mail_edit_operation_unauthorized": "You are not authorized to do this change for your account.", + "mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail}'", + "mail_unavailable": "This e-mail address is reserved for the admins group", + "mailbox_disabled": "E-mail turned off for user {user}", + "mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space", + "main_domain_change_failed": "Unable to change the main domain", + "main_domain_changed": "The main domain has been changed", + "migration_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": "Finally, 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 backups** of any critical data or app. More info on https://doc.yunohost.org/backup;\n - **Be patient** after launching the migration: depending on your Internet connection and hardware, it might take up to an hour for everything to upgrade properly;\n - **Reach the community** on the forum if you need help troubleshooting issues.", + "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_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 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_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_not_enough_space": "Make sufficient space available in {path} to run the migration.", + "migration_postgresql_previous_not_installed": "PostgreSQL was not installed on your system. Nothing to do.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 is installed, but not PostgreSQL 15!? Something weird might have happened on your system :(…", + "migration_python_venv_rebuild_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_python_venv_rebuild_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_python_venv_rebuild_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_python_venv_rebuild_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}", + "migration_python_venv_rebuild_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_python_venv_rebuild_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`", + "migration_0031_terms_of_services": "This migration is purely an informational message about the fact that the YunoHost project now publishes Terms of Services related to the technical and community services.", + "migration_0036_cleaning_up": "Cleaning up cache and packages not useful anymore…", + "migration_0036_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_0036_general_warning": "Finally, 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 backups** of any critical data or app. More info on https://doc.yunohost.org/backup;\n - **Be patient** after launching the migration: depending on your Internet connection and hardware, it might take up to an hour for everything to upgrade properly;\n - **Reach the community** on the forum if you need help troubleshooting issues.", + "migration_0036_main_upgrade": "Starting main upgrade…", + "migration_0036_apt_lists_file_still_exists": "The legacy file '{file}' still exists while it shouldn't. It will be renamed as '{file}.legacy_bookworm'.", + "migration_0036_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade:", + "migration_0036_not_bullseye": "The current Debian distribution is not Bookworm! If you already ran the Bookworm -> Trixie 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_0036_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.", + "migration_0036_patch_yunohost_dpkg": "Applying patch on dpkg database to workaround conflict issues…", + "migration_0036_patching_sources_list": "Patching the sources.lists file…", + "migration_0036_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:", + "migration_0036_start": "Starting migration to Trixie…", + "migration_0036_still_on_bookworm_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Bookworm.", + "migration_0036_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Trixie.", + "migration_0036_yunohost_upgrade": "Starting YunoHost core upgrade…", + "migration_0037_upgrade_dkim_keys_disclaimer": "Running this migration upgrade legacy 1024 bits DKIM keys into 2048 bits in order to improve mail deliverability. Following domains are concerned and some of them could need to update DKIM keys in your DNS zone just after the migration: {domains}\nIMPORTANT: In order to avoid potential blacklisting and delivery issue, this migration must be run at a time when your server is not sending email. For safety, you can stop postfix service before the migration and restart it 1 hour after your DKIM keys has been updated in your DNS zones.", + "migration_0037_upgrade_dkim_keys_pending_mails": "{pending_mails} mails are in your mail queue. In order to avoid potential blacklisting and delivery issue, this migration must be run at a time when your server is not sending email. You can stop temporarily postfix and use postsuper -d ALL to free your mail queue. To see which emails are in queue use postqueue -p", + "migration_0037_upgrade_dkim_keys_failed": "Unable to generate a new 2048 bits key for {domains}.", + "migration_0037_upgrade_dkim_keys_manual_action": "In order to finish the migration process, you have to update DKIM public keys in these DNS zones: {domains}\nRun the diagnosis or use the DNS tab in 'Webadmin > Domains' or with yunohost domain dns suggest DOMAIN. If you have decided to stop postfix, don't miss to restart it 1 hour after editing your last DNS zone.", + "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_description_0031_terms_of_services": "Terms of services", + "migration_description_0032_firewall_config": "Internal firewall config file migration", + "migration_description_0033_rework_permission_infos": "Rework the way app permissions are stored", + "migration_description_0034_fix_missing_admins_aliases": "Fix missing mail aliases for the admins group", + "migration_description_0035_fix_apps_nodejs_version": "Fix nodejs versions in app systemd configurations", + "migration_description_0036_migrate_to_trixie": "Upgrade the system to Debian Trixie and YunoHost 13", + "migration_description_0037_upgrade_dkim_keys": "Upgrade DKIM keys to improve mail deliverability", + "migration_ldap_backup_before_migration": "Creating a backup of LDAP database and apps settings prior to the actual migration.", + "migration_ldap_can_not_backup_before_migration": "The backup of the system could not be completed before the migration failed. Error: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Could not migrate… trying to roll back the system.", + "migration_ldap_rollback_success": "System rolled back.", + "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_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'.", + "migrations_no_migrations_to_run": "No migrations to run", + "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_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`.", + "nftables_unavailable": "You cannot play with nftables here. You are either in a container or your kernel does not support it", + "noninteractive_task": "Non-interactive task", + "not_enough_disk_space": "Not enough free space on '{path}'", + "operation_interrupted": "The operation was manually interrupted?", + "other_available_options": "… and {n} other available options not shown", + "password_confirmation_not_the_same": "The password and its confirmation do not match", + "password_listed": "This password is among the most used passwords in the world. Please choose something more unique.", + "password_too_long": "Please choose a password shorter than 127 characters", + "password_too_simple_1": "The password needs to be at least 8 characters long", + "password_too_simple_2": "The password needs to be at least 8 characters long and contain a digit, upper and lower characters", + "password_too_simple_3": "The password needs to be at least 8 characters long and contain a digit, upper, lower and special characters", + "password_too_simple_4": "The password needs to be at least 12 characters long and contain a digit, upper, lower and special characters", + "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, alphanumeric and -_. characters only", + "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", + "pattern_email": "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_fullname": "Must be a valid full 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}", + "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", + "pattern_username": "Must be lower-case alphanumeric, dot, dash and underscore characters only", + "permission_already_allowed": "Group '{group}' already has permission '{permission}' enabled", + "permission_already_disallowed": "Group '{group}' already has permission '{permission}' disabled", + "permission_cannot_remove_main": "Removing a main permission is not allowed", + "permission_cant_add_to_all_users": "The permission {permission} can not be added to all users.", + "permission_created": "Permission '{permission}' created", + "permission_creation_failed": "Could not create permission '{permission}': {error}", + "permission_currently_allowed_for_all_users": "This permission is currently granted to all users in addition to other groups. You probably want to either remove the 'all_users' permission or remove the other groups it is currently granted to.", + "permission_deleted": "Permission '{permission}' deleted", + "permission_deletion_failed": "Could not delete permission '{permission}': {error}", + "permission_not_found": "Permission '{permission}' not found", + "permission_protected": "Permission {permission} is protected. You cannot add or remove the visitors group to/from this permission.", + "permission_require_account": "Permission {permission} only makes sense for users having an account, and therefore cannot be enabled for visitors.", + "permission_update_failed": "Could not update permission '{permission}': {error}", + "permission_updated": "Permission '{permission}' updated", + "port_already_closed": "Port {port} is already closed", + "port_already_opened": "Port {port} is already opened", + "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}'", + "regenconf_file_copy_failed": "Could not copy the new configuration file '{new}' to '{conf}'", + "regenconf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by regen-conf (category {category}) but was kept back.", + "regenconf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated", + "regenconf_file_manually_removed": "The configuration file '{conf}' was removed manually, and will not be created", + "regenconf_file_remove_failed": "Could not remove the configuration file '{conf}'", + "regenconf_file_removed": "Configuration file '{conf}' removed", + "regenconf_file_updated": "Configuration file '{conf}' updated", + "regenconf_need_to_explicitly_specify_ssh": "The ssh configuration has been manually modified, but you need to explicitly specify category 'ssh' with --force to actually apply the changes.", + "regenconf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost (category {category}).", + "regenconf_pending_applying": "Applying pending configuration for category '{category}'…", + "regenconf_up_to_date": "The configuration is already up-to-date for category '{category}'", + "regenconf_updated": "Configuration updated for '{category}'", + "regenconf_would_be_updated": "The configuration would have been updated for category '{category}'", + "regex_incompatible_with_tile": "/!\\ Packagers! Permission '{permission}' has show_tile set to 'true' and you therefore cannot define a regex URL as the main URL", + "regex_with_only_domain": "You can't use a regex for domain, only for path", + "registrar_infos": "Registrar infos", + "restore_already_installed_app": "An app with the ID '{app}' is already installed", + "restore_already_installed_apps": "The following apps can't be restored because they are already installed: {apps}", + "restore_backup_too_old": "This backup archive can not be restored because it comes from a too-old YunoHost version.", + "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_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_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!", + "server_reboot": "The server will reboot", + "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers}]", + "server_shutdown": "The server will shut down", + "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers}]", + "service_add_failed": "Could not add the service '{service}'", + "service_added": "The service '{service}' was added", + "service_already_started": "The service '{service}' is running already", + "service_already_stopped": "The service '{service}' has already been stopped", + "service_cmd_exec_failed": "Could not execute the command '{command}'", + "service_description_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_mysql": "Stores app data (SQL database)", + "service_description_nftables": "Manages open and close connection ports to services", + "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_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-portal-api": "Manages interactions between the different 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.", + "service_disabled": "The service '{service}' will not be started anymore when system boots.", + "service_enable_failed": "Could not make the service '{service}' automatically start at boot.", + "service_enabled": "The service '{service}' will now be automatically started during system boots.", + "service_not_reloading_because_conf_broken": "Not reloading/restarting service '{name}' because its configuration is broken: {errors}", + "service_reload_failed": "Could not reload the service '{service}'", + "service_reload_or_restart_failed": "Could not reload or restart the service '{service}'", + "service_reloaded": "Service '{service}' reloaded", + "service_reloaded_or_restarted": "The service '{service}' was reloaded or restarted", + "service_remove_failed": "Could not remove the service '{service}'", + "service_removed": "Service '{service}' removed", + "service_restart_failed": "Could not restart the service '{service}'", + "service_restarted": "Service '{service}' restarted", + "service_start_failed": "Could not start the service '{service}'", + "service_started": "Service '{service}' started", + "service_stop_failed": "Unable to stop the service '{service}'", + "service_stopped": "Service '{service}' stopped", + "service_unknown": "Unknown service '{service}'", + "session_expired": "Session expired", + "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": "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`.", + "tools_upgrade": "Upgrading system packages", + "tools_upgrade_failed": "Could not upgrade packages: {packages_list}", + "tos_dyndns_acknowledgement": "You chose to register a DynDNS domain which is a service provided by the YunoHost project. Considering that domain names are an important aspect of long-term digital services, we remind you to read carefully the corresponding Terms of Services, in particular the section regarding those free domain names: .", + "tos_postinstall_acknowledgement": "The YunoHost project is a team of volunteers who have made common cause to create a free operating system for servers, called YunoHost. The YunoHost software is published under the AGPLv3 license (). In connection with this software, the project administers and makes available several technical and community services for various purposes. By using these services, you agree to be bound by the following Terms of Services: .", + "unable_authenticate": "Failed to authenticate session", + "unbackup_app": "{app} will not be saved", + "unexpected_error": "Something unexpected went wrong: {error}", + "unknown_error_reading_file": "Unknown error while trying to read file {file}: {error}", + "unknown_group": "Unknown system group '{group}'", + "unknown_main_domain_path": "Unknown domain or path for '{app}'. You need to specify a domain and a path to be able to specify a URL for permission.", + "unknown_user": "Unknown system user '{user}'", + "unlimit": "No quota", + "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…", + "upgrading_packages": "Upgrading packages…", + "upnp_dev_not_found": "No UPnP device found", + "upnp_disabled": "UPnP turned off", + "upnp_enabled": "UPnP turned on", + "upnp_port_open_failed": "Could not open port via UPnP", + "user_already_exists": "The user '{user}' already exists", + "user_cannot_delete_last_admin": "The user '{user}' is the last user in the group 'admins' and will not be deleted.", + "user_created": "User created", + "user_creation_failed": "Could not create user {user}: {error}", + "user_deleted": "User deleted", + "user_deletion_failed": "Could not delete user {user}: {error}", + "user_home_creation_failed": "Could not create home folder '{home}' for user", + "user_import_bad_file": "Your CSV file is not correctly formatted it will be ignored to avoid potential data loss", + "user_import_bad_line": "Incorrect line {line}: {details}", + "user_import_cannot_edit_or_delete_admins": "Unable to edit or delete '{user}' via import because user is admin", + "user_import_failed": "The users import operation completely failed", + "user_import_missing_columns": "The following columns are missing: {columns}", + "user_import_nothing_to_do": "No user needs to be imported", + "user_import_partial_failed": "The users import operation partially failed", + "user_import_success": "Users successfully imported", + "user_unknown": "Unknown user: {user}", + "user_update_failed": "Could not update user {user}: {error}", + "user_updated": "User info changed", + "visitors": "Visitors", + "yunohost_already_installed": "YunoHost is already installed", + "yunohost_api": "YunoHost API", + "yunohost_configured": "YunoHost is now configured", + "yunohost_installing": "Installing YunoHost…", + "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", + "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - 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://doc.yunohost.org/admin." +} diff --git a/locales/eo.json b/locales/eo.json new file mode 100644 index 0000000..8a5cf27 --- /dev/null +++ b/locales/eo.json @@ -0,0 +1,501 @@ +{ + "aborting": "Aborti.", + "action_invalid": "Nevalida ago « {action} »", + "additional_urls_already_added": "Plia URL '{url}' jam aldonita en la aldona URL por permeso '{permission}'", + "additional_urls_already_removed": "Plia URL '{url}' jam forigita en la aldona URL por permeso '{permission}'", + "admin_password": "Pasvorto de la estro", + "already_up_to_date": "Nenio por fari. Ĉio estas jam ĝisdatigita.", + "app_action_broke_system": "Ĉi tiu ago ŝajne rompis ĉi tiujn gravajn servojn: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Ĉi tiuj postulataj servoj devas funkcii por funkciigi ĉi tiun agon: {services}. Provu rekomenci ilin por daŭrigi (kaj eble esploru, kial ili malsupreniras).", + "app_already_installed": "{app} estas jam instalita", + "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_argument_choice_invalid": "Uzu unu el ĉi tiuj elektoj '{choices}' por la argumento '{name}' anstataŭ '{value}'", + "app_argument_invalid": "Elektu validan valoron por la argumento '{name}': {error}", + "app_change_url_identical_domains": "Malnovaj kaj novaj domajno/URL estas la sama ('{domain}{path}'), nenio fareblas.", + "app_change_url_no_script": "La app '{app_name}' ankoraŭ ne subtenas URL-modifon. Eble vi devus altgradigi ĝin.", + "app_change_url_success": "{app} URL nun estas {domain} {path}", + "app_extraction_failed": "Ne povis ĉerpi la instalajn dosierojn", + "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.", + "app_id_invalid": "Nevalida apo ID", + "app_install_failed": "Ne povis instali {app}: {error}", + "app_install_files_invalid": "Ĉi tiuj dosieroj ne povas esti instalitaj", + "app_install_script_failed": "Eraro okazis en la skripto de instalado de la app", + "app_location_unavailable": "Ĉi tiu URL aŭ ne haveblas, aŭ konfliktas kun la jam instalita (j) apliko (j):\n{apps}", + "app_make_default_location_already_used": "Ne povis fari '{app}' la defaŭltan programon sur la domajno, '{domain}' estas jam uzata de '{other_app}'", + "app_manifest_install_ask_admin": "Elektu administran uzanton por ĉi tiu programo", + "app_manifest_install_ask_domain": "Elektu la domajnon, kie ĉi tiu programo devas esti instalita", + "app_manifest_install_ask_is_public": "Ĉu ĉi tiu programo devas esti eksponita al anonimaj vizitantoj?", + "app_manifest_install_ask_password": "Elektu administradan pasvorton por ĉi tiu programo", + "app_manifest_install_ask_path": "Elektu la vojon, kie ĉi tiu programo devas esti instalita", + "app_not_correctly_installed": "{app} ŝajnas esti malĝuste instalita", + "app_not_installed": "Ne povis trovi {app} en la listo de instalitaj programoj: {all_apps}", + "app_not_properly_removed": "{app} ne estis ĝuste forigita", + "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_remove_after_failed_install": "Forigado de la programo post la instalado-fiasko…", + "app_removed": "{app} forigita", + "app_requirements_checking": "Kontrolante bezonatajn pakaĵojn por {app}…", + "app_restore_failed": "Ne povis restarigi la programon '{app}': {error}", + "app_restore_script_failed": "Eraro okazis ene de la App Restarigu Skripton", + "app_sources_fetch_failed": "Ne povis akiri fontajn dosierojn, ĉu la URL estas ĝusta?", + "app_start_backup": "Kolekti dosierojn por esti subtenata por {app}…", + "app_start_install": "Instali {app}…", + "app_start_remove": "Forigado {app}…", + "app_start_restore": "Restarigi {app}…", + "app_unknown": "Nekonata apliko", + "app_unsupported_remote_type": "Malkontrolita fora speco uzita por la apliko", + "app_upgrade_app_name": "Nun ĝisdatigu {app}…", + "app_upgrade_failed": "Ne povis ĝisdatigi {app}: {error}", + "app_upgrade_script_failed": "Eraro okazis en la skripto pri ĝisdatiga programo", + "app_upgrade_several_apps": "La sekvaj apliko estos altgradigitaj: {apps}", + "app_upgrade_some_app_failed": "Iuj aplikoj ne povis esti altgradigitaj", + "app_upgraded": "{app} altgradigita", + "apps_already_up_to_date": "Ĉiuj aplikoj estas jam ĝisdatigitaj", + "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!", + "apps_catalog_updating": "Ĝisdatigante katalogo de aplikoj…", + "ask_main_domain": "Ĉefa domajno", + "ask_new_admin_password": "Nova administrada pasvorto", + "ask_new_domain": "Nova domajno", + "ask_new_path": "Nova vojo", + "ask_password": "Pasvorto", + "ask_user_domain": "Domajno uzi por la retpoŝta adreso de la uzanto kaj XMPP-konto", + "backup_abstract_method": "Ĉi tiu rezerva metodo ankoraŭ efektiviĝis", + "backup_actually_backuping": "Krei rezervan arkivon de la kolektitaj dosieroj…", + "backup_applying_method_copy": "Kopii ĉiujn dosierojn por sekurigi…", + "backup_applying_method_custom": "Voki la laŭmendan rezervan metodon '{method}'…", + "backup_applying_method_tar": "Krei la rezervon TAR Arkivo…", + "backup_archive_app_not_found": "Ne povis trovi {app} en la rezerva arkivo", + "backup_archive_broken_link": "Ne povis aliri la rezervan ar archiveivon (rompita ligilo al {path})", + "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_corrupted": "I aspektas kiel la rezerva arkivo '{archive}' estas koruptita: {error}", + "backup_archive_name_exists": "Rezerva arkivo kun ĉi tiu nomo jam ekzistas.", + "backup_archive_name_unknown": "Nekonata loka rezerva ar archiveivo nomata '{name}'", + "backup_archive_open_failed": "Ne povis malfermi la rezervan ar archiveivon", + "backup_archive_system_part_not_available": "Sistemo parto '{part}' ne haveblas en ĉi tiu rezervo", + "backup_archive_writing_error": "Ne povis aldoni la dosierojn '{source}' (nomitaj en la ar theivo '{dest}') por esti rezervitaj en la kunpremita arkivo '{archive}'", + "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_cant_mount_uncompress_archive": "Ne povis munti la nekompresitan ar archiveivon kiel protektita kontraŭ skribo", + "backup_cleaning_failed": "Ne povis purigi la provizoran rezervan dosierujon", + "backup_copying_to_organize_the_archive": "Kopiante {size} MB por organizi la ar archiveivon", + "backup_couldnt_bind": "Ne povis ligi {src} al {dest}.", + "backup_created": "Sekurkopio kreita: {name}", + "backup_creation_failed": "Ne povis krei la rezervan ar archiveivon", + "backup_csv_addition_failed": "Ne povis aldoni dosierojn al sekurkopio en la CSV-dosiero", + "backup_csv_creation_failed": "Ne povis krei la CSV-dosieron bezonatan por restarigo", + "backup_custom_backup_error": "Propra rezerva metodo ne povis preterpasi la paŝon \"sekurkopio\"", + "backup_custom_mount_error": "Propra rezerva metodo ne povis preterpasi la paŝon 'monto'", + "backup_delete_error": "Ne povis forigi '{path}'", + "backup_deleted": "Rezerva forigita: {name}", + "backup_hook_unknown": "La rezerva hoko '{hook}' estas nekonata", + "backup_method_copy_finished": "Rezerva kopio finis", + "backup_method_custom_finished": "Propra rezerva metodo '{method}' finiĝis", + "backup_method_tar_finished": "TAR-rezerva ar archiveivo kreita", + "backup_mount_archive_for_restore": "Preparante arkivon por restarigo…", + "backup_no_uncompress_archive_dir": "Ne ekzistas tia nekompremita arkiva dosierujo", + "backup_output_directory_forbidden": "Elektu malsaman elirejan dosierujon. Sekurkopioj ne povas esti kreitaj en sub-dosierujoj /bin, /boot, /dev, /ktp, /lib, /root, /run, /sbin, /sys, /usr, /var aŭ /home/yunohost.backup/archives", + "backup_output_directory_not_empty": "Vi devas elekti malplenan eligitan dosierujon", + "backup_output_directory_required": "Vi devas provizi elirejan dosierujon por la sekurkopio", + "backup_output_symlink_dir_broken": "Via arkiva dosierujo '{path}' estas rompita ligilo. Eble vi forgesis restarigi aŭ munti aŭ enŝovi la stokadon, al kiu ĝi notas.", + "backup_running_hooks": "Kurado de apogaj hokoj…", + "backup_system_part_failed": "Ne eblis sekurkopi la sistemon de '{part}'", + "backup_unable_to_organize_files": "Ne povis uzi la rapidan metodon por organizi dosierojn en la ar archiveivo", + "backup_with_no_backup_script_for_app": "La app '{app}' ne havas sekretan skripton. Ignorante.", + "backup_with_no_restore_script_for_app": "La apliko \"{app}\" ne havas restarigan skripton, vi ne povos aŭtomate restarigi la sekurkopion de ĉi tiu apliko.", + "cannot_open_file": "Ne povis malfermi dosieron {file} (kialo: {error})", + "cannot_write_file": "Ne povis skribi dosieron {file} (kialo: {error})", + "certmanager_acme_not_configured_for_domain": "Atestilo por la domajno '{domain}' ne ŝajnas esti ĝuste instalita. Bonvolu ekzekuti 'cert-instali' por ĉi tiu regado unue.", + "certmanager_attempt_to_renew_nonLE_cert": "La atestilo por la domajno '{domain}' ne estas elsendita de Let's Encrypt. Ne eblas renovigi ĝin aŭtomate!", + "certmanager_attempt_to_renew_valid_cert": "La atestilo por la domajno '{domain}' ne finiĝos! (Vi eble uzos --force se vi scias kion vi faras)", + "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain}! (Uzu --forte pretervidi)", + "certmanager_cannot_read_cert": "Io malbona okazis, kiam mi provis malfermi aktualan atestilon por domajno {domain} (dosiero: {file}), kialo: {reason}", + "certmanager_cert_install_success": "Ni Ĉifru atestilon nun instalitan por la domajno '{domain}'", + "certmanager_cert_install_success_selfsigned": "Mem-subskribita atestilo nun instalita por la domajno '{domain}'", + "certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain}'", + "certmanager_cert_signing_failed": "Ne povis subskribi la novan atestilon", + "certmanager_certificate_fetching_or_enabling_failed": "Provante uzi la novan atestilon por {domain} ne funkciis…", + "certmanager_domain_cert_not_selfsigned": "La atestilo por domajno {domain} ne estas mem-subskribita. Ĉu vi certas, ke vi volas anstataŭigi ĝin? (Uzu '--force' por fari tion.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "La DNS 'A' rekordo por la domajno '{domain}' diferencas de la IP de ĉi tiu servilo. Se vi lastatempe modifis vian A-registron, bonvolu atendi ĝin propagandi (iuj DNS-disvastigaj kontroliloj estas disponeblaj interrete). (Se vi scias, kion vi faras, uzu '--no-checks' por malŝalti tiujn ĉekojn.)", + "certmanager_domain_http_not_working": "Ŝajnas ke la domajno {domain} ne atingeblas per HTTP. Kontrolu, ke via DNS kaj NGINX-agordo ĝustas", + "certmanager_hit_rate_limit": "Tro multaj atestiloj jam eldonitaj por ĉi tiu ĝusta aro de domajnoj {domain} antaŭ nelonge. Bonvolu reprovi poste. Vidu https://letsencrypt.org/docs/rate-limits/ por pliaj detaloj", + "certmanager_no_cert_file": "Ne povis legi la atestan dosieron por la domajno {domain} (dosiero: {file})", + "certmanager_self_ca_conf_file_not_found": "Ne povis trovi agorddosieron por mem-subskriba aŭtoritato (dosiero: {file})", + "certmanager_unable_to_parse_self_CA_name": "Ne povis trapasi nomon de mem-subskribinta aŭtoritato (dosiero: {file})", + "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_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_warning": "Averto: Ĉi tiu aplikaĵo povas funkcii, sed ne bone integras en YunoHost. Iuj funkcioj kiel ekzemple aliĝilo kaj sekurkopio / restarigo eble ne haveblos. Instali ĉiuokaze? [{answers}] ", + "corrupted_json": "Koruptita JSON legis de {ressource} (Kialo: {error})", + "corrupted_toml": "Korupta TOML legita el {ressource} (kialo: {error})", + "corrupted_yaml": "Korupta YAML legita de {ressource} (kialo: {error})", + "diagnosis_basesystem_hardware": "Arkitekturo de servila aparataro estas {virt} {arch}", + "diagnosis_basesystem_host": "Servilo funkcias Debian {debian_version}", + "diagnosis_basesystem_kernel": "Servilo funkcias Linuksan kernon {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Vi prizorgas malkonsekvencajn versiojn de la YunoHost-pakoj… plej probable pro malsukcesa aŭ parta ĝisdatigo.", + "diagnosis_basesystem_ynh_main_version": "Servilo funkcias YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} versio: {version} ({repo})", + "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_description_basesystem": "Baza sistemo", + "diagnosis_description_dnsrecords": "Registroj DNS", + "diagnosis_description_ip": "Interreta konektebleco", + "diagnosis_description_mail": "Retpoŝto", + "diagnosis_description_ports": "Ekspoziciaj havenoj", + "diagnosis_description_regenconf": "Sistemaj agordoj", + "diagnosis_description_services": "Servo kontrolas staton", + "diagnosis_description_systemresources": "Rimedaj sistemoj", + "diagnosis_description_web": "Reta", + "diagnosis_diskusage_low": "Stokado {mountpoint} (sur aparato {device}) nur restas {free} ({free_percent}%) spaco restanta (el {total}). Estu zorgema.", + "diagnosis_diskusage_ok": "Stokado {mountpoint} (sur aparato {device}) ankoraŭ restas {free} ({free_percent}%) spaco (el {total})!", + "diagnosis_diskusage_verylow": "Stokado {mountpoint} (sur aparato {device} ) nur restas {free} ({free_percent}%) spaco restanta (el {total}). Vi vere konsideru purigi iom da spaco !", + "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_dns_bad_conf": "Iuj DNS-registroj mankas aŭ malĝustas por domajno {domain} (kategorio {category})", + "diagnosis_dns_discrepancy": "La DNS-registro kun tipo {type} kaj nomo {name} ne kongruas kun la rekomendita agordo:
Nuna valoro: {current}
Esceptita valoro: {content}", + "diagnosis_dns_good_conf": "DNS-registroj estas ĝuste agorditaj por domajno {domain} (kategorio {category})", + "diagnosis_dns_missing_record": "Laŭ la rekomendita DNS-agordo, vi devas aldoni DNS-registron kun.
Tipo: {type}
Nomo: {name}
Valoro: {content}", + "diagnosis_dns_point_to_doc": "Bonvolu kontroli la dokumentaron ĉe https://doc.yunohost.org/dns_config se vi bezonas helpon pri agordo de DNS-registroj.", + "diagnosis_everything_ok": "Ĉio aspektas bone por {category}!", + "diagnosis_failed": "Malsukcesis preni la diagnozan rezulton por kategorio '{category}': {error}", + "diagnosis_failed_for_category": "Diagnozo malsukcesis por kategorio '{category}': {error}", + "diagnosis_found_errors": "Trovis {errors} signifa(j) afero(j) rilata al {category}!", + "diagnosis_found_errors_and_warnings": "Trovis {errors} signifaj problemo (j) (kaj {warnings} averto) rilataj al {category}!", + "diagnosis_found_warnings": "Trovitaj {warnings} ero (j) kiuj povus esti plibonigitaj por {category}.", + "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.", + "diagnosis_http_connection_error": "Rilata eraro: ne povis konektiĝi al la petita domajno, tre probable ĝi estas neatingebla.", + "diagnosis_http_could_not_diagnose": "Ne povis diagnozi, ĉu atingeblas domajno de ekstere.", + "diagnosis_http_could_not_diagnose_details": "Eraro: {error}", + "diagnosis_http_hairpinning_issue": "Via loka reto ŝajne ne havas haŭtadon.", + "diagnosis_http_hairpinning_issue_details": "Ĉi tio probable estas pro via ISP-skatolo / enkursigilo. Rezulte, homoj de ekster via loka reto povos aliri vian servilon kiel atendite, sed ne homoj de interne de la loka reto (kiel vi, probable?) Kiam uzas la domajnan nomon aŭ tutmondan IP. Eble vi povas plibonigi la situacion per rigardado al https://doc.yunohost.org/dns_local_network", + "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.", + "diagnosis_http_ok": "Domajno {domain} atingebla per HTTP de ekster la loka reto.", + "diagnosis_http_partially_unreachable": "Domajno {domain} ŝajnas neatingebla per HTTP de ekster la loka reto en IPv {failed}, kvankam ĝi funkcias en IPv {passed}.", + "diagnosis_http_timeout": "Tempolimigita dum provado kontakti vian servilon de ekstere. Ĝi ŝajnas esti neatingebla.
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. Vi ankaŭ devas certigi, ke la servo nginx funkcias
3. Pri pli kompleksaj agordoj: certigu, ke neniu fajroŝirmilo aŭ reverso-prokuro ne interbatalas.", + "diagnosis_http_unreachable": "Domajno {domain} ŝajnas neatingebla per HTTP de ekster la loka reto.", + "diagnosis_ignored_issues": "(+ {nb_ignored} ignorataj aferoj))", + "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_ip_connected_ipv4": "La servilo estas konektita al la interreto per IPv4 !", + "diagnosis_ip_connected_ipv6": "La servilo estas konektita al la interreto per IPv6 !", + "diagnosis_ip_dnsresolution_working": "Rezolucio pri domajna nomo funkcias !", + "diagnosis_ip_global": "Tutmonda IP: {global} ", + "diagnosis_ip_local": "Loka IP: {local} ", + "diagnosis_ip_no_ipv4": "La servilo ne havas funkciantan IPv4.", + "diagnosis_ip_no_ipv6": "La servilo ne havas funkciantan IPv6.", + "diagnosis_ip_not_connected_at_all": "La servilo tute ne ŝajnas esti konektita al la Interreto !?", + "diagnosis_ip_weird_resolvconf": "DNS-rezolucio ŝajnas funkcii, sed ŝajnas ke vi uzas kutiman /etc/resolv.conf .", + "diagnosis_ip_weird_resolvconf_details": "La dosiero /etc/resolv.conf devas esti ligilo al /etc/resolvconf/run/resolv.conf indikante 127.0.0.1 (dnsmasq). Se vi volas permane agordi DNS-solvilojn, bonvolu redakti /etc/resolv.dnsmasq.conf .", + "diagnosis_mail_blocklist_listed_by": "Via IP aŭ domajno {item} estas listigita en {blocklist_name}", + "diagnosis_mail_blocklist_ok": "La IP kaj domajnoj uzataj de ĉi tiu servilo ne ŝajnas esti listigitaj nigre", + "diagnosis_mail_blocklist_reason": "La negra listo estas: {reason}", + "diagnosis_mail_blocklist_website": "Post identigi kial vi listigas kaj riparis ĝin, bonvolu peti forigi vian IP aŭ domenion sur {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Ne-SMTP-servo respondita sur la haveno 25 sur IPv {ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Povas esti ke alia maŝino respondas anstataŭ via servilo.", + "diagnosis_mail_ehlo_could_not_diagnose": "Ne povis diagnozi ĉu postfiksa poŝta servilo atingebla de ekstere en IPv {ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Eraro: {error}", + "diagnosis_mail_ehlo_ok": "La SMTP-poŝta servilo atingeblas de ekstere kaj tial kapablas ricevi retpoŝtojn !", + "diagnosis_mail_ehlo_unreachable": "La SMTP-poŝta servilo estas neatingebla de ekstere sur IPv {ipversion}. Ĝi ne povos ricevi retpoŝtojn.", + "diagnosis_mail_ehlo_unreachable_details": "Ne povis malfermi rilaton sur la haveno 25 al via servilo en IPv {ipversion}. Ĝi ŝajnas esti neatingebla.
1. La plej ofta kaŭzo por ĉi tiu afero estas, ke la haveno 25 ne estas ĝuste sendita al via servilo .
2. Vi ankaŭ devas certigi, ke servo-prefikso funkcias.
3. Pri pli kompleksaj agordoj: certigu, ke neniu fajroŝirmilo aŭ reverso-prokuro ne interbatalas.", + "diagnosis_mail_ehlo_wrong": "Malsama SMTP-poŝta servilo respondas pri IPv {ipversion}. Via servilo probable ne povos ricevi retpoŝtojn.", + "diagnosis_mail_ehlo_wrong_details": "La EHLO ricevita de la fora diagnozilo en IPv {ipversion} diferencas de la domajno de via servilo.
Ricevita EHLO: {wrong_ehlo}
Atendita: {right_ehlo}
La plej ofta kaŭzo por ĉi tiu afero estas, ke la haveno 25 ne estas ĝuste sendita al via servilo . Alternative, certigu, ke neniu fajroŝirmilo aŭ reverso-prokuro ne interbatalas.", + "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_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_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://doc.yunohost.org/admin/get_started/providers/vpn/
- Finfine eblas ankaŭ ŝanĝo de provizanto", + "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_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_ok": "Via inversa DNS estas ĝuste agordita!", + "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_mail_outgoing_port_25_blocked_details": "Vi unue provu malŝlosi elirantan havenon 25 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_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://doc.yunohost.org/admin/get_started/providers/vpn/
- Vi ankaŭ povas konsideri ŝanĝi al pli neta neŭtraleco-amika provizanto", + "diagnosis_mail_outgoing_port_25_ok": "La SMTP-poŝta servilo kapablas sendi retpoŝtojn (eliranta haveno 25 ne estas blokita).", + "diagnosis_mail_queue_ok": "{nb_pending} pritraktataj retpoŝtoj en la retpoŝtaj vostoj", + "diagnosis_mail_queue_too_big": "Tro multaj pritraktataj retpoŝtoj en retpoŝto ({nb_pending} retpoŝtoj)", + "diagnosis_mail_queue_unavailable": "Ne povas konsulti multajn pritraktitajn retpoŝtojn en vosto", + "diagnosis_mail_queue_unavailable_details": "Eraro: {error}", + "diagnosis_never_ran_yet": "Ŝajnas, ke ĉi tiu servilo estis instalita antaŭ nelonge kaj estas neniu diagnoza raporto por montri. Vi devas komenci kurante plenan diagnozon, ĉu de la retadministro aŭ uzante 'yunohost diagnosis run' el la komandlinio.", + "diagnosis_no_cache": "Neniu diagnoza kaŝmemoro por kategorio '{category}'", + "diagnosis_ports_could_not_diagnose": "Ne povis diagnozi, ĉu haveblaj havenoj de ekstere.", + "diagnosis_ports_could_not_diagnose_details": "Eraro: {error}", + "diagnosis_ports_forwarding_tip": "Por solvi ĉi tiun problemon, vi plej verŝajne devas agordi la plusendon de haveno en via interreta enkursigilo kiel priskribite en https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Eksponi ĉi tiun havenon necesas por {category} funkcioj (servo {service})", + "diagnosis_ports_ok": "Haveno {port} atingeblas de ekstere.", + "diagnosis_ports_partially_unreachable": "Haveno {port} ne atingebla de ekstere en IPv {failed}.", + "diagnosis_ports_unreachable": "Haveno {port} ne atingeblas de ekstere.", + "diagnosis_ram_low": "La sistemo havas {available} ({available_percent}%) RAM forlasita de {total}. Estu zorgema.", + "diagnosis_ram_ok": "La sistemo ankoraŭ havas {available} ({available_percent}%) RAM forlasita de {total}.", + "diagnosis_ram_verylow": "La sistemo nur restas {available} ({available_percent}%) RAM! (el {total})", + "diagnosis_regenconf_allgood": "Ĉiuj agordaj dosieroj kongruas kun la rekomendita agordo!", + "diagnosis_regenconf_manually_modified": "Agordodosiero {file} ŝajnas esti permane modifita.", + "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_security_vulnerable_to_meltdown_details": "Por ripari tion, vi devas ĝisdatigi vian sistemon kaj rekomenci por ŝarĝi la novan linux-kernon (aŭ kontaktu vian servilan provizanton se ĉi tio ne funkcias). Vidu https://meltdownattack.com/ por pliaj informoj.", + "diagnosis_services_bad_status": "Servo {service} estas {status} :(", + "diagnosis_services_bad_status_tip": "Vi povas provi rekomenci la servon , kaj se ĝi ne funkcias, rigardu La servaj registroj en reteja (el la komandlinio, vi povas fari tion per yunohost service restart {service} kajyunohost service log {service}).", + "diagnosis_services_conf_broken": "Agordo estas rompita por servo {service} !", + "diagnosis_services_running": "Servo {service} funkcias!", + "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_swap_ok": "La sistemo havas {total} da interŝanĝoj!", + "diagnosis_unknown_categories": "La jenaj kategorioj estas nekonataj: {categories}", + "domain_cannot_remove_main": "Vi ne povas forigi '{domain}' ĉar ĝi estas la ĉefa domajno, vi bezonas unue agordi alian domajnon kiel la ĉefan domajnon per uzado de 'yunohost domain main-domain -n ', jen la listo de kandidataj domajnoj. : {other_domains}", + "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_cert_gen_failed": "Ne povis generi atestilon", + "domain_created": "Domajno kreita", + "domain_creation_failed": "Ne eblas krei domajnon {domain}: {error}", + "domain_deleted": "Domajno forigita", + "domain_deletion_failed": "Ne eblas forigi domajnon {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "Ĉi tiu komando montras al vi la *rekomenditan* agordon. Ĝi efektive ne agordas la DNS-agordon por vi. Via respondeco agordi vian DNS-zonon en via registristo laŭ ĉi tiu rekomendo.", + "domain_dyndns_already_subscribed": "Vi jam abonis DynDNS-domajnon", + "domain_exists": "La domajno jam ekzistas", + "domain_hostname_failed": "Ne povis agordi novan gastigilon. Ĉi tio eble kaŭzos problemon poste (eble bone).", + "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", + "domains_available": "Haveblaj domajnoj:", + "done": "Farita", + "download_bad_status_code": "{url} redonita statuskodo {code}", + "download_ssl_error": "SSL-eraro dum konekto al {url}", + "download_timeout": "{url} prenis tro da tempo por respondi, rezignis.", + "download_unknown_error": "Eraro dum elŝutado de datumoj de {url}: {error}", + "downloading": "Elŝutante…", + "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_lock_not_available": "Ĉi tiu komando ne povas funkcii nun ĉar alia programo uzas la seruron de dpkg (la administrilo de paka sistemo)", + "dyndns_could_not_check_available": "Ne povis kontroli ĉu {domain} haveblas sur {provider}.", + "dyndns_domain_not_provided": "Provizanto DynDNS {provider} ne povas provizi domajnon {domain}.", + "dyndns_ip_update_failed": "Ne povis ĝisdatigi IP-adreson al DynDNS", + "dyndns_ip_updated": "Ĝisdatigis vian IP sur DynDNS", + "dyndns_key_not_found": "DNS-ŝlosilo ne trovita por la domajno", + "dyndns_no_domain_registered": "Neniu domajno registrita ĉe DynDNS", + "dyndns_provider_unreachable": "Ne povas atingi la provizanton DynDNS {provider}: ĉu via YunoHost ne estas ĝuste konektita al la interreto aŭ la dyneta servilo malŝaltiĝas.", + "dyndns_unavailable": "La domajno '{domain}' ne haveblas.", + "error_changing_file_permissions": "Eraro dum ŝanĝo de permesoj por {path}: {error}", + "error_removing": "Eraro dum la forigo de {path}: {error}", + "error_writing_file": "Eraro skribinte dosieron {file}: {error}", + "extracting": "Eltirante…", + "field_invalid": "Nevalida kampo '{field}'", + "file_does_not_exist": "La dosiero {path} ne ekzistas.", + "file_not_exist": "Dosiero ne ekzistas: '{path}'", + "firewall_reload_failed": "Ne eblis reŝargi la firewall. Pliaj informoj en ensaluto.", + "firewall_reloaded": "Fajroŝirmilo reŝarĝis", + "global_settings_setting_admin_strength": "Admin pasvorta forto", + "global_settings_setting_nginx_compatibility_help": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_postfix_compatibility_help": "Kongruo vs sekureca kompromiso por la Postfix-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_smtp_allow_ipv6_help": "Permesu la uzon de IPv6 por ricevi kaj sendi poŝton", + "global_settings_setting_ssh_compatibility_help": "Kongruo vs sekureca kompromiso por la SSH-servilo. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", + "global_settings_setting_user_strength": "Uzanto pasvorta forto", + "good_practices_about_admin_password": "Vi nun estas por difini novan administran pasvorton. La pasvorto devas esti almenaŭ 8 signojn - kvankam estas bone praktiki uzi pli longan pasvorton (t.e. pasfrazon) kaj/aŭ uzi variaĵon de signoj (majuskloj, minuskloj, ciferoj kaj specialaj signoj).", + "good_practices_about_user_password": "Vi nun estas por difini novan uzantan pasvorton. La pasvorto devas esti almenaŭ 8 signojn - kvankam estas bone praktiki uzi pli longan pasvorton (t.e. pasfrazon) kaj/aŭ variaĵon de signoj (majuskloj, minuskloj, ciferoj kaj specialaj signoj).", + "group_already_exist": "Grupo {group} jam ekzistas", + "group_already_exist_on_system": "Grupo {group} jam ekzistas en la sistemaj grupoj", + "group_already_exist_on_system_but_removing_it": "Grupo {group} jam ekzistas en la sistemaj grupoj, sed YunoHost forigos ĝin…", + "group_cannot_be_deleted": "La grupo {group} ne povas esti forigita permane.", + "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_primary_group": "La grupo '{group}' ne povas esti redaktita permane. Ĝi estas la primara grupo celita enhavi nur unu specifan uzanton.", + "group_cannot_edit_visitors": "La grupo 'vizitantoj' ne povas esti redaktita permane. Ĝi estas speciala grupo reprezentanta anonimajn vizitantojn", + "group_created": "Grupo '{group}' kreita", + "group_creation_failed": "Ne povis krei la grupon '{group}': {error}", + "group_deleted": "Grupo '{group}' forigita", + "group_deletion_failed": "Ne povis forigi la grupon '{group}': {error}", + "group_unknown": "La grupo '{group}' estas nekonata", + "group_update_failed": "Ne povis ĝisdatigi la grupon '{group}': {error}", + "group_updated": "Ĝisdatigita \"{group}\" grupo", + "group_user_already_in_group": "Uzanto {user} jam estas en grupo {group}", + "group_user_not_in_group": "Uzanto {user} ne estas en grupo {group}", + "hook_exec_failed": "Ne povis funkcii skripto: {path}", + "hook_exec_not_terminated": "Skripto ne finiĝis ĝuste: {path}", + "hook_json_return_error": "Ne povis legi revenon de hoko {path}. Eraro: {msg}. Kruda enhavo: {raw_content}", + "hook_list_by_invalid": "Ĉi tiu posedaĵo ne povas esti uzata por listigi hokojn", + "hook_name_unknown": "Nekonata hoko-nomo '{name}'", + "installation_complete": "Kompleta instalado", + "invalid_url": "Nevalida URL{url} (ĉu ĉi tiu retejo ekzistas?)", + "log_app_action_run": "Funkciigu agon de la apliko '{}'", + "log_app_change_url": "Ŝanĝu la URL de la apliko '{}'", + "log_app_install": "Instalu la aplikon '{}'", + "log_app_makedefault": "Faru '{}' la defaŭlta apliko", + "log_app_remove": "Forigu la aplikon '{}'", + "log_app_upgrade": "Ĝisdatigu la aplikon '{}'", + "log_available_on_yunopaste": "Ĉi tiu protokolo nun haveblas per {url}", + "log_backup_restore_app": "Restarigu '{}' de rezerva ar archiveivo", + "log_backup_restore_system": "Restarigi sistemon de rezerva arkivo", + "log_corrupted_md_file": "La YAD-metadata dosiero asociita kun protokoloj estas damaĝita: '{md_file}\nEraro: {error} '", + "log_does_exists": "Ne estas operacio kun la nomo '{log}', uzu 'yunohost log list' por vidi ĉiujn disponeblajn operaciojn", + "log_domain_add": "Aldonu '{}' domajnon en sisteman agordon", + "log_domain_main_domain": "Faru de '{}' la ĉefa domajno", + "log_domain_remove": "Forigi domon '{}' de agordo de sistemo", + "log_dyndns_subscribe": "Aboni al YunoHost-subdominio '{}'", + "log_dyndns_update": "Ĝisdatigu la IP asociita kun via subdominio YunoHost '{}'", + "log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log share {name}' por akiri helpon", + "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Instalu atestilon Let's Encrypt sur '{}' regado", + "log_letsencrypt_cert_renew": "Renovigu '{}' Let's Encrypt atestilon", + "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", + "log_link_to_log": "Plena ŝtipo de ĉi tiu operacio: '{desc} '", + "log_operation_unit_unclosed_properly": "Operaciumo ne estis fermita ĝuste", + "log_regen_conf": "Regeneri sistemajn agordojn '{}'", + "log_remove_on_failed_install": "Forigu '{}' post malsukcesa instalado", + "log_selfsigned_cert_install": "Instalu mem-subskribitan atestilon sur '{}' domajno", + "log_tools_migrations_migrate_forward": "Kuru migradoj", + "log_tools_postinstall": "Afiŝu vian servilon YunoHost", + "log_tools_reboot": "Reklamu vian servilon", + "log_tools_shutdown": "Enŝaltu vian servilon", + "log_tools_upgrade": "Ĝisdatigu sistemajn pakaĵojn", + "log_user_create": "Aldonu uzanton '{}'", + "log_user_delete": "Forigi uzanton '{}'", + "log_user_group_create": "Krei grupon '{}'", + "log_user_group_delete": "Forigi grupon '{}'", + "log_user_group_update": "Ĝisdatigi grupon '{}'", + "log_user_update": "Ĝisdatigu uzantinformojn de '{}'", + "mail_alias_remove_failed": "Ne povis forigi retpoŝton alias '{mail}'", + "mail_domain_unknown": "Nevalida retadreso por domajno '{domain}'. Bonvolu uzi domajnon administritan de ĉi tiu servilo.", + "mail_forward_remove_failed": "Ne povis forigi retpoŝton plusendante '{mail}'", + "mail_unavailable": "Ĉi tiu retpoŝta adreso estas rezervita kaj aŭtomate estos atribuita al la unua uzanto", + "mailbox_disabled": "Retpoŝto malŝaltita por uzanto {user}", + "mailbox_used_space_dovecot_down": "La poŝta servo de Dovecot devas funkcii, se vi volas akcepti uzitan poŝtan keston", + "main_domain_change_failed": "Ne eblas ŝanĝi la ĉefan domajnon", + "main_domain_changed": "La ĉefa domajno estis ŝanĝita", + "migrations_already_ran": "Tiuj migradoj estas jam faritaj: {ids}", + "migrations_dependencies_not_satisfied": "Rulu ĉi tiujn migradojn: '{dependencies_id}', antaŭ migrado {id}.", + "migrations_exclusive_options": "'--auto', '--skip' kaj '--force-rerun' estas reciproke ekskluzivaj ebloj.", + "migrations_failed_to_load_migration": "Ne povis ŝarĝi migradon {id}: {error}", + "migrations_list_conflict_pending_done": "Vi ne povas uzi ambaŭ '--previous' kaj '--done' samtempe.", + "migrations_loading_migration": "Ŝarĝante migradon {id}…", + "migrations_migration_has_failed": "Migrado {id} ne kompletigis, abolis. Eraro: {exception}", + "migrations_must_provide_explicit_targets": "Vi devas provizi eksplicitajn celojn kiam vi uzas '--skip' aŭ '--force-rerun'", + "migrations_need_to_accept_disclaimer": "Por funkciigi la migradon {id}, via devas akcepti la sekvan malakcepton:\n---\n{disclaimer}\n---\nSe vi akceptas funkcii la migradon, bonvolu rekonduki la komandon kun la opcio '--accept-disclaimer'.", + "migrations_no_migrations_to_run": "Neniuj migradoj por funkcii", + "migrations_no_such_migration": "Estas neniu migrado nomata '{id}'", + "migrations_not_pending_cant_skip": "Tiuj migradoj ankoraŭ ne estas pritraktataj, do ne eblas preterlasi: {ids}", + "migrations_pending_cant_rerun": "Tiuj migradoj ankoraŭ estas pritraktataj, do ne plu rajtas esti ekzekutitaj: {ids}", + "migrations_running_forward": "Kuranta migrado {id}…", + "migrations_skip_migration": "Salti migradon {id}…", + "migrations_success_forward": "Migrado {id} kompletigita", + "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`.", + "nftables_unavailable": "Vi ne povas ludi kun nftables ĉi tie. Vi estas en ujo aŭ via kerno ne subtenas ĝin", + "not_enough_disk_space": "Ne sufiĉe libera spaco sur '{path}'", + "operation_interrupted": "La operacio estis permane interrompita?", + "password_listed": "Ĉi tiu pasvorto estas inter la plej uzataj pasvortoj en la mondo. Bonvolu elekti ion pli unikan.", + "password_too_simple_1": "Pasvorto devas esti almenaŭ 8 signojn longa", + "password_too_simple_2": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklojn kaj minusklojn", + "password_too_simple_3": "La pasvorto bezonas almenaŭ 8 signojn kaj enhavas ciferon, majusklon, pli malaltan kaj specialajn signojn", + "password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn", + "pattern_backup_archive_name": "Devas esti valida dosiernomo kun maksimume 30 signoj, alfanombraj kaj -_. signoj nur", + "pattern_domain": "Devas esti valida domajna nomo (t.e. mia-domino.org)", + "pattern_email": "Devas esti valida retpoŝta adreso (t.e. iu@ekzemple.com)", + "pattern_mailbox_quota": "Devas esti grandeco kun la sufikso b/k/M/G/T aŭ 0 por ne havi kvoton", + "pattern_password": "Devas esti almenaŭ 3 signoj longaj", + "pattern_password_app": "Bedaŭrinde, pasvortoj ne povas enhavi jenajn signojn: {forbidden_chars}", + "pattern_port_or_range": "Devas esti valida haveno-nombro (t.e. 0-65535) aŭ gamo da havenoj (t.e. 100:200)", + "pattern_username": "Devas esti minuskulaj literoj kaj minuskloj nur", + "permission_already_allowed": "Grupo '{group}' jam havas rajtigitan permeson '{permission}'", + "permission_already_disallowed": "Grupo '{group}' jam havas permeson '{permission}' malebligita", + "permission_cannot_remove_main": "Forigo de ĉefa permeso ne rajtas", + "permission_created": "Permesita '{permission}' kreita", + "permission_creation_failed": "Ne povis krei permeson '{permission}': {error}", + "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.", + "permission_deleted": "Permesita \"{permission}\" forigita", + "permission_deletion_failed": "Ne povis forigi permeson '{permission}': {error}", + "permission_not_found": "Permesita \"{permission}\" ne trovita", + "permission_require_account": "Permesilo {permission} nur havas sencon por uzantoj, kiuj havas konton, kaj tial ne rajtas esti ebligitaj por vizitantoj.", + "permission_update_failed": "Ne povis ĝisdatigi permeson '{permission}': {error}", + "permission_updated": "Ĝisdatigita \"{permission}\" rajtigita", + "port_already_closed": "Haveno {port} estas jam fermita", + "port_already_opened": "Haveno {port} estas jam malfermita", + "regenconf_dry_pending_applying": "Kontrolado de pritraktata agordo, kiu estus aplikita por kategorio '{category}'…", + "regenconf_failed": "Ne povis regeneri la agordon por kategorio(j): {categories}", + "regenconf_file_backed_up": "Agordodosiero '{conf}' estis rezervita al '{backup}'", + "regenconf_file_copy_failed": "Ne povis kopii la novan agordodosieron '{new}' al '{conf}'", + "regenconf_file_kept_back": "La agorda dosiero '{conf}' estas atendita forigi per regen-conf (kategorio {category}), sed ĝi estis konservita.", + "regenconf_file_manually_modified": "La agorddosiero '{conf}' estis modifita permane kaj ne estos ĝisdatigita", + "regenconf_file_manually_removed": "La dosiero de agordo '{conf}' estis forigita permane, kaj ne estos kreita", + "regenconf_file_remove_failed": "Ne povis forigi la agordodosieron '{conf}'", + "regenconf_file_removed": "Agordodosiero '{conf}' forigita", + "regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita", + "regenconf_now_managed_by_yunohost": "La agorda dosiero '{conf}' nun estas administrata de YunoHost (kategorio {category}).", + "regenconf_pending_applying": "Aplikante pritraktata agordo por kategorio '{category}'…", + "regenconf_up_to_date": "La agordo jam estas ĝisdatigita por kategorio '{category}'", + "regenconf_updated": "Agordo ĝisdatigita por '{category}'", + "regenconf_would_be_updated": "La agordo estus aktualigita por la kategorio '{category}'", + "restore_already_installed_app": "App kun la ID '{app}' estas jam instalita", + "restore_cleaning_failed": "Ne eblis purigi la adresaron de provizora restarigo", + "restore_complete": "Restarigita", + "restore_confirm_yunohost_installed": "Ĉu vi vere volas restarigi jam instalitan sistemon? [{answers}]", + "restore_extracting": "Eltirante bezonatajn dosierojn el la ar theivo…", + "restore_failed": "Ne povis restarigi sistemon", + "restore_hook_unavailable": "Restariga skripto por '{part}' ne haveblas en via sistemo kaj ankaŭ ne en la ar theivo", + "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)", + "restore_not_enough_disk_space": "Ne sufiĉa spaco (spaco: {free_space} B, necesa spaco: {needed_space} B, sekureca marĝeno: {margin} B)", + "restore_nothings_done": "Nenio estis restarigita", + "restore_removing_tmp_dir_failed": "Ne povis forigi malnovan provizoran dosierujon", + "restore_running_app_script": "Restarigi la programon '{app}'…", + "restore_running_hooks": "Kurantaj restarigaj hokoj…", + "restore_system_part_failed": "Ne povis restarigi la sisteman parton '{part}'", + "root_password_desynchronized": "La pasvorta administranto estis ŝanĝita, sed YunoHost ne povis propagandi ĉi tion al la radika pasvorto!", + "server_reboot": "La servilo rekomenciĝos", + "server_reboot_confirm": "Ĉu la servilo rekomencos tuj, ĉu vi certas? [{answers}]", + "server_shutdown": "La servilo haltos", + "server_shutdown_confirm": "La servilo haltos tuj, ĉu vi certas? [{answers}]", + "service_add_failed": "Ne povis aldoni la servon '{service}'", + "service_added": "La servo '{service}' estis aldonita", + "service_already_started": "La servo '{service}' jam funkcias", + "service_already_stopped": "La servo '{service}' jam ĉesis", + "service_cmd_exec_failed": "Ne povis plenumi la komandon '{command}'", + "service_description_dnsmasq": "Traktas rezolucion de domajna nomo (DNS)", + "service_description_dovecot": "Permesas al retpoŝtaj klientoj aliri / serĉi retpoŝton (per IMAP kaj POP3)", + "service_description_fail2ban": "Protektas kontraŭ bruta forto kaj aliaj specoj de atakoj de la interreto", + "service_description_mysql": "Butikigas datumojn de programoj (SQL datumbazo)", + "service_description_nftables": "Administras malfermajn kaj fermajn konektajn havenojn al servoj", + "service_description_nginx": "Servas aŭ permesas atingi ĉiujn retejojn gastigita sur via servilo", + "service_description_postfix": "Uzita por sendi kaj ricevi retpoŝtojn", + "service_description_redis-server": "Specialita datumbazo uzita por rapida datumo atingo, atendovicoj kaj komunikadoj inter programoj", + "service_description_slapd": "Stokas uzantojn, domajnojn kaj rilatajn informojn", + "service_description_ssh": "Permesas al vi konekti al via servilo kun fora terminalo (SSH protokolo)", + "service_description_yunohost-api": "Mastrumas interagojn inter la YunoHost retinterfaco kaj la sistemo", + "service_disable_failed": "Ne povis fari la servon '{service}' ne komenci ĉe la ekkuro.", + "service_disabled": "La servo '{service}' ne plu komenciĝos kiam sistemo ekos.", + "service_enable_failed": "Ne povis fari la servon '{service}' aŭtomate komenci ĉe la ekkuro.", + "service_enabled": "La servo '{service}' nun aŭtomate komenciĝos dum sistemaj botoj.", + "service_reload_failed": "Ne povis reŝargi la servon '{service}'", + "service_reload_or_restart_failed": "Ne povis reŝargi aŭ rekomenci la servon '{service}'", + "service_reloaded": "Servo '{service}' reŝargita", + "service_reloaded_or_restarted": "La servo '{service}' estis reŝarĝita aŭ rekomencita", + "service_remove_failed": "Ne povis forigi la servon '{service}'", + "service_removed": "Servo '{service}' forigita", + "service_restart_failed": "Ne povis rekomenci la servon '{service}'", + "service_restarted": "Servo '{service}' rekomencis", + "service_start_failed": "Ne povis komenci la servon '{service}'", + "service_started": "Servo '{service}' komenciĝis", + "service_stop_failed": "Ne povis maldaŭrigi la servon '{service}'", + "service_stopped": "Servo '{service}' ĉesis", + "service_unknown": "Nekonata servo '{service}'", + "ssowat_conf_generated": "SSOwat-agordo generita", + "system_upgraded": "Sistemo ĝisdatigita", + "system_username_exists": "Uzantnomo jam ekzistas en la listo de uzantoj de sistemo", + "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`.", + "unbackup_app": "App '{app}' ne konserviĝos", + "unexpected_error": "Io neatendita iris malbone: {error}", + "unknown_error_reading_file": "Nekonata eraro dum provi legi dosieron {file} (kialo: {error})", + "unknown_group": "Nekonata grupo \"{group}\"", + "unknown_user": "Nekonata uzanto '{user}'", + "unlimit": "Neniu kvoto", + "unrestore_app": "App '{app}' ne restarigos", + "update_apt_cache_failed": "Ne eblis ĝisdatigi la kaŝmemoron de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}", + "update_apt_cache_warning": "Io iris malbone dum la ĝisdatigo de la kaŝmemoro de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}", + "updating_apt_cache": "Akirante haveblajn ĝisdatigojn por sistemaj pakoj…", + "upgrading_packages": "Ĝisdatigi pakojn…", + "upnp_dev_not_found": "Neniu UPnP-aparato trovita", + "upnp_disabled": "UPnP malŝaltis", + "upnp_enabled": "UPnP ŝaltis", + "upnp_port_open_failed": "Ne povis malfermi havenon per UPnP", + "user_already_exists": "La uzanto '{user}' jam ekzistas", + "user_created": "Uzanto kreita", + "user_creation_failed": "Ne povis krei uzanton {user}: {error}", + "user_deleted": "Uzanto forigita", + "user_deletion_failed": "Ne povis forigi uzanton {user}: {error}", + "user_home_creation_failed": "Ne povis krei dosierujon '{home}' por uzanto", + "user_unknown": "Nekonata uzanto: {user}", + "user_update_failed": "Ne povis ĝisdatigi uzanton {user}: {error}", + "user_updated": "Uzantinformoj ŝanĝis", + "yunohost_already_installed": "YunoHost estas jam instalita", + "yunohost_configured": "YunoHost nun estas agordita", + "yunohost_installing": "Instalante YunoHost…", + "yunohost_not_installed": "YunoHost ne estas ĝuste instalita. Bonvolu prilabori 'yunohost tools postinstall'", + "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://doc.yunohost.org/admin." +} diff --git a/locales/es.json b/locales/es.json new file mode 100644 index 0000000..0cb2e05 --- /dev/null +++ b/locales/es.json @@ -0,0 +1,887 @@ +{ + "aborting": "Cancelando.", + "action_invalid": "Acción inválida '{action}'", + "additional_urls_already_added": "URL adicional '{url}' ya añadida en la URL adicional para permiso «{permission}»", + "additional_urls_already_removed": "URL adicional '{url}' ya eliminada en la URL adicional para permiso «{permission}»", + "admin_password": "Contraseña administrativa", + "admins": "Administradores", + "all_users": "Todos los usuarios de YunoHost", + "already_up_to_date": "Nada que hacer. Todo está actualizado.", + "app_action_broke_system": "Esta acción parece que ha roto estos servicios importantes: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Estos servicios necesarios deberían estar funcionando para ejecutar esta acción: {services}. Pruebe a reiniciarlos para continuar (y posiblemente investigar por qué están caídos).", + "app_action_failed": "No se ha podido ejecutar la acción {action} para la aplicación {app}", + "app_already_installed": "{app} ya está instalada", + "app_already_installed_cant_change_url": "Esta aplicación ya está instalada. La URL no se puede cambiar solo con esta función. Marque `app changeurl` si está disponible.", + "app_arch_not_supported": "Esta aplicación solo se puede instalar en arquitecturas {required} pero la arquitectura de tu servidor es {current}", + "app_argument_choice_invalid": "Elija un valor válido para el argumento '{name}': '{value}' no se encuentra entre las opciones disponibles ({choices})", + "app_argument_invalid": "Elija un valor válido para el argumento «{name}»: {error}", + "app_change_url_failed": "No es possible cambiar la URL para {app}: {error}", + "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain}{path}'), no se realizarán cambios.", + "app_change_url_no_script": "La aplicación «{app_name}» aún no permite la modificación de URLs. Quizás debería actualizarla.", + "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", + "app_change_url_success": "El URL de la aplicación {app} es ahora {domain} {path}", + "app_config__core_name": "Fichas y permisos", + "app_config_permission_allowed": "Grupos/cuentas autorizados", + "app_config_permission_allowed_warn_protected": "NB: este permiso esta 'protegido' y el grupo 'visitantes' no puede ser agregado/suprimido desde los grupos autorizados actualmente.", + "app_config_permission_description": "Descripción", + "app_config_permission_description_help": "Esta funcionalidad es útil solo si usas el modo 'descriptivo' del portal", + "app_config_permission_extraperm_section_name": "Autorización'{perm}'", + "app_config_permission_label": "Etiqueta", + "app_config_permission_location": "Corresponde a [{absolute_url}] ({absolute_url})", + "app_config_permission_logo": "Usar un logo personalizado", + "app_config_permission_logo_help": "Solo los archivos PNG estan soportados", + "app_config_permission_show_tile": "Mostrar la teja en el portal", + "app_config_unable_to_apply": "No se pudieron aplicar los valores del panel configuración.", + "app_config_unable_to_read": "No se pudieron leer los valores del panel configuración.", + "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_extraction_failed": "No se pudieron extraer los archivos de instalación", + "app_failed_to_download_asset": "Error al descargar el recurso '{source_id}' ({url}) para {app}: {out}", + "app_full_domain_unavailable": "Lamentablemente esta aplicación tiene que instalarse en un dominio propio pero ya hay otras aplicaciones instaladas en el dominio «{domain}». Podría usar un subdomino dedicado a esta aplicación en su lugar.", + "app_id_invalid": "ID de la aplicación no válida", + "app_install_failed": "No se pudo instalar {app}: {error}", + "app_install_files_invalid": "Estos archivos no se pueden instalar", + "app_install_script_failed": "Ha ocurrido un error en el guión de instalación de la aplicación", + "app_location_unavailable": "Este URL o no está disponible o está en conflicto con otra(s) aplicación(es) instalada(s):\n{apps}", + "app_make_default_location_already_used": "No pudo hacer que la aplicación «{app}» sea la predeterminada en el dominio, «{domain}» ya está siendo usado por la aplicación «{other_app}»", + "app_manifest_install_ask_admin": "Elija un usuario administrativo para esta aplicación", + "app_manifest_install_ask_domain": "Seleccione el dominio donde esta app debería ser instalada", + "app_manifest_install_ask_init_admin_permission": "¿Quién debe tener acceso a las funciones de administración de esta aplicación? (Esto puede cambiarse posteriormente)", + "app_manifest_install_ask_init_main_permission": "¿Quién debería tener acceso a esta aplicación? (Esto puede cambiarse posteriormente)", + "app_manifest_install_ask_is_public": "¿Debería exponerse esta aplicación a visitantes anónimos?", + "app_manifest_install_ask_password": "Elija una contraseña de administración para esta aplicación", + "app_manifest_install_ask_path": "Seleccione la ruta de URL (después del dominio) donde esta aplicación debería ser instalada", + "app_not_correctly_installed": "La aplicación {app} 8 parece estar incorrectamente instalada", + "app_not_enough_disk": "Esta aplicación requiere {required} espacio libre.", + "app_not_enough_ram": "Esta aplicación requiere {required} de RAM para ser instalada/actualizada, pero solo hay {current} disponible actualmente.", + "app_not_installed": "No se pudo encontrar «{app}» en la lista de aplicaciones instaladas: {all_apps}", + "app_not_properly_removed": "La {app} 0 no ha sido desinstalada correctamente", + "app_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_remove_after_failed_install": "Eliminando la aplicación tras el fallo de instalación…", + "app_removed": "{app} Desinstalado", + "app_requirements_checking": "Comprobando los paquetes necesarios para {app}…", + "app_resource_failed": "Falló la asignación, desasignación o actualización de recursos para {app}: {error}", + "app_restore_failed": "No se pudo restaurar la aplicación «{app}»: {error}", + "app_restore_script_failed": "Ha ocurrido un error dentro del script de restauración de aplicaciones", + "app_sources_fetch_failed": "No se pudieron obtener los archivos con el código fuente, ¿es el URL correcto?", + "app_start_backup": "Obteniendo archivos para el respaldo de {app}…", + "app_start_install": "Instalando {app}…", + "app_start_remove": "Eliminando {app} …", + "app_start_restore": "Restaurando {app}…", + "app_unknown": "Aplicación desconocida", + "app_unsupported_remote_type": "Tipo remoto no soportado por la aplicación", + "app_upgrade_app_name": "Actualizando {app}…", + "app_upgrade_bad_quality": "Esta aplicación está marcada como obsoleta en el catálogo de aplicaciones de YunoHost. Esto podría ser un problema temporal mientras los mantenedores intentan arreglarlo. Momentáneamente, está deshabilitada la actualización de esta aplicación.", + "app_upgrade_broke_the_system": "La actualezación de {app} funcionó aparentemente, pero dej/ el sistema roto y por lo tanto se considera un fallo.", + "app_upgrade_cli_bad_quality": "Saltando actualizaciones de {app} porque está marcada como rota en el catálogo de aplicaciones de YunoHost.", + "app_upgrade_cli_up_to_date": "{app} ya está actualizada ({current_version})", + "app_upgrade_cli_url_required": "{app} no está en el catálogo (¿ya no más?) y por lo tanto no se puede actualizar automáticamente. Deberías usar `yunohost app upgrade {app}` para entregar la URL del repositorio usando la opción `-u`.", + "app_upgrade_cli_will_force_upgrade": "{app} será forzada a actualizar ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} será actualizada de {current_version} a {new_version}", + "app_upgrade_continuing_with_other_apps": "Fallo al actualizar {app}, pero continuando con la actualización de otras aplicaciones de igual manera (porque se usó `--continue-on-failure`)", + "app_upgrade_fail_requirements": "Está disponible una nueva versión para esta aplicación ({new_version}), pero no se cumplen ciertos requerimientos:\n{failed_requirements}", + "app_upgrade_failed": "Error al actualizar {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "Falli al actualizar la aplicación '{app}', y dejó el sistema roto.", + "app_upgrade_script_failed": "Ha ocurrido un error en el script de actualización de la app", + "app_upgrade_several_apps": "Las siguientes aplicaciones se actualizarán: {apps}", + "app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones", + "app_upgrade_specific_channel_msg": "Ten en cuenta que actualmente estás usando `{channel}` comi fuente para actualizaciones. Asegúrate de revisar la discusión en curso [aquí]({pr_url}).", + "app_upgrade_up_to_date": "Forzar la actualización de la aplicación (a la misma versión) puede ser útil para volver a compilas la aplicación y sus configuraciones.", + "app_upgrade_upgradable": "La aplicación puede ser actualizada de la versión {current_version} a {new_version}", + "app_upgraded": "Actualizado {app}", + "app_yunohost_version_not_supported": "Esta aplicación requiere YunoHost >= {required} pero la versión actualmente instalada es {current}.", + "apps_already_up_to_date": "Todas las aplicaciones están ya actualizadas", + "apps_catalog_failed_to_download": "No se puede descargar el catálogo de aplicaciones {apps_catalog}: {error}", + "apps_catalog_obsolete_cache": "El caché del catálogo de aplicaciones está vacío u obsoleto.", + "apps_catalog_update_success": "¡El catálogo de aplicaciones ha sido actualizado!", + "apps_catalog_updating": "Actualiza el catálogo de aplicaciones…", + "apps_no_target_can_be_upgraded": "Ninguna aplicación se puede actualizar", + "ask_admin_fullname": "Nombre completo del administrador", + "ask_admin_username": "Nombre de usuario del administrador", + "ask_dyndns_recovery_password": "Password de recuperación de DynDNS", + "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_explain_during_unsubscribe": "Por favor introduzca la password de recuperación de este dominio DynDNS.", + "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.", + "ask_fullname": "Nombre completo", + "ask_main_domain": "Dominio principal", + "ask_new_admin_password": "Nueva contraseña administrativa", + "ask_new_domain": "Nuevo dominio", + "ask_new_path": "Nueva ruta", + "ask_password": "Contraseña", + "ask_user_domain": "Dominio a usar para la dirección de correo del usuario", + "automatic_task": "Tareas automatizadas", + "backup_abstract_method": "Este método de respaldo aún no se ha implementado", + "backup_actually_backuping": "Creando un archivo de respaldo de los archivos obtenidos…", + "backup_applying_method_copy": "Copiando todos los archivos en el respaldo…", + "backup_applying_method_custom": "Llamando al método de copia de seguridad personalizado {method}…", + "backup_applying_method_tar": "Creando el archivo TAR de respaldo…", + "backup_archive_app_not_found": "No se pudo encontrar «{app}» en el archivo de respaldo", + "backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path})", + "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).", + "backup_archive_corrupted": "Parece que el archivo de respaldo '{archive}' está corrupto : {error}", + "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_archive_system_part_not_available": "La parte del sistema «{part}» no está disponible en esta copia de seguridad", + "backup_archive_writing_error": "No se pudieron añadir los archivos «{source}» (llamados en el archivo «{dest}») para ser respaldados en el archivo comprimido «{archive}»", + "backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size}MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)", + "backup_cant_mount_uncompress_archive": "No se pudo montar el archivo descomprimido como protegido contra escritura", + "backup_cleaning_failed": "No se pudo limpiar la carpeta de respaldo temporal", + "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar el archivo", + "backup_couldnt_bind": "No se pudo enlazar {src} con {dest}.", + "backup_create_size_estimation": "El archivo contendrá aproximadamente {size} de datos.", + "backup_created": "Copia de seguridad creada: {name}", + "backup_creation_failed": "No se pudo crear el archivo de respaldo", + "backup_csv_addition_failed": "No se pudo añadir archivos para respaldar en el archivo CSV", + "backup_csv_creation_failed": "No se pudo crear el archivo CSV necesario para la restauración", + "backup_custom_backup_error": "El método de respaldo personalizado no pudo superar el paso de «copia de seguridad»", + "backup_custom_mount_error": "El método de respaldo personalizado no pudo superar el paso «mount»", + "backup_delete_error": "No se pudo eliminar «{path}»", + "backup_deleted": "Copia de seguridad eliminada: {name}", + "backup_hook_unknown": "El gancho «{hook}» de la copia de seguridad es desconocido", + "backup_method_copy_finished": "Terminada la copia de seguridad", + "backup_method_custom_finished": "Terminado el método «{method}» de respaldo personalizado", + "backup_method_tar_finished": "Creado el archivo TAR de respaldo", + "backup_mount_archive_for_restore": "Preparando el archivo para restaurarlo…", + "backup_no_file_collected": "Fallo al recolectar archivos para ser respaldados", + "backup_no_uncompress_archive_dir": "No existe tal directorio de archivos sin comprimir", + "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_output_symlink_dir_broken": "El directorio de su archivo «{path}» es un enlace simbólico roto. Tal vez olvidó (re)montarlo o conectarlo al medio de almacenamiento al que apunta.", + "backup_running_hooks": "Ejecutando los hooks de copia de respaldo…", + "backup_system_part_failed": "No se pudo respaldar la parte del sistema «{part}»", + "backup_unable_to_organize_files": "No se pudo usar el método rápido de organización de los archivos en el archivo", + "backup_with_no_backup_script_for_app": "La aplicación «{app}» no tiene un guión de respaldo. Omitiendo.", + "backup_with_no_restore_script_for_app": "«{app}» no tiene un script de restauración, no podá restaurar automáticamente la copia de seguridad de esta aplicación.", + "cannot_open_file": "No se pudo abrir el archivo {file} (motivo: {error})", + "cannot_write_file": "No se pudo escribir el archivo {file} (motivo: {error})", + "certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque en su configuración de nginx falta el código correcto… Por favor, asegúrate que la configuración de nginx es correcta ejecutando en la terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio «{domain}» no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", + "certmanager_attempt_to_renew_valid_cert": "¡El certificado para el dominio «{domain}» no está a punto de expirar! (Puede usar --force si sabe lo que está haciendo)", + "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain}! (Use --force para omitir este mensaje)", + "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain} (archivo: {file}), razón: {reason}", + "certmanager_cert_install_failed": "La instalación del certificado Let's Encrypt a fallado para {domains}", + "certmanager_cert_install_failed_selfsigned": "La instalación del certificado autofirmado ha fallado para {domains}", + "certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain}»", + "certmanager_cert_install_success_selfsigned": "Instalado correctamente un certificado autofirmado para el dominio «{domain}»", + "certmanager_cert_renew_failed": "La renovación del certificado Let's Encrypt ha fallado para {domains}", + "certmanager_cert_renew_success": "Renovado correctamente el certificado de Let's Encrypt para el dominio «{domain}»", + "certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado", + "certmanager_certificate_fetching_or_enabling_failed": "El intento de usar el nuevo certificado para {domain} no ha funcionado…", + "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain} no es un certificado autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use «--force» para hacerlo)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Los registros DNS para el dominio '{domain}' son diferentes para la IP de este servidor. Por favor comprueba la categoría de los 'registros DNS' (básicos) en la página de diagnóstico para mayor información. Si has modificado recientemente tu registro 'A', espera a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabes lo que estás haciendo, usa '--no-checks' para desactivar estos marcadores)", + "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain} a través de HTTP. Por favor compruebe en los diagnósticos la categoría 'Web'para más información. (Si sabe lo que está haciendo, utilice '--no-checks' para no realizar estas comprobaciones.)", + "certmanager_domain_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.)", + "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para este conjunto exacto de dominios {domain}. Pruebe de nuevo más tarde. Vea para más detalles https://letsencrypt.org/docs/rate-limits/", + "certmanager_no_cert_file": "No se pudo leer el certificado para el dominio {domain} (archivo: {file})", + "certmanager_self_ca_conf_file_not_found": "No se pudo encontrar el archivo de configuración para la autoridad de autofirma (archivo: {file})", + "certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file})", + "config_action_disabled": "No se ha podido ejecutar la acción '{action}' porque está desactivada, asegúrese de cumplir sus restricciones. ayuda: {help}", + "config_action_failed": "Error al ejecutar la acción '{action}': {error}", + "config_apply_failed": "Falló la aplicación de la nueva configuración: {error}", + "config_cant_set_value_on_section": "No puede establecer un único valor en una sección de configuración completa.", + "config_forbidden_keyword": "'{keyword}' es una palabra reservada, no puedes crear ni usar un panel de configuración con una pregunta que use esta id.", + "config_forbidden_readonly_type": "El tipo '{type}' no puede establecerse como solo lectura, utilice otro tipo para representar este valor (arg id relevante: '{id}').", + "config_no_panel": "No se ha encontrado ningún panel de configuración.", + "config_unknown_filter_key": "La clave de filtrado '{filter_key}' es incorrecta.", + "confirm_app_install_danger": "¡PELIGRO! ¡Esta aplicación sigue siendo experimental (si no es expresamente no funcional)! Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe tu sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", + "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de YunoHost. La instalación de aplicaciones de terceros puede comprometer la integridad y seguridad de tu sistema. Probablemente NO deberías instalarla a menos que sepas lo que estás haciendo. NO se proporcionará NINGÚN SOPORTE si esta aplicación no funciona o rompe su sistema… Si de todos modos quieres correr ese riesgo, escribe '{answers}'", + "confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers}] ", + "confirm_app_insufficient_ram": "Esta aplicación requiere de más RAM para ser instalada que la que hay 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}]", + "confirm_tos_acknowledgement": "Entiendo las condiciones de uso [{answers}]", + "corrupted_json": "Lectura corrupta de JSON desde {ressource} (motivo: {error})", + "corrupted_toml": "Lectura corrupta de TOML desde {ressource} (motivo: {error})", + "corrupted_yaml": "Lectura corrupta de YAML desde {ressource} (motivo: {error})", + "danger": "Peligro:", + "diagnosis_apps_allgood": "Todas las aplicaciones instaladas respetan las prácticas básicas de empaquetado", + "diagnosis_apps_bad_quality": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", + "diagnosis_apps_broken": "Esta aplicación está etiquetada como defectuosa en el catálogo de aplicaciones YunoHost. Podría ser un problema temporal mientras las personas responsables corrigen el asunto. Mientras tanto, la actualización de esta aplicación está desactivada.", + "diagnosis_apps_deprecated_practices": "La versión instalada de esta aplicación usa aún prácticas de empaquetado obsoletas. Deberías actualizarla.", + "diagnosis_apps_issue": "Se ha detectado un problema con la aplicación {app}", + "diagnosis_apps_not_in_app_catalog": "Esta aplicación se encuentra ausente o ya no figura en el catálogo de aplicaciones de YunoHost. Deberías considerar desinstalarla ya que no recibirá actualizaciones y podría comprometer la integridad y seguridad de tu sistema.", + "diagnosis_apps_outdated_packaging_format": "Esta aplicación utiliza un formato de empaquetamiento obsoleto que pronto no tendrá más soporte. Debería realmente pensar en actualizarlo.", + "diagnosis_apps_outdated_ynh_requirement": "La versión instalada de esta aplicación solo necesita YunoHost >= 2.x o 3.x, lo que hace pensar que no está al día con la buena practicas de empaquetado. Deberías actualizarla.", + "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": "La arquitectura material del servidor es {virt} {arch}", + "diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}", + "diagnosis_basesystem_host": "El servidor está ejecutando Debian {debian_version}", + "diagnosis_basesystem_kernel": "El servidor está ejecutando el núcleo de Linux {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Está ejecutando versiones inconsistentes de los paquetes de YunoHost… probablemente debido a una actualización parcial o fallida.", + "diagnosis_basesystem_ynh_main_version": "El servidor está ejecutando YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} versión: {version} ({repo})", + "diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡No se volvera a comprobar de momento!)", + "diagnosis_cant_run_because_of_dep": "No se puede ejecutar el diagnóstico para {category} mientras haya problemas importantes relacionados con {dep}.", + "diagnosis_description_apps": "Aplicaciones", + "diagnosis_description_basesystem": "Sistema de base", + "diagnosis_description_dnsrecords": "Registro DNS", + "diagnosis_description_ip": "Conectividad a Internet", + "diagnosis_description_mail": "Correo electrónico", + "diagnosis_description_ports": "Exposición de puertos", + "diagnosis_description_regenconf": "Configuraciones de sistema", + "diagnosis_description_services": "Comprobación del estado de los servicios", + "diagnosis_description_systemresources": "Recursos del sistema", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en el dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible (de {total}). Ten cuidado.", + "diagnosis_diskusage_ok": "¡El almacenamiento {mountpoint} (en el dispositivo {device}) todavía tiene {free} ({free_percent}%) de espacio libre (de {total})!", + "diagnosis_diskusage_verylow": "El almacenamiento {mountpoint}(en el dispositivo {device}) sólo tiene {free} ({free_percent}%) de espacio disponible(de {total}). ¡Deberías limpiar algo de espacio!", + "diagnosis_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_dns_bad_conf": "Algunos registros DNS faltan o están mal cofigurados para el dominio {domain} (categoría {category})", + "diagnosis_dns_discrepancy": "El siguiente registro DNS parace que no sigue la configuración recomendada
Tipo: {type}
Nombre: {name}
Valor Actual: {current}
Valor esperado: {content}", + "diagnosis_dns_good_conf": "La configuración de registros DNS es correcta para {domain} (categoría {category})", + "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS con las informaciones siguientes. Tipo: {type}
Nombre: {name}
Valor: {content}", + "diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://doc.yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.", + "diagnosis_dns_specialusedomain": "El dominio {domain} se basa en un dominio de primer nivel (TLD) de usos especiales como .local o .test y no debería tener entradas DNS reales.", + "diagnosis_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_domain_expiration_error": "¡Algunos dominios expirarán MUY PRONTO!", + "diagnosis_domain_expiration_not_found": "No se pudo revisar la fecha de expiración para algunos dominios", + "diagnosis_domain_expiration_not_found_details": "¿Parece que la información de WHOIS para el dominio {domain} no contiene información sobre la fecha de expiración?", + "diagnosis_domain_expiration_success": "Sus dominios están registrados y no expirarán pronto.", + "diagnosis_domain_expiration_warning": "¡Algunos dominios expirarán pronto!", + "diagnosis_domain_expires_in": "{domain} expira en {days} días.", + "diagnosis_domain_not_found_details": "¡El dominio {domain} no existe en la base de datos WHOIS o ha expirado!", + "diagnosis_everything_ok": "¡Todo correcto en {category}!", + "diagnosis_failed": "Error al obtener el resultado del diagnóstico para la categoría '{category}': {error}", + "diagnosis_failed_for_category": "Error de diagnóstico para la categoría '{category}': {error}", + "diagnosis_found_errors": "¡Encontrado(s) error(es) significativo(s) {errors} relacionado(s) con {category}!", + "diagnosis_found_errors_and_warnings": "¡Encontrado(s) error(es) significativo(s) {errors} (y aviso(s) {warnings}) relacionado(s) con {category}!", + "diagnosis_found_warnings": "Encontrado elemento(s) {warnings} que puede(n) ser mejorado(s) para {category}.", + "diagnosis_high_number_auth_failures": "Ultimamente ha habido un gran número de errores de autenticación. Asegúrate de que Fail2Ban está ejecutándose y correctamente configurado, o usa un puerto SSH personalizado como se explica en https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Parece que otra máquina (quizás el router de conexión a internet) haya respondido en vez de tu servidor.
1. La causa más común es que el puerto 80 (y el 443) no hayan sido redirigidos a tu servidor.
2. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso están interfiriendo.", + "diagnosis_http_connection_error": "Error de conexión: Ne se pudo conectar al dominio solicitado.", + "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_http_hairpinning_issue": "Parece que tu red local no tiene la opción hairpinning activada.", + "diagnosis_http_hairpinning_issue_details": "Esto quizás es debido a tu router o máquina en el ISP. Como resultado, la gente fuera de tu red local podrá acceder a tu servidor como es de esperar, pero no así las persona que estén dentro de la red local (como tu probablemente) o cuando usen el nombre de dominio o la IP global. Quizás puedes mejorar o arreglar esta situación leyendo https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "Parece que la configuración nginx de este dominio haya sido modificada manualmente, esto no deja que YunoHost pueda diagnosticar si es accesible mediante HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Para arreglar este asunto, estudia las diferencias mediante el comando yunohost tools regen-conf nginx --dry-run --with-diff y si te parecen bien aplica los cambios mediante yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "El Dominio {domain} es accesible desde internet a través de HTTP.", + "diagnosis_http_partially_unreachable": "El dominio {domain} parece que no es accesible mediante HTTP desde fuera de la red local mediante IPv{failed}, aunque si que funciona mediante IPv{passed}.", + "diagnosis_http_special_use_tld": "Le dominio {domain} está basado en un dominio de primer nivel (TLD) de uso especial, como un .local o .test y no debería estar expuesto fuera de la red local.", + "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_unreachable": "El dominio {domain} esta fuera de alcance desde internet y a través de HTTP.", + "diagnosis_ignore_already_filtered": "(Ya existe un filtro de diagnostico {category} con estos parámetros )", + "diagnosis_ignore_criteria_error": "Los parámetros deben ser bajo el formato siguiente llave=valor (e.g. domain=yolo.test)", + "diagnosis_ignore_filter_added": "Filtro de diagnostico para {category} añadido", + "diagnosis_ignore_filter_removed": "Filtro de diagnóstico para{category} borrado", + "diagnosis_ignore_missing_criteria": "Debes por lo menos ingresar un parámetro dentro de las categorías a ignorar", + "diagnosis_ignore_no_filter_found": "(No existe filtros de diagnostico para la categoría {category} que corresponde a estos parámetros)", + "diagnosis_ignore_no_issue_found": "Ningún incidente que corresponde al parametro indicado fue encontrado.", + "diagnosis_ignored_issues": "(+ {nb_ignored} problema(s) ignorado(s))", + "diagnosis_ip_broken_dnsresolution": "Parece que no funciona la resolución de nombre de dominio por alguna razón… ¿Hay algún firewall bloqueando peticiones DNS?", + "diagnosis_ip_broken_resolvconf": "La resolución de nombres de dominio parece no funcionar en tu servidor, lo que parece estar relacionado con que /etc/resolv.conf no apunta a 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "¡El servidor está conectado a internet a través de IPv4!", + "diagnosis_ip_connected_ipv6": "¡El servidor está conectado a internet a través de IPv6!", + "diagnosis_ip_dnsresolution_working": "¡DNS no está funcionando!", + "diagnosis_ip_global": "IP Global: {global}", + "diagnosis_ip_local": "IP Local: {local}", + "diagnosis_ip_no_ipv4": "El servidor no cuenta con ipv4 funcional.", + "diagnosis_ip_no_ipv6": "El servidor no cuenta con IPv6 funcional.", + "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://doc.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_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://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "¿¡Está conectado el servidor a internet!?", + "diagnosis_ip_weird_resolvconf": "La resolución de nombres de dominio DNS funciona, aunque parece que estás utilizando /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_mail_blocklist_listed_by": "Tu IP o dominio {item} está marcado como maligno en {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Las IP y los dominios utilizados en este servidor no parece que estén en ningún listado maligno (blocklist)", + "diagnosis_mail_blocklist_reason": "El motivo de estar en la lista maligna es: {reason}", + "diagnosis_mail_blocklist_website": "Cuando averigües y arregles el motivo por el que apareces en la lista maligna, no dudes en solicitar que tu IP o dominio sea retirado de la {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Un servicio que no es SMTP respondió en el puerto 25 mediante IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Podría ser debido a otra máquina en lugar de tu servidor.", + "diagnosis_mail_ehlo_could_not_diagnose": "No pudimos diagnosticar si el servidor de correo postfix es accesible desde el exterior utilizando IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", + "diagnosis_mail_ehlo_ok": "¡El servidor de correo SMTP puede contactarse desde el exterior por lo que puede recibir correos!", + "diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos.", + "diagnosis_mail_ehlo_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_wrong": "Un servidor diferente de SMTP está respondiendo mediante IPv{ipversion}. Es probable que tu servidor no pueda recibir correos.", + "diagnosis_mail_ehlo_wrong_details": "El EHLO recibido por el diagnosticador remoto de IPv{ipversion} es diferente del dominio de tu servidor.
EHLO recibido: {wrong_ehlo}
EHLO esperado: {right_ehlo}
La causa más común de este error suele ser que el puerto 25 no está correctamente enrutado hacia tu servidor. Así mismo asegurate que ningún firewall ni reverse-proxy está interfiriendo.", + "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_different_from_ehlo_domain_details": "El DNS inverso actual es: {rdns_domain}
Valor esperado: {ehlo_domain}", + "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_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://doc.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_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_ok": "¡Las DNS inversas están bien configuradas!", + "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_mail_outgoing_port_25_blocked_details": "Primeramente, deberías intentar desbloquear el puerto de salida 25 en la interfaz de control de tu router o en la interfaz de tu proveedor de hosting. (Algunos hostings pueden necesitar que les abras un ticket de soporte para esto).", + "diagnosis_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://doc.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_ok": "El servidor de email SMTP puede mandar emails (puerto saliente 25 no está bloqueado).", + "diagnosis_mail_queue_ok": "{nb_pending} correos esperando e la cola de correos electrónicos", + "diagnosis_mail_queue_too_big": "Demasiados correos electrónicos pendientes en la cola ({nb_pending} correos electrónicos)", + "diagnosis_mail_queue_unavailable": "No se ha podido consultar el número de correos electrónicos pendientes en la cola", + "diagnosis_mail_queue_unavailable_details": "Error: {error}", + "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_no_cache": "Todavía no hay una caché de diagnóstico para la categoría '{category}'", + "diagnosis_package_installed_from_sury": "Algunos paquetes del sistema deberían ser devueltos a una versión anterior", + "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_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_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://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "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_partially_unreachable": "El port {port} no es accesible desde el exterior mediante IPv{failed}.", + "diagnosis_ports_unreachable": "El puerto {port} no es accesible desde internet.", + "diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente, es síntoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.
Resumen de los procesos terminados:
\n{kills_summary}", + "diagnosis_ram_low": "Al sistema le queda {available} ({available_percent}%) de RAM de un total de {total}. Cuidado.", + "diagnosis_ram_ok": "El sistema aún tiene {available} ({available_percent}%) de RAM de un total de {total}.", + "diagnosis_ram_verylow": "¡Al sistema le queda solamente {available} ({available_percent}%) de RAM! (De un total de {total})", + "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_rfkill_wifi": "El periférico Wi-Fi esta desactivado, una advertencia del sistema podria impedir la instalación des aplicaciones", + "diagnosis_rfkill_wifi_details": "Este aviso se entremete en varios resultados de comandos y puede romper aplicaciones. En general puedes configurartu código país asi : sudo raspi-config. Aqui el error:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "¡El sistema de ficheros raíz solo tiene un total de {space}! ¡Vas a quedarte sin espacio rápidamente! Se recomienda tener al menos 16GB para ese sistema de ficheros.", + "diagnosis_rootfstotalspace_warning": "¡El sistema de ficheros raíz solo tiene un total de {space}! Podría ser suficiente, pero cuidado, puedes rellenarlo rápidamente… Se recomienda tener al menos 16GB para el sistema de ficheros raíz.", + "diagnosis_security_vulnerable_to_meltdown": "Pareces vulnerable al colapso de vulnerabilidad crítica de seguridad", + "diagnosis_security_vulnerable_to_meltdown_details": "Para corregir esto, debieras actualizar y reiniciar tu sistema para cargar el nuevo kernel de Linux (o contacta tu proveedor si esto no funciona). Más información en https://meltdownattack.com/ .", + "diagnosis_services_bad_status": "El servicio {service} está {status} :(", + "diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del serviciode la administración web (desde la línea de comandos puedes hacerlo con yunohost service restart {service} y yunohost service log {service}).", + "diagnosis_services_conf_broken": "¡Mala configuración para el servicio {service}!", + "diagnosis_services_running": "¡El servicio {service} está en ejecución!", + "diagnosis_sshd_config_inconsistent": "Parece que el puerto SSH ha sido modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, hay un nuevo parámetro global 'security.ssh.ssh_port' disponible para evitar modificar manualmente la configuración.", + "diagnosis_sshd_config_inconsistent_details": "Por favor ejecute yunohost settings set security.ssh.ssh_port -v TU_PUERTO_SSH para definir el puerto SSH, y compruebe las diferencias yunohost tools regen-conf ssh --dry-run --with-diff y yunohost tools regen-conf ssh --force para resetear tu configuración a las recomendaciones de YunoHost.", + "diagnosis_sshd_config_insecure": "Parece que la configuración SSH ha sido modificada manualmente, y es insegura porque no tiene ninguna instrucción 'AllowGroups' o 'AllowUsers' para limitar el acceso a los usuarios autorizados.", + "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_swap_ok": "El sistema tiene {total} de espacio de intercambio!", + "diagnosis_swap_tip": "Por favor, tenga cuidado y sepa que si el servidor contiene swap en una tarjeta SD o un disco duro de estado sólido, esto reducirá drásticamente la vida útil del dispositivo.", + "diagnosis_unknown_categories": "Las siguientes categorías están desconocidas: {categories}", + "diagnosis_using_stable_codename": "apt (el gestor de paquetes del sistema) está configurado actualmente para instalar paquetes de nombre en clave 'estable', en lugar del nombre en clave de la versión actual de Debian (bookworm).", + "diagnosis_using_stable_codename_details": "Esto suele deberse a una configuración incorrecta de su proveedor de alojamiento. Esto es peligroso, porque tan pronto como la siguiente versión de Debian se convierta en la nueva 'estable', apt querrá actualizar todos los paquetes del sistema sin pasar por un procedimiento de migración adecuado. Se recomienda arreglar esto editando la fuente de apt para el repositorio base de Debian, y reemplazar la palabra clave stable por bookworm. El fichero de configuración correspondiente debería ser /etc/apt/sources.list, o un fichero en /etc/apt/sources.list.d/.", + "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.", + "disk_space_not_sufficient_install": "No hay espacio libre suficiente para instalar esta aplicación", + "disk_space_not_sufficient_update": "No hay espacio libre suficiente para actualizar esta aplicación", + "domain_cannot_remove_main": "No puede eliminar '{domain}' ya que es el dominio principal, primero debe configurar otro dominio como el dominio principal usando 'yunohost domain main-domain -n '; Aquí está la lista de dominios candidatos: {other_domains}", + "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}'.", + "domain_cert_gen_failed": "No se pudo generar el certificado", + "domain_config_acme_eligible": "Requisitos ACME", + "domain_config_acme_eligible_explain": "Este dominio no parece estar preparado para un certificado Let's Encrypt. Compruebe la configuración DNS y la accesibilidad del servidor HTTP. Las secciones \"Registros DNS\" y \"Web\" de la página de diagnóstico pueden ayudarte a entender qué está mal configurado.", + "domain_config_api_protocol": "Protocolo de API", + "domain_config_auth_application_key": "LLave de Aplicación", + "domain_config_auth_application_secret": "LLave de aplicación secreta", + "domain_config_auth_consumer_key": "Llave de consumidor", + "domain_config_auth_entrypoint": "Punto de entrada de la API", + "domain_config_auth_key": "Llave de autenticación", + "domain_config_auth_secret": "Secreto de autenticación", + "domain_config_auth_token": "Token de autenticación", + "domain_config_cert_install": "Instalar el certificado Let's Encrypt", + "domain_config_cert_issuer": "Autoridad de certificación", + "domain_config_cert_name": "Certificado", + "domain_config_cert_no_checks": "Ignorar las comprobaciones de diagnóstico", + "domain_config_cert_renew": "Renovar el certificado Let's Encrypt", + "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": "Estado del certificado", + "domain_config_cert_summary_abouttoexpire": "El certificado actual está a punto de caducar. Pronto debería renovarse automáticamente.", + "domain_config_cert_summary_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_ok": "Muy bien, ¡el certificado actual tiene buena pinta!", + "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_validity": "Validez", + "domain_config_custom_css": "Hoja de estilo CSS personalizada", + "domain_config_custom_css_help": "Reservado a los administradores que desean personalisar el diseño del portal usuario", + "domain_config_default_app": "App predeterminada", + "domain_config_default_app_help": "Los usuarios serán automáticamente redirigidos a esta aplicación cuando visiten este dominio. Si no especifica una aplicación, estos serán redirigidos al formulario del portal de usuarios.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Mostrar la lista de las aplicaciones públicas a los visitantes", + "domain_config_enable_public_apps_page_help": "Los visitantes verán una pagina de « aplicaciones publicas » en lugar de encontrarse con el formulario de conexión.", + "domain_config_feature_name": "Funcionalidades", + "domain_config_mail_in": "Correos entrantes", + "domain_config_mail_out": "Correos salientes", + "domain_config_portal_logo": "Logo personalizado", + "domain_config_portal_logo_help": "Los formatos aceptados .svg, .png, .jpg. prefiere un .svg monocromatico con fill: currentColor asi el logo se adaptara al tema.", + "domain_config_portal_name": "Personalización de la portada", + "domain_config_portal_public_intro": "Presentación publica personalizada", + "domain_config_portal_public_intro_help": "Puedes usar HTML, estilos básicos seran afectados a los elementos genéricos.", + "domain_config_portal_theme": "Tema de colores por defecto", + "domain_config_portal_theme_help": "Los usuarios pueden cambiar el tema de color en sus parámetros.", + "domain_config_portal_tile_theme": "Apariencia de las tejas de aplicaciones", + "domain_config_portal_title": "Titulo personalizado", + "domain_config_portal_user_intro": "Introducción de usuario personalizada", + "domain_config_portal_user_intro_help": "Puede usa HTML. los estilos base serán aplicados a los elementos genéricos.", + "domain_config_search_engine": "URL del buscador", + "domain_config_search_engine_help": "Es una funcionalidad optativa que permite mostrar la barra de búsqueda en el portal de usuario (para usar la como pagina principal de su navegador). Debe ser una URL con una cadena de vacia como `https://duckduckgo.com/?q=`, con `q=` como parámetro vació de duckduckgo", + "domain_config_search_engine_name": "Nombre del buscador", + "domain_config_show_other_domains_apps": "Mostrar aplicaciones de otros dominios", + "domain_created": "Dominio creado", + "domain_creation_failed": "No se puede crear el dominio {domain}: {error}", + "domain_deleted": "Dominio eliminado", + "domain_deletion_failed": "No se puede eliminar el dominio {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "Este comando muestra la configuración *recomendada*. No configura las entradas DNS por ti. Es tu responsabilidad configurar la zona DNS en su registrador según esta recomendación.", + "domain_dns_conf_special_use_tld": "Este dominio se basa en un dominio de primer nivel (TLD) de usos especiales como .local o .test y no debería tener entradas DNS reales.", + "domain_dns_push_already_up_to_date": "Registros ya al día, nada que hacer.", + "domain_dns_push_failed": "La actualización de las entradas DNS ha fallado.", + "domain_dns_push_failed_to_list": "Error al enumerar los registros actuales mediante la API del registrador: {error}", + "domain_dns_push_managed_in_parent_domain": "La configuración automática de los registros DNS es administrada desde el dominio superior {parent_domain}.", + "domain_dns_push_not_applicable": "La configuración automática de los registros DNS no puede realizarse en el dominio {domain}. Deberías configurar manualmente los registros DNS siguiendo la documentación.", + "domain_dns_push_partial_failure": "Entradas DNS actualizadas parcialmente: algunas advertencias/errores reportados.", + "domain_dns_push_record_failed": "No se pudo {action} registrar {type}/{name}: {error}", + "domain_dns_push_success": "¡Registros DNS actualizados!", + "domain_dns_pushing": "Empujando registros DNS…", + "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_registrar_managed_in_parent_domain": "Este dominio es un subdominio de {parent_domain_link}. La configuración del registrador de DNS debe administrarse en el panel de configuración de {parent_domain}.", + "domain_dns_registrar_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://doc.yunohost.org/dns_config.", + "domain_dns_registrar_supported": "YunoHost detectó automáticamente que este dominio es manejado por el registrador **{registrar}**. Si lo desea, YunoHost configurará automáticamente esta zona DNS, si le proporciona las credenciales de API adecuadas. Puede encontrar documentación sobre cómo obtener sus credenciales de API en esta página: https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (También puede configurar manualmente sus registros DNS siguiendo la documentación en https://doc.yunohost.org/dns_config)", + "domain_dns_registrar_use_auto": "Usar la funcionalidad de DNS automatico", + "domain_dns_registrar_yunohost": "Este dominio es un nohost.me / nohost.st / ynh.fr y, por lo tanto, YunoHost maneja automáticamente su configuración de DNS sin ninguna configuración adicional. (vea el comando 'yunohost dyndns update')", + "domain_dyndns_already_subscribed": "Ya se ha suscrito a un dominio de DynDNS", + "domain_exists": "El dominio ya existe", + "domain_hostname_failed": "No se pudo establecer un nuevo nombre de anfitrión («hostname»). Esto podría causar problemas más tarde (no es seguro… podría ir bien).", + "domain_registrar_is_not_configured": "El registrador aún no ha configurado el dominio {domain}.", + "domain_remove_confirm_apps_removal": "La supresión de este dominio también eliminará las siguientes aplicaciones:\n{apps}\n\n¿Seguro? [{answers}]", + "domain_uninstall_app_first": "Estas aplicaciones siguen instaladas en tu dominio:\n{apps}\n\nPor favor desinstálalas con el comando 'yunohost app remove the_app_id' o cámbialas a otro dominio usando pulsando aquí", + "log_link_to_log": "Registro completo de esta operación: «{desc}»", + "log_operation_unit_unclosed_properly": "La unidad de operación no se ha cerrado correctamente", + "log_regen_conf": "Regenerar la configuración del sistema «{}»", + "log_remove_on_failed_install": "Eliminar «{}» después de una instalación fallida", + "log_resource_snippet": "Aprovisionar/desaprovisionar/actualizar un recurso", + "log_selfsigned_cert_install": "Instalar el certificado auto-firmado en el dominio '{}'", + "log_settings_reset": "Restablecer ajuste", + "log_settings_reset_all": "Restablecer todos los ajustes", + "log_settings_set": "Aplicar ajustes", + "log_tools_migrations_migrate_forward": "Inicializa la migración", + "log_tools_postinstall": "Posinstalación del servidor YunoHost", + "log_tools_reboot": "Reiniciar el servidor", + "log_tools_shutdown": "Apagar el servidor", + "log_tools_update": "Buscando actualizaciones del sistema y actualizando las aplicaciones del catálogo", + "log_tools_upgrade": "Actualizar paquetes del sistema", + "log_user_create": "Añadir usuario «{}»", + "log_user_delete": "Eliminar usuario «{}»", + "log_user_group_create": "Crear grupo «{}»", + "log_user_group_delete": "Eliminar grupo «{}»", + "log_user_group_update": "Actualizar grupo «{}»", + "log_user_import": "Importar usuarios", + "log_user_update": "Actualizar la información de usuario de «{}»", + "mail_alias_remove_failed": "No se pudo eliminar el alias de correo «{mail}»", + "mail_alias_unauthorized": "No están autorizado a agregar aliases ligados al dominio '{domain}'", + "mail_already_exists": "La dirección '{mail}' ya existe", + "mail_domain_unknown": "Dirección de correo no válida para el dominio «{domain}». Use un dominio administrado por este servidor.", + "mail_edit_operation_unauthorized": "No tiene autorización para hacer este cambio en su cuenta.", + "mail_forward_remove_failed": "No se pudo eliminar el reenvío de correo «{mail}»", + "mail_unavailable": "Esta dirección de correo electrónico está reservada para el grupo de administradores", + "mailbox_disabled": "Correo desactivado para usuario {user}", + "mailbox_used_space_dovecot_down": "El servicio de buzón Dovecot debe estar activo si desea recuperar el espacio usado del buzón", + "main_domain_change_failed": "No se pudo cambiar el dominio principal", + "main_domain_changed": "El dominio principal ha cambiado", + "migration_0027_cleaning_up": "Limpiando el cache y los paquetes que no son más útiles…", + "migration_0027_delayed_api_restart": "El API de YunoHost estará automáticamente reiniciado en 15 segundos. Estará inaccesible por segundos, y tendera que ingresar de nuevo.", + "migration_0027_general_warning": "Finalmente, tenga en cuenta que esta migración es una operación delicada. El equipo de YunoHost ha hecho todo lo posible por examinarla y probarla, pero la migración aún puede romper partes del sistema o de sus aplicaciones.\n\nPor lo tanto, se recomienda:\n\n - **Realizar copias de seguridad** de todos los datos o aplicaciones críticos. Más información en https://doc.yunohost.org/backup;\n - **Ser paciente** después de iniciar la migración: dependiendo de su conexión a Internet y de su material, la actualización puede tardar hasta una hora en completarse correctamente;\n - **Contactar a la comunidad** en el foro si necesita ayuda para resolver problemas.", + "migration_0027_main_upgrade": "Inicio de la actualización del sistema …", + "migration_0027_modified_files": "Anota que los archivos siguiente fueron modificados manualmente y podrían estar reiniciados al aplicar actualización: {manually_modified_files}", + "migration_0027_not_bullseye": "¡ Bullseye no es la distribución Debian actual ! Si ya migraron de Bullseye a Bookworm, este incidente indica que no se logro la migración completa. Se recomienda investigar con el equipo de soporte y para esto los necesitaran los registros **completos** de la migración, que pueden encontrar en Herramientas > Registros.", + "migration_0027_not_enough_free_space": "¡ Queda muy poco espacio disponible en /var/ ! debes disponer de al menos 1Go disponible para iniciar esta migración.", + "migration_0027_patch_yunohost_conflicts": "Aplicación de un correctivo para resolver el problema de conflicto …", + "migration_0027_patching_sources_list": "Corrigiendo del archivo sources.lists.…", + "migration_0027_problematic_apps_warning": "Por favor, tenga presente que se han detectado aplicaciones instaladas que pueden presentar problemas. Parece que no se instalaron a partir del catálogo de aplicaciones de YunoHost o que no están marcadas como 'funcionales'. Por lo tanto, no se puede garantizar que continúen funcionando después de la actualización: {problematic_apps}", + "migration_0027_start": "Inicio de la migración hacia Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Algo mal ocurrió durante la actualización del sistema. Parece estar en Debian Bullseye todavía.", + "migration_0027_system_not_fully_up_to_date": "Su sistema no esta totalmente actualizado. Por favor actualice de manera clásica antes de proceder a la migración hacia Bookworm.", + "migration_0027_yunohost_upgrade": "Inicio de la actualización de corazón de YunoHost.…", + "migration_not_enough_space": "Borra suficiente espacio en {path} para iniciar la migración.", + "migration_postgresql_previous_not_installed": "PostgreSQL no fue installado en su sistema. Nada por hacer.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 esta installado, pero no PostgreSQL 15!? algo extraño paso en su sistema :(…", + "migration_python_venv_rebuild_broken_app": "{app} esta ignorada porque virtualenv no puede ser facilmente reconstruido. Puedes resolver la situación forzando la actualización de esta aplicación usando `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_disclaimer_base": "Después de la la migración a Debian Bookrworm, ciertas aplicaciones de Python deben estar parcialmente reconstruidas para estar convertidas hacia la nueva versión de Python entregada con Debian ( técnicamente : el virtuelenv debe reconstruirse). Esperando, estas aplicaciones Python pueden no funcionar. Yunohost puede intentar reconstruir virtualenv para algunas de ellas, como indicado abajo. Para otras aplicaciones, o si el intento fallo, deberán forzar la reconstrucción manualmente haciendo una actualización de estas.", + "migration_python_venv_rebuild_disclaimer_ignored": "Los ambientes virtuales no pueden ser reconstruidos automáticamente para estas aplicaciones. Debe furzar la actualización en linea de comando con `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "La reconstrucción del ambiente virtual sera probado para :{rebuild_apps} (¡y esto puede demorar!)", + "migration_python_venv_rebuild_failed": "Fue imposible reconstituir el ambiente python para {app}. La aplicación puede no funcionar mientras este problema no esta resuelto. Debe forzar la actualización con la ayuda de la interfaces de comando ingresando `yunohost app upgrade --force {app}'.", + "migration_python_venv_rebuild_in_progress": "Estamos intentando reconstruir el ambiente virtual python para `{app}`", + "migration_0031_terms_of_services": "Esta migración es simplemente un informativo sobre el hecho que desde ahora el proyecto YunoHost pública condiciones de uso asociadas a los servicios técnico y comunitarios.", + "migration_description_0027_migrate_to_bookworm": "Actualizar el sistema a Debian Bookworm y YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Supression de los antiguos permisos para XMPP. Metronome es desde ahora una aplicación", + "migration_description_0029_postgresql_13_to_15": "Migrar las bases de datos desde PostgreSQL 13 hacia 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Reparar la aplicación Python después de la migración Bookwoorm", + "migration_description_0031_terms_of_services": "Términos de uso", + "migration_description_0032_firewall_config": "Migración. Del archivo de configuración del cortafuego interno", + "migration_description_0033_rework_permission_infos": "Rearmando la manera de guardar las autorizaciones de las aplicaciones", + "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.", + "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_rollback_success": "Sistema revertido.", + "migrations_already_ran": "Esas migraciones ya se han realizado: {ids}", + "migrations_dependencies_not_satisfied": "Ejecutar estas migraciones: «{dependencies_id}» antes de migrar {id}.", + "migrations_exclusive_options": "«--auto», «--skip», and «--force-rerun» son opciones mutuamente excluyentes.", + "migrations_failed_to_load_migration": "No se pudo cargar la migración {id}: {error}", + "migrations_list_conflict_pending_done": "No puede usar «--previous» y «--done» al mismo tiempo.", + "migrations_loading_migration": "Cargando migración {id}…", + "migrations_migration_has_failed": "La migración {id} no se ha completado, cancelando. Error: {exception}", + "migrations_must_provide_explicit_targets": "Necesita proporcionar objetivos explícitos al usar «--skip» or «--force-rerun»", + "migrations_need_to_accept_disclaimer": "Para ejecutar la migración {id} debe aceptar el siguiente descargo de responsabilidad:\n---\n{disclaimer}\n---\nSi acepta ejecutar la migración, vuelva a ejecutar la orden con la opción «--accept-disclaimer».", + "migrations_no_migrations_to_run": "No hay migraciones que ejecutar", + "migrations_no_such_migration": "No hay ninguna migración llamada «{id}»", + "migrations_not_pending_cant_skip": "Esas migraciones no están pendientes, así que no pueden ser omitidas: {ids}", + "migrations_pending_cant_rerun": "Esas migraciones están aún pendientes, así que no se pueden volver a ejecutar: {ids}", + "migrations_running_forward": "Ejecutando migración {id}…", + "migrations_skip_migration": "Omitiendo migración {id}…", + "migrations_success_forward": "Migración {id} completada", + "migrations_to_be_ran_manually": "La migración {id} hay que ejecutarla manualmente. Vaya a Herramientas → Migraciones en la página web de administración o ejecute `yunohost tools migrations run`.", + "nftables_unavailable": "No puede modificar nftables aquí. O bien está en un 'container' o su kernel no soporta esta opción", + "noninteractive_task": "Tarea no interactiva", + "not_enough_disk_space": "No hay espacio libre suficiente en «{path}»", + "operation_interrupted": "¿La operación fue interrumpida manualmente?", + "other_available_options": "… y {n} otras opciones disponibles no mostradas", + "password_confirmation_not_the_same": "La contraseña y su confirmación no coinciden", + "password_listed": "Esta contraseña se encuentra entre las contraseñas más utilizadas del mundo. Por favor, elija algo menos común y más robusto.", + "password_too_long": "Elija una contraseña de menos de 127 caracteres", + "password_too_simple_1": "La contraseña debe tener al menos 8 caracteres de longitud", + "password_too_simple_2": "La contraseña debe ser de al menos 8 caracteres de longitud e incluir un número y caracteres en mayúsculas y minúsculas", + "password_too_simple_3": "La contraseña debe ser de al menos 8 caracteres de longitud e incluir un número y caracteres en mayúsculas, minúsculas y caracteres especiales", + "password_too_simple_4": "La contraseña debe ser de al menos 12 caracteres de longitud e incluir un número, mayúsculas, minúsculas y caracteres especiales", + "pattern_backup_archive_name": "Debe ser un nombre de archivo válido con un máximo de 30 caracteres, solo se admiten caracteres alfanuméricos y los caracteres -_. (guiones y punto)", + "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", + "pattern_email": "Debe ser una dirección de correo electrónico válida, sin el símbolo '+' (ej. alguien@ejemplo.com)", + "pattern_email_forward": "Debe ser una dirección de correo electrónico válida, se acepta el símbolo '+' (por ejemplo, alguien+etiqueta@ejemplo.com)", + "pattern_fullname": "Debe ser un nombre completo válido (al menos 3 caracteres)", + "pattern_mailbox_quota": "Debe ser un tamaño con el sufijo «b/k/M/G/T» o «0» para no tener una cuota", + "pattern_password": "Debe contener al menos 3 caracteres", + "pattern_password_app": "Las contraseñas no pueden incluir los siguientes caracteres: {forbidden_chars}", + "pattern_port_or_range": "Debe ser un número de puerto válido (es decir entre 0-65535) o un intervalo de puertos (por ejemplo 100:200)", + "pattern_username": "Solo puede contener caracteres alfanuméricos o el guión bajo", + "permission_already_allowed": "El grupo «{group}» ya tiene el permiso «{permission}» activado", + "permission_already_disallowed": "El grupo '{group}' ya tiene el permiso '{permission}' deshabilitado", + "permission_cannot_remove_main": "No está permitido eliminar un permiso principal", + "permission_cant_add_to_all_users": "El permiso {permission} no se puede agregar a todos los usuarios.", + "permission_created": "Creado el permiso «{permission}»", + "permission_creation_failed": "No se pudo crear el permiso «{permission}»: {error}", + "permission_currently_allowed_for_all_users": "Este permiso se concede actualmente a todos los usuarios además de los otros grupos. Probablemente quiere o eliminar el permiso de «all_users» o eliminar los otros grupos a los que está otorgado actualmente.", + "permission_deleted": "Eliminado el permiso «{permission}»", + "permission_deletion_failed": "No se pudo eliminar el permiso «{permission}»: {error}", + "permission_not_found": "No se encontró el permiso «{permission}»", + "permission_protected": "Permiso {permission} está protegido. No puede agregar o quitar el grupo de visitantes a/desde este permiso.", + "permission_require_account": "El permiso {permission} solo tiene sentido para usuarios con una cuenta y, por lo tanto, no se puede activar para visitantes.", + "permission_update_failed": "No se pudo actualizar el permiso '{permission}': {error}", + "permission_updated": "Actualizado el permiso «{permission}»", + "port_already_closed": "El puerto {port} ya está cerrado", + "port_already_opened": "El puerto {port} ya está abierto", + "postinstall_low_rootfsspace": "El sistema de archivos raíz tiene un espacio total inferior a 10 GB, ¡lo cual es bastante preocupante! ¡Es probable que se quede sin espacio en disco muy rápidamente! Se recomienda tener al menos 16 GB para el sistema de archivos raíz. Si desea instalar YunoHost a pesar de esta advertencia, vuelva a ejecutar la instalación posterior con --force-diskspace", + "pydantic_type_error": "Tipo invalido.", + "pydantic_type_error_none_not_allowed": "Valor requerido.", + "pydantic_type_error_str": "Tipo inválido, texto esperado.", + "pydantic_value_error_color": "Color invalido, debe ser un nombre de color o un valor hexadecimal.", + "pydantic_value_error_const": "Valor inesperado, elija entre {permitted}", + "pydantic_value_error_date": "Formato de fecha invalido", + "pydantic_value_error_email": "El valor no es un correo electrónico valido", + "pydantic_value_error_number_not_ge": "El valor debe ser superior o egal a {limit_value}.", + "pydantic_value_error_number_not_le": "El valor deber ser menor o egal a {limit_value}.", + "pydantic_value_error_str_regex": "Cadena invalida; el valor no respeta el patrón '{pattern}'", + "pydantic_value_error_time": "Formato de tiempo invalido", + "pydantic_value_error_url_extra": "URL invalido, carácter adicional encontrado después de una URL valida : '{extra}'", + "pydantic_value_error_url_host": "URL del servidor invalida", + "pydantic_value_error_url_port": "Puerto invalido, no puede superar 65536", + "pydantic_value_error_url_scheme": "Modelo de URL invalido o ausente", + "regenconf_dry_pending_applying": "Comprobando la configuración pendiente que habría sido aplicada para la categoría «{category}»…", + "regenconf_failed": "No se pudo regenerar la configuración para la(s) categoría(s): {categories}", + "regenconf_file_backed_up": "Archivo de configuración «{conf}» respaldado en «{backup}»", + "regenconf_file_copy_failed": "No se pudo copiar el nuevo archivo de configuración «{new}» a «{conf}»", + "regenconf_file_kept_back": "Se espera que el archivo de configuración «{conf}» sea eliminado por regen-conf (categoría {category}) pero ha sido retenido.", + "regenconf_file_manually_modified": "El archivo de configuración «{conf}» ha sido modificado manualmente y no será actualizado", + "regenconf_file_manually_removed": "El archivo de configuración «{conf}» ha sido eliminado manualmente y no se creará", + "regenconf_file_remove_failed": "No se pudo eliminar el archivo de configuración «{conf}»", + "regenconf_file_removed": "Eliminado el archivo de configuración «{conf}»", + "regenconf_file_updated": "Actualizado el archivo de configuración «{conf}»", + "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.", + "regenconf_now_managed_by_yunohost": "El archivo de configuración «{conf}» está gestionado ahora por YunoHost (categoría {category}).", + "regenconf_pending_applying": "Aplicando la configuración pendiente para la categoría '{category}'…", + "regenconf_up_to_date": "Ya está actualizada la configuración para la categoría «{category}»", + "regenconf_updated": "Configuración actualizada para '{category}'", + "regenconf_would_be_updated": "La configuración habría sido actualizada para la categoría «{category}»", + "regex_incompatible_with_tile": "/!\\ Empaquetadores! El permiso '{permission}' tiene show_tile establecido en 'true' y, por lo tanto, no puede definir una URL de expresión regular como la URL principal", + "regex_with_only_domain": "No puede usar una expresión regular para el dominio, solo para la ruta", + "registrar_infos": "Información sobre el registrador", + "restore_already_installed_app": "Una aplicación con el ID «{app}» ya está instalada", + "restore_already_installed_apps": "Las siguientes aplicaciones no se pueden restaurar porque ya están instaladas: {apps}", + "restore_backup_too_old": "Este archivo de copia de seguridad no se puede restaurar porque proviene de una versión de YunoHost demasiado antigua.", + "restore_cleaning_failed": "No se pudo limpiar el directorio temporal de restauración", + "restore_complete": "Restauración completada", + "restore_confirm_yunohost_installed": "¿Realmente desea restaurar un sistema ya instalado? [{answers}]", + "restore_extracting": "Extrayendo los archivos necesarios para el archivo…", + "restore_failed": "No se pudo restaurar el sistema", + "restore_hook_unavailable": "El script de restauración para «{part}» no está disponible en su sistema y tampoco en el archivo", + "restore_may_be_not_enough_disk_space": "Parece que su sistema no tiene suficiente espacio (libre: {free_space} B, espacio necesario: {needed_space} B, margen de seguridad: {margin} B)", + "restore_not_enough_disk_space": "Espacio insuficiente (espacio: {free_space} B, espacio necesario: {needed_space} B, margen de seguridad: {margin} B)", + "restore_nothings_done": "No se ha restaurado nada", + "restore_removing_tmp_dir_failed": "No se pudo eliminar un directorio temporal antiguo", + "restore_running_app_script": "Restaurando la aplicación «{app}»…", + "restore_running_hooks": "Ejecutando los ganchos de restauración…", + "restore_system_part_failed": "No se pudo restaurar la parte del sistema «{part}»", + "root_password_changed": "la contraseña de root fue cambiada", + "root_password_desynchronized": "La contraseña de administración ha sido cambiada pero ¡YunoHost no pudo propagar esto a la contraseña de root!", + "server_reboot": "El servidor se reiniciará", + "server_reboot_confirm": "El servidor se reiniciará inmediatamente ¿está seguro? [{answers}]", + "server_shutdown": "El servidor se apagará", + "server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers}]", + "service_add_failed": "No se pudo añadir el servicio «{service}»", + "service_added": "Se agregó el servicio '{service}'", + "service_already_started": "El servicio «{service}» ya está funcionando", + "service_already_stopped": "El servicio «{service}» ya ha sido detenido", + "service_cmd_exec_failed": "No se pudo ejecutar la orden «{command}»", + "service_description_dnsmasq": "Maneja la resolución de nombres de dominio (DNS)", + "service_description_dovecot": "Permite a los clientes de correo acceder/obtener correo (vía IMAP y POP3)", + "service_description_fail2ban": "Protege contra ataques de fuerza bruta y otras clases de ataques desde Internet", + "service_description_mysql": "Almacena los datos de la aplicación (base de datos SQL)", + "service_description_nftables": "Gestiona los puertos de conexiones abiertos y cerrados a los servicios", + "service_description_nginx": "Sirve o proporciona acceso a todos los sitios web alojados en su servidor", + "service_description_opendkim": "Fi4mar los correos electrónicos saliente utilizando DKIM para evitar que sean marcados como spam", + "service_description_postfix": "Usado para enviar y recibir correos", + "service_description_postgresql": "Almacena datos de aplicaciones (base de datos SQL)", + "service_description_redis-server": "Una base de datos especializada usada para el acceso rápido de datos, cola de tareas y comunicación entre programas", + "service_description_slapd": "Almacena usuarios, dominios e información relacionada", + "service_description_ssh": "Permite conectar a su servidor remotamente mediante un terminal (protocolo SSH)", + "service_description_yunohost-api": "Gestiona las interacciones entre la interfaz web de YunoHost y el sistema", + "service_description_yunohost-portal-api": "Administra las interacciones entre diferentes interfaces web del portal y el sistema", + "service_description_yunomdns": "Le permite llegar a su servidor usando 'yunohost.local' en su red local", + "service_disable_failed": "No se pudo hacer que el servicio '{service}' no se iniciara en el arranque.", + "service_disabled": "El servicio '{service}' ya no se iniciará cuando se inicie el sistema.", + "service_enable_failed": "No se pudo hacer que el servicio '{service}' se inicie automáticamente en el arranque.", + "service_enabled": "El servicio '{service}' ahora se iniciará automáticamente durante el arranque del sistema.", + "service_not_reloading_because_conf_broken": "No recargar/reiniciar el servicio '{name}' porque su configuración está rota: {errors}", + "service_reload_failed": "No se pudo recargar el servicio «{service}»", + "service_reload_or_restart_failed": "No se pudo recargar o reiniciar el servicio «{service}»", + "service_reloaded": "Servicio '{service}' recargado", + "service_reloaded_or_restarted": "El servicio '{service}' fue recargado o reiniciado", + "service_remove_failed": "No se pudo eliminar el servicio «{service}»", + "service_removed": "Servicio '{service}' eliminado", + "service_restart_failed": "No se pudo reiniciar el servicio «{service}»", + "service_restarted": "Servicio '{service}' reiniciado", + "service_start_failed": "No se pudo iniciar el servicio «{service}»", + "service_started": "El servicio '{service}' comenzó", + "service_stop_failed": "Imposible detener el servicio '{service}'", + "service_stopped": "Servicio '{service}' detenido", + "service_unknown": "Servicio desconocido '{service}'", + "session_expired": "Sesión vencida", + "show_tile_cant_be_enabled_for_regex": "No puede habilitar 'show_tile' en este momento porque la URL para el permiso '{permission}' es una expresión regular", + "show_tile_cant_be_enabled_for_url_not_defined": "No puede habilitar 'show_tile' en este momento, porque primero debe definir una URL para el permiso '{permission}'", + "ssowat_conf_generated": "La configuración del SSO y del portal fueron regenerados", + "system_upgraded": "Sistema actualizado", + "system_username_exists": "El nombre de usuario ya existe en la lista de usuarios del sistema", + "this_action_broke_dpkg": "Esta acción rompió dpkg/APT(los gestores de paquetes del sistema)… Puedes tratar de solucionar este problema conectándote mediante SSH y ejecutando `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", + "tools_upgrade": "Actualizando paquetes del sistema", + "tools_upgrade_failed": "No se pudieron actualizar los paquetes: {packages_list}", + "tos_dyndns_acknowledgement": "Eligiste registrar un dominio con DynDNS, el cual es un servicio entregado por el proyecto YunoHost. Considerando que este nombre de domino es un elemento digital clave a largo plazo, te recordamos que debes leer con cuidado los términos de servicios, en especifico la sección de los dominios gratuitos : .", + "tos_postinstall_acknowledgement": "El proyecto YunoHost es un equipo de voluntarios quienes se unieron para crear un sistema operativo libre para servidor, llamado YunoHost. El programa YunoHost esta publicado bajo licencia AGPLv3 (). Relativo a eso, el proyecto administra y disponibiliza varios servicios técnicos y comunautarios diversos. Usando estos aceptan la condiciones siguientes : .", + "unable_authenticate": "La autentificacion falló", + "unbackup_app": "{app} no se guardará", + "unexpected_error": "Algo inesperado salió mal: {error}", + "unknown_error_reading_file": "Error desconocido al intentar leer el archivo {file} (motivo: {error})", + "unknown_group": "Grupo «{group}» desconocido", + "unknown_main_domain_path": "Dominio o ruta desconocidos para '{app}'. Debe especificar un dominio y una ruta para poder especificar una URL para el permiso.", + "unknown_user": "Usuario «{user}» desconocido", + "unlimit": "Sin cuota", + "unrestore_app": "{app} no será restaurada", + "update_apt_cache_failed": "Imposible actualizar la caché de APT (gestor de paquetes de Debian). Aquí tienes un volcado de las líneas de sources.list que podrían ayudarte a identificar las líneas problemáticas:\n{sourceslist}", + "update_apt_cache_warning": "Algo fue mal durante la actualización de la caché de APT (gestor de paquetes de Debian). Aquí tiene un volcado de las líneas de sources.list que podría ayudarle a identificar las líneas problemáticas:\n{sourceslist}", + "updating_apt_cache": "Obteniendo las actualizaciones disponibles para los paquetes del sistema…", + "upgrading_packages": "Actualizando paquetes…", + "upnp_dev_not_found": "No se encontró ningún dispositivo UPnP", + "upnp_disabled": "UPnP desactivado", + "upnp_enabled": "UPnP activado", + "upnp_port_open_failed": "No se pudo abrir el puerto vía UPnP", + "user_already_exists": "El usuario «{user}» ya existe", + "user_cannot_delete_last_admin": "La cuenta '{user}' es el único administrador por lo cual no sera suprimido.", + "user_created": "Usuario creado", + "user_creation_failed": "No se pudo crear el usuario {user}: {error}", + "user_deleted": "Usuario eliminado", + "user_deletion_failed": "No se pudo eliminar el usuario {user}: {error}", + "user_home_creation_failed": "No se pudo crear la carpeta de inicio '{home}' para el usuario", + "user_import_bad_file": "Su archivo CSV no tiene el formato correcto, se ignorará para evitar una posible pérdida de datos", + "user_import_bad_line": "Línea incorrecta {line}: {details}", + "user_import_cannot_edit_or_delete_admins": "Imposible editar o suprimir '{user}' vía la función importar porque el usuario es administrador", + "user_import_failed": "La operación de importación de usuarios falló por completo", + "user_import_missing_columns": "Faltan las siguientes columnas: {columns}", + "user_import_nothing_to_do": "Ningún usuario necesita ser importado", + "user_import_partial_failed": "La operación de importación de usuarios falló parcialmente", + "user_import_success": "Usuarios importados exitosamente", + "user_unknown": "Usuario desconocido: {user}", + "user_update_failed": "No se pudo actualizar el usuario {user}: {error}", + "user_updated": "Cambiada la información de usuario", + "visitors": "Visitantes", + "yunohost_already_installed": "YunoHost ya está instalado", + "yunohost_api": "API YunoHost", + "yunohost_configured": "YunoHost está ahora configurado", + "yunohost_installing": "Instalando YunoHost…", + "yunohost_not_installed": "YunoHost no está correctamente instalado. Ejecute «yunohost tools postinstall»", + "yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, por favor considere:\n - diagnosticar problemas potenciales a través de la sección 'Diagnóstico' del administrador web (o 'yunohost diagnosis run' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo YunoHost' en la documentación del administrador: https://doc.yunohost.org/admin." +} diff --git a/locales/eu.json b/locales/eu.json new file mode 100644 index 0000000..e0fc489 --- /dev/null +++ b/locales/eu.json @@ -0,0 +1,919 @@ +{ + "aborting": "Bertan behera uzten.", + "action_invalid": "'{action}' eragiketa baliogabea da", + "additional_urls_already_added": "'{url}' URL gehigarria '{permission}' baimenerako gehitu da lehendik ere", + "additional_urls_already_removed": "'{url}' URL gehigarriari '{permission}' baimena kendu zaio lehendik ere", + "admin_password": "Administrazio-pasahitza", + "admins": "Administratzaileek", + "all_users": "YunoHosten erabiltzaile guztiek", + "already_up_to_date": "Ez dago egiteko ezer. Guztia dago egunean.", + "app_action_broke_system": "Eragiketa honek {services} zerbitzu garrantzitsua(k) hondatu d(it)uela dirudi", + "app_action_cannot_be_ran_because_required_services_down": "{services} zerbitzuak martxan egon beharko lirateke eragiketa hau exekutatu ahal izateko. Saia zaitez zerbitzuok berrabiarazten (eta ikertu zergatik ez diren abiarazi).", + "app_action_failed": "{app} aplikaziorako {action} eragiketak huts egin du", + "app_already_installed": "{app} instalatuta dago lehendik ere", + "app_already_installed_cant_change_url": "Aplikazio hau instalatuta dago lehendik ere. URLa ezin da aldatu aukera honekin. Markatu 'app changeurl' erabilgarri badago.", + "app_arch_not_supported": "Aplikazio hau {required} arkitekturan instala daiteke bakarrik, baina zure zerbitzariaren arkitektura {current} da", + "app_argument_choice_invalid": "Hautatu ({choices}) aukeretako bat '{name}' argumenturako: '{value}' ez dago aukera horien artean", + "app_argument_invalid": "Aukeratu balio egoki bat '{name}' argumenturako: {error}", + "app_change_url_failed": "Ezin izan da {app} aplikazioaren URLa aldatu: {error}", + "app_change_url_identical_domains": "Domeinu zahar eta berriaren bidea bera dira: ('{domain}{path}'), ez dago ezer egitekorik.", + "app_change_url_no_script": "'{app_name}' aplikazioak oraingoz ez du URLa moldatzerik onartzen. Agian eguneratu beharko zenuke.", + "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_change_url_success": "{app} aplikazioaren URLa {domain}{path} da orain", + "app_config__core_name": "Lauzak eta baimenak", + "app_config_permission_allowed": "Sarbidea baimenduta duten taldeak/erabiltzaileak", + "app_config_permission_allowed_warn_protected": "Ohart ongi: baimena 'babestuta' da, eta, beraz, 'bisitariak' taldea ezin da gehitu/kendu baimendutako taldeetatik.", + "app_config_permission_description": "Deskribapena", + "app_config_permission_description_help": "Erabilgarria da soilik atariaren modu deskribatzailea erabiltzen baduzu", + "app_config_permission_extraperm_section_name": "'{perm}' baimena", + "app_config_permission_label": "Etiketa", + "app_config_permission_location": "[{absolute_url}]({absolute_url})(r)i dagokio", + "app_config_permission_logo": "Logo propioa", + "app_config_permission_logo_help": "PNG fitxategiak soilik", + "app_config_permission_show_tile": "Erakutsi lauza atarian", + "app_config_unable_to_apply": "Konfigurazio-aukeren ezarpenak huts egin du.", + "app_config_unable_to_read": "Konfigurazio-aukeren irakurketak huts egin du.", + "app_corrupt_source": "YunoHostek deskargatu du {app} aplikaziorako '{source_id}' ({url}) baliabidea, baina ez dator bat espero zen egiaztapen-baturarekin. 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 egiaztapen-batura: {expected_sha256}\n Deskargatutakoaren sha256 egiaztapen-batura: {computed_sha256}\n Deskargatutako fitxategiaren tamaina: {size}", + "app_extraction_failed": "Ezinezkoa izan da instalazio fitxategiak ateratzea", + "app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}", + "app_full_domain_unavailable": "Aplikazio honek bere domeinu propioa behar du, baina beste aplikazio batzuk daude instalatuta lehendik ere '{domain}' domeinuan. Azpidomeinu bat erabil zenezake instalatu nahi duzun aplikaziorako.", + "app_id_invalid": "Aplikazio ID okerra", + "app_install_failed": "Ezin da {app} instalatu: {error}", + "app_install_files_invalid": "Ezin dira fitxategi hauek instalatu", + "app_install_script_failed": "Errore bat gertatu da aplikazioaren instalatzailearen aginduetan", + "app_location_unavailable": "URL hau ez dago erabilgarri edota lehendik ere instalatutako aplikazioren batekin talka egiten du:\n{apps}", + "app_make_default_location_already_used": "Ezin da '{app}' '{domain}' domeinuan lehenetsi, '{other_app}'(e)k lehendik ere erabiltzen duelako", + "app_manifest_install_ask_admin": "Aukeratu administratzaile bat aplikazio honetarako", + "app_manifest_install_ask_domain": "Aukeratu zein domeinutan instalatu nahi duzun aplikazioa", + "app_manifest_install_ask_init_admin_permission": "Nork izan beharko luke aplikazio honetako administrazio-aukeretara sarbidea? (Aldatzea dago)", + "app_manifest_install_ask_init_main_permission": "Nork izan beharko luke aplikazio honetara sarbidea? (Aldatzea dago)", + "app_manifest_install_ask_is_public": "Saiorik hasi gabeko bisitarientzat ikusgai egon beharko luke aplikazioak?", + "app_manifest_install_ask_password": "Aukeratu administrazio-pasahitz bat aplikazio honetarako", + "app_manifest_install_ask_path": "Aukeratu aplikazio hau instalatzeko URLaren bidea (domeinuaren atzeko aldean)", + "app_not_correctly_installed": "Ez dirudi {app} ondo instalatuta dagoenik", + "app_not_enough_disk": "Aplikazio honek {required} espazio libre behar ditu.", + "app_not_enough_ram": "Aplikazio honek {required} RAM behar ditu instalatu edo bertsio-berritzeko, baina {current} bakarrik daude erabilgarri une honetan.", + "app_not_installed": "Ezinezkoa izan da {app} aurkitzea instalatutako aplikazioen zerrendan: {all_apps}", + "app_not_properly_removed": "Ezinezkoa izan da {app} guztiz ezabatzea", + "app_packaging_format_not_supported": "Aplikazio hau ezin da instalatu YunoHostek ez duelako paketea ezagutzen. Sistema eguneratzea hausnartu beharko zenuke ziur asko.", + "app_remove_after_failed_install": "Aplikazioa kentzen instalatzerakoan errorea dela-eta…", + "app_removed": "{app} desinstalatu da", + "app_requirements_checking": "{app}(e)k behar dituen betekizunak egiaztatzen…", + "app_resource_failed": "{app} aplikaziorako baliabideen eguneratzeak / prestaketak / askapenak huts egin du: {error}", + "app_restore_failed": "Ezinezkoa izan da {app} lehengoratzea: {error}", + "app_restore_script_failed": "Errorea gertatu da aplikazioa lehengoratzeko aginduan", + "app_sources_fetch_failed": "Ezinezkoa izan da fitxategiak eskuratzea, zuzena al da URLa?", + "app_start_backup": "{app}(r)en babeskopia egiteko fitxategiak eskuratzen…", + "app_start_install": "{app} instalatzen…", + "app_start_remove": "{app} kentzen…", + "app_start_restore": "{app} lehengoratzen…", + "app_unknown": "Aplikazio ezezaguna", + "app_unsupported_remote_type": "Aplikazioak darabilen urruneko motak ez du babesik (Unsupported remote type)", + "app_upgrade_app_name": "Orain {app} eguneratzen…", + "app_upgrade_bad_quality": "Aplikazioa hondatutzat ageri da YunoHosten aplikazioen katalogoan. Behin-behineko kontua izan daiteke arduradunek arazoa konpondu bitartean. Oraingoz, ezin da aplikazioa bertsio-berritu.", + "app_upgrade_broke_the_system": "Badirudi {app} bertsio-berritu dela, baina sistema hondatu du eta, hortaz, hutsegitetzat hartu da.", + "app_upgrade_cli_bad_quality": "{app} aplikazioa bertsio-berritu gabe utziko da hondatutzat ageri delako YunoHosten aplikazioen katalogoan.", + "app_upgrade_cli_up_to_date": "{app} dagoeneko egunean dago ({current_version})", + "app_upgrade_cli_url_required": "{app} ez dago katalogoan (egoteari utzi dio?), eta, hortaz, ezin da automatikoki bertsio-berritu. `yunohost app upgrade {app}`erabili beharko zenuke gordailuaren URLa zehazteko, `-u` aukerarekin.", + "app_upgrade_cli_will_force_upgrade": "{app} bertsio-berritzera behartuko da ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} {current_version}(e)tik {new_version}(e)ra bertsio-berrituko da", + "app_upgrade_continuing_with_other_apps": "{app} aplikazioaren bertsio-berritzeak huts egin du, baina beste aplikazio batzuk bertsio-berritzen jarraituko da (`--continue-on-failure` aukera erabili delako)", + "app_upgrade_fail_requirements": "Aplikazio honen bertsio berriago bat dago ({new_version}), baina ez dira betekizun guztiak betetzen:\n{failed_requirements}", + "app_upgrade_failed": "{app} bertsio-berritzeak huts egin du: {error}", + "app_upgrade_failed_and_broke_the_system": "{app} aplikazioaren bertsio-berritzeak huts egin du, eta sistema hondatu du.", + "app_upgrade_script_failed": "Errore bat gertatu da aplikazioaren eguneratze aginduan", + "app_upgrade_several_apps": "Honako aplikazioak eguneratuko dira: {apps}", + "app_upgrade_some_app_failed": "Ezinezkoa izan da aplikazio batzuk eguneratzea", + "app_upgrade_specific_channel_msg": "Bertsio-berritzeko iturri gisa `{channel}`erabiltzen ari zara une honetan. Irakurri zer esaten ari diren [hemen]({pr_url}).", + "app_upgrade_up_to_date": "Aplikazioa bertsio-berritzera behartzea (bertsio bera erabiliz) erabilgarri izan daiteke batzuetan aplikazioa eta konfigurazioa birsortzeko.", + "app_upgrade_upgradable": "Aplikazioa bertsio-berritu daiteke {current_version} bertsiotik {new_version} bertsiora", + "app_upgrade_url_required": "Aplikazioa ez dago katalogoan (egoteari utzi dio?), eta, hortaz, bertsio-berritzeko ardura zurea da aurrerantzean.
`yunohost app upgrade ` komandoa erabil dezakezu eta `-u` aukerarekin gordailuaren URLa zehaztu.", + "app_upgraded": "{app} eguneratu da", + "app_yunohost_version_not_supported": "Aplikazio honek YunoHost >= {required} behar du, baina unean instalatutako bertsioa {current} da.", + "apps_already_up_to_date": "Aplikazio guztiak egunean daude lehendik ere", + "apps_catalog_failed_to_download": "Ezin da {apps_catalog} aplikazioen zerrenda eskuratu: {error}", + "apps_catalog_obsolete_cache": "Aplikazioen katalogoaren katxea hutsik edo zaharkituta dago.", + "apps_catalog_update_success": "Aplikazioen katalogoa eguneratu da!", + "apps_catalog_updating": "Aplikazioen katalogoa eguneratzen…", + "apps_confirm_partial_upgrade": "Bertsio-berritzeko aplikazio batzuk ezin izan dira bertsio-berritu. Besteekin jarraitu nahi al duzu?", + "apps_no_target_can_be_upgraded": "Ez dago bertsio-berritzeko aplikaziorik", + "apps_upgrade_cancelled": "Bertsio-berritze batzuk zain zeuden beste aplikazio batzuetarako, baina bertan behera utzi dira (erabili `--continue-on-failure`edonola ere jarraitzeko): {apps}", + "ask_admin_fullname": "Administratzailearen izen osoa", + "ask_admin_username": "Administratzailearen erabiltzaile-izena", + "ask_dyndns_recovery_password": "DynDNS berreskuratze-pasahitza", + "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.", + "ask_dyndns_recovery_password_explain_unavailable": "DynDNS domeinu hau erregistratuta dago lehendik ere. Domeinua zeuk erregistratu bazenuen, sartu berreskuratze-pasahitza domeinua berreskuratzeko.", + "ask_fullname": "Izen osoa", + "ask_main_domain": "Domeinu nagusia", + "ask_new_admin_password": "Administrazio-pasahitz berria", + "ask_new_domain": "Domeinu berria", + "ask_new_path": "Bide berria", + "ask_password": "Pasahitza", + "ask_user_domain": "Erabiltzailearen posta elektronikorako erabiliko den domeinua", + "automatic_task": "Ataza automatikoa", + "backup_abstract_method": "Babeskopia modu hau oraindik ez da go erabilgarri", + "backup_actually_backuping": "Bildutako fitxategiekin babeskopia sortzen…", + "backup_app_script_failed": "{app} aplikazioaren babeskopia egiteko fitxategiak biltzeak huts egin du.", + "backup_applying_method_copy": "Babeskopiarako fitxategi guztiak kopiatzen…", + "backup_applying_method_custom": "'{method}' neurrira egindako babeskopia sortzen…", + "backup_applying_method_tar": "Babeskopiaren TAR fitxategia sortzen…", + "backup_archive_app_not_found": "Ezin izan da {app} aurkitu babeskopia fitxategian", + "backup_archive_broken_link": "Ezin izan da babeskopiaren fitxategia eskuratu ({path}ra esteka okerra)", + "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).", + "backup_archive_corrupted": "Badirudi '{archive}' babeskopia fitxategia kaltetuta dagoela: {error}", + "backup_archive_name_exists": "Lehendik ere 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", + "backup_archive_writing_error": "Ezinezkoa izan da '{source}' ('{dest}' fitxategiak eskatu dituenak) fitxategia '{archive}' konprimatutako babeskopian sartzea", + "backup_ask_for_copying_if_needed": "Behin-behinean {size}MB erabili nahi dituzu babeskopia gauzatu ahal izateko? (Horrela egiten da fitxategi batzuk ezin direlako modu eraginkorragoan prestatu.)", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "{name} babeskopia ezabatu da {newname} izeneko babeskopia berriago batek ordeztu duelako", + "backup_cant_mount_uncompress_archive": "Ezinezkoa izan da deskonprimatutako fitxategia muntatzea idazketa-babesa duelako", + "backup_cleaning_failed": "Ezinezkoa izan da behin-behineko babeskopien karpeta hustea", + "backup_copying_to_organize_the_archive": "{size}MB kopiatzen fitxategia antolatzeko", + "backup_couldnt_bind": "Ezin izan da {src} {dest}-ra lotu.", + "backup_create_size_estimation": "Fitxategiak {size} datu inguru izango ditu.", + "backup_created": "Babeskopia sortu da: {name}", + "backup_creation_failed": "Ezinezkoa izan da babeskopiaren fitxategia sortzea", + "backup_csv_addition_failed": "Ezinezkoa izan da fitxategiak CSV fitxategira kopiatzea", + "backup_csv_creation_failed": "Ezinezkoa izan da lehengoratzeko beharrezkoak diren CSV fitxategiak sortzea", + "backup_custom_backup_error": "Neurrira egindako babeskopiak ezin izan du 'babeskopia egin' urratsetik haratago egin", + "backup_custom_mount_error": "Neurrira egindako babeskopiak ezin izan du 'muntatu' urratsetik haratago egin", + "backup_delete_error": "Ezinezkoa izan da '{path}' ezabatzea", + "backup_deleted": "Babeskopia ezabatu da: {name}", + "backup_hook_unknown": "Babeskopiaren '{hook}' kakoa ezezaguna da", + "backup_method_copy_finished": "Babeskopiak amaitu du", + "backup_method_custom_finished": "'{method}' neurrira egindako babeskopiak amaitu du", + "backup_method_tar_finished": "TAR babeskopia artxiboa sortu da", + "backup_mount_archive_for_restore": "Lehengoratzeko fitxategiak prestatzen…", + "backup_no_file_collected": "Babeskopia egiteko fitxategiak biltzeak huts egin du", + "backup_no_uncompress_archive_dir": "Ez dago horrelako deskonprimatutako fitxategi katalogorik", + "backup_output_directory_forbidden": "Aukeratu beste katalogo bat emaitza gordetzeko. Babeskopiak ezin dira sortu /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var edo /home/yunohost.backup/archives azpi-katalogoetan", + "backup_output_directory_not_empty": "Aukeratu hutsik dagoen katalogo bat", + "backup_output_directory_required": "Babeskopia non gorde nahi duzun zehaztu behar duzu", + "backup_output_symlink_dir_broken": "'{path}' fitxategi-katalogoaren symlink-a ez dabil. Agian [ber]muntatzea ahaztu zaizu edo euskarria atakara konektatzea ahaztu duzu.", + "backup_running_hooks": "Babeskopien kakoak exekutatzen…", + "backup_system_part_failed": "Ezinezkoa izan da sistemaren '{part}' atalaren babeskopia egitea", + "backup_unable_to_organize_files": "Ezinezkoa izan da modu azkarra erabiltzea fitxategiko artxiboak prestatzeko", + "backup_with_no_backup_script_for_app": "'{app}' aplikazioak ez du babeskopia egiteko agindurik. Ez da kontuan hartuko.", + "backup_with_no_restore_script_for_app": "{app}(e)k ez du lehengoratzeko agindurik, ezingo duzu aplikazio hau automatikoki lehengoratu.", + "cannot_open_file": "Ezinezkoa izan da {file} fitxategia irekitzea (zergatia: {error})", + "cannot_write_file": "Ezinezkoa izan da {file} fitxategia idaztea (zergatia: {error})", + "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_attempt_to_renew_nonLE_cert": "'{domain}' domeinurako ziurtagiria ez da Let's Encryptek jaulkitakoa. Ezin da automatikoki berriztu!", + "certmanager_attempt_to_renew_valid_cert": "'{domain}' domeinurako ziurtagiria iraungitzear dago! (Zertan ari zaren baldin badakizu, --force erabil dezakezu)", + "certmanager_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)", + "certmanager_cannot_read_cert": "Arazoren bat egon da {domain} (fitxategia: {file}) domeinurako oraingo ziurtagiria irekitzen saiatzerakoan, zergatia: {reason}", + "certmanager_cert_install_failed": "Let's Encrypt zirutagiriaren instalazioak huts egin du honako domeinurako: {domains}", + "certmanager_cert_install_failed_selfsigned": "Norberak sinatutako zirutagiriaren instalazioak huts egin du honako domeinurako: {domains}", + "certmanager_cert_install_success": "Let's Encrypt ziurtagiria instalatu da '{domain}' domeinurako", + "certmanager_cert_install_success_selfsigned": "Norberak sinatutako ziurtagiria instalatu da '{domain}' domeinurako", + "certmanager_cert_renew_failed": "Let's Encrypt zirutagiriaren berrizteak huts egin du honako domeinurako: {domains}", + "certmanager_cert_renew_success": "Let's Encrypt ziurtagiria berriztu da '{domain}' domeinurako", + "certmanager_cert_signing_failed": "Ezinezkoa izan da ziurtagiri berria sinatzea", + "certmanager_certificate_fetching_or_enabling_failed": "{domain} domeinurako ziurtagiri berriak kale egin du…", + "certmanager_domain_cert_not_selfsigned": "{domain} domeinurako ziurtagiria ez da norberak sinatutakoa. Ziur al zaude ordezkatu nahi duzula? (Erabili '--force' hori egiteko.)", + "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.)", + "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_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.)", + "certmanager_hit_rate_limit": "{domain} domeinu-multzorako ziurtagiri gehiegi jaulki dira lehendik ere. Saiatu 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})", + "certmanager_unable_to_parse_self_CA_name": "Ezinezkoa izan da norberak sinatutako ziurtagiriaren izena prozesatzea (fitxategia: {file})", + "config_action_disabled": "Ezin izan da '{action}' eragiketa exekutatu desgaituta dagoelako; egiaztatu bere mugak betetzen dituzula. Laguntza: {help}", + "config_action_failed": "'{action}' eragiketa exekutatzeak huts egin du: {error}", + "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_forbidden_keyword": "'{keyword}' etiketa sistemak bakarrik erabil dezake; ezin da ID hau daukan baliorik sortu edo erabili.", + "config_forbidden_readonly_type": "'{type}' mota ezin da ezarri readonly bezala; beste mota bat erabili balio hau emateko (argudioaren ida: '{id}').", + "config_no_panel": "Ez da konfigurazio-panelik aurkitu.", + "config_unknown_filter_key": "'{filter_key}' filtroaren kakoa ez da zuzena.", + "confirm_app_install_danger": "KONTUZ! Aplikazio hau esperimentala da (edo ez dabil)! Ez zenuke instalatu beharko zertan ari zaren ez badakizu. Aplikazio hau ez badabil edo sistema kaltetzen badu, EZ DA LAGUNTZARIK EMANGO… aurrera jarraitu nahi al duzu hala ere? Aukeratu '{answers}'", + "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}'", + "confirm_app_install_warning": "Adi: litekeena da aplikazio hau ibiltzea, baina ez dago YunoHostera egina. Ezaugarri batzuk, SSO edo babeskopia/lehengoratzea esaterako, desgaituta egon daitezke. Instalatu hala ere? [{answers}] ", + "confirm_app_insufficient_ram": "Aplikazio honek unean erabilgarri dagoen baino RAM gehiago behar du instalatzeko. 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: Aztertu aplikazioaren jakinarazpenak jarraitu baino lehen, baliteke jakin beharreko zerbait esatea. [{answers}]", + "confirm_tos_acknowledgement": "Zerbitzuen Baldintzak irakurri eta ulertzen ditut [{answers}]", + "corrupted_json": "{ressource}-eko/go JSONa kaltetuta dago (zergatia: {error})", + "corrupted_toml": "{ressource}-eko/go TOMLa kaltetuta dago (zergatia: {error})", + "corrupted_yaml": "{ressource}-eko/go YAMLa kaltetuta dago (zergatia: {error})", + "danger": "Arriskua:", + "diagnosis_apps_allgood": "Instalatutako aplikazioak bat datoz oinarrizko pakete-jarraibideekin", + "diagnosis_apps_bad_quality": "Aplikazioa hondatutzat ageri da YunoHosten aplikazioen katalogoan. Behin-behineko kontua izan daiteke arduradunek arazoa konpondu bitartean. Oraingoz, ezin da aplikazioa bertsio-berritu.", + "diagnosis_apps_broken": "Aplikazioa hondatutzat ageri da YunoHosten aplikazioen katalogoan. Behin-behineko kontua izan daiteke arduradunek arazoa konpondu bitartean. Oraingoz, ezin da aplikazioa bertsio-berritu.", + "diagnosis_apps_deprecated_practices": "Instalatutako aplikazio honen bertsioak oraindik darabiltza zaharkitutako pakete-jarraibideak. Eguneratzea hausnartu beharko zenuke.", + "diagnosis_apps_issue": "Arazo bat dago {app} aplikazioarekin", + "diagnosis_apps_not_in_app_catalog": "Aplikazio hau ez da YunoHosten aplikazioen katalogokoa. Iraganean egon bazen eta orain ez badago, desinstalatzea litzateke onena, ez baitu eguneraketarik jasoko eta sistemaren integritate eta segurtasuna arriskuan jar lezakeelako.", + "diagnosis_apps_outdated_packaging_format": "Aplikazio honek zaharkitutako pakete-formatu bat darabil, eta aurki YunoHostekin bateragarri izateari utziko dio. Eguneratzen saiatu beharko zinateke.", + "diagnosis_apps_outdated_ynh_requirement": "Instalatutako aplikazio honen bertsioak yunohost >= 2.x, 3.x edo 4.x baino ez du behar, eta horrek eguneratua izan ez dela eta egungo pakete-jardunbideekin bat ez datorrela iradokitzen du. Eguneratzen saiatu beharko zinateke.", + "diagnosis_apps_security_issue_error": "{app} aplikazioaren bertsioa '{current_version}' da, zeinak segurtasun arazo LARRI bat baitu: {title}. Gomendagarria da LEHENBAILEHEN '{fixed_in_version}' bertsiora berritzea. Informazio gehiago: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "{app} aplikazioaren bertsioa '{current_version}' da, zeinak segurtasun arazo ertain bat baitu: {title}. Gomendagarria da '{fixed_in_version}' bertsiora berritzea. Informazio gehiago: {more_infos_list}", + "diagnosis_backports_in_sources_list": "Dirudienez apt (pakete kudeatzailea) backports gordailua erabiltzeko konfiguratuta dago. Zertan ari zaren ez badakizu, ez zenuke backports gordailuetako aplikaziorik instalatu beharko, ezegonkortasun eta gatazkak eragin ditzaketelako sistemarekin.", + "diagnosis_basesystem_hardware": "Zerbitzariaren arkitektura {virt} {arch} da", + "diagnosis_basesystem_hardware_model": "Zerbitzariaren modeloa {model} da", + "diagnosis_basesystem_host": "Zerbitzariak Debian {debian_version} darabil", + "diagnosis_basesystem_kernel": "Zerbitzariak Linuxen {kernel_version} kernela darabil", + "diagnosis_basesystem_ynh_inconsistent_versions": "YunoHost paketeen bertsioak ez datoz bat… ziur asko noizbait eguneraketa batek kale egin edo erabat amaitu ez zuelako.", + "diagnosis_basesystem_ynh_main_version": "Zerbitzariak YunoHosten {main_version} ({repo}) darabil", + "diagnosis_basesystem_ynh_single_version": "{package} bertsioa: {version} ({repo})", + "diagnosis_cache_still_valid": "(Katxea oraindik baliogarria da {category} ataleko diagnostikorako. Ez da berrabiaraziko!)", + "diagnosis_cant_run_because_of_dep": "Ezinezkoa da diagnostikoa abiaraztea {category} atalerako {dep}(r)i lotutako arazo garrantzitsuak / garrantzitsuek dirau(t)en artean.", + "diagnosis_description_apps": "Aplikazioak", + "diagnosis_description_basesystem": "Sistemaren oinarria", + "diagnosis_description_dnsrecords": "DNS erregistroak", + "diagnosis_description_ip": "Internet konexioa", + "diagnosis_description_mail": "Posta elektronikoa", + "diagnosis_description_ports": "Ataken irisgarritasuna", + "diagnosis_description_regenconf": "Sistemaren ezarpenak", + "diagnosis_description_services": "Zerbitzuen egoeraren egiaztapena", + "diagnosis_description_systemresources": "Sistemaren baliabideak", + "diagnosis_description_web": "Weba", + "diagnosis_diskusage_low": "{mountpoint} fitxategi-sistemak ({device} euskarrian) edukieraren {free} (%{free_percent}a) bakarrik ditu erabilgarri ({total} orotara). Kontuz ibili.", + "diagnosis_diskusage_ok": "{mountpoint} fitxategi-sistemak ({device} euskarrian) edukieraren {free} (%{free_percent}a) ditu oraindik erabilgarri ({total} orotara)!", + "diagnosis_diskusage_verylow": "{mountpoint} fitxategi-sistemak ({device} euskarrian) edukieraren {free} (%{free_percent}a) bakarrik ditu erabilgarri ({total} orotara). Zertxobait hustu beharko zenuke!", + "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_bad_conf": "DNS balio batzuk falta dira edo ez dira zuzenak {domain} domeinurako ({category} atala)", + "diagnosis_dns_discrepancy": "Ez dirudi honako DNS balioak bat datozenik proposatutako konfigurazioarekin:
Mota: {type}
Izena: {name}
Oraingo balioa: {current}
Proposatutako balioa: {content}", + "diagnosis_dns_good_conf": "DNS ezarpenak zuzen konfiguratuta daude {domain} domeinurako ({category} atala)", + "diagnosis_dns_missing_record": "Proposatutako DNS konfigurazioaren arabera, honako informazioa gehitu beharko zenuke DNS erregistroan:
Mota: {type}
Izena: {name}
Balioa: {content}", + "diagnosis_dns_point_to_doc": "Irakurri dokumentazioa DNS erregistroekin laguntza behar baduzu.", + "diagnosis_dns_specialusedomain": "{domain} domeinua top-level domain (TLD) erabilera berezikoa da .local edo .test bezala eta horregatik ez du DNS erregistrorik erabiltzeko beharrik.", + "diagnosis_dns_try_dyndns_update_force": "Domeinu honen DNS konfigurazioa YunoHostek kudeatu beharko luke automatikoki. Gertatuko ez balitz, eguneratzera behartu zenezake yunohost dyndns update --force erabiliz.", + "diagnosis_domain_expiration_error": "Domeinu batzuk IRAUNGITZEAR daude!", + "diagnosis_domain_expiration_not_found": "Ezin da domeinu batzuen iraungitze data egiaztatu", + "diagnosis_domain_expiration_not_found_details": "Badirudi {domain} domeinuari buruzko WHOIS informazioak ez duela zehazten noiz iraungiko den?", + "diagnosis_domain_expiration_success": "Domeinuak erregistratuta daude eta ez dira oraingoz iraungiko.", + "diagnosis_domain_expiration_warning": "Domeinu batzuk iraungitzear daude!", + "diagnosis_domain_expires_in": "{domain} {days} egun barru iraungiko da.", + "diagnosis_domain_not_found_details": "{domain} domeinua ez da WHOISen datubasean existitzen edo iraungi da!", + "diagnosis_everything_ok": "Badirudi guztia zuzen dagoela {category} atalean!", + "diagnosis_failed": "'{category}' ataleko diagnostikoa lortzeak huts egin du: {error}", + "diagnosis_failed_for_category": "'{category}' ataleko diagnostikoak huts egin du: {error}", + "diagnosis_found_errors": "{category} atalari dago(z)kion {errors} arazo aurkitu d(ir)a!", + "diagnosis_found_errors_and_warnings": "{category} atalari dago(z)kion {errors} arazo (eta {warnings} abisu) aurkitu d(ir)a!", + "diagnosis_found_warnings": "{category} atalari dagokion eta hobetu daite(z)keen {warnings} abisu aurkitu d(ir)a.", + "diagnosis_high_number_auth_failures": "Azken aldian kale egin duten saio-hasiera saiakera ugari egon dira. Egiaztatu fail2ban martxan dabilela eta egoki konfiguratuta dagoela, edo erabili beste ataka bat SSHrako dokumentazioan azaldu bezala.", + "diagnosis_http_bad_status_code": "Zerbitzari hau ez den beste gailu batek erantzun omen dio eskaerari (agian routerrak).
1. Arazo honen zergati ohikoena da 80. (eta 443.) ataka egoki birbideratuta ez egotea.
2. Konfigurazio konplexua badarabilzu, egiaztatu suebakiak edo reverse-proxyk oztopatzen ez dutela.", + "diagnosis_http_connection_error": "Arazoa konexioan: ezin izan da domeinu horretara konektatu, litekeena da eskuragarri ez egotea.", + "diagnosis_http_could_not_diagnose": "Ezinezkoa izan da domeinuak IPv{ipversion} kanpotik eskuragarri dauden egiaztatzea.", + "diagnosis_http_could_not_diagnose_details": "Errorea: {error}", + "diagnosis_http_hairpinning_issue": "Dirudienez zure sareak ez du hairpinninga gaituta.", + "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_nginx_conf_not_up_to_date": "Domeinu honen nginx ezarpenak eskuz moldatu direla dirudi eta YunoHostek ezin du egiaztatu HTTP bidez eskuragarri dagoenik.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Egoera konpontzeko, ikuskatu desberdintasunak yunohost tools regen-conf nginx --dry-run --with-diff komandoaren bidez eta, proposatutako aldaketak onartzen badituzu, ezarri itzazu yunohost tools regen-conf nginx --force erabiliz.", + "diagnosis_http_ok": "{domain} domeinua HTTP bidez bisitatu daiteke sare lokaletik kanpo.", + "diagnosis_http_partially_unreachable": "Badirudi {domain} domeinua ezin dela bisitatu HTTP bidez IPv{failed} sare lokaletik kanpo, bai ordea IPv{passed} erabiliz.", + "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_http_timeout": "Denbora agortu da sare lokaletik kanpo zure zerbitzarira konektatzeko ahaleginean. Eskuragarri ez dagoela dirudi.
1. Arazo honen zergati ohikoena da 80. (eta 443.) ataka egoki birbideratuta ez egotea.
2. Badaezpada egiaztatu nginx martxan dagoela.
3. Konfigurazio konplexuetan, egiaztatu suebakiak edo reverse-proxyk konexioa oztopatzen ez dutela.", + "diagnosis_http_unreachable": "Badirudi {domain} domeinua ez dagoela eskuragarri HTTP bidez sare lokaletik kanpo.", + "diagnosis_ignore_already_filtered": "(Badago lehendik ere irizpide horiek dituen {category} atalaren diagnostikorako iragazkia)", + "diagnosis_ignore_criteria_error": "Irizpideek forma hau izan behar dute: gakoa=balorea (adib. domain=yolo.test)", + "diagnosis_ignore_filter_added": "{category} atalaren diagnostikorako iragazkia gehitu da", + "diagnosis_ignore_filter_removed": "{category} atalaren diagnostikorako iragazkia kendu da", + "diagnosis_ignore_missing_criteria": "Gutxienez irizpide bat gehitu behar duzu atalaren diagnostikoak kontuan har ez dezan", + "diagnosis_ignore_no_filter_found": "(Ez dago irizpide horiek dituen {category} atalaren diagnostikorako iragazkirik)", + "diagnosis_ignore_no_issue_found": "Ez da arazorik aurkitu emandako irizpideekin.", + "diagnosis_ignored_issues": "(kontuan hartu ez d(ir)en + {nb_ignored} arazo)", + "diagnosis_ip_broken_dnsresolution": "Domeinu izenaren ebazpena kaltetuta dagoela dirudi… Suebakiren bat ote dago DNS eskaerak oztopatzen?", + "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_connected_ipv4": "Zerbitzaria IPv4 bidez dago Internetera konektatuta!", + "diagnosis_ip_connected_ipv6": "Zerbitzaria IPv6 bidez dago Internetera konektatuta!", + "diagnosis_ip_dnsresolution_working": "Domeinu izenaren ebazpena badabil!", + "diagnosis_ip_global": "IP orokorra: {global}", + "diagnosis_ip_local": "IP lokala: {local}", + "diagnosis_ip_no_ipv4": "Zerbitzariak ez du dabilen IPv4rik.", + "diagnosis_ip_no_ipv6": "Zerbitzariak ez du dabilen IPv6rik.", + "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_important": "IPv6 automatikoki ezarri ohi du sistemak edo hornitzaileak, erabilgarri baldin badago. Bestela, eskuz ezarri beharko dituzu aukera batzuk honako dokumentazioan azaldu bezala: https://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "Badirudi zerbitzaria ez dagoela Internetera konektatuta!?", + "diagnosis_ip_weird_resolvconf": "DNS ebazpena badabilela dirudi, baina antza denez moldatutako /etc/resolv.conf fitxategia erabiltzen ari zara.", + "diagnosis_ip_weird_resolvconf_details": "/etc/resolv.conf fitxategia /etc/resolvconf/run/resolv.conf-ren esteka sinbolikoa izan behar da, 127.0.0.1 (dnsmasq) adierazi behar duena. DNS ebazleak eskuz konfiguratu nahi badituzu, aldatu /etc/resolv.dnsmasq.conf fitxategia.", + "diagnosis_mail_blocklist_listed_by": "Zure domeinua edo {item} IPa {blocklist_name} zerrenda beltzean ageri da", + "diagnosis_mail_blocklist_ok": "Ez dirudi zerbitzari honek darabiltzan IPak eta domeinuak inolako zerrenda beltzean daudenik", + "diagnosis_mail_blocklist_reason": "Zerrenda beltzean egotearen zergatia zera da: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Badirudi 'open resolver'i egiten diola aipamena.
Honek esan nahi du, normalean, zure zerbitzaria ez dela bere DNS lokala erabiltzen ari, baizik eta publiko, ireki bat.
Berrikusi /etc/resolv.conf fitxategiko edukia, nameserver 127.0.0.1 izan beharko luke bere baitan.
Fitxategia automatikoki sortzen denez, ez ezazu eskuz editatu. Egiaztatu zure DHCP ezarpenak (edo zure VPN ezarpenak halakorik erabiliz gero), edo, VPS hornitzaile batek egindako Debian irudi bat erabili baduzu, bilatu cloudinit konfigurazioa.
YunoHost laguntza-kanaletan ongi etorria zara gai honi buruzko laguntza lortzeko.
Zerrenda beltzaren arrazoia honakoa da: {reason}", + "diagnosis_mail_blocklist_website": "Zerrenda beltzean zergatik zauden ulertu eta konpondu ondoren, {blocklist_website} webgunean zure IP edo domeinua bertatik atera dezatela eska dezakezu", + "diagnosis_mail_ehlo_bad_answer": "SMTP ez den zerbitzu batek erantzun du IPv{ipversion}ko 25. atakan", + "diagnosis_mail_ehlo_bad_answer_details": "Litekeena da zure zerbitzaria ez den beste gailu batek erantzun izana.", + "diagnosis_mail_ehlo_could_not_diagnose": "Ezinezkoa izan da postfix posta zerbitzaria IPv{ipversion}az kanpo eskuragarri dagoen egiaztatzea.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Errorea: {error}", + "diagnosis_mail_ehlo_ok": "SMTP posta zerbitzaria eskuragarri dago kanpoko saretik eta, beraz, posta elektronikoa jasotzeko gai da!", + "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_unreachable_details": "Ezinezkoa izan da zure zerbitzariko 25. atakari konektatzea IPv{ipversion} erabiliz. Badirudi ez dagoela eskuragarri.
1. Arazo honen zergati ohikoena da 25. ataka egoki birbideratuta ez egotea.
2. Egiaztatu postfix zerbitzua martxan dagoela.
3. Konfigurazio konplexuagoetan: egiaztatu suebaki edo reverse-proxyak konexioa oztopatzen ez dutela.", + "diagnosis_mail_ehlo_wrong": "Zurea ez den SMTP posta zerbitzari batek erantzun du IPv{ipversion}an. Litekeena da zure zerbitzariak posta elektronikorik jaso ezin izatea.", + "diagnosis_mail_ehlo_wrong_details": "Kanpo-diagnostikatzaileak IPv{ipversion}an jaso duen EHLOa eta zure zerbitzariaren domeinukoa ez datoz bat.
Jasotako EHLOa: {wrong_ehlo}
Esperotakoa: {right_ehlo}
Arazo honen zergati ohikoena 25. ataka zuzen konfiguratuta ez egotea da. Edo agian suebaki edo reverse-proxya oztopo izan daiteke.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Alderantzizko DNSa ez dago zuzen konfiguratuta IPv{ipversion}an. Litekeena da hartzaileak posta elektroniko batzuk jaso ezin izatea edo mezuok spam gisa etiketatuak izatea.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Oraingo alderantzizko DNSa: {rdns_domain}
Esperotako balioa: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "Ez da alderantzizko DNSrik ezarri IPv{ipversion}rako. Litekeena da hartzaileak posta elektroniko batzuk jaso ezin izatea edo mezuok spam gisa etiketatuak izatea.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Operadore batzuek ez dute alderantzizko DNSa konfiguratzen uzten (edo funtzioa ez dabil…). Hori 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://doc.yunohost.org/vpn_advantage
- Edo operadore desberdin batera aldatu", + "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_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_fcrdns_ok": "Alderantzizko DNSa zuzen konfiguratuta dago!", + "diagnosis_mail_outgoing_port_25_blocked": "SMTP posta zerbitzariak ezin ditu posta elektronikoak bidali 25. ataka itxita dagoelako IPv{ipversion}n.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Lehenik eta behin operadorearen routerreko aukeretan saiatu beharko zinateke 25. ataka desblokeatzen. (Hosting enpresaren arabera, beraiekin harremanetan jartzea beharrezkoa izango da).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Operadore batzuei bost axola zaie Interneten 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://doc.yunohost.org/vpn_advantage
- Edo operadore desberdin batera aldatu", + "diagnosis_mail_outgoing_port_25_ok": "SMTP posta zerbitzaria posta elektronikoa bidaltzeko gai da (25. atakaren irteera ez dago blokeatuta).", + "diagnosis_mail_queue_ok": "Posta elektronikoaren ilaran zain dauden mezuak: {nb_pending}", + "diagnosis_mail_queue_too_big": "Mezu gehiegi posta elektronikoaren ilaran: ({nb_pending} mezu)", + "diagnosis_mail_queue_unavailable": "Ezinezkoa da ilaran zenbat posta elektroniko dauden kontsultatzea", + "diagnosis_mail_queue_unavailable_details": "Errorea: {error}", + "diagnosis_never_ran_yet": "Badirudi zerbitzari hau duela gutxi konfiguratu dela eta oraindik ez dago erakusteko diagnostikorik. Diagnostiko osoa abiarazi beharko zenuke, administrazio-webgunetik edo 'yunohost diagnosis run' komandoa exekutatuz.", + "diagnosis_no_cache": "Oraindik ez dago '{category}' atalerako diagnostikoaren katxerik", + "diagnosis_package_installed_from_sury": "Sistemaren pakete batzuen lehenagoko bertsioak beharko lirateke", + "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 arazoak sortzea PHP7.3 aplikazioak Stretch sistema eragilean instalatu zituzten kasu batzuetan. Egoera hau konpontzeko, honako komando hau exekutatu beharko zenuke: {cmd_to_fix}", + "diagnosis_package_security_issue_error": "'{package}' sistemaren paketearen bertsioa '{current_version}' da, zeinak segurtasun arazo LARRI bat baitu: {title}. Gomendagarria da LEHENBAILEHEN '{fixed_in_version}' bertsiora berritzea. Informazio gehiago: {more_infos_list}", + "diagnosis_package_security_issue_warning": "'{package}' sistemaren paketearen bertsioa '{current_version}' da, zeinak segurtasun arazo ertain bat baitu: {title}. Gomendagarria da '{fixed_in_version}' bertsiora berritzea. Informazio gehiago: {more_infos_list}", + "diagnosis_ports_could_not_diagnose": "Ezinezkoa izan da atakak IPv{ipversion} erabiliz kanpotik eskuragarri dauden egiaztatzea.", + "diagnosis_ports_could_not_diagnose_details": "Errorea: {error}", + "diagnosis_ports_forwarding_tip": "Arazoa konpontzeko, litekeena da operadorearen routerrean ataken birbideraketa konfiguratu behar izatea, https://doc.yunohost.org/admin/get_started/post_install/dns_config/-n agertzen den bezala", + "diagnosis_ports_needed_by": "{category} funtzioetarako ezinbestekoa da ataka hau eskuragarri egotea ({service} zerbitzua)", + "diagnosis_ports_ok": "{port}. ataka eskuragarri dago kanpotik.", + "diagnosis_ports_partially_unreachable": "{port}. ataka ez dago eskuragarri kanpotik IPv{failed} erabiliz.", + "diagnosis_ports_unreachable": "{port}. ataka ez dago eskuragarri kanpotik.", + "diagnosis_processes_killed_by_oom_reaper": "Memoria agortu eta sistemak prozesu batzuk amaituarazi behar izan ditu. Honek esan nahi du sistemak ez duela memoria nahikoa edo prozesuren batek memoria gehiegi behar duela. Amaituarazi d(ir)en prozesua(k):\n{kills_summary}", + "diagnosis_ram_low": "Sistemak RAM memoriaren {available} ditu erabilgarri; memoria guztiaren ({total}) %{available_percent}a. Adi ibili.", + "diagnosis_ram_ok": "Sistemak RAM memoriaren {available} ditu oraindik erabilgarri; memoria guztiaren ({total}) %{available_percent}a.", + "diagnosis_ram_verylow": "Sistemak RAM memoriaren {available} baino ez ditu erabilgarri; memoria guztiaren ({total}) %{available_percent}a.", + "diagnosis_regenconf_allgood": "Konfigurazio-fitxategi guztiak bat datoz gomendatutako ezarpenekin!", + "diagnosis_regenconf_manually_modified": "Dirudienez {file} konfigurazio fitxategia eskuz aldatu da.", + "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", + "diagnosis_rfkill_wifi": "Wi-Fi txartela desgaituta dago eta sistemaren ohartarazpen batek aplikazioen instalazioak eragotzi ditzake", + "diagnosis_rfkill_wifi_details": "Ohartarazpena komando askotan ageri da, aplikazio batzuk hautsiz. Herrialdearen kodea zehaztuz konpon daiteke sudo raspi-config komandoaren bidez. Hau da errorea:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "'root' fitxategi-sistemak {space} baino ez ditu erabilgarri, eta hori kezkagarria da! Litekeena da oso laster memoriarik gabe geratzea! 'root' fitxategi-sistemak gutxienez 16GB erabilgarri izatea da gomendioa.", + "diagnosis_rootfstotalspace_warning": "'root' fitxategi-sistemak {space} baino ez ditu. Agian ez da arazorik egongo, baina kontuz ibili edo memoriarik gabe gera zaitezke laster… 'root' fitxategi-sistemak gutxienez 16GB erabilgarri izatea da gomendioa.", + "diagnosis_security_vulnerable_to_meltdown": "Badirudi Meltdown izeneko segurtasun arazo larriak eragin diezazukela", + "diagnosis_security_vulnerable_to_meltdown_details": "Arazoa konpontzeko, sistema eguneratu eta berrabiarazi beharko zenuke linux-en kernel berriagoa erabiltzeko (edo zerbitzariaren arduradunarekin jarri harremanetan). Ikus https://meltdownattack.com/ argibide gehiagorako.", + "diagnosis_services_bad_status": "{service} zerbitzua {status} dago :(", + "diagnosis_services_bad_status_tip": "Zerbitzua berrabiarazten saia zaitezke eta nahikoa ez bada, aztertu zerbitzuaren erregistroa administrazio-atarian. (komandoak nahiago badituzu yunohost service restart {service} eta yunohost service log {service} hurrenez hurren).", + "diagnosis_services_conf_broken": "{service} zerbitzuko konfigurazioa hondatuta dago!", + "diagnosis_services_running": "{service} zerbitzua martxan dago!", + "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.", + "diagnosis_sshd_config_insecure": "Badirudi SSH konfigurazioa eskuz aldatu dela eta ez da segurua ez duelako 'AllowGroups' edo 'AllowUsers' baldintzarik jartzen fitxategien atzitzea oztopatzeko.", + "diagnosis_swap_none": "Sistemak ez du swap-ik. Gutxienez {recommended} izaten saiatu beharko zinateke, sistema memoriarik gabe gera ez dadin.", + "diagnosis_swap_notsomuch": "Sistemak {total} swap baino ez ditu. Gutxienez {recommended} izaten saiatu beharko zinateke sistema memoriarik gabe gera ez dadin.", + "diagnosis_swap_ok": "Sistemak {total} swap dauzka!", + "diagnosis_swap_tip": "Kontuan hartu zerbitzari honen swap memoria SD edo SSD euskarri batean gordetzeak euskarri horren bizi-iraupena izugarri laburtu dezakeela.", + "diagnosis_unknown_categories": "Honako atalak ez dira ezagutzen: {categories}", + "diagnosis_using_stable_codename": "apt (sistemaren pakete kudeatzailea) 'stable' (egonkorra) izen-kodea duten paketeak instalatzeko ezarrita dago une honetan, eta ez uneko Debianen bertsioaren (bookworm) izen-kodea.", + "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 bookworm gakoarekin ordezkatuta. Ezarpen-fitxategia /etc/apt/sources.list izan beharko litzateke, edo /etc/apt/sources.list.d/ direktorioko fitxategiren bat.", + "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 desgaitu nahi badituzu, kendu testing gakoa /etc/apt/sources.list.d/yunohost.list fitxategitik.", + "disk_space_not_sufficient_install": "Ez dago aplikazio hau instalatzeko nahikoa espaziorik", + "disk_space_not_sufficient_update": "Ez dago aplikazio hau eguneratzeko nahikoa espaziorik", + "domain_cannot_remove_main": "Ezin duzu '{domain}' ezabatu domeinu nagusia delako. Beste domeinu bat ezarri beharko duzu nagusi bezala 'yunohost domain main-domain -n ' erabiliz; honako hauek dituzu aukeran: {other_domains}", + "domain_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_cert_gen_failed": "Ezinezkoa izan da ziurtagiria sortzea", + "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_api_protocol": "API protokoloa", + "domain_config_auth_application_key": "Aplikazioaren gakoa", + "domain_config_auth_application_secret": "Aplikazioaren gako sekretua", + "domain_config_auth_consumer_key": "Erabiltzailearen gakoa", + "domain_config_auth_entrypoint": "APIaren sarrera", + "domain_config_auth_key": "Autentifikazio gakoa", + "domain_config_auth_secret": "Autentifikazioaren \"secret\"a", + "domain_config_auth_token": "Token autentifikazioa", + "domain_config_cert_install": "Instalatu Let's Encrypt ziurtagiria", + "domain_config_cert_issuer": "Ziurtagiriaren jaulkitzailea", + "domain_config_cert_name": "Ziurtagiria", + "domain_config_cert_no_checks": "Muzin egin diagnostikoaren egiaztapenei", + "domain_config_cert_renew": "Berritu Let's Encrypt ziurtagiria", + "domain_config_cert_renew_help": "Ziurtagiria automatikoki berrituko da baliozkoa den azken 15 egunetan. Eskuz berritu dezakezu hala nahi baduzu. (Ez da gomendagarria).", + "domain_config_cert_summary": "Ziurtagiriaren egoera", + "domain_config_cert_summary_abouttoexpire": "Uneko ziurtagiria iraungitzear dago. Aurki berritu beharko litzateke automatikoki.", + "domain_config_cert_summary_expired": "LARRIA: Uneko ziurtagiria ez da baliozkoa! HTTPS ezin da erabili!", + "domain_config_cert_summary_letsencrypt": "Primeran! Baliozko Let's Encrypt zirutagiria erabiltzen ari zara!", + "domain_config_cert_summary_ok": "Ados, uneko ziurtagiriak itxura ona du!", + "domain_config_cert_summary_selfsigned": "ADI: Uneko zirutagiria norberak sinatutakoa da. Web-nabigatzaileek bisitariak izutuko dituen mezu bat erakutsiko dute!", + "domain_config_cert_validity": "Balizokotasuna", + "domain_config_custom_css": "CSS estilo-orri pertsonalizatua", + "domain_config_custom_css_help": "Administrari adituentzako da, erabiltzaile-ataria pertsonalizatu nahi badute", + "domain_config_default_app": "Lehenetsitako aplikazioa", + "domain_config_default_app_help": "Jendea automatikoki birbideratuko da aplikazio honetara domeinu hau bisitatzerakoan. Aplikaziorik ezarri ezean, jendea saioa hasteko atarira birbideratuko da.", + "domain_config_dns_name": "DNSa", + "domain_config_enable_public_apps_page": "Erakutsi bisitariei aplikazio publikoen zerrenda", + "domain_config_enable_public_apps_page_help": "Bisitariek 'aplikazio publikoen' orria kusiko dute atarira iristerakoan, saioa hasteko formularioaren bidez.", + "domain_config_feature_name": "Ezaugarriak", + "domain_config_mail_in": "Jasotako mezuak", + "domain_config_mail_out": "Bidalitako mezuak", + "domain_config_portal_logo": "Logo pertsonalizatua", + "domain_config_portal_logo_help": ".svg, .png eta .jpeg onartzen ditu. Lehenetsi fill: currentColor duen .svg monokromatiko bat logoa gai desberdinetara egokitu dadin.", + "domain_config_portal_name": "Atariaren pertsonalizazioa", + "domain_config_portal_public_intro": "Sarrera publiko pertsonalizatua", + "domain_config_portal_public_intro_help": "HTML erabili dezakezu; oinarrizko estiloak aplikatuko zaizkie elementu orokorrei.", + "domain_config_portal_theme": "Defektuzko kolore-gaia", + "domain_config_portal_theme_help": "Erabiltzaileek beste bat aukeratu dezakete euren ezarpenetan.", + "domain_config_portal_tile_theme": "Aplikazioen lauzak erakusteko gaia", + "domain_config_portal_title": "Titulu pertsonalizatua", + "domain_config_portal_user_intro": "Erabiltzailearen aurkezpen pertsonalizatua", + "domain_config_portal_user_intro_help": "HTML erabili dezakezu; oinarrizko estiloak aplikatuko zaizkie elementu orokorrei.", + "domain_config_search_engine": "Bilaketa motorraren URLa", + "domain_config_search_engine_help": "Aukerako ezaugarria da, atarian bilaketa-barra bat erakustea ahalbidetzen duena (adibidez YunoHosten ataria nabigatzaileko hasierako orri ezarri nahi baduzu). Kontsulta-katea hutsik duen URL bat izan behar da, 'https://duckduckgo.com/?q=' esaterako, 'q=' duckduckgo-ren kontsulta parametro bezala", + "domain_config_search_engine_name": "Bilaketa motorraren izena", + "domain_config_show_other_domains_apps": "Erakutsi beste domeinuetako aplikazioak", + "domain_created": "Sortu da domeinua", + "domain_creation_failed": "Ezin da {domain} domeinua sortu: {error}", + "domain_deleted": "Domeinua ezabatu da", + "domain_deletion_failed": "Ezin da {domain} ezabatu: {error}", + "domain_dns_conf_is_just_a_recommendation": "Komando honek *iradokitako* konfigurazioa erakusten du. Ez du DNS konfigurazioa zugatik ezartzen. Zure ardura da DNS gunea zure erregistro-enpresaren gomendioen arabera ezartzea.", + "domain_dns_conf_special_use_tld": "Domeinu hau top-level domain (TLD) erabilera bereziko motakoa da .local edo .test bezala eta ez du DNS ezarpenik behar.", + "domain_dns_push_already_up_to_date": "Ezarpenak dagoeneko egunean daude, ez dago zereginik.", + "domain_dns_push_failed": "DNS ezarpenen eguneratzeak huts egin du.", + "domain_dns_push_failed_to_list": "APIa erabiliz uneko erregistroak antzemateak huts egin du: {error}", + "domain_dns_push_managed_in_parent_domain": "DNS ezarpenak automatikoki konfiguratzeko funtzioa {parent_domain} domeinu nagusian kudeatzen da.", + "domain_dns_push_not_applicable": "Ezin da {domain} domeinurako DNS konfigurazio automatiko funtzioa erabili. DNS erregistroak eskuz ezarri beharko zenituzke gidaorriei erreparatuz: https://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "DNS ezarpenak erdipurdi eguneratu dira: jakinarazpen/errore batzuk egon dira.", + "domain_dns_push_record_failed": "{type}/{name} ezarpenak {action} huts egin du: {error}", + "domain_dns_push_success": "DNS ezarpenak eguneratu dira!", + "domain_dns_pushing": "DNS ezarpenak bidaltzen…", + "domain_dns_registrar_experimental": "Oraingoz, YunoHosten kideek ez dute **{registrar}** erregistro-enpresaren APIa nahi beste probatu eta aztertu. Funtzioa **oso esperimentala** da — kontuz!", + "domain_dns_registrar_managed_in_parent_domain": "Domeinu hau {parent_domain_link}(r)en azpidomeinua da. DNS ezarpenak {parent_domain}(r)en konfigurazio atalean kudeatu behar dira.", + "domain_dns_registrar_not_supported": "YunoHostek ezin izan du domeinu honen erregistro-enpresa automatikoki antzeman. Eskuz konfiguratu beharko dituzu DNS ezarpenak gidalerroei erreparatuz: https://doc.yunohost.org/dns_config.", + "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://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Baduzu DNS erregistroak eskuz konfiguratzeko aukera ere, gidalerro hauetan ageri den bezala: https://doc.yunohost.org/dns_config)", + "domain_dns_registrar_use_auto": "Erabili DNS automatikoaren ezaugarria", + "domain_dns_registrar_yunohost": "Hau nohost.me / nohost.st / ynh.fr domeinu bat da eta, beraz, DNS ezarpenak automatikoki kudeatzen ditu YunoHostek, bestelako ezer konfiguratu beharrik gabe. (ikus 'yunohost dyndns update' komandoa)", + "domain_dyndns_already_subscribed": "Lehendik ere izena eman duzu DynDNS domeinu batean", + "domain_exists": "Lehendik ere existitzen da domeinu hau", + "domain_hostname_failed": "Ezin da hostname berria ezarri. Honek arazoak ekar litzake etorkizunean (litekeena da ondo egotea).", + "domain_registrar_is_not_configured": "Oraindik ez da {domain} domeinurako erregistro-enpresa ezarri.", + "domain_remove_confirm_apps_removal": "Domeinu hau ezabatzean aplikazio hauek desinstalatuko dira:\n{apps}\n\nZiur al zaude? [{answers}]", + "domain_uninstall_app_first": "Honako aplikazio hauek domeinuan instalatuta daude:\n{apps}\n\nDesinstalatu 'yunohost app remove the_app_id' exekutatuz edo alda itzazu beste domeinu batera 'yunohost app change-url the_app_id' erabiliz domeinua ezabatu baino lehen", + "domain_unknown": "'{domain}' domeinua ezezaguna da", + "domains_available": "Erabilgarri dauden domeinuak:", + "done": "Egina", + "download_bad_status_code": "{url} helbideak {code} egoera kodea agertu du", + "download_ssl_error": "SSL errorea {url}-(e)ra konektatzean", + "download_timeout": "{url}(e)k denbora gehiegi behar izan du erantzuteko, bertan behera utzi du zerbitzariak.", + "download_unknown_error": "Errorea {url}(e)tik deskargatzerakoan: {error}", + "downloading": "Deskargatzen…", + "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'.", + "dpkg_lock_not_available": "Ezin da komando hau une honetan exekutatu beste aplikazio batek dpkg (sistemaren paketeen kudeatzailea) blokeatuta duelako, erabiltzen ari baita", + "dyndns_could_not_check_available": "Ezinezkoa izan da {domain} {provider}(e)n eskuragarri dagoen egiaztatzea.", + "dyndns_domain_not_provided": "{provider} DynDNS enpresak ezin du {domain} domeinua eskaini.", + "dyndns_ip_update_failed": "Ezin izan da IP helbidea DynDNSan eguneratu", + "dyndns_ip_updated": "IP helbidea DynDNS-n eguneratu da", + "dyndns_key_not_found": "Ez da domeinurako DNS gakorik aurkitu", + "dyndns_no_domain_registered": "Ez dago DynDNSrekin izena emandako domeinurik", + "dyndns_no_recovery_password": "Ez da berreskuratze-pasahitzik zehaztu! Domeinuaren gaineko kontrola galduz gero, YunoHost taldeko administratzailearekin jarri beharko zara harremanetan!", + "dyndns_provider_unreachable": "Ezin da DynDNS {provider} enpresarekin konektatu: agian zure YunoHost zerbitzaria ez dago Internetera konektatuta edo dynette zerbitzaria ez dago martxan.", + "dyndns_set_recovery_password_denied": "Berreskuratze-pasahitza ezartzeak huts egin du: gako okerra", + "dyndns_set_recovery_password_failed": "Berreskuratze-pasahitza ezartzeak huts egin du: {error}", + "dyndns_set_recovery_password_invalid_password": "Berreskuratze-pasahitza ezartzeak huts egin du: pasahitza ez da nahikoa sendoa", + "dyndns_set_recovery_password_success": "Berreskuratze-pasahitza ezarri da!", + "dyndns_set_recovery_password_unknown_domain": "Berreskuratze-pasahitza ezartzeak huts egin du: domeinua ez dago erregistratuta", + "dyndns_subscribe_failed": "Ezin izan da DynDNS domeinua harpidetu: {error}", + "dyndns_subscribed": "DynDNS domeinua harpidetu da", + "dyndns_too_many_requests": "YunoHosten dyndns zerbitzuak zuk egindako eskaera gehiegi jaso ditu, itxaron ordubete inguru berriro saiatu baino lehen.", + "dyndns_unavailable": "'{domain}' domeinua ez dago eskuragarri.", + "dyndns_unsubscribe_already_unsubscribed": "Domeinuaren harpidetza utzita dago lehendik ere", + "dyndns_unsubscribe_denied": "Domeinuaren harpidetza uzteak huts egin du: datu okerrak", + "dyndns_unsubscribe_failed": "Ezin izan da DynDNS domeinuaren harpidetza utzi: {error}", + "dyndns_unsubscribed": "DynDNS domeinuaren harpidetza utzi da", + "error_changing_file_permissions": "Errorea {path}-i eragiten dioten baimenak aldatzean: {error}", + "error_removing": "Errorea {path} ezabatzerakoan: {error}", + "error_writing_file": "Errorea {file} fitxategia idazterakoan: {error}", + "extracting": "Ateratzen…", + "field_invalid": "'{field}' ez da baliogarria", + "file_does_not_exist": "{path} fitxategia ez da existitzen.", + "file_not_exist": "Fitxategia ez da existitzen: '{path}'", + "firewall_reload_failed": "Ezinezkoa izan da suebakia birkargatzea. Informazio gehiago erregistroetan.", + "firewall_reloaded": "Suebakia birkargatu da", + "global_settings_reset_success": "Berrezarri defektuzko ezarpenak", + "global_settings_setting_admin_strength": "Administrazio-pasahitzaren segurtasuna", + "global_settings_setting_admin_strength_help": "Betekizun hauek pasahitza lehenbizikoz sortzerakoan edo aldatzerakoan baino ez dira bete behar", + "global_settings_setting_antispam_name": "SPAMaren aurkakoa", + "global_settings_setting_backup_compress_tar_archives": "Konprimatu babeskopiak", + "global_settings_setting_backup_compress_tar_archives_help": "Babeskopia berriak sortzean, konprimitu fitxategiak (.tar.gz) konprimitu gabeko fitxategien (.tar) ordez. Aukera hau gaitzean babeskopiek espazio gutxiago beharko dute, baina hasierako prozesua luzeagoa izango da eta CPUari lan handiagoa eragingo dio.", + "global_settings_setting_backup_name": "Babeskopia", + "global_settings_setting_dns_custom_resolvers_enabled": "Erabili DNS ebazle (resolver) propioak", + "global_settings_setting_dns_custom_resolvers_enabled_help": "Defektuz, YunoHostek Europan kokatutako eta fidagarritzat hartzen diren ebazleen (resolver) zerrenda bat erabiltzen du. Litekeena da erabiltzaile adituek beste ebazle batzuk ezarri nahi izatea.", + "global_settings_setting_dns_custom_resolvers_list": "Norberak ezarritako ebazleen helbideak", + "global_settings_setting_dns_custom_resolvers_list_help": "Gutxienez 2 DNS ebazleren zerrenda erabiltzen den IP protokoloko (IPv4/IPv6). Adibidea: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "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.", + "global_settings_setting_email_name": "ePosta", + "global_settings_setting_enable_blocklists": "Gaitu blokeo-zerrendak sarrera-trafikorako", + "global_settings_setting_enable_blocklists_help": "Mezu baztergarriak galarazi aldera, spamcop.net, spamhaus.org eta abuseat.org ekimenek zerrendatutako zerbitzariak blokeatzen ditu. Honek arazoak eragin ditzake hirugarren horiek zerrendatutakoak kaltegarriak ez diren posta zerbitzari batzuentzat; kasu horietan, ez da jasoko zerbitzari horietatik bidalitako postarik.", + "global_settings_setting_experimental_name": "Esperimentala", + "global_settings_setting_misc_name": "Beste batzuk", + "global_settings_setting_network_name": "Sarea", + "global_settings_setting_nginx_compatibility": "NGINXekin bateragarritasuna", + "global_settings_setting_nginx_compatibility_help": "Bateragarritasun eta segurtasun arteko gatazka NGINX web zerbitzarirako. Zifraketari eragiten dio (eta segurtasunari lotutako beste kontu batzuei)", + "global_settings_setting_nginx_name": "NGINX (web zerbitzaria)", + "global_settings_setting_nginx_redirect_to_https": "Behartu HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Birbideratu HTTP eskaerak HTTPSra (EZ ITZALI hau ez badakizu zertan ari zaren!)", + "global_settings_setting_password_name": "Pasahitzak", + "global_settings_setting_passwordless_sudo": "Baimendu administratzaileek 'sudo' erabiltzea pasahitzak berriro idatzi beharrik gabe", + "global_settings_setting_pop3_enabled": "Gaitu POP3", + "global_settings_setting_pop3_enabled_help": "Gaitu POP3 protokoloa ePosta zerbitzarirako. POP3 posta elektronikoko bezeroen postontzietan sartzeko protokolo zaharragoa da, baita arinagoa ere, baina ezaugarri gutxiago ditu IMAPek baino (defektuz gaituta)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Baimendu erabiltzaileek euren ePosta helbide nagusia editatzea", + "global_settings_setting_portal_allow_edit_email_alias": "Baimendu erabiltzaileek ePosta ezizenak gehitzea, kentzea eta editatzea", + "global_settings_setting_portal_allow_edit_email_alias_help": "Baimendu ezean, administrariei eskatu behar dizkiete aldaketak.", + "global_settings_setting_portal_allow_edit_email_forward": "Baimendu erabiltzaileek ePosta birbidalketak gehitzea, kentzea eta editatzea", + "global_settings_setting_portal_allow_edit_email_forward_help": "Baimendu ezean, administrariei eskatu behar dizkiete aldaketak.", + "global_settings_setting_portal_allow_edit_email_help": "Baimendu ezean, administrariei eskatu behar dizkiete aldaketak.", + "global_settings_setting_portal_name": "Ataria", + "global_settings_setting_postfix_compatibility": "Postfixekin bateragarritasuna", + "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_postfix_name": "Postfix (SMTP ePosta zerbitzaria)", + "global_settings_setting_root_access_explain": "Linux sistemetan 'root' administratzaile gorena da. YunoHosten testuinguruan, zuzeneko 'root' SSH saioa desgaituta dago defektuz, zerbitzariaren sare lokaletik ez bada. 'administratzaileak' taldeko kideek sudo komandoa erabili dezakete root bailitzan jarduteko terminalaren bidez. Hala ere, lagungarri izan liteke root pasahitz (sendo) bat izatea sistema arazteko egoeraren batean administratzaile arruntek saiorik hasi ezin balute.", + "global_settings_setting_root_access_name": "Aldatu root pasahitza", + "global_settings_setting_root_password": "root pasahitz berria", + "global_settings_setting_root_password_confirm": "root pasahitz berria (egiaztatu)", + "global_settings_setting_security_experimental_enabled": "Segurtasun ezaugarri esperimentalak", + "global_settings_setting_security_experimental_enabled_help": "Gaitu segurtasun funtzio esperimentalak (ez ezazu egin ez badakizu zertan ari zaren!)", + "global_settings_setting_security_name": "Segurtasuna", + "global_settings_setting_smtp_allow_ipv6": "Gaitu IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Gaitu IPv6 posta elektronikoa jaso eta bidaltzeko", + "global_settings_setting_smtp_backup_mx_domains": "Bigarren mailako MX gisa jarduteko domeinuak", + "global_settings_setting_smtp_backup_mx_domains_help": "Zerbitzari honek zerrendan agertzen den domeinurako *bigarren mailako* MX domeinu gisa jardun dezake. Domeinurako lehenetsitako MX lortu ezin denean (adibidez, itzalaldi baten ondorioz), mezuak bigarren zerbitzari horretara bidaliko dira —gehienez 20 egunez mantenduko dituena— eta berriro eskuragarri dagoenean benetako helburura helarazten saiatuko da. Hainbat domeinu zehaztu daitezke, komaz bereizita.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Baimendutako posta elektronikoen MXren SMTP babeskopia", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Bigarren mailako MX gisa jarduten duenean, baimendutako hartzaileen posta elektronikoko helbideen zerrenda zehatza eman beharko da (bestela, mezuak ukatu eta baztertuko dira). Hainbat sarrera eman daitezke, komaz bereizita.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Gaitu SMTP errelea", + "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.", + "global_settings_setting_smtp_relay_host": "SMTP errele-ostatatzailea", + "global_settings_setting_smtp_relay_password": "SMTP relay pasahitza", + "global_settings_setting_smtp_relay_port": "SMTP relay ataka", + "global_settings_setting_smtp_relay_user": "SMTP relay erabiltzailea", + "global_settings_setting_ssh_compatibility": "SSH bateragarritasuna", + "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_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Pasahitz bidezko autentifikazioa", + "global_settings_setting_ssh_password_authentication_help": "Baimendu pasahitz bidezko autentikazioa SSHrako", + "global_settings_setting_ssh_port": "SSH ataka", + "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.", + "global_settings_setting_tls_passthrough_enabled": "Gaitu TLS-passthrough / SNI-n oinarritutako birbidalketa", + "global_settings_setting_tls_passthrough_enabled_help": "Funtzio aurreratua da reverse-proxy erabiliz domeinu oso bat beste makina batera desbideratzeko *trafikoa deszifratu gabe*. Erabilgarria da IP berarekin makina bat baino gehiago zerbitzatu nahi dituzunean, makina bakoitzak dagozkion SSL amaierak kudeatzeko aukera mantenduz.", + "global_settings_setting_tls_passthrough_explain": "Ezaugarri hau AURRERATUA eta ESPERIMENTALA da, eta aldaketa handiak eragingo ditu zerbitzari honen nginx konfigurazioan. EZ erabili ez badakizu zertan ari zaren! Kontuan izan fail2ban ezin dela inplementatu proxy atzeko zerbitzarian (nfttables-ek ezin dute trafiko maltzurra debekatu, IP pakete guztiak zerbitzari nagusitik datozelaren itxura dutelako). Horrez gain, oraingoz, proxy atzeko zerbitzariaren nginx konfigurazioa eskuz moldatu behar da `proxy_protocol` onartzeko.", + "global_settings_setting_tls_passthrough_list": "Birbidalketen zerrenda", + "global_settings_setting_tls_passthrough_list_help": "Zerrendak DOMEINUA;HELBURUA;ATAKA egitura izan behar du. Adibidez: domeinua.eus;192.168.1.42;443 edo domeinua.eus;zerbitzaria.local;8123", + "global_settings_setting_tls_passthrough_name": "TLS-passthrough / SNIn oinarritutako birbidalketa", + "global_settings_setting_user_strength": "Erabiltzaile-pasahitzaren segurtasuna", + "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-gunera sartzeko baimendutako IPak", + "global_settings_setting_webadmin_allowlist_enabled_help": "Baimendu IP zehatz batzuk bakarrik administrazio-gunerako.", + "global_settings_setting_webadmin_allowlist_help": "Administrazio-atarira sar daitezken IP helbideak. CIDR notazioa ahalbidetzen da.", + "global_settings_setting_webadmin_name": "Web administrazioa", + "good_practices_about_admin_password": "Administrazio-pasahitz berria ezartzear zaude. Pasahitzak 8 karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", + "good_practices_about_user_password": "Erabiltzaile-pasahitz berria ezartzear zaude. Pasahitzak 8 karaktere izan beharko lituzke gutxienez, baina gomendagarria da pasahitz luzeagoa erabiltzea (esaldi bat, esaterako) edota karaktere desberdinak erabiltzea (hizki larriak, txikiak, zenbakiak eta karaktere bereziak).", + "group_already_exist": "{group} taldea existitzen da lehendik ere", + "group_already_exist_on_system": "{group} taldea existitzen da lehendik ere sistemaren taldeetan", + "group_already_exist_on_system_but_removing_it": "{group} taldea existitzen da sistemaren taldeetan, baina YunoHostek ezabatuko du…", + "group_cannot_be_deleted": "{group} taldea ezin da eskuz ezabatu.", + "group_cannot_edit_all_users": "'all_users' taldea ezin da eskuz moldatu. YunoHosten izena emanda dauden erabiltzaile guztiak barne dituen talde berezia da", + "group_cannot_edit_primary_group": "'{group}' taldea ezin da eskuz moldatu. Erabiltzaile zehatz bakar bat duen talde nagusia da.", + "group_cannot_edit_visitors": "'bisitariak' taldea ezin da eskuz moldatu. Saiorik hasi gabeko bisitariak barne hartzen dituen talde berezia da", + "group_cannot_remove_last_admin": "'{user}' erabiltzailea 'admins' taldeko azken erabiltzailea da, eta ez da bertatik kenduko.", + "group_created": "'{group}' taldea sortu da", + "group_creation_failed": "Ezinezkoa izan da '{group}' taldea sortzea: {error}", + "group_deleted": "'{group}' taldea ezabatu da", + "group_deletion_failed": "Ezinezkoa izan da '{group}' taldea ezabatzea: {error}", + "group_mailalias_add": "'{mail}' ePosta ezizena jarri zaio '{group}' taldeari", + "group_mailalias_remove": "'{mail}' ePosta ezizena kendu zaio '{group}' taldeari", + "group_no_change": "Ez da ezer aldatu behar '{group}' talderako", + "group_unknown": "'{group}' taldea ezezaguna da", + "group_update_aliases": "'{group}' taldearen ezizenak eguneratzen", + "group_update_failed": "Ezinezkoa izan da '{group}' taldea eguneratzea: {error}", + "group_updated": "'{group}' taldea eguneratu da", + "group_user_add": "'{user}' erabiltzailea '{group}' taldera gehituko da", + "group_user_already_in_group": "{user} erabiltzailea {group} taldean dago lehendik ere", + "group_user_not_in_group": "{user} erabiltzailea ez dago {group} taldean", + "group_user_remove": "'{user}' erabiltzailea '{group}' taldetik kenduko da", + "hook_exec_failed": "Ezinezkoa izan da agindua exekutatzea: {path}", + "hook_exec_not_terminated": "Aginduak ez du behar bezala amaitu: {path}", + "hook_json_return_error": "Ezin izan da {path} aginduaren erantzuna irakurri. Errorea: {msg}. Jatorrizko edukia: {raw_content}", + "hook_list_by_invalid": "Aukera hau ezin da 'hook'ak zerrendatzeko erabili", + "hook_name_unknown": "'{name}' 'hook' izen ezezaguna", + "installation_complete": "Instalazioa amaitu da", + "invalid_credentials": "Pasahitz edo erabiltzaile-izen baliogabea", + "invalid_number": "Zenbaki bat izan behar da", + "invalid_password": "Pasahitz baliogabea", + "invalid_regex": "'Regexa' ez da zuzena: '{regex}'", + "invalid_shell": "Shell baliogabea: {shell}", + "invalid_url": "{url}-(e)ra konektatzeak huts egin du… agian zerbitzua ez dago martxan, edo ez zaude IPv4/IPv6 bidez ondo konektatuta Internetera.", + "ldap_attribute_already_exists": "'{attribute}' LDAP funtzioa existitzen da lehendik ere eta '{value}' balioa dauka", + "ldap_server_down": "Ezin da LDAP zerbitzarira konektatu", + "ldap_server_is_down_restart_it": "LDAP zerbitzaria ez dago martxan, saia zaitez berrabiarazten…", + "log_app_action_run": "'{}' aplikazioaren eragiketa exekutatu", + "log_app_change_url": "'{}' aplikazioaren URLa aldatu", + "log_app_config_set": "Ezarri '{}' aplikazioko konfigurazioa", + "log_app_install": "'{}' aplikazioa instalatu", + "log_app_makedefault": "Lehenetsi '{}' aplikazioa", + "log_app_remove": "Ezabatu '{}' aplikazioa", + "log_app_upgrade": "'{}' aplikazioa eguneratu", + "log_available_on_yunopaste": "Erregistroa {url} estekan ikus daiteke", + "log_backup_create": "Sortu babeskopia fitxategia", + "log_backup_restore_app": "Lehengoratu '{}' babeskopia fitxategi bat erabiliz", + "log_backup_restore_system": "Lehengoratu sistema babeskopia fitxategi batetik", + "log_corrupted_md_file": "Erregistroei lotutako YAML metadatu fitxategia kaltetuta dago: '{md_file}\nErrorea: {error}'", + "log_diagnosis_run": "Abiarazi diagnostikoa", + "log_does_exists": "Ez dago '{log}' izena duen eragiketa-erregistrorik; erabili 'yunohost log list' eragiketa-erregistro guztiak ikusteko", + "log_domain_add": "Gehitu '{}' domeinua", + "log_domain_config_set": "Aldatu '{}' domeinuko ezarpenak", + "log_domain_dns_push": "Bidali '{}' domeinuaren DNS ezarpenak", + "log_domain_main_domain": "Lehenetsi '{}' domeinua", + "log_domain_remove": "Ezabatu '{}' domeinua", + "log_dyndns_subscribe": "Erregistratu YunoHosten '{}' azpidomeinua", + "log_dyndns_unsubscribe": "Utzi '{}' YunoHost azpidomeinuaren erregistroa", + "log_dyndns_update": "Eguneratu YunoHosten '{}' domeinuari lotutako IP helbidea", + "log_help_to_get_failed_log": "Ezin izan da '{desc}' eragiketa exekutatu. Laguntza nahi baduzu partekatu eragiketa honen erregistro osoa 'yunohost log share {name}' komandoa erabiliz", + "log_help_to_get_log": "'{desc}' eragiketaren erregistroa ikusteko, exekutatu 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Instalatu Let's Encrypt ziurtagiria '{}' domeinurako", + "log_letsencrypt_cert_renew": "Berriztu '{}' Let's Encrypt ziurtagiria", + "log_link_to_failed_log": "Ezinezkoa izan da '{desc}' eragiketa exekutatzea. Laguntza nahi izanez gero, partekatu erakigeta honen erregistro osoa hemen sakatuz", + "log_link_to_log": "Eragiketa honen erregistro osoa: '{desc}'", + "log_operation_unit_unclosed_properly": "Eragiketa ez da modu egokian itxi", + "log_regen_conf": "Berregin '{}' sistemaren konfigurazioa", + "log_remove_on_failed_install": "Ezabatu '{}' instalazioak huts egin ondoren", + "log_resource_snippet": "Baliabide bat eguneratzen / eskuratzen / eskuragarritasuna uzten", + "log_selfsigned_cert_install": "Instalatu '{}' domeinurako norberak sinatutako ziurtagiria", + "log_settings_reset": "Berrezarri ezarpenak", + "log_settings_reset_all": "Berrezarri ezarpen guztiak", + "log_settings_set": "Aplikatu ezarpenak", + "log_tools_migrations_migrate_forward": "Exekutatu migrazioak", + "log_tools_postinstall": "Abiarazi YunoHost zerbitzaria instalatu ondorengo prozesua", + "log_tools_reboot": "Berrabiarazi zerbitzaria", + "log_tools_shutdown": "Itzali zerbitzaria", + "log_tools_update": "Sistemaren eguneraketak eskuratzen eta aplikazioen katalogoa freskatzen", + "log_tools_upgrade": "Eguneratu sistemaren paketeak", + "log_user_create": "Gehitu '{}' erabiltzailea", + "log_user_delete": "Ezabatu '{}' erabiltzailea", + "log_user_group_create": "Sortu '{}' taldea", + "log_user_group_delete": "Ezabatu '{}' taldea", + "log_user_group_update": "Moldatu '{}' taldea", + "log_user_import": "Inportatu erabiltzaileak", + "log_user_update": "Eguneratu '{}' erabiltzailearen informazioa", + "mail_alias_remove_failed": "Ezin izan da '{mail}' e-mail ezizena ezabatu", + "mail_alias_unauthorized": "Ez duzu baimenik '{domain}' domeinurako ezizenak gehitzeko", + "mail_already_exists": "'{mail}' posta helbidea lehendik ere dago", + "mail_domain_unknown": "Ezinezkoa da posta elektroniko hori '{domain}' domeinurako erabiltzea. Erabili zerbitzari honek kudeatzen duen domeinu bat.", + "mail_edit_operation_unauthorized": "Ez duzu baimenik kontuaren aldaketa hau egiteko.", + "mail_forward_remove_failed": "Ezinezkoa izan da '{mail}' posta elektronikoko birbidalketa ezabatzea", + "mail_unavailable": "Helbide elektroniko hau administratzaileen taldearentzat gorde da", + "mailbox_disabled": "Posta elektronikoa desgaituta dago {user} erabiltzailearentzat", + "mailbox_used_space_dovecot_down": "Dovecot mailbox zerbitzua martxan egon behar da postak erabilitako espazioa ezagutzeko", + "main_domain_change_failed": "Ezin da domeinu nagusia aldatu", + "main_domain_changed": "Domeinu nagusia aldatu da", + "migration_0027_cleaning_up": "Erabilgarri izateari utzi dioten katxe eta paketeak garbitzen…", + "migration_0027_delayed_api_restart": "YunoHosten APIa 15 segundu barru berrabiaraziko da automatikoki. Litekeena da tarte batez erabilgarri egoteari uztea eta ondoren berriro hasi beharko duzu saioa.", + "migration_0027_general_warning": "Azkenik, kontuan izan migrazioa **tentuz ibiltzeko prozedura** dela. YunoHosten taldeak ahal izan duen guztia egin du berrikusi eta probatzeko, baina, hala ere, sistemaren atalak edo aplikazioak honda litzake.\n\nBeraz, gomendagarria da:\n - Datu edo aplikazio garrantzitsuen **babeskopia egitea**. Informazio gehiagorako: https://doc.yunohost.org/backup;\n - **Ez izan presarik** migrazioa abiaraztean: Internet konexioaren eta hardwarearen arabera, litekeena da ordu apur batzuk ere behar izatea guztia dagokion bezala bertsio-berritzeko.\n - **Bisitatu foroa** arazoren bat izanez gero laguntza eskatzeko.", + "migration_0027_main_upgrade": "Bertsio-berritze nagusia abiarazten…", + "migration_0027_modified_files": "Honako fitxategiak eskuz moldatu direla antzeman da eta litekeena da bertsio-berritzeak gainean idaztea: {manually_modified_files}", + "migration_0027_not_bullseye": "Zerbitzariak darabilen Debian bertsioa ez da Bullseye! Lehendik ere Bullseye -> Bookworm 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.", + "migration_0027_not_enough_free_space": "/var/-en erabilgarri dagoen espazioa oso txikia da! Gutxienez GB 1 izan beharko zenuke erabilgarri migrazioari ekiteko.", + "migration_0027_patch_yunohost_conflicts": "Arazo gatazkatsu bati adabakia jartzen…", + "migration_0027_patching_sources_list": "sources.lists fitxategiari adabakia jartzen…", + "migration_0027_problematic_apps_warning": "Kontuan izan ziur asko gatazkatsuak izango diren honako aplikazioak aurkitu direla. Badirudi ez zirela YunoHost aplikazioen katalogotik instalatu, edo ez daude 'badabiltza' bezala etiketatuta. Ondorioz, ezin da bermatu eguneratu ondoren funtzionatzen jarraituko dutenik: {problematic_apps}", + "migration_0027_start": "Bookworm-erako migrazioa abiarazten…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Zerbaitek kale egin du bertsio-berritze nagusian; sistemak oraindik Debian Bullseye darabilela dirudi.", + "migration_0027_system_not_fully_up_to_date": "Sistema ez dago erabat egunean. Egizu eguneratze arrunt bat Bookworm-erako migrazioa abiarazi baino lehen.", + "migration_0027_yunohost_upgrade": "YunoHosten muineko bertsio-berriztea abiarazten…", + "migration_not_enough_space": "Egin nahikoa toki {path}-en mugrazioa exekutatu ahal izateko.", + "migration_postgresql_previous_not_installed": "PostgreSQL ez zegoen sisteman instalatuta. Ez dago egiteko ezer.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 instalatuta dago, baina PostgreSQL 15 ez? Zerbait arraroa gertatu zaio zure sistemari :( …", + "migration_python_venv_rebuild_broken_app": "{app} aplikazioaren virtualenv-a ezin da modu errazean berreraiki eta egin gabe utziko da. Konpondu aplikazioa bertsio-berritzen 'yunohost app upgrade --force {app}' erabiliz.", + "migration_python_venv_rebuild_disclaimer_base": "Debian Bookworm-era eguneratu ondoren, Python-en aplikazio batzuk partzialki berreraiki behar dira Debian-ek dakarren Python-en bertsio berrira bihurtzeko (termino teknikoetan: 'virtualenv' deritzaiona birsortu egin behar da). Bitartean, baliteke Python aplikazio horiek ez funtzionatzea. YunoHost virtualenv-a berreraikitzen saia daiteke horietako batzuentzat, jarraian zehazten den bezala. Beste aplikazio batzuetarako, edo berreraikitzeko saiakerak huts egiten badu, aplikazio horiek eskuz bertsio-berritzera behartu beharko dituzu.", + "migration_python_venv_rebuild_disclaimer_ignored": "Ezin dira automatikoki berreraiki aplikazio horien virtualenvs-ak. Bertsio-berritzera behartu behar dituzu honako komandoarekin: 'yunohost app upgrade --force APP': {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "Honako aplikazioen virtualenvs-ak berreraikitzen saiatuko gara (NB: eragiketak luze jo dezake!): {rebuild_apps}", + "migration_python_venv_rebuild_failed": "Huts egin du {app} aplikazioaren Python virtualenv-a berreraikitzeak. Litekeena da aplikazioa ezin erabili izatea egoera konpondu arte. Saiatu aplikazioa bertsio-berritzen 'yunohost app upgrade --force {app}' erabiliz.", + "migration_python_venv_rebuild_in_progress": "{app} aplikazioaren Python virtualenv-a berreraikitze lanetan", + "migration_0031_terms_of_services": "Migrazio hau informazio mezu bat baino ez da: YunoHost proiektuak zerbitzu tekniko eta komunitarioei buruzko Zerbitzu-baldintzak argitaratzen ditu orain.", + "migration_0036_cleaning_up": "Erabilgarri izateari utzi dioten katxe eta paketeak garbitzen…", + "migration_0036_delayed_api_restart": "YunoHosten APIa 15 segundu barru berrabiaraziko da automatikoki. Litekeena da tarte batez erabilgarri egoteari uztea eta ondoren berriro hasi beharko duzu saioa.", + "migration_0036_general_warning": "Azkenik, kontuan izan migrazioa **tentuz ibiltzeko prozedura** dela. YunoHosten taldeak ahal izan duen guztia egin du berrikusi eta probatzeko, baina, hala ere, sistemaren atalak edo aplikazioak honda litzake.\n\nBeraz, gomendagarria da:\n - Datu edo aplikazio garrantzitsuen **babeskopia egitea**. Informazio gehiagorako: https://doc.yunohost.org/backup;\n - **Ez izan presarik** migrazioa abiaraztean: Internet konexioaren eta hardwarearen arabera, litekeena da ordu apur batzuk ere behar izatea guztia dagokion bezala bertsio-berritzeko.\n - **Bisitatu foroa** arazoren bat izanez gero laguntza eskatzeko.", + "migration_0036_main_upgrade": "Bertsio-berritze nagusia abiarazten…", + "migration_0036_modified_files": "Honako fitxategiak eskuz moldatu direla antzeman da, eta litekeena da bertsio-berritzeak gainean idaztea:", + "migration_0036_not_bullseye": "Zerbitzariak darabilen Debian bertsioa ez da Bookworm! Lehendik ere Bookworm -> Trixie 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.", + "migration_0036_not_enough_free_space": "/var/-en erabilgarri dagoen espazioa oso txikia da! Gutxienez GB 1 izan beharko zenuke erabilgarri migrazioari ekiteko.", + "migration_0036_patch_yunohost_dpkg": "Gatazkak saihesteko dpkg datu-baseari adabakia jartzen…", + "migration_0036_patching_sources_list": "sources.lists fitxategiari adabakia jartzen…", + "migration_0036_problematic_apps_warning": "Kontuan izan ziur asko gatazkatsuak izango diren honako aplikazioak aurkitu direla. Badirudi ez zirela YunoHost aplikazioen katalogotik instalatu, edo ez daude 'badabiltza' bezala etiketatuta. Ondorioz, ezin da bermatu eguneratu ondoren funtzionatzen jarraituko dutenik:", + "migration_0036_start": "Trixie-rako migrazioa abiarazten…", + "migration_0036_still_on_bookworm_after_main_upgrade": "Zerbaitek kale egin du bertsio-berritze nagusian; sistemak oraindik Debian Bookworm darabilela dirudi.", + "migration_0036_system_not_fully_up_to_date": "Sistema ez dago erabat egunean. Egizu eguneratze arrunt bat Trixie-rako migrazioa abiarazi baino lehen.", + "migration_0036_yunohost_upgrade": "YunoHosten muineko bertsio-berriztea abiarazten…", + "migration_description_0027_migrate_to_bookworm": "Bertsio-berritu sistema Debian Bookworm eta YunoHost 12-ra", + "migration_description_0028_delete_legacy_xmpp_permission": "Ezabatu XMPPren baimen zaharrak, Metronome orain aplikazio bat da", + "migration_description_0029_postgresql_13_to_15": "Migratu datu-baseak PostgreSQL 13tik 15era", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Konpondu Python aplikazioa bookworm-en migrazioa eta gero", + "migration_description_0031_terms_of_services": "Zerbitzu-baldintzak", + "migration_description_0032_firewall_config": "Barneko suebakiaren konfigurazio-fitxategiaren migrazioa", + "migration_description_0033_rework_permission_infos": "Berrantolatu gordetako aplikazio-baimenak", + "migration_description_0034_fix_missing_admins_aliases": "Konpondu administrazio talderako falta diren ePosta ezizenak", + "migration_description_0035_fix_apps_nodejs_version": "Konpondu nodejs bertsioa aplikazioaren systemd konfigurazioetan", + "migration_description_0036_migrate_to_trixie": "Bertsio-berritu sistema Debian Trixie eta YunoHost 13-ra", + "migration_ldap_backup_before_migration": "Sortu LDAP datubase eta aplikazioen ezarpenen babeskopia migrazioa abiarazi baino lehen.", + "migration_ldap_can_not_backup_before_migration": "Sistemaren babeskopiak ez du amaitu migrazioak huts egin baino lehen. Errorea: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Ezin izan da migratu… sistema lehengoratzen saiatzen.", + "migration_ldap_rollback_success": "Sistema lehengoratu da.", + "migrations_already_ran": "Honako migrazio hauek amaitu dute lehendik ere: {ids}", + "migrations_dependencies_not_satisfied": "Exekutatu honako migrazioak: '{dependencies_id}', {id} migratu baino lehen.", + "migrations_exclusive_options": "'--auto', '--skip', eta '--force-rerun' aukerek batak bestea baztertzen du.", + "migrations_failed_to_load_migration": "Ezinezkoa izan da {id} migrazioa kargatzea: {error}", + "migrations_list_conflict_pending_done": "Ezin dituzu '--previous' eta '--done' aldi berean erabili.", + "migrations_loading_migration": "{id} migrazioa kargatzen…", + "migrations_migration_has_failed": "{id} migrazioak ez du amaitu, geldiarazten. Errorea: {exception}", + "migrations_must_provide_explicit_targets": "'--skip' edo '--force-rerun' aukerak erabiltzean jomuga zehatzak zehaztu behar dituzu", + "migrations_need_to_accept_disclaimer": "{id} migrazioa abiarazteko, honako baldintzak onartu behar dituzu:\n---\n{disclaimer}\n---\nMigrazioa onartzen baduzu, berrabiarazi prozesua komandoan '--accept-disclaimer' aukera gehituz.", + "migrations_no_migrations_to_run": "Ez dago exekutatzeko migraziorik", + "migrations_no_such_migration": "Ez dago '{id}' izeneko migraziorik", + "migrations_not_pending_cant_skip": "Migrazio hauek ez daude exekutatzeke eta, beraz, ez dago saihesteko aukerarik: {ids}", + "migrations_pending_cant_rerun": "Migrazio hauek oraindik ez dira exekutatu eta, beraz, ezin dira berriro abiarazi: {ids}", + "migrations_running_forward": "{id} migrazioa exekutatzen…", + "migrations_skip_migration": "{id} migrazioa saihesten…", + "migrations_success_forward": "{id} migrazioak amaitu du", + "migrations_to_be_ran_manually": "{id} migrazioa eskuz abiarazi behar da. Joan Tresnak → Migrazioak atalera administrazio-gunean edo bestela exekutatu 'yunohost tools migrations run'.", + "nftables_unavailable": "Ezin dituzu iptaulak hemen moldatu; edukiontzi bat erabiltzen ari zara edo kernelak ez du aukera hau onartzen", + "noninteractive_task": "Interakziorik gabeko ataza", + "not_enough_disk_space": "Ez dago nahikoa espazio librerik '{path}'-n", + "operation_interrupted": "Eragiketa eskuz geldiarazi da?", + "other_available_options": "… eta erakusten ez diren beste {n} aukera daude", + "password_confirmation_not_the_same": "Pasahitzak ez datoz bat", + "password_listed": "Pasahitz hau munduan erabilienetarikoa da. Aukeratu bereziagoa den beste bat.", + "password_too_long": "Aukeratu 127 karaktere baino laburragoa den pasahitz bat", + "password_too_simple_1": "Pasahitzak 8 karaktere izan behar ditu gutxienez", + "password_too_simple_2": "Pasahitzak 8 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat eta txikiren bat izan behar ditu", + "password_too_simple_3": "Pasahitzak 8 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", + "password_too_simple_4": "Pasahitzak 12 karaktere izan behar ditu gutxienez eta zenbakiren bat, hizki larriren bat, txikiren bat eta karaktere bereziren bat izan behar ditu", + "pattern_backup_archive_name": "Fitxategiaren izenak 30 karaktere izan ditzake gehienez, alfanumerikoak eta ._- baino ez", + "pattern_domain": "Domeinu izen baliagarri bat izan behar da (adibidez: nire-domeinua.eus)", + "pattern_email": "Helbide elektroniko baliagarri bat izan behar da, '+' karaktererik gabe (adibidez: izena@domeinua.eus)", + "pattern_email_forward": "Helbide elektroniko baliagarri bat izan behar da, '+' karakterea onartzen da (adibidez: izena+urtea@domeinua.eus)", + "pattern_fullname": "Baliozko izen oso bat izan behar da (gutxienez hiru karaktere)", + "pattern_mailbox_quota": "Tamainak b/k/M/G/T zehaztu behar du edo 0 mugarik ezarri nahi ez bada", + "pattern_password": "Gutxienez hiru karaktere izan behar ditu", + "pattern_password_app": "Barka, baina pasahitzek ezin dituzte honako karaktereak izan: {forbidden_chars}", + "pattern_port_or_range": "Ataka zenbaki (0-65535) edo errenkada (100:200) baliagarri bat izan behar da", + "pattern_username": "Txikiz idatzitako karaktere alfanumerikoak eta azpiko marra soilik eduki ditzake", + "permission_already_allowed": "'{group} taldeak badauka lehendik ere '{permission}' baimena", + "permission_already_disallowed": "'{group}' taldeak desgaituta dauka lehendik ere '{permission} baimena", + "permission_cannot_remove_main": "Ezin da baimen nagusi bat kendu", + "permission_cant_add_to_all_users": "{permission} baimena ezin da erabiltzaile guztiei ezarri.", + "permission_created": "'{permission}' baimena sortu da", + "permission_creation_failed": "Ezinezkoa izan da '{permission}' baimena sortzea: {error}", + "permission_currently_allowed_for_all_users": "Baimen hau erabiltzaile guztiei esleitzen zaie eta baita beste talde batzuei ere. Litekeena da 'all users' baimena edo esleituta duten taldeei baimena kendu nahi izatea.", + "permission_deleted": "'{permission}' baimena ezabatu da", + "permission_deletion_failed": "Ezinezkoa izan da '{permission}' baimena ezabatzea: {error}", + "permission_not_found": "Ez da '{permission}' baimena aurkitu", + "permission_protected": "'{permission}' baimena babestuta dago. Ezin duzu bisitarien taldea baimen honetara gehitu / baimen honetatik kendu.", + "permission_require_account": "'{permission}' baimena zerbitzarian kontua duten erabiltzaileentzat da eta, beraz, ezin da gaitu bisitarientzat.", + "permission_update_failed": "Ezinezkoa izan da '{permission}' baimena aldatzea: {error}", + "permission_updated": "'{permission}' baimena moldatu da", + "port_already_closed": "{port}. ataka itxita dago lehendik", + "port_already_opened": "{port}. ataka lehendik ere irekita", + "postinstall_low_rootfsspace": "'root' fitxategi-sistemak 10 GB edo espazio gutxiago dauka, kezkatzekoa dena! Litekeena da espaziorik gabe geratzea aurki! Gomendagarria da 'root' fitxategi-sistemak gutxienez 16 GB libre izatea. Jakinarazpen honen ondoren YunoHost instalatzen jarraitu nahi baduzu, berrabiarazi agindua '--force-diskspace' gehituz", + "pydantic_type_error": "Mota baliogabea.", + "pydantic_type_error_none_not_allowed": "Balioa beharrezkoa da.", + "pydantic_type_error_str": "Mota baliogabea, katea espero da.", + "pydantic_value_error_color": "Kolore baliogabea, balioa izena edo hex kolorea izan behar da.", + "pydantic_value_error_const": "Ustekabeko balioa; aukeratu hauen artean {permitted}", + "pydantic_value_error_date": "Data-formatu baliogabea", + "pydantic_value_error_email": "Balioa ez da baliozko ePosta helbidea", + "pydantic_value_error_number_not_ge": "Balioa {limit_value} edo handiagoa izan behar da.", + "pydantic_value_error_number_not_le": "Balioa {limit_value} edo txikiagoa izan behar da.", + "pydantic_value_error_str_regex": "Kate baliogabea; balioak ez du '{pattern}' eredua errespetatzen", + "pydantic_value_error_time": "Denbora-formatu baliogabea", + "pydantic_value_error_url_extra": "URL baliogabea, aparteko karaktereak topatu dira baliozko URLaren ondoren: '{extra}'", + "pydantic_value_error_url_host": "Zerbitzariaren URL baliogabea", + "pydantic_value_error_url_port": "URLaren ataka baliogabea, ataka ezin da 65535 baino handiagoa izan", + "pydantic_value_error_url_scheme": "URL eskema falta da edo baliogabea da", + "regenconf_dry_pending_applying": "'{category}' atalari aplikatu behar zitzaion baina aplikatu gabeko konfigurazioa egiaztatzen…", + "regenconf_failed": "Ezinezkoa izan da honako atal(ar)en konfigurazioa berregitea: {categories}", + "regenconf_file_backed_up": "'{conf} konfigurazio fitxategia '{backup}' babeskopian kopiatu da", + "regenconf_file_copy_failed": "Ezinezkoa izan da '{new}' konfigurazio fitxategi berria '{conf}'-(e)n kopiatzea", + "regenconf_file_kept_back": "'{conf}' konfigurazio fitxategia regen-conf-ek ({category} atala) ezabatzekoa zen, baina mantendu egin da.", + "regenconf_file_manually_modified": "'{conf}' konfigurazio fitxategia eskuz moldatu da eta ez da eguneratuko", + "regenconf_file_manually_removed": "'{conf}' konfigurazio fitxategia eskuz ezabatu da eta ez da berriro sortuko", + "regenconf_file_remove_failed": "Ezinezkoa izan da '{conf}' konfigurazio fitxategia ezabatzea", + "regenconf_file_removed": "'{conf}' konfigurazio fitxategia ezabatu da", + "regenconf_file_updated": "'{conf}' konfigurazio fitxategia eguneratu da", + "regenconf_need_to_explicitly_specify_ssh": "SSH ezarpenak eskuz aldatu dira, baina, aldaketak erabiltzeko, '--force' zehaztu behar duzu 'ssh' atalean.", + "regenconf_now_managed_by_yunohost": "'{conf}' konfigurazio fitxategia YunoHostek kudeatzen du orain ({category} atala).", + "regenconf_pending_applying": "'{category}' atalerako konfigurazioa ezartzen…", + "regenconf_up_to_date": "Konfigurazioa egunean dago lehendik ere '{category}' atalerako", + "regenconf_updated": "'{category}' atalerako ezarpenak eguneratu dira", + "regenconf_would_be_updated": "'{category}' atalerako konfigurazioa eguneratu izango litzatekeen", + "regex_incompatible_with_tile": "/!\\ Pakete-arduradunak! {permission}' baimenak show_tile aukera 'true' bezala dauka eta horregatik ezin duzue regex URLa URL nagusi bezala ezarri", + "regex_with_only_domain": "Ezin duzu regex domeinuetarako erabili; bideetarako bakarrik", + "registrar_infos": "Erregistro-enpresaren informazioa", + "restore_already_installed_app": "'{app}' IDa duen aplikazioa lehendik instalatuta dago", + "restore_already_installed_apps": "Honako aplikazioak ezin dira lehengoratu lehendik ere instalatuta daudelako: {apps}", + "restore_backup_too_old": "Babeskopia fitxategi hau ezin da lehengoratu YunoHosten bertsio zaharregi batetik datorrelako.", + "restore_cleaning_failed": "Ezin izan dira lehengoratzeko behin-behineko fitxategiak ezabatu", + "restore_complete": "Lehengoratzea amaitu da", + "restore_confirm_yunohost_installed": "Ziur al zaude lehendik instalatuta dagoen sistema lehengoratu nahi duzula? [{answers}]", + "restore_extracting": "Behar diren fitxategiak ateratzen…", + "restore_failed": "Ezin izan da sistema lehengoratu", + "restore_hook_unavailable": "'{part}'-(e)rako lehengoratze agindua ez dago erabilgarri ez sisteman ezta fitxategian ere", + "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_nothings_done": "Ez da ezer lehengoratu", + "restore_removing_tmp_dir_failed": "Ezinezkoa izan da behin-behineko direktorio zaharra ezabatzea", + "restore_running_app_script": "'{app}' aplikazioa lehengoratzen…", + "restore_running_hooks": "Lehengoratzeko 'hook'ak exekutatzen…", + "restore_system_part_failed": "Ezinezkoa izan da sistemaren '{part}' atala lehengoratzea", + "root_password_changed": "root pasahitza aldatu da", + "root_password_desynchronized": "Administratzailearen pasahitza aldatu da, baina YunoHostek ezin izan du aldaketa root pasahitzera hedatu!", + "server_reboot": "Zerbitzaria berrabiaraziko da", + "server_reboot_confirm": "Zerbitzaria berehala berrabiaraziko da, ziur al zaude? [{answers}]", + "server_shutdown": "Zerbitzaria itzaliko da", + "server_shutdown_confirm": "Zerbitzaria berehala itzaliko da, ziur al zaude? [{answers}]", + "service_add_failed": "Ezinezkoa izan da '{service}' zerbitzua gehitzea", + "service_added": "'{service}' zerbitzua gehitu da", + "service_already_started": "'{service}' zerbitzua matxan dago lehendik ere", + "service_already_stopped": "'{service}' zerbitzua geldiarazi da lehendik ere", + "service_cmd_exec_failed": "Ezin izan da '{command}' komandoa exekutatu", + "service_description_dnsmasq": "Domeinuen izenen ebazpena (DNSa) kudeatzen du", + "service_description_dovecot": "Posta elektronikorako programei mezuak jasotzea ahalbidetzen die (IMAP eta POP3 bidez)", + "service_description_fail2ban": "Internetetik datozen bortxaz egindako saiakerak eta bestelako erasoak ekiditen ditu", + "service_description_mysql": "Aplikazioen datuak gordetzen ditu (SQL datubasea)", + "service_description_nftables": "Zerbitzuen konexiorako atakak ireki eta ixteko kudeatzailea da", + "service_description_nginx": "Zerbitzariak ostatazen dituen webguneak ikusgai egiten ditu", + "service_description_opendkim": "Bidalitako ePostak DKIM erabiliz sinatzen ditu, enpresa handiek mezu baztergarritzat hartzeko arriskua txikitzeko asmoz", + "service_description_postfix": "Posta elektronikoa bidali eta jasotzeko erabiltzen da", + "service_description_postgresql": "Aplikazioen datuak gordetzen ditu (SQL datubasea)", + "service_description_redis-server": "Datuak bizkor atzitzeko, zereginak lerratzeko eta programen arteko komunikaziorako datubase berezi bat da", + "service_description_slapd": "Erabiltzaileak, domeinuak eta hauei lotutako informazioa gordetzen du", + "service_description_ssh": "Zerbitzarira sare lokaletik kanpo konektatzea ahalbidetzen du (SSH protokoloa)", + "service_description_yunohost-api": "YunoHosten web-interfazearen eta sistemaren arteko hartuemana kudeatzen du", + "service_description_yunohost-portal-api": "Atariaren interfaze desberdinen eta sistemaren arteko interakzioak kudeatzen ditu", + "service_description_yunomdns": "Sare lokalean zerbitzarira 'yunohost.local' erabiliz konektatzea ahalbidetzen du", + "service_disable_failed": "Ezin izan da '{service}' zerbitzua geldiarazi zerbitzaria abiaraztean.", + "service_disabled": "'{service}' zerbitzua ez da etorkizunean zerbitzaria abiaraztearekin batera exekutatuko.", + "service_enable_failed": "Ezin izan da '{service}' zerbitzua sistema abiaraztearekin batera exekutatzea lortu.", + "service_enabled": "'{service}' zerbitzua ez da automatikoki exekutatuko sistema abiaraztean.", + "service_not_reloading_because_conf_broken": "Ez da '{name}' zerbitzua birkargatu/berrabiarazi konfigurazioa kaltetuta dagoelako: {errors}", + "service_reload_failed": "Ezin izan da '{service}' zerbitzua birkargatu", + "service_reload_or_restart_failed": "Ezin izan da '{service}' zerbitzua birkargatu edo berrabiarazi", + "service_reloaded": "'{service}' zerbitzua birkargatu da", + "service_reloaded_or_restarted": "'{service}' zerbitzua birkargatu edo berrabiarazi da", + "service_remove_failed": "Ezin izan da '{service}' zerbitzua ezabatu", + "service_removed": "'{service}' zerbitzua ezabatu da", + "service_restart_failed": "Ezin izan da '{service}' zerbitzua berrabiarazi", + "service_restarted": "'{service}' zerbitzua berrabiarazi da", + "service_start_failed": "Ezin izan da '{service}' zerbitzua abiarazi", + "service_started": "'{service}' zerbitzua abiarazi da", + "service_stop_failed": "Ezin da '{service}' zerbitzua geldiarazi", + "service_stopped": "'{service}' zerbitzua geldiarazi da", + "service_unknown": "'{service}' zerbitzu ezezaguna", + "session_expired": "Saioa iraungi da", + "show_tile_cant_be_enabled_for_regex": "Ezin duzu 'show_tile' gaitu une honetan, '{permission}' baimenerako URLa regex delako", + "show_tile_cant_be_enabled_for_url_not_defined": "Ezin duzu 'show_tile' gaitu une honetan, '{permission}' baimenerako URL bat zehaztu behar duzulako", + "ssowat_conf_generated": "SSOwat eta atariaren ezarpenak berregin dira", + "system_upgraded": "Sistema eguneratu da", + "system_username_exists": "Erabiltzaile izena existitzen da lehendik ere sistemaren erabiltzaileen zerrendan", + "this_action_broke_dpkg": "Eragiketa honek dpkg/APT (sistemaren pakete kudeatzaileak) kaltetu ditu… Arazoa konpontzeko SSH bidez konektatu eta 'sudo apt install --fix-broken' edota 'sudo dpkg --configure -a' exekutatu dezakezu.", + "tools_upgrade": "Sistemaren paketeak eguneratzen", + "tools_upgrade_failed": "Ezin izan dira paketeak eguneratu: {packages_list}", + "tos_dyndns_acknowledgement": "DynDNS domeinu bat erregistratzea aukeratu zenuen, YunoHost proiektuaren eskaintzen baitan. Domeinu-izenak epe luzeko zerbitzu digitalen alderdi garrantzitsua direnez, gogorarazten dizugu arretaz irakur ditzazula dagozkion Zerbitzu-baldintzak, bereziki domeinu-izen libre horiei dagokien atala: .", + "tos_postinstall_acknowledgement": "YunoHost boluntario talde baten proiektua da, zerbitzarietarako sistema eragile ireki bat sortzeko kausa komuna duena. YunoHosten softwareak AGPLv3 lizentzia dauka (). Proiektuak zerbitzu tekniko eta komunitario anitz administratu eta eskuragarri jartzen ditu, hainbat helburutarako. Zerbitzu horiek erabiltzean, Zerbitzu-baldintzak onartzen dituzu: .", + "unable_authenticate": "Huts egin du saioaren autentikazioak", + "unbackup_app": "{app} ez da gordeko", + "unexpected_error": "Ezusteko zerbaitek huts egin du: {error}", + "unknown_error_reading_file": "Errore ezezaguna {file} fitxategia irakurtzen saiatzerakoan (zergatia: {error})", + "unknown_group": "'{group}' talde ezezaguna", + "unknown_main_domain_path": "{app} aplikaziorako domeinu edo bide ezezaguna. Domeinua eta bidea zehaztu behar dituzu baimena emateko URLa ahalbidetzeko.", + "unknown_user": "'{user}' erabiltzailea ezezaguna da", + "unlimit": "Mugarik ez", + "unrestore_app": "{app} ez da lehengoratuko", + "update_apt_cache_failed": "Ezin da APT Debian-en pakete kudeatzailearen katxea eguneratu. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "update_apt_cache_warning": "Zerbaitek huts egin du APT Debian-en pakete kudeatzailearen katxea eguneratzean. Hemen dituzu sources.list fitxategiaren lerroak, arazoa identifikatzeko baliagarria izan dezakezuna:\n{sourceslist}", + "updating_apt_cache": "Sistemaren paketeen eguneraketak eskuratzen…", + "upgrading_packages": "Paketeak bertsio-berritzen…", + "upnp_dev_not_found": "Ez da UPnP gailurik aurkitu", + "upnp_disabled": "UPnP itzalita dago", + "upnp_enabled": "UPnP piztuta dago", + "upnp_port_open_failed": "Ezin izan da UPnP bidez ataka zabaldu", + "user_already_exists": "'{user}' erabiltzailea existitzen da lehendik ere", + "user_cannot_delete_last_admin": "'{user}' erabiltzailea 'admins' taldeko azken erabiltzailea da, eta ez da ezabatuko.", + "user_created": "Erabiltzailea sortu da", + "user_creation_failed": "Ezin izan da '{user}' erabiltzailea sortu: {error}", + "user_deleted": "Erabiltzailea ezabatu da", + "user_deletion_failed": "Ezin izan da '{user}' ezabatu: {error}", + "user_home_creation_failed": "Ezin izan da erabiltzailearentzat '{home}' direktorioa sortu", + "user_import_bad_file": "CSV fitxategiak ez du formatu egokia eta ekidingo da balizko datuen galera saihesteko", + "user_import_bad_line": "{line} lerro okerra: {details}", + "user_import_cannot_edit_or_delete_admins": "Ezin da '{user}' erabiltzailea editatu edo ezabatu (inportazio bidez) administratzailea delako", + "user_import_failed": "Erabiltzaileak inportatzeko eragiketak huts egin du", + "user_import_missing_columns": "Honako zutabeak falta dira: {columns}", + "user_import_nothing_to_do": "Ez dago erabiltzaileak inportatu beharrik", + "user_import_partial_failed": "Erabiltzaileak inportatzeko eragiketak erdizka huts egin du", + "user_import_success": "Erabiltzaileak arazorik gabe inportatu dira", + "user_unknown": "Erabiltzaile ezezaguna: {user}", + "user_update_failed": "Ezin izan da {user} erabiltzailea eguneratu: {error}", + "user_updated": "Erabiltzailearen informazioa aldatu da", + "visitors": "Bisitariek", + "yunohost_already_installed": "YunoHost instalatuta dago lehendik ere", + "yunohost_api": "YunoHosten APIa", + "yunohost_configured": "YunoHost konfiguratuta dago", + "yunohost_installing": "YunoHost instalatzen…", + "yunohost_not_installed": "YunoHost ez da zuzen instalatu. Exekutatu 'yunohost tools postinstall'", + "yunohost_postinstall_end_tip": "Instalatu ondorengo prozesua amaitu da! Sistemaren konfigurazioa bukatzeko:\n- erabili 'Diagnostikoak' gunea ohiko arazoei aurre hartzeko. Abiarazi administrazio-gunean edo exekutatu 'yunohost diagnosis run';\n- irakurri 'Finalizing your setup' eta 'Getting to know YunoHost' atalak. Dokumentazioan aurki ditzakezu: https://doc.yunohost.org/admin.", + "migration_0036_apt_lists_file_still_exists": "'{file}' fitxategia zaharkitua dago, eta ez litzateke hemen egon beharko. Berrizendatuko da: '{file}.legacy_bookworm'." +} diff --git a/locales/fa.json b/locales/fa.json new file mode 100644 index 0000000..25a485e --- /dev/null +++ b/locales/fa.json @@ -0,0 +1,560 @@ +{ + "aborting": "رها کردن.", + "action_invalid": "اقدام نامعتبر '{action}'", + "additional_urls_already_added": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}' اضافه شده است", + "additional_urls_already_removed": "نشانی اینترنتی اضافی '{url}' قبلاً در نشانی اینترنتی اضافی برای اجازه '{permission}'حذف شده است", + "admin_password": "گذرواژه مدیریت", + "admins": "مدیرها", + "all_users": "همه کاربران یونوهاست", + "already_up_to_date": "کاری برای انجام دادن نیست. همه چیز در حال حاضر به روز است.", + "app_action_broke_system": "این اقدام به نظر می رسد سرویس های مهمی را خراب کرده است: {services}", + "app_action_cannot_be_ran_because_required_services_down": "برای اجرای این عملیات سرویس هایی که مورد نیازاند و باید اجرا شوند: {services}. سعی کنید آنها را مجدداً راه اندازی کنید (و علت خرابی احتمالی آنها را بررسی کنید).", + "app_already_installed": "{app} قبلاً نصب شده است", + "app_already_installed_cant_change_url": "این برنامه قبلاً نصب شده است. URL فقط با این عملکرد قابل تغییر نیست. در صورت موجود بودن برنامه `app changeurl` را بررسی کنید.", + "app_argument_choice_invalid": "برای آرگومان '{name}' از یکی از این گزینه ها '{choices}' استفاده کنید", + "app_argument_invalid": "یک مقدار معتبر انتخاب کنید برای استدلال '{name}':{error}", + "app_change_url_identical_domains": "دامنه /url_path قدیمی و جدیدیکسان هستند ('{domain}{path}') ، کاری برای انجام دادن نیست.", + "app_change_url_no_script": "برنامه '{app_name}' هنوز از تغییر URL پشتیبانی نمی کند. شاید باید آن را ارتقا دهید.", + "app_change_url_success": "{app} URL اکنون {domain} {path} است", + "app_extraction_failed": "فایل های نصبی استخراج نشد", + "app_full_domain_unavailable": "متأسفیم ، این برنامه باید در دامنه خود نصب شود ، اما سایر برنامه ها قبلاً در دامنه '{domain}' نصب شده اند.شما به جای آن می توانید از یک زیر دامنه اختصاص داده شده به این برنامه استفاده کنید.", + "app_id_invalid": "شناسه برنامه نامعتبر است", + "app_install_failed": "نصب {app} امکان پذیر نیست: {error}", + "app_install_files_invalid": "این فایل ها قابل نصب نیستند", + "app_install_script_failed": "خطایی در درون اسکریپت نصب برنامه رخ داده است", + "app_location_unavailable": "این نشانی وب یا در دسترس نیست یا با برنامه (هایی) که قبلاً نصب شده در تعارض است:\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_is_public": "آیا این برنامه باید در معرض دید بازدیدکنندگان ناشناس قرار گیرد؟", + "app_manifest_install_ask_password": "گذرواژه مدیریتی را برای این برنامه انتخاب کنید", + "app_manifest_install_ask_path": "مسیر URL (بعد از دامنه) را انتخاب کنید که این برنامه باید در آن نصب شود", + "app_not_correctly_installed": "به نظر می رسد {app} به اشتباه نصب شده است", + "app_not_installed": "{app} در لیست برنامه های نصب شده یافت نشد: {all_apps}", + "app_not_properly_removed": "{app} به درستی حذف نشده است", + "app_packaging_format_not_supported": "این برنامه قابل نصب نیست زیرا قالب بسته بندی آن توسط نسخه YunoHost شما پشتیبانی نمی شود. احتمالاً باید ارتقاء سیستم خود را در نظر بگیرید.", + "app_remove_after_failed_install": "حذف برنامه در پی شکست نصب…", + "app_removed": "{app} حذف نصب شد", + "app_requirements_checking": "در حال بررسی بسته های مورد نیاز برای {app}…", + "app_restore_failed": "{app} بازیابی نشد: {error}", + "app_restore_script_failed": "خطایی در داخل اسکریپت بازیابی برنامه رخ داده است", + "app_sources_fetch_failed": "نمی توان فایل های منبع را واکشی کرد ، آیا URL درست است؟", + "app_start_backup": "در حال جمع آوری فایل ها برای پشتیبان گیری {app}…", + "app_start_install": "در حال نصب {app}…", + "app_start_remove": "در حال حذف {app}…", + "app_start_restore": "درحال بازیابی {app}…", + "app_unknown": "کارهٔ ناشناخته", + "app_unsupported_remote_type": "نوع راه دور پشتیبانی نشده برای برنامه استفاده می شود", + "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} ارتقا یافت", + "apps_already_up_to_date": "همه کاره‌ها در حال حاضر به‌روز هستند", + "apps_catalog_failed_to_download": "بارگیری کاتالوگ برنامه {apps_catalog} امکان پذیر نیست: {error}", + "apps_catalog_obsolete_cache": "حافظه پنهان کاتالوگ برنامه خالی یا منسوخ شده است.", + "apps_catalog_update_success": "کاتالوگ برنامه به روز شد!", + "apps_catalog_updating": "در حال به روز رسانی کاتالوگ برنامه…", + "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_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_corrupted": "به نظر می رسد بایگانی پشتیبان '{archive}' خراب است: {error}", + "backup_archive_name_exists": "بایگانی پشتیبان با این نام در حال حاضر وجود دارد.", + "backup_archive_name_unknown": "بایگانی پشتیبان محلی ناشناخته با نام '{name}'", + "backup_archive_open_failed": "بایگانی پشتیبان باز نشد", + "backup_archive_system_part_not_available": "بخش سیستم '{part}' در این نسخه پشتیبان در دسترس نیست", + "backup_archive_writing_error": "فایل های '{source}' (که در بایگانی '{dest}' نامگذاری شده اند) برای پشتیبان گیری به بایگانی فشرده '{archive}' اضافه نشد", + "backup_ask_for_copying_if_needed": "آیا می خواهید پشتیبان گیری را با استفاده از {size} مگابایت به طور موقت انجام دهید؟ (این روش استفاده می شود زیرا برخی از پرونده ها با استفاده از روش کارآمدتری تهیه نمی شوند.)", + "backup_cant_mount_uncompress_archive": "بایگانی فشرده سازی نشده را نمی توان به عنوان حفاظت از نوشتن مستقر کرد", + "backup_cleaning_failed": "پوشه موقت پشتیبان گیری پاکسازی نشد", + "backup_copying_to_organize_the_archive": "در حال کپی {size} مگابایت برای سازماندهی بایگانی", + "backup_couldnt_bind": "نمی توان {src} را به {dest} متصل کرد.", + "backup_create_size_estimation": "بایگانی حاوی حدود {size} داده است.", + "backup_created": "نسخه پشتیبان ایجاد شد", + "backup_creation_failed": "نسخه پشتیبان بایگانی ایجاد نشد", + "backup_csv_addition_failed": "فایلهای پشتیبان به فایل CSV اضافه نشد", + "backup_csv_creation_failed": "فایل CSV مورد نیاز برای بازیابی ایجاد نشد", + "backup_custom_backup_error": "روش پشتیبان گیری سفارشی نمی تواند مرحله 'backup' را پشت سر بگذارد", + "backup_custom_mount_error": "روش پشتیبان گیری سفارشی نمی تواند از مرحله 'mount' عبور کند", + "backup_delete_error": "'{path}' حذف نشد", + "backup_deleted": "نسخه پشتیبان حذف شد", + "backup_hook_unknown": "قلاب پشتیبان '{hook}' ناشناخته است", + "backup_method_copy_finished": "نسخه پشتیبان نهایی شد", + "backup_method_custom_finished": "روش پشتیبان گیری سفارشی '{method}' به پایان رسید", + "backup_method_tar_finished": "بایگانی پشتیبان TAR ایجاد شد", + "backup_mount_archive_for_restore": "در حال آماده سازی بایگانی برای بازگردانی…", + "backup_no_uncompress_archive_dir": "چنین فهرست بایگانی فشرده نشده ایی وجود ندارد", + "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_output_symlink_dir_broken": "فهرست بایگانی شما '{path}' یک پیوند symlink خراب است. شاید فراموش کرده اید که مجدداً محل ذخیره سازی که به آن اشاره می کند را دوباره نصب یا وصل کنید.", + "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} فاقد اسکریپت بازگردانی است ، نمی توانید پشتیبان گیری این برنامه را به طور خودکار بازیابی کنید.", + "cannot_open_file": "فایل {file} باز نشد (دلیل: {error})", + "cannot_write_file": "نمی توان فایل {file} را نوشت (دلیل: {error})", + "certmanager_acme_not_configured_for_domain": "در حال حاضر نمی توان چالش ACME را برای {domain} اجرا کرد زیرا nginx conf آن فاقد قطعه کد مربوطه است… لطفاً مطمئن شوید که پیکربندی nginx شما به روز است با استفاده از دستور `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonLE_cert": "گواهی دامنه '{domain}' توسط Let's Encrypt صادر نشده است. به طور خودکار تمدید نمی شود!", + "certmanager_attempt_to_renew_valid_cert": "گواهی دامنه '{domain}' در حال انقضا نیست! (اگر می دانید چه کار می کنید می توانید از --force استفاده کنید)", + "certmanager_attempt_to_replace_valid_cert": "شما در حال تلاش برای بازنویسی یک گواهی خوب و معتبر برای دامنه {domain} هستید! (استفاده از --force برای bypass)", + "certmanager_cannot_read_cert": "هنگام باز کردن گواهینامه فعلی مشکلی پیش آمده است برای دامنه {domain} (فایل: {file}) ، علّت: {reason}", + "certmanager_cert_install_success": "هم اینک گواهی اجازه رمزگذاری برای دامنه '{domain}' نصب شده است", + "certmanager_cert_install_success_selfsigned": "گواهی خود امضا شده اکنون برای دامنه '{domain}' نصب شده است", + "certmanager_cert_renew_success": "گواهی اجازه رمزنگاری برای دامنه '{domain}' تمدید شد", + "certmanager_cert_signing_failed": "گواهی جدید امضا نشده است", + "certmanager_certificate_fetching_or_enabling_failed": "تلاش برای استفاده از گواهینامه جدید برای {domain} جواب نداد…", + "certmanager_domain_cert_not_selfsigned": "گواهی دامنه {domain} خود امضا نشده است. آیا مطمئن هستید که می خواهید آن را جایگزین کنید؟ (برای این کار از '--force' استفاده کنید.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "سوابق DNS برای دامنه '{domain}' با IP این سرور متفاوت است. لطفاً برای اطلاعات بیشتر ، دسته 'DNS records' (پایه) را در عیب یابی بررسی کنید. اگر اخیراً رکورد A خود را تغییر داده اید ، لطفاً منتظر انتشار آن باشید (برخی از چکرهای انتشار DNS بصورت آنلاین در دسترس هستند). (اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این چک ها استفاده کنید.)", + "certmanager_domain_http_not_working": "به نظر می رسد دامنه {domain} از طریق HTTP قابل دسترسی نیست. لطفاً برای اطلاعات بیشتر ، دسته \"وب\" را در عیب یابی بررسی کنید. (اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این چک ها استفاده کنید.)", + "certmanager_domain_not_diagnosed_yet": "هنوز هیچ نتیجه تشخیصی و عیب یابی دامنه {domain} وجود ندارد. لطفاً در بخش عیب یابی ، دسته های 'سوابق ساناد' و 'وب'مجدداً عیب یابی را اجرا کنید تا بررسی شود که آیا دامنه‌ای برای گواهی اجازه رمزنگاری آماده است. (یا اگر می دانید چه کار می کنید ، از '--no-checks' برای خاموش کردن این بررسی ها استفاده کنید.)", + "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})", + "certmanager_unable_to_parse_self_CA_name": "نتوانست نام مرجع خودامضائی را تجزیه و تحلیل کند (فایل: {file})", + "confirm_app_install_danger": "خطرناک! این برنامه هنوز آزمایشی است (اگر صراحتاً کار نکند)! احتمالاً نباید آن را نصب کنید مگر این‌که بدانید در حال انجام چه کاری هستید. اگر این کاره‌ کار نکرد یا سیستم شما را خراب کرد، هیچ پشتیبانی ارائه نخواهد شد… اگر به هر حال مایل به پذیرش این خطر هستید ، '{answers}' را تایپ کنید", + "confirm_app_install_thirdparty": "خطرناک! این برنامه بخشی از فهرست برنامه YunoHost نیست. نصب برنامه های شخص ثالث ممکن است یکپارچگی و امنیت سیستم شما را به خطر بیندازد. احتمالاً نباید آن را نصب کنید مگر اینکه بدانید در حال انجام چه کاری هستید. اگر این برنامه کار نکرد یا سیستم شما را خراب کرد ، هیچ پشتیبانی ارائه نخواهدشد… به هر حال اگر مایل به پذیرش این خطر هستید ، '{answers}' را تایپ کنید", + "confirm_app_install_warning": "هشدار: این کاره ممکن است کار کند ، اما در یونو‌هاست یکپارچه نشده است. برخی از ویژگی ها مانند ورود به سیستم و پشتیبان گیری/بازیابی ممکن است در دسترس نباشد. به هر حال نصب شود؟ [{answers}] ", + "corrupted_json": "جی سان خراب شده از {ressource} میخواند (دلیل: {error})", + "corrupted_toml": "TOML خراب از {ressource} (دلیل: {error})", + "corrupted_yaml": "YAML خراب از {ressource} (دلیل: {error})", + "diagnosis_backports_in_sources_list": "به نظر می رسد apt (مدیریت بسته) برای استفاده از مخزن پشتیبان پیکربندی شده است. مگر اینکه واقعاً بدانید چه کار می کنید ، ما به شدت از نصب بسته های پشتیبان خودداری می کنیم، زیرا به احتمال زیاد باعث ایجاد ناپایداری یا تداخل در سیستم شما می شود.", + "diagnosis_basesystem_hardware": "معماری سخت افزاری سرور {virt} {arch} است", + "diagnosis_basesystem_hardware_model": "مدل سرور {model} میباشد", + "diagnosis_basesystem_host": "سرور نسخه {debian_version} دبیان را اجرا می کند", + "diagnosis_basesystem_kernel": "سرور نسخه {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_cant_run_because_of_dep": "در حالی که مشکلات مهمی در ارتباط با {dep} وجود دارد ، نمی توان عیب یابی را برای {category} اجرا کرد.", + "diagnosis_description_basesystem": "سیستم پایه", + "diagnosis_description_dnsrecords": "رکورد DNS", + "diagnosis_description_ip": "اتصال به اینترنت", + "diagnosis_description_mail": "ایمیل", + "diagnosis_description_ports": "ارائه پورت ها", + "diagnosis_description_regenconf": "تنظیمات سیستم", + "diagnosis_description_services": "بررسی وضعیّت سرویس ها", + "diagnosis_description_systemresources": "منابع سیستم", + "diagnosis_description_web": "وب", + "diagnosis_diskusage_low": "‏ذخیره سازی {mountpoint} (روی دستگاه {device}) فقط {free} ({free_percent}%) فضا باقی مانده(از {total}). مراقب باشید.", + "diagnosis_diskusage_ok": "‏ذخیره سازی {mountpoint} (روی دستگاه {device}) هنوز {free} فضا در دسترس دارد ({free_percent}%) فضای باقی مانده (از {total})!", + "diagnosis_diskusage_verylow": "‏ذخیره سازی {mountpoint} (روی دستگاه {device}) فقط {free} ({free_percent}%) فضا باقی مانده (از {total}). شما واقعاً باید پاکسازی فضای ذخیره ساز را در نظر بگیرید!", + "diagnosis_display_tip": "برای مشاهده مسائل پیدا شده ، می توانید به بخش تشخیص webadmin بروید یا از خط فرمان 'yunohost diagnosis show --issues --human-readable' را اجرا کنید.", + "diagnosis_dns_bad_conf": "برخی از سوابق DNS برای دامنه {domain} (دسته {category}) وجود ندارد یا نادرست است", + "diagnosis_dns_discrepancy": "به نظر می رسد پرونده DNS زیر از پیکربندی توصیه شده پیروی نمی کند:
نوع: {type}
نام: {name}
ارزش فعلی: {current}
مقدار مورد انتظار: {content}", + "diagnosis_dns_good_conf": "سوابق DNS برای دامنه {domain} (دسته {category}) به درستی پیکربندی شده است", + "diagnosis_dns_missing_record": "با توجه به پیکربندی DNS توصیه شده ، باید یک رکورد DNS با اطلاعات زیر اضافه کنید.
نوع: {type}
نام: {name}
ارزش: {content}", + "diagnosis_dns_point_to_doc": "لطفاً اسناد را در https://doc.yunohost.org/dns_config برسی و مطالعه کنید، اگر در مورد پیکربندی سوابق DNS به کمک نیاز دارید.", + "diagnosis_dns_specialusedomain": "دامنه {domain} بر اساس یک دامنه سطح بالا (TLD) مخصوص استفاده است و بنابراین انتظار نمی رود که دارای سوابق DNS واقعی باشد.", + "diagnosis_dns_try_dyndns_update_force": "پیکربندی DNS این دامنه باید به طور خودکار توسط YunoHost مدیریت شود. اگر اینطور نیست ، می توانید سعی کنید به زور یک به روز رسانی را با استفاده از yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "برخی از دامنه ها به زودی منقضی می شوند!", + "diagnosis_domain_expiration_not_found": "بررسی تاریخ انقضا برخی از دامنه ها امکان پذیر نیست", + "diagnosis_domain_expiration_not_found_details": "به نظر می رسد اطلاعات WHOIS برای دامنه {domain} حاوی اطلاعات مربوط به تاریخ انقضا نیست؟", + "diagnosis_domain_expiration_success": "دامنه های شما ثبت شده است و به این زودی منقضی نمی شود.", + "diagnosis_domain_expiration_warning": "برخی از دامنه ها به زودی منقضی می شوند!", + "diagnosis_domain_expires_in": "{domain} در {days} روز منقضی می شود.", + "diagnosis_domain_not_found_details": "دامنه {domain} در پایگاه داده WHOIS وجود ندارد یا منقضی شده است!", + "diagnosis_everything_ok": "همه چیز برای {category} خوب به نظر می رسد!", + "diagnosis_failed": "نتیجه معاینه و عیب یابی برای دسته '{category}' واکشی نشد: {error}", + "diagnosis_failed_for_category": "عیب یابی برای دسته '{category}' ناموفق بود: {error}", + "diagnosis_found_errors": "{errors} مشکلات مهم مربوط به {category} پیدا شد!", + "diagnosis_found_errors_and_warnings": "{errors} مسائل مهم (و {warnings} هشدارها) مربوط به {category} پیدا شد!", + "diagnosis_found_warnings": "مورد (های) {warnings} یافت شده که می تواند دسته {category} را بهبود بخشد.", + "diagnosis_http_bad_status_code": "به نظر می رسد دستگاه دیگری (شاید روتر اینترنتی شما) به جای سرور شما پاسخ داده است.
1. شایع ترین علت برای این مشکل ، پورت 80 است (و 443) به درستی به سرور شما ارسال نمی شوند.
2. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.", + "diagnosis_http_connection_error": "خطای اتصال: ارتباط با دامنه درخواست شده امکان پذیر نیست، به احتمال زیاد غیرقابل دسترسی است.", + "diagnosis_http_could_not_diagnose": "نمی توان تشخیص داد که در IPv{ipversion} دامنه ها از خارج قابل دسترسی هستند یا خیر.", + "diagnosis_http_could_not_diagnose_details": "خطا: {error}", + "diagnosis_http_hairpinning_issue": "به نظر می رسد در شبکه محلی شما hairpinning فعال نشده است.", + "diagnosis_http_hairpinning_issue_details": "این احتمالاً به دلیل جعبه / روتر ISP شما است. در نتیجه ، افراد خارج از شبکه محلی شما می توانند به سرور شما مطابق انتظار دسترسی پیدا کنند ، اما افراد داخل شبکه محلی (احتمالاً مثل شما؟) هنگام استفاده از نام دامنه یا IP جهانی. ممکن است بتوانید وضعیت را بهبود بخشید با نگاهی به https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "به نظر می رسد که پیکربندی nginx این دامنه به صورت دستی تغییر کرده است و از تشخیص YunoHost در صورت دسترسی به HTTP جلوگیری می کند.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "برای برطرف کردن وضعیّت ، تفاوت را با استفاده از خط فرمان بررسی کنیدyunohost tools regen-conf nginx --dry-run --with-diff و اگر خوب است ، تغییرات را اعمال کنید با استفاده از فرمان yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "دامنه {domain} از طریق HTTP از خارج از شبکه محلی قابل دسترسی است.", + "diagnosis_http_partially_unreachable": "به نظر می رسد که دامنه {domain} از طریق HTTP از خارج از شبکه محلی در IPv{failed} غیرقابل دسترسی است، اگرچه در IPv{passed} کار می کند.", + "diagnosis_http_timeout": "زمان تلاش برای تماس با سرور از خارج به پایان رسید. به نظر می رسد غیرقابل دسترسی است.
1. شایع ترین علت برای این مشکل ، پورت 80 است (و 443) به درستی به سرور شما ارسال نمی شوند.
2. همچنین باید مطمئن شوید که سرویس nginx در حال اجرا است
3. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.", + "diagnosis_http_unreachable": "به نظر می رسد دامنه {domain} از خارج از شبکه محلی از طریق HTTP قابل دسترسی نیست.", + "diagnosis_ignored_issues": "(+ {nb_ignored} مسئله (ها) نادیده گرفته شده)", + "diagnosis_ip_broken_dnsresolution": "به نظر می رسد تفکیک پذیری نام دامنه به دلایلی خراب شده است… آیا فایروال درخواست های DNS را مسدود می کند؟", + "diagnosis_ip_broken_resolvconf": "به نظر می رسد تفکیک پذیری نام دامنه در سرور شما شکسته شده است ، که به نظر می رسد مربوط به /etc/resolv.conf و اشاره نکردن به 127.0.0.1 میباشد.", + "diagnosis_ip_connected_ipv4": "سرور از طریق IPv4 به اینترنت متصل است!", + "diagnosis_ip_connected_ipv6": "سرور از طریق IPv6 به اینترنت متصل است!", + "diagnosis_ip_dnsresolution_working": "تفکیک پذیری نام دامنه کار می کند!", + "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://doc.yunohost.org/ipv6.اگر نمی توانید IPv6 را فعال کنید یا اگر برای شما بسیار فنی به نظر می رسد ، می توانید با خیال راحت این هشدار را نادیده بگیرید.", + "diagnosis_ip_not_connected_at_all": "به نظر می رسد سرور اصلا به اینترنت متصل نیست !؟", + "diagnosis_ip_weird_resolvconf": "اینطور که پیداست تفکیک پذیری DNS کار می کند ، اما به نظر می رسد از سفارشی استفاده می کنید /etc/resolv.conf.", + "diagnosis_ip_weird_resolvconf_details": "پرونده /etc/resolv.conf باید یک پیوند همراه برای /etc/resolvconf/run/resolv.conf خود اشاره می کند به 127.0.0.1 (dnsmasq). اگر می خواهید راه حل های DNS را به صورت دستی پیکربندی کنید ، لطفاً ویرایش کنید /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blocklist_listed_by": "IP یا دامنه شما {item}در لیست سیاه {blocklist_name} قرار دارد", + "diagnosis_mail_blocklist_ok": "به نظر می رسد IP ها و دامنه های مورد استفاده این سرور در لیست سیاه قرار ندارند", + "diagnosis_mail_blocklist_reason": "دلیل لیست سیاه: {reason}", + "diagnosis_mail_blocklist_website": "پس از شناسایی دلیل لیست شدن و رفع آن، با خیال راحت درخواست کنید IP یا دامنه شما حذف شود از {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "یک سرویس غیر SMTP در پورت 25 در IPv{ipversion} پاسخ داد", + "diagnosis_mail_ehlo_bad_answer_details": "ممکن است به دلیل پاسخ دادن دستگاه دیگری به جای سرور شما باشد.", + "diagnosis_mail_ehlo_could_not_diagnose": "نمی توان تشخیص داد که آیا سرور ایمیل postfix از خارج در IPv{ipversion} قابل دسترسی است یا خیر.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "خطا: {error}", + "diagnosis_mail_ehlo_ok": "سرور ایمیل SMTP از خارج قابل دسترسی است و بنابراین می تواند ایمیل دریافت کند!", + "diagnosis_mail_ehlo_unreachable": "سرور ایمیل SMTP از خارج در IPv {ipversion} غیرقابل دسترسی است. قادر به دریافت ایمیل نخواهد بود.", + "diagnosis_mail_ehlo_unreachable_details": "اتصال روی پورت 25 سرور شما در IPv{ipversion} باز نشد. به نظر می رسد غیرقابل دسترس است.
1. شایع ترین علت این مشکل ، پورت 25 است به درستی به سرور شما ارسال نشده است.
2. همچنین باید مطمئن شوید که سرویس postfix در حال اجرا است.
3. در تنظیمات پیچیده تر: مطمئن شوید که هیچ فایروال یا پروکسی معکوسی تداخل نداشته باشد.", + "diagnosis_mail_ehlo_wrong": "یک سرور ایمیل SMTP متفاوت در IPv{ipversion} پاسخ می دهد. سرور شما احتمالاً نمی تواند ایمیل دریافت کند.", + "diagnosis_mail_ehlo_wrong_details": "EHLO دریافت شده توسط تشخیص دهنده از راه دور در IPv{ipversion} با دامنه سرور شما متفاوت است.
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_4": "برخی از ارائه دهندگان به شما اجازه نمی دهند DNS معکوس خود را پیکربندی کنید (یا ممکن است ویژگی آنها شکسته شود…). اگر به همین دلیل مشکلاتی را تجربه می کنید ، راه حل های زیر را در نظر بگیرید: - برخی از ISP ها جایگزین ارائه می دهند با استفاده از رله سرور ایمیل اگرچه به این معنی است که رله می تواند از ترافیک ایمیل شما جاسوسی کند.
- یک جایگزین دوستدار حریم خصوصی استفاده از VPN * با IP عمومی اختصاصی * برای دور زدن این نوع محدودیت ها است. ببینید https://doc.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_details": "ابتدا باید DNS معکوس را پیکربندی کنید با {ehlo_domain} در رابط روتر اینترنت یا رابط ارائه دهنده میزبانی تان. (ممکن است برخی از ارائه دهندگان میزبانی از شما بخواهند که برای این کار تیکت پشتیبانی ارسال کنید).", + "diagnosis_mail_fcrdns_ok": "DNS معکوس شما به درستی پیکربندی شده است!", + "diagnosis_mail_outgoing_port_25_blocked": "سرور ایمیل SMTP نمی تواند به سرورهای دیگر ایمیل ارسال کند زیرا درگاه خروجی 25 در IPv {ipversion} مسدود شده است.", + "diagnosis_mail_outgoing_port_25_blocked_details": "ابتدا باید سعی کنید پورت خروجی 25 را در رابط اینترنت روتر یا رابط ارائه دهنده میزبانی خود باز کنید. (ممکن است برخی از ارائه دهندگان میزبانی از شما بخواهند که برای این کار تیکت پشتیبانی ارسال کنید).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "برخی از ارائه دهندگان به شما اجازه نمی دهند پورت خروجی 25 را رفع انسداد کنید زیرا به بی طرفی شبکه اهمیتی نمی دهند.
- برخی از آنها جایگزین را ارائه می دهند با استفاده از رله سرور ایمیل اگرچه به این معنی است که رله می تواند از ترافیک ایمیل شما جاسوسی کند.
- یک جایگزین دوستدار حریم خصوصی استفاده از VPN * با IP عمومی اختصاصی * برای دور زدن این نوع محدودیت ها است. ببینید https://doc.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_never_ran_yet": "به نظر می رسد این سرور به تازگی راه اندازی شده است و هنوز هیچ گزارش تشخیصی برای نمایش وجود ندارد. شما باید با اجرای یک عیب یابی و تشخیص کامل، از طریق رابط مدیریت تحت وب webadmin یا با استفاده از 'yunohost diagnosis run' از خط فرمان معاینه و تشخیص عیب یابی را شروع کنید.", + "diagnosis_no_cache": "هنوز هیچ حافظه نهانی معاینه و عیب یابی برای دسته '{category}' وجود ندارد", + "diagnosis_package_installed_from_sury": "برخی از بسته های سیستمی باید کاهش یابد", + "diagnosis_package_installed_from_sury_details": "برخی از بسته ها ناخواسته از مخزن شخص ثالث به نام Sury نصب شده اند. تیم یونو‌هاست استراتژی مدیریت این بسته ها را بهبود بخشیده ، اما انتظار می رود برخی از تنظیماتی که برنامه های PHP7.3 را در حالی که هنوز بر روی Stretch نصب شده اند نصب کرده اند ، ناسازگاری های باقی مانده ای داشته باشند. برای رفع این وضعیت ، باید دستور زیر را اجرا کنید: {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "نمی توان تشخیص داد پورت ها از خارج در IPv{ipversion} قابل دسترسی هستند یا خیر.", + "diagnosis_ports_could_not_diagnose_details": "خطا: {error}", + "diagnosis_ports_forwarding_tip": "برای رفع این مشکل، به احتمال زیاد باید انتقال پورت را در روتر اینترنت خود پیکربندی کنید همانطور که شرح داده شده در https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "افشای این پورت برای ویژگی های {category} (سرویس {service}) مورد نیاز است", + "diagnosis_ports_ok": "پورت {port} از خارج قابل دسترسی است.", + "diagnosis_ports_partially_unreachable": "پورت {port} از خارج در {failed}IPv قابل دسترسی نیست.", + "diagnosis_ports_unreachable": "پورت {port} از خارج قابل دسترسی نیست.", + "diagnosis_processes_killed_by_oom_reaper": "برخی از فرآیندها اخیراً توسط سیستم از بین رفته اند زیرا حافظه آن تمام شده است. این به طور معمول نشانه کمبود حافظه در سیستم یا فرآیندی است که حافظه زیادی را از بین می برد. خلاصه فرآیندهای کشته شده:\n{kills_summary}", + "diagnosis_ram_low": "این سیستم فقط {available} ({available_percent}٪) حافظه در دسترس دارد! (از {total}). مراقب باشید.", + "diagnosis_ram_ok": "این سیستم هنوز {available} ({available_percent}٪) حافظه در دسترس دارد از مجموع {total}.", + "diagnosis_ram_verylow": "این سیستم فقط {available} ({available_percent}٪) حافظه در دسترس دارد! (از {total})", + "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} است که بسیار نگران کننده است! احتمالاً خیلی زود فضای دیسک شما تمام می شود! توصیه می شود حداقل 16 گیگابایت و بیشتر فضا برای سیستم فایل ریشه داشته باشید.", + "diagnosis_rootfstotalspace_warning": "سیستم فایل ریشه در مجموع فقط {space} دارد. ممکن است اشکالی نداشته باشد ، اما مراقب باشید زیرا در نهایت ممکن است فضای دیسک شما به سرعت تمام شود… توصیه می شود حداقل 16 گیگابایت و بیشتر فضا برای سیستم فایل ریشه داشته باشید.", + "diagnosis_security_vulnerable_to_meltdown": "به نظر می رسد شما در برابر آسیب پذیری امنیتی بحرانی Meltdown آسیب پذیر هستید", + "diagnosis_security_vulnerable_to_meltdown_details": "برای رفع این مشکل ، باید سیستم خود را ارتقا دهید و مجدداً راه اندازی کنید تا هسته لینوکس جدید بارگیری شود (یا در صورت عدم کارکرد با ارائه دهنده سرور خود تماس بگیرید). برای اطلاعات بیشتر به https://meltdownattack.com/ مراجعه کنید.", + "diagnosis_services_bad_status": "سرویس {service} {status} است :(", + "diagnosis_services_bad_status_tip": "می توانید سعی کنید سرویس را راه اندازی مجدد کنید، و اگر کار نمی کند ، نگاهی داشته باشید بهسرویس در webadmin ثبت می شود (از خط فرمان ، می توانید این کار را انجام دهید با yunohost service restart {service} و yunohost service log {service}).", + "diagnosis_services_conf_broken": "پیکربندی سرویس {service} خراب است!", + "diagnosis_services_running": "سرویس {service} در حال اجرا است!", + "diagnosis_sshd_config_inconsistent": "به نظر می رسد که پورت SSH به صورت دستی در/etc/ssh/sshd_config تغییر یافته است. از زمان YunoHost 4.2 ، یک تنظیم جهانی جدید 'security.ssh.port' برای جلوگیری از ویرایش دستی پیکربندی در دسترس است.", + "diagnosis_sshd_config_inconsistent_details": "لطفاً اجراکنید yunohost settings set security.ssh.port -v YOUR_SSH_PORT برای تعریف پورت SSH و بررسی کنید yunohost tools regen-conf ssh --dry-run --with-diff و yunohost tools regen-conf ssh --force برای تنظیم مجدد تنظیمات خود به توصیه YunoHost.", + "diagnosis_sshd_config_insecure": "به نظر می رسد که پیکربندی SSH به صورت دستی تغییر یافته است و مطمئن نیست زیرا هیچ دستورالعمل 'AllowGroups' یا 'AllowUsers' برای محدود کردن دسترسی به کاربران مجاز ندارد.", + "diagnosis_swap_none": "این سیستم به هیچ وجه swap ندارد. برای جلوگیری از شرایطی که حافظه سیستم شما تمام می شود ، باید حداقل {recommended} swap را در نظر بگیرید.", + "diagnosis_swap_notsomuch": "سیستم فقط {total} swap دارد. برای جلوگیری از شرایطی که حافظه سیستم شما تمام می شود ، باید حداقل {recommended} را در نظر بگیرید.", + "diagnosis_swap_ok": "سیستم {total} swap دارد!", + "diagnosis_swap_tip": "لطفاً مراقب و آگاه باشید، اگر سرور میزبانی swap را روی کارت SD یا حافظه SSD انجام دهد ، ممکن است طول عمر دستگاه را به شدت کاهش دهد.", + "diagnosis_unknown_categories": "دسته های زیر ناشناخته است: {categories}", + "disk_space_not_sufficient_install": "فضای کافی برای نصب این برنامه در دیسک باقی نمانده است", + "disk_space_not_sufficient_update": "برای به روزرسانی این برنامه فضای دیسک کافی باقی نمانده است", + "domain_cannot_remove_main": "شما نمی توانید '{domain}' را حذف کنید زیرا دامنه اصلی است ، ابتدا باید با استفاده از 'yunohost domain main-domain -n ' دامنه دیگری را به عنوان دامنه اصلی تعیین کنید. در اینجا لیست دامنه های کاندید وجود دارد: {other_domains}", + "domain_cannot_remove_main_add_new_one": "شما نمی توانید '{domain}' را حذف کنید زیرا دامنه اصلی و تنها دامنه شما است، ابتدا باید دامنه دیگری را با 'yunohost domain add ' اضافه کنید، سپس با استفاده از 'yunohost domain main-domain -n ' به عنوان دامنه اصلی تنظیم شده. و بعد از آن می توانید'{domain}' را حذف کنید با استفاده از'yunohost domain remove {domain}'.", + "domain_cert_gen_failed": "گواهی تولید نشد", + "domain_created": "دامنه ایجاد شد", + "domain_creation_failed": "ایجاد دامنه {domain} امکان پذیر نیست: {error}", + "domain_deleted": "دامنه حذف شد", + "domain_deletion_failed": "حذف دامنه {domain} امکان پذیر نیست: {error}", + "domain_dns_conf_is_just_a_recommendation": "این دستور پیکربندی * توصیه شده * را به شما نشان می دهد. این وظیفه شماست که مطابق این توصیه ، منطقه DNS خود را در ثبت کننده خود پیکربندی کنید. در واقع پیکربندی DNS را برای شما تنظیم نمی کند.", + "domain_dyndns_already_subscribed": "شما قبلاً در یک دامنه DynDNS مشترک شده اید", + "domain_exists": "دامنه از قبل وجود دارد", + "domain_hostname_failed": "نام میزبان جدید قابل تنظیم نیست. این ممکن است بعداً مشکلی ایجاد کند (ممکن هم هست خوب باشد).", + "domain_remove_confirm_apps_removal": "حذف این دامنه برنامه های زیر را حذف می کند:\n{apps}\n\nآیا طمئن هستید که میخواهید انجام دهید؟ [{answers}]", + "domain_uninstall_app_first": "این برنامه ها هنوز روی دامنه شما نصب هستند:\n{apps}\n\nلطفاً قبل از اقدام به حذف دامنه ، آنها را با استفاده از 'برنامه yunohost remove the_app_id' حذف کرده یا با استفاده از 'yunohost app change-url the_app_id' به دامنه دیگری منتقل کنید", + "domains_available": "دامنه های موجود:", + "done": "انجام شد", + "download_bad_status_code": "{url} کد وضعیّت بازگشتی {code}", + "download_ssl_error": "خطای SSL هنگام اتصال به {url}", + "download_timeout": "پاسخ {url} خیلی طول کشید ، منصرف شو.", + "download_unknown_error": "خطا هنگام بارگیری داده ها از {url}: {error}", + "downloading": "در حال بارگیری…", + "dpkg_is_broken": "شما نمی توانید این کار را در حال حاضر انجام دهید زیرا dpkg/APT (اداره کنندگان سیستم بسته ها) به نظر می رسد در وضعیت خرابی است… می توانید با اتصال از طریق SSH و اجرا این فرمان `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a` مشکل را حل کنید.", + "dpkg_lock_not_available": "این دستور در حال حاضر قابل اجرا نیست زیرا به نظر می رسد برنامه دیگری از قفل dpkg (مدیر بسته سیستم) استفاده می کند", + "dyndns_could_not_check_available": "بررسی نشد که آیا {domain} در {provider} در دسترس است یا خیر.", + "dyndns_domain_not_provided": "ارائه دهنده DynDNS {provider} نمی تواند دامنه {domain} را ارائه دهد.", + "dyndns_ip_update_failed": "آدرس IP را به DynDNS به روز نکرد", + "dyndns_ip_updated": "IP خود را در DynDNS به روز کرد", + "dyndns_key_not_found": "کلید DNS برای دامنه یافت نشد", + "dyndns_no_domain_registered": "هیچ دامنه ای با DynDNS ثبت نشده است", + "dyndns_provider_unreachable": "دسترسی به ارائه دهنده DynDNS {provider} امکان پذیر نیست: یا YunoHost شما به درستی به اینترنت متصل نیست یا سرور dynette خراب است.", + "dyndns_unavailable": "دامنه '{domain}' در دسترس نیست.", + "error_changing_file_permissions": "خطا هنگام تغییر مجوزهای {path}: {error}", + "error_removing": "خطا هنگام حذف {path}: {error}", + "error_writing_file": "خطا هنگام نوشتن فایل {file}: {error}", + "extracting": "استخراج…", + "field_invalid": "فیلد نامعتبر '{field}'", + "file_does_not_exist": "فایل {path} وجود ندارد.", + "file_not_exist": "فایل وجود ندارد: '{path}'", + "firewall_reload_failed": "بارگیری مجدد فایروال امکان پذیر نیست. اطلاعات بیشتر در گزارش.", + "firewall_reloaded": "فایروال بارگیری مجدد شد", + "global_settings_setting_admin_strength": "الزامات گذرواژه رمز عبور مدیر", + "global_settings_setting_backup_compress_tar_archives_help": "هنگام ایجاد پشتیبان جدید ، بایگانی های فشرده (.tar.gz) را به جای بایگانی های فشرده نشده (.tar) انتخاب کنید. N.B. : فعال کردن این گزینه به معنای ایجاد آرشیوهای پشتیبان سبک تر است ، اما روش پشتیبان گیری اولیه به طور قابل توجهی طولانی تر و سنگین تر بر روی CPU خواهد بود.", + "global_settings_setting_nginx_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای وب‌کارساز انجینکس. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_postfix_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای کارساز Postfix. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_security_experimental_enabled_help": "فعال کردن ویژگی های امنیتی آزمایشی (اگر نمی دانید در حال انجام چه کاری هستید این کار را انجام ندهید!)", + "global_settings_setting_smtp_allow_ipv6_help": "اجازه دهید از IPv6 برای دریافت و ارسال نامه استفاده شود", + "global_settings_setting_smtp_relay_enabled_help": "میزبان رله SMTP برای ارسال نامه به جای این نمونه yunohost استفاده می شود. اگر در یکی از این شرایط قرار دارید مفید است: پورت 25 شما توسط ارائه دهنده ISP یا VPS شما مسدود شده است، شما یک IP مسکونی دارید که در DUHL ذکر شده است، نمی توانید DNS معکوس را پیکربندی کنید یا این سرور مستقیماً در اینترنت نمایش داده نمی شود و می خواهید از یکی دیگر برای ارسال ایمیل استفاده کنید.", + "global_settings_setting_smtp_relay_password": "گذرواژهٔ میزبان رله SMTP", + "global_settings_setting_smtp_relay_port": "پورت رله SMTP", + "global_settings_setting_smtp_relay_user": "حساب کاربری رله SMTP", + "global_settings_setting_ssh_compatibility_help": "سازگاری در مقابل مبادله امنیتی برای کارساز SSH. روی رمزها (و سایر جنبه های مرتبط با امنیت) تأثیر می گذارد", + "global_settings_setting_ssh_port": "درگاه SSH", + "global_settings_setting_user_strength": "الزامات قدرت گذرواژه کاربر", + "global_settings_setting_webadmin_allowlist_enabled_help": "فقط به برخی از IP ها اجازه دسترسی به مدیریت وب را بدهید.", + "global_settings_setting_webadmin_allowlist_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_be_deleted": "گروه {group} را نمی توان به صورت دستی حذف کرد.", + "group_cannot_edit_all_users": "گروه 'all_users' را نمی توان به صورت دستی ویرایش کرد. این یک گروه ویژه است که شامل همه کاربران ثبت شده در YunoHost میباشد", + "group_cannot_edit_primary_group": "گروه '{group}' را نمی توان به صورت دستی ویرایش کرد. این گروه اصلی شامل تنها یک کاربر خاص است.", + "group_cannot_edit_visitors": "ویرایش گروه 'visitors' بازدیدکنندگان به صورت دستی امکان پذیر نیست. این گروه ویژه، نمایانگر بازدیدکنندگان ناشناس است", + "group_created": "گروه '{group}' ایجاد شد", + "group_creation_failed": "گروه '{group}' ایجاد نشد: {error}", + "group_deleted": "گروه '{group}' حذف شد", + "group_deletion_failed": "گروه '{group}' حذف نشد: {error}", + "group_unknown": "گروه '{group}' ناشناخته است", + "group_update_failed": "گروه '{group}' به روز نشد: {error}", + "group_updated": "گروه '{group}' به روز شد", + "group_user_already_in_group": "کاربر {user} در حال حاضر در گروه {group} است", + "group_user_not_in_group": "کاربر {user} در گروه {group} نیست", + "hook_exec_failed": "اسکریپت اجرا نشد: {path}", + "hook_exec_not_terminated": "اسکریپت به درستی به پایان نرسید: {path}", + "hook_json_return_error": "بازگشت از قلاب {path} خوانده نشد. خطا: {msg}. محتوای خام: {raw_content}", + "hook_list_by_invalid": "از این ویژگی نمی توان برای فهرست قلاب ها استفاده کرد", + "hook_name_unknown": "نام قلاب ناشناخته '{name}'", + "installation_complete": "عملیّات نصب کامل شد", + "invalid_number": "باید یک عدد باشد", + "invalid_regex": "عبارت منظم نامعتبر: '{regex}'", + "invalid_url": "اتصال به {url} انجام نشد … شاید سرویس خاموش باشد یا در IPv4/IPv6 به درستی به اینترنت متصل نشده باشید.", + "ldap_server_down": "دسترسی به سرور LDAP امکان پذیر نیست", + "ldap_server_is_down_restart_it": "سرویس LDAP خاموش است ، سعی کنید آن را دوباره راه اندازی کنید…", + "log_app_action_run": "عملکرد برنامه '{}' را اجرا کنید", + "log_app_change_url": "نشانی وب برنامه '{}' را تغییر دهید", + "log_app_install": "برنامه '{}' را نصب کنید", + "log_app_makedefault": "\"{}\" را برنامه پیش فرض قرار دهید", + "log_app_remove": "برنامه '{}' را حذف کنید", + "log_app_upgrade": "برنامه '{}' را ارتقاء دهید", + "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 list' استفاده کنید", + "log_domain_add": "دامنه '{}' را به پیکربندی سیستم اضافه کنید", + "log_domain_main_domain": "'{}' را دامنه اصلی کنید", + "log_domain_remove": "دامنه '{}' را از پیکربندی سیستم حذف کنید", + "log_dyndns_subscribe": "مشترک شدن در زیر دامنه YunoHost '{}'", + "log_dyndns_update": "IP مرتبط با '{}' زیر دامنه YunoHost خود را به روز کنید", + "log_help_to_get_failed_log": "عملیات '{desc}' کامل نشد. لطفاً برای دریافت راهنمایی و کمک ، گزارش کامل این عملیات را با استفاده از دستور 'yunohost log share {name}' به اشتراک بگذارید", + "log_help_to_get_log": "برای مشاهده گزارش عملیات '{desc}'، از دستور 'yunohost log show {name}' استفاده کنید", + "log_letsencrypt_cert_install": "گواهی اجازه رمزگذاری را در دامنه '{}' نصب کنید", + "log_letsencrypt_cert_renew": "تمدید '{}' گواهی اجازه رمزگذاری", + "log_link_to_failed_log": "عملیّات '{desc}' کامل نشد. لطفاً گزارش کامل این عملیات را ارائه دهید بواسطه اینجا را کلیک کنید برای دریافت کمک", + "log_link_to_log": "گزارش کامل این عملیات: {desc}'", + "log_operation_unit_unclosed_properly": "واحد عملیّات به درستی بسته نشده است", + "log_regen_conf": "بازسازی تنظیمات سیستم '{}'", + "log_remove_on_failed_install": "پس از نصب ناموفق '{}' را حذف کنید", + "log_selfsigned_cert_install": "گواهی خود امضا شده را در دامنه '{}' نصب کنید", + "log_tools_migrations_migrate_forward": "اجرای مهاجرت ها", + "log_tools_postinstall": "اسکریپت پس از نصب سرور YunoHost خود را نصب کنید", + "log_tools_reboot": "سرور خود را راه اندازی مجدد کنید", + "log_tools_shutdown": "سرور خود را خاموش کنید", + "log_tools_upgrade": "بسته های سیستم را ارتقا دهید", + "log_user_create": "کاربر '{}' را اضافه کنید", + "log_user_delete": "کاربر '{}' را حذف کنید", + "log_user_group_create": "ایجاد گروه '{}'", + "log_user_group_delete": "حذف گروه '{}'", + "log_user_group_update": "به روزرسانی گروه '{}'", + "log_user_update": "به روزرسانی اطلاعات کاربر '{}'", + "mail_alias_remove_failed": "نام مستعار ایمیل '{mail}' حذف نشد", + "mail_domain_unknown": "آدرس ایمیل نامعتبر برای دامنه '{domain}'. لطفاً از دامنه ای که توسط این سرور اداره می شود استفاده کنید.", + "mail_forward_remove_failed": "ارسال ایمیل '{mail}' حذف نشد", + "mail_unavailable": "این آدرس ایمیل محفوظ است و باید به طور خودکار به اولین کاربر اختصاص داده شود", + "mailbox_disabled": "ایمیل برای کاربر {user} خاموش است", + "mailbox_used_space_dovecot_down": "اگر می خواهید فضای صندوق پستی استفاده شده را واکشی کنید ، سرویس صندوق پستی Dovecot باید فعال باشد", + "main_domain_change_failed": "تغییر دامنه اصلی امکان پذیر نیست", + "main_domain_changed": "دامنه اصلی تغییر کرده است", + "migration_ldap_backup_before_migration": "ایجاد پشتیبان از پایگاه داده LDAP و تنظیمات کاره‌‌ها قبل از مهاجرت واقعی.", + "migration_ldap_can_not_backup_before_migration": "نمی توان پشتیبان گیری سیستم را قبل از شکست مهاجرت تکمیل کرد. خطا: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "نمی توان مهاجرت کرد… تلاش برای بازگرداندن سیستم.", + "migration_ldap_rollback_success": "سیستم برگردانده شد.", + "migrations_already_ran": "این مهاجرت ها قبلاً انجام شده است: {ids}", + "migrations_dependencies_not_satisfied": "این مهاجرت ها را اجرا کنید: '{dependencies_id}' ، قبل از مهاجرت {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_no_migrations_to_run": "مهاجرتی برای اجرا وجود ندارد", + "migrations_no_such_migration": "مهاجرتی به نام '{id}' وجود ندارد", + "migrations_not_pending_cant_skip": "این مهاجرت ها معلق نیستند ، بنابراین نمی توان آنها را رد کرد: {ids}", + "migrations_pending_cant_rerun": "این مهاجرت ها هنوز در انتظار هستند ، بنابراین نمی توان آنها را دوباره اجرا کرد: {ids}", + "migrations_running_forward": "مهاجرت در حال اجرا {id}»…", + "migrations_skip_migration": "رد کردن مهاجرت {id}…", + "migrations_success_forward": "مهاجرت {id} تکمیل شد", + "migrations_to_be_ran_manually": "مهاجرت {id} باید به صورت دستی اجرا شود. لطفاً به صفحه Tools → Migrations در صفحه webadmin بروید، یا `yunohost tools migrations run` را اجرا کنید.", + "nftables_unavailable": "در اینجا نمی توانید با nftables بازی کنید. شما یا در ظرفی هستید یا هسته شما آن را پشتیبانی نمی کند", + "not_enough_disk_space": "فضای آزاد کافی در '{path}' وجود ندارد", + "operation_interrupted": "عملیات به صورت دستی قطع شد؟", + "password_listed": "این رمز در بین پر استفاده ترین رمزهای عبور در جهان قرار دارد. لطفاً چیزی منحصر به فرد تر انتخاب کنید.", + "password_too_simple_1": "گذرواژه باید حداقل 8 کاراکتر باشد", + "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_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_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}", + "permission_updated": "مجوز '{permission}' به روز شد", + "port_already_closed": "درگاه {port} قبلاً بسته شده است", + "port_already_opened": "پورت {port} قبلاً باز است", + "postinstall_low_rootfsspace": "فضای فایل سیستم اصلی کمتر از 10 گیگابایت است که بسیار نگران کننده است! به احتمال زیاد خیلی زود فضای دیسک شما تمام می شود! توصیه می شود حداقل 16 گیگابایت برای سیستم فایل ریشه داشته باشید. اگر می خواهید 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 به صورت دستی تغییر یافته است ، اما شما باید صراحتاً دسته \"ssh\" را با --force برای اعمال تغییرات در واقع مشخص کنید.", + "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 اصلی تعریف کنید", + "regex_with_only_domain": "شما نمی توانید از عبارات منظم برای دامنه استفاده کنید، فقط برای مسیر قابل استفاده است", + "restore_already_installed_app": "کاره‌ای با شناسه '{app}' در حال حاضر نصب شده است", + "restore_already_installed_apps": "کاره‌های زیر به دلیل نصب بودن قابل بازیابی نیستند: {apps}", + "restore_backup_too_old": "این بایگانی پشتیبان را نمی توان بازیابی کرد زیرا با نسخه خیلی قدیمی YunoHost تهیه شده است.", + "restore_cleaning_failed": "فهرست بازسازی موقت پاک نشد", + "restore_complete": "مرمت به پایان رسید", + "restore_confirm_yunohost_installed": "آیا واقعاً می خواهید سیستمی که هم اکنون نصب شده را بازیابی کنید؟ [{answers}]", + "restore_extracting": "استخراج فایل های مورد نیاز از بایگانی…", + "restore_failed": "سیستم بازیابی نشد", + "restore_hook_unavailable": "اسکریپت ترمیم و بازسازی برای '{part}' در سیستم شما در دسترس نیست و همچنین در بایگانی نیز وجود ندارد", + "restore_may_be_not_enough_disk_space": "به نظر می رسد سیستم شما فضای کافی ندارد (فضای آزاد: {free_space} B ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)", + "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_desynchronized": "گذرواژه مدیریت تغییر کرد ، اما یونو‌هاست نتوانست این را به رمز عبور ریشه منتقل کند!", + "server_reboot": "سرور راه اندازی مجدد می شود", + "server_reboot_confirm": "سرور بلافاصله راه اندازی مجدد می شود، آیا مطمئن هستید؟ [{answers}]", + "server_shutdown": "سرور خاموش می شود", + "server_shutdown_confirm": "آیا مطمئن هستید که سرور بلافاصله خاموش می شود؟ [{answers}]", + "service_add_failed": "سرویس '{service}' اضافه نشد", + "service_added": "سرویس '{service}' اضافه شد", + "service_already_started": "سرویس '{service}' در حال اجرا است", + "service_already_stopped": "سرویس '{service}' قبلاً متوقف شده است", + "service_cmd_exec_failed": "نمی توان دستور '{command}' را اجرا کرد", + "service_description_dnsmasq": "کنترل تفکیک پذیری نام دامنه (DNS)", + "service_description_dovecot": "به کلاینت های ایمیل اجازه می دهد تا به ایمیل دسترسی/واکشی داشته باشند (از طریق IMAP و POP3)", + "service_description_fail2ban": "در برابر حملات وحشیانه و انواع دیگر حملات از طریق اینترنت محافظت می کند", + "service_description_mysql": "ذخیره داده های کاره (پایگاه داده SQL)", + "service_description_nftables": "باز و بسته شدن پورت های اتصال به سرویس ها را مدیریت می کند", + "service_description_nginx": "به همه وب سایت هایی که روی سرور شما میزبانی شده اند سرویس می دهد یا دسترسی به آنها را فراهم می کند", + "service_description_postfix": "برای ارسال و دریافت ایمیل استفاده می شود", + "service_description_redis-server": "یک پایگاه داده تخصصی برای دسترسی سریع به داده‌ها ، صف وظیفه و ارتباط بین کاره‌ها استفاده می شود", + "service_description_slapd": "کاربران ، دامنه ها و اطلاعات مرتبط را ذخیره می کند", + "service_description_ssh": "به شما امکان می دهد از راه دور از طریق ترمینال (پروتکل SSH) به سرور خود متصل شوید", + "service_description_yunohost-api": "تعاملات بین رابط وب YunoHost و سیستم را مدیریت می کند", + "service_description_yunomdns": "به شما امکان می دهد با استفاده از 'yunohost.local' در شبکه محلی به سرور خود برسید", + "service_disable_failed": "نتوانست باعث شود سرویس '{service}' در هنگام راه اندازی شروع نشود.", + "service_disabled": "هنگام راه اندازی سیستم ، سرویس '{service}' دیگر راه اندازی نمی شود.", + "service_enable_failed": "انجام سرویس '{service}' به طور خودکار در هنگام راه اندازی امکان پذیر نیست.", + "service_enabled": "سرویس '{service}' اکنون بطور خودکار در هنگام بوت شدن سیستم راه اندازی می شود.", + "service_reload_failed": "سرویس '{service}' بارگیری نشد", + "service_reload_or_restart_failed": "سرویس \"{service}\" بارگیری یا راه اندازی مجدد نشد", + "service_reloaded": "سرویس '{service}' بارگیری مجدد شد", + "service_reloaded_or_restarted": "سرویس '{service}' بارگیری یا راه اندازی مجدد شد", + "service_remove_failed": "سرویس '{service}' حذف نشد", + "service_removed": "سرویس '{service}' حذف شد", + "service_restart_failed": "سرویس \"{service}\" راه اندازی مجدد نشد", + "service_restarted": "سرویس '{service}' راه اندازی مجدد شد", + "service_start_failed": "سرویس '{service}' شروع نشد", + "service_started": "سرویس '{service}' شروع شد", + "service_stop_failed": "سرویس '{service}' متوقف نمی شود", + "service_stopped": "سرویس '{service}' متوقف شد", + "service_unknown": "سرویس ناشناخته '{service}'", + "show_tile_cant_be_enabled_for_regex": "شما نمی توانید \"show_tile\" را درست فعال کنید ، چرا که آدرس اینترنتی مجوز '{permission}' یک عبارت منظم است", + "show_tile_cant_be_enabled_for_url_not_defined": "شما نمی توانید \"show_tile\" را در حال حاضر فعال کنید ، زیرا ابتدا باید یک آدرس اینترنتی برای مجوز '{permission}' تعریف کنید", + "ssowat_conf_generated": "پیکربندی SSOwat بازسازی شد", + "system_upgraded": "سیستم ارتقا یافت", + "system_username_exists": "نام کاربری قبلاً در لیست کاربران سیستم وجود دارد", + "this_action_broke_dpkg": "این اقدام dpkg/APT (مدیران بسته های سیستم) را خراب کرد… می توانید با اتصال از طریق SSH و اجرای فرمان `sudo apt install --fix -break` و/یا` sudo dpkg --configure -a` این مشکل را حل کنید.", + "unbackup_app": "{app} ذخیره نمی شود", + "unexpected_error": "مشکل غیر منتظره ای پیش آمده: {error}", + "unknown_error_reading_file": "خطای ناشناخته هنگام تلاش برای خواندن فایل {file} (دلیل: {error})", + "unknown_group": "گروه '{group}' ناشناخته", + "unknown_main_domain_path": "دامنه یا مسیر ناشناخته برای '{app}'. شما باید یک دامنه و یک مسیر را مشخص کنید تا بتوانید یک آدرس اینترنتی برای مجوز تعیین کنید.", + "unknown_user": "کاربر'{user}' ناشناخته", + "unlimit": "بدون سهمیه", + "unrestore_app": "{app} بازیابی نمی شود", + "update_apt_cache_failed": "امکان بروزرسانی حافظه پنهان APT (مدیر بسته دبیان) وجود ندارد. در اینجا مجموعه ای از خطوط source.list هست که ممکن است به شناسایی خطوط مشکل ساز کمک کند:\n{sourceslist}", + "update_apt_cache_warning": "هنگام به روز رسانی حافظه پنهان APT (مدیر بسته دبیان) مشکلی پیش آمده. در اینجا مجموعه ای از خطوط source.list موجود میباشد که ممکن است به شناسایی خطوط مشکل ساز کمک کند:\n{sourceslist}", + "updating_apt_cache": "در حال واکشی و دریافت ارتقاء موجود برای بسته های سیستم…", + "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_unknown": "کاربر ناشناس: {user}", + "user_update_failed": "کاربر {user} به روز نشد: {error}", + "user_updated": "اطلاعات کاربر تغییر کرد", + "yunohost_already_installed": "YunoHost قبلاً نصب شده است", + "yunohost_configured": "YunoHost اکنون پیکربندی شده است", + "yunohost_installing": "در حال نصب YunoHost…", + "yunohost_not_installed": "YunoHost به درستی نصب نشده است. لطفا 'yunohost tools postinstall' را اجرا کنید", + "yunohost_postinstall_end_tip": "پس از نصب کامل شد! برای نهایی کردن تنظیمات خود ، لطفاً موارد زیر را در نظر بگیرید:\n - تشخیص مشکلات احتمالی از طریق بخش \"عیب یابی\" webadmin (یا 'yunohost diagnosis run' در خط فرمان) ؛\n - خواندن قسمت های \"نهایی کردن راه اندازی خود\" و \"آشنایی با YunoHost\" در اسناد مدیریت: https://doc.yunohost.org/admin." +} diff --git a/locales/fi.json b/locales/fi.json new file mode 100644 index 0000000..ecfabec --- /dev/null +++ b/locales/fi.json @@ -0,0 +1,5 @@ +{ + "aborting": "Keskeytetään.", + "action_invalid": "Virheellinen toiminta '{action}'", + "password_too_simple_1": "Salasanan pitää olla ainakin 8 merkin pituinen" +} diff --git a/locales/fr.json b/locales/fr.json new file mode 100644 index 0000000..a83ae56 --- /dev/null +++ b/locales/fr.json @@ -0,0 +1,926 @@ +{ + "aborting": "Annulation en cours.", + "action_invalid": "Action '{action}' incorrecte", + "additional_urls_already_added": "L'URL supplémentaire '{url}' a déjà été ajoutée pour la permission '{permission}'", + "additional_urls_already_removed": "L'URL supplémentaire '{url}' a déjà été supprimée pour la permission '{permission}'", + "admin_password": "Mot de passe d'administration", + "admins": "Comptes administrateurs", + "all_users": "Tous les comptes YunoHost", + "already_up_to_date": "Il n'y a rien à faire. Tout est déjà à jour.", + "app_action_broke_system": "Cette action semble avoir cassé des services importants : {services}", + "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).", + "app_action_failed": "Échec de la commande {action} de l'application {app}", + "app_already_installed": "{app} est déjà installé", + "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_arch_not_supported": "Cette application ne peut être installée que sur les architectures {required}. L'architecture de votre serveur est {current}", + "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_change_url_failed": "Impossible de modifier l'url de {app} : {error}", + "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_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_change_url_success": "L'URL de l'application {app} a été changée en {domain}{path}", + "app_config__core_name": "Tuiles et permissions", + "app_config_permission_allowed": "Groupes/comptes autorisés", + "app_config_permission_allowed_warn_protected": "NB : cette autorisation est 'protégée' et le groupe 'visiteurs' ne peut donc pas être ajouté/supprimé des groupes autorisés.", + "app_config_permission_description": "Description", + "app_config_permission_description_help": "Cette fonctionnalité n'est vraiment utile que si vous utilisez le mode 'descriptif' du portail", + "app_config_permission_extraperm_section_name": "Autorisation '{perm}'", + "app_config_permission_label": "Libellé", + "app_config_permission_location": "Correspond à [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Utilisation d'un logo personnalisé", + "app_config_permission_logo_help": "Seuls les fichiers PNG sont pris en charge", + "app_config_permission_show_tile": "Afficher la tuile dans le portail", + "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.", + "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}", + "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", + "app_failed_to_download_asset": "Échec du téléchargement de la ressource '{source_id}' ({url}) pour {app} : {out}", + "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.", + "app_id_invalid": "Identifiant d'application invalide", + "app_install_failed": "Impossible d'installer {app} : {error}", + "app_install_files_invalid": "Fichiers d'installation incorrects", + "app_install_script_failed": "Une erreur est survenue dans le script d'installation de l'application", + "app_location_unavailable": "Cette URL n'est pas disponible ou est en conflit avec une application existante :\n{apps}", + "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_manifest_install_ask_admin": "Choisissez un compte administrateur pour cette application", + "app_manifest_install_ask_domain": "Choisissez le domaine sur lequel vous souhaitez installer cette application", + "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)", + "app_manifest_install_ask_is_public": "Cette application devrait-elle être visible par les visiteurs anonymes ?", + "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_not_correctly_installed": "{app} semble être mal installé", + "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 à jour mais seule {current} de mémoire est disponible actuellement.", + "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_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.", + "app_remove_after_failed_install": "Suppression de l'application après l'échec de l'installation…", + "app_removed": "{app} désinstallé", + "app_requirements_checking": "Vérification des prérequis pour {app}…", + "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}", + "app_restore_failed": "Impossible de restaurer {app} : {error}", + "app_restore_script_failed": "Une erreur s'est produite dans le script de restauration de l'application", + "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, l'URL est-elle correcte ?", + "app_start_backup": "Collecte des fichiers devant être sauvegardés pour {app}…", + "app_start_install": "Installation de {app}…", + "app_start_remove": "Suppression de {app}…", + "app_start_restore": "Restauration de {app}…", + "app_unknown": "Application inconnue", + "app_unsupported_remote_type": "Ce type de commande à distance utilisé pour cette application n'est pas supporté", + "app_upgrade_app_name": "Mise à jour de {app}…", + "app_upgrade_bad_quality": "Cette application est actuellement signalée comme défectueuse dans le catalogue d'applications de YunoHost. Il peut s'agir d'un problème temporaire pendant que les mainteneur⋅euse⋅s tentent de le résoudre. En attendant, la mise à jour de cette application est désactivée.", + "app_upgrade_broke_the_system": "La mise à jour de l'application {app} semble avoir fonctionné, mais elle a laissé le système dans un état non fonctionnel et est donc considérée comme un échec.", + "app_upgrade_cli_bad_quality": "La mise à jour pour {app} est ignorée car elle est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost.", + "app_upgrade_cli_up_to_date": "{app} est déjà à jour ({current_version})", + "app_upgrade_cli_url_required": "{app} ne figure pas (plus ?) dans le catalogue et ne peut donc pas être mis à jour automatiquement. Vous devez utiliser `yunohost app upgrade {app}` pour fournir l'URL du dépôt à l'aide de l'option `-u`.", + "app_upgrade_cli_will_force_upgrade": "{app} sera mise à jour de force ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} sera mis à jour de la version {current_version} à la version {new_version}", + "app_upgrade_continuing_with_other_apps": "La mise à jour de {app} a échoué, mais poursuite de la mise à niveau des autres applications malgré tout (car l'option `--continue-on-failure` a été utilisée)", + "app_upgrade_fail_requirements": "Une nouvelle version est disponible pour cette application ({new_version}), mais certaines conditions ne sont pas remplies :\n{failed_requirements}", + "app_upgrade_failed": "Impossible de mettre à jour {app} : {error}", + "app_upgrade_failed_and_broke_the_system": "La mise à jour de l'application '{app}' a échoué, laissant le système dans un état cassé.", + "app_upgrade_script_failed": "Une erreur s'est produite durant l'exécution du script de mise à jour de l'application", + "app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}", + "app_upgrade_some_app_failed": "Certaines applications n'ont pas été mises à jour", + "app_upgrade_specific_channel_msg": "Veuillez noter que vous utilisez actuellement `{channel}` comme source pour les mises à jour. Assurez-vous de consulter la discussion en cours [ici]({pr_url}).", + "app_upgrade_up_to_date": "La mise à jour forcée de l'application (vers la même version) peut parfois être utile pour reconstruire ou restaurer l'application et ses configurations.", + "app_upgrade_upgradable": "L'application peut être mise à jour de la version {current_version} à la version {new_version}", + "app_upgrade_url_required": "Cette application ne figure pas (plus ?) dans le catalogue ; vous devez donc vous charger manuellement de ses mises à jour.
Depuis la ligne de commande, vous pouvez utiliser la commande `yunohost app upgrade ` et indiquer l'URL du dépôt à l'aide de l'option `-u`.", + "app_upgraded": "{app} mis à jour", + "app_yunohost_version_not_supported": "Cette application nécessite une version de YunoHost >= {required}. La version installée est {current}.", + "apps_already_up_to_date": "Toutes les applications sont déjà à jour", + "apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog} : {error}", + "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_updating": "Mise à jour du catalogue des applications…", + "apps_confirm_partial_upgrade": "Certaines applications pour lesquelles une mise à jour a été demandée ne peuvent pas être mises à jour. Voulez-vous quand même continuer avec les autres ?", + "apps_no_target_can_be_upgraded": "Aucune application ne peut être mise à jour", + "apps_upgrade_cancelled": "Les mises à jour étaient encore en attente pour plusieurs autres applications, mais leur mise à jour a été annulée (utilisez `--continue-on-failure` pour continuer quand même) : {apps}", + "ask_admin_fullname": "Nom complet du compte administrateur", + "ask_admin_username": "Nom du compte d'administration", + "ask_dyndns_recovery_password": "Mot de passe de récupération pour DynDNS", + "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_explain_during_unsubscribe": "Veuillez saisir le mot de passe de récupération pour ce domaine DynDNS.", + "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.", + "ask_fullname": "Nom complet (Nom et Prénom)", + "ask_main_domain": "Domaine principal", + "ask_new_admin_password": "Nouveau mot de passe d'administration", + "ask_new_domain": "Nouveau domaine", + "ask_new_path": "Nouveau chemin", + "ask_password": "Mot de passe", + "ask_user_domain": "Domaine à utiliser pour l'adresse email", + "automatic_task": "Tâche automatique", + "backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter", + "backup_actually_backuping": "Création d'une archive de sauvegarde à partir des fichiers collectés…", + "backup_app_script_failed": "Échec de la collecte des fichiers à sauvegarder pour {app}.", + "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_archive_app_not_found": "{app} n'a pas été trouvée dans l'archive de la sauvegarde", + "backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path})", + "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}", + "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_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_before_upgrade_deleted_because_replaced_by_newer_backup": "La sauvegarde {name} a été supprimée car elle a été remplacée par une sauvegarde plus récente {newname}", + "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l'archive décompressée", + "backup_cleaning_failed": "Impossible de nettoyer le dossier temporaire de sauvegarde", + "backup_copying_to_organize_the_archive": "Copie de {size} Mo pour organiser l'archive", + "backup_couldnt_bind": "Impossible de lier {src} avec {dest}.", + "backup_create_size_estimation": "L'archive contiendra environ {size} de données.", + "backup_created": "Sauvegarde créée : {name}", + "backup_creation_failed": "Impossible de créer l'archive de la sauvegarde", + "backup_csv_addition_failed": "Impossible d'ajouter des fichiers à sauvegarder dans le fichier CSV", + "backup_csv_creation_failed": "Impossible de créer le fichier CSV nécessaire à la restauration", + "backup_custom_backup_error": "Échec de la méthode de sauvegarde personnalisée à l'étape 'backup'", + "backup_custom_mount_error": "Échec de la méthode de sauvegarde personnalisée à l'étape 'mount'", + "backup_delete_error": "Impossible de supprimer '{path}'", + "backup_deleted": "Sauvegarde supprimée : {name}", + "backup_hook_unknown": "Script de sauvegarde '{hook}' inconnu", + "backup_method_copy_finished": "La copie de la sauvegarde est terminée", + "backup_method_custom_finished": "La méthode de sauvegarde personnalisée '{method}' est terminée", + "backup_method_tar_finished": "L'archive TAR de la sauvegarde a été créée", + "backup_mount_archive_for_restore": "Préparation de l'archive pour restauration…", + "backup_no_file_collected": "Échec de la collecte des fichiers à sauvegarder", + "backup_no_uncompress_archive_dir": "Ce dossier d'archive décompressée n'existe pas", + "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_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.", + "backup_running_hooks": "Exécution des scripts de sauvegarde…", + "backup_system_part_failed": "Impossible de sauvegarder la partie '{part}' du système", + "backup_unable_to_organize_files": "Impossible d'utiliser la méthode rapide pour organiser les fichiers dans l'archive", + "backup_with_no_backup_script_for_app": "L'application {app} n'a pas de script de sauvegarde. Ignorer.", + "backup_with_no_restore_script_for_app": "{app} n'a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.", + "cannot_open_file": "Impossible d'ouvrir le fichier {file} (raison : {error})", + "cannot_write_file": "Ne peut pas écrire le fichier {file} (raison : {error})", + "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_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_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_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_install_success": "Le certificat Let's Encrypt est maintenant installé pour le domaine '{domain}'", + "certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine '{domain}'", + "certmanager_cert_renew_failed": "Le renouvellement du certificat Let's Encrypt a échoué pour {domains}", + "certmanager_cert_renew_success": "Certificat Let's Encrypt renouvelé pour le domaine '{domain}'", + "certmanager_cert_signing_failed": "Impossible de signer le nouveau certificat", + "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain} a échoué…", + "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_domain_dns_ip_differs_from_public_ip": "Les enregistrements DNS du domaine '{domain}' sont différents de l'adresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section Diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles.)", + "certmanager_domain_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_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.)", + "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", + "certmanager_no_cert_file": "Impossible de lire le fichier du certificat pour le domaine {domain} (fichier : {file})", + "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})", + "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_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_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_no_panel": "Aucun panneau de configuration trouvé.", + "config_unknown_filter_key": "La clé de filtre '{filter_key}' est incorrecte.", + "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}'", + "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_insufficient_ram": "ATTENTION ! Cette application requiert plus de RAM que disponible actuellement. Même si cette application pouvait fonctionner, son processus d'installation/mise à jour nécessite une grande quantité de RAM. Votre serveur pourrait donc geler et planter lamentablement. Si vous voulez prendre ce risque, tapez '{answers}'", + "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}]", + "confirm_tos_acknowledgement": "J'ai lu et compris les conditions d'utilisation [{answers}]", + "corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource} (raison : {error})", + "corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource} (raison : {error})", + "corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource} (raison : {error})", + "danger": "Danger :", + "diagnosis_apps_allgood": "Toutes les applications installées respectent les pratiques de packaging de base", + "diagnosis_apps_bad_quality": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.", + "diagnosis_apps_broken": "Cette application est actuellement signalée comme cassée dans le catalogue d'applications de YunoHost. Cela peut être un problème temporaire. En attendant que les mainteneurs tentent de résoudre le problème, la mise à jour de cette application est désactivée.", + "diagnosis_apps_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_issue": "Un problème a été détecté pour l'application {app}", + "diagnosis_apps_not_in_app_catalog": "Cette application ne figure pas dans le catalogue de YunoHost. Si elle l'était dans le passé et a été supprimée, vous devriez envisager de désinstaller cette application car elle ne recevra pas de mises à jour et peut compromettre l'intégrité et la sécurité de votre système.", + "diagnosis_apps_outdated_packaging_format": "Cette application utilise un format de packaging obsolète qui ne sera bientôt plus supportée par YunoHost. Vous devriez vraiment envisager de la mettre à jour.", + "diagnosis_apps_outdated_ynh_requirement": "La version installée de cette application nécessite obligatoirement une version de YunoHost >= 2.x, 3.x ou 4.x, ce qui tend à indiquer qu'elle n'est pas à jour avec les pratiques recommandées de packaging et des helpers. Vous devriez vraiment envisager de la mettre à jour.", + "diagnosis_apps_security_issue_error": "L'application {app} est actuellement dans la version '{current_version}', qui est vulnérable à un problème de sécurité MAJEUR : {title}. Il est recommandé de la mettre à jour DÈS QUE POSSIBLE vers la version '{fixed_in_version}'. Plus d'informations : {more_infos_list}", + "diagnosis_apps_security_issue_warning": "L'application {app} est actuellement dans la version '{current_version}', qui présente une vulnérabilité modérée : {title}. Il est recommandé de la mettre à jour vers la version '{fixed_in_version}'. Plus d'informations : {more_infos_list}", + "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.", + "diagnosis_basesystem_hardware": "L'architecture du serveur est {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Le modèle/architecture du serveur est {model}", + "diagnosis_basesystem_host": "Le serveur utilise Debian {debian_version}", + "diagnosis_basesystem_kernel": "Le serveur utilise le noyau Linux {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Vous exécutez des versions incohérentes des packages YunoHost… très probablement en raison d'une mise à jour échouée ou partielle.", + "diagnosis_basesystem_ynh_main_version": "Le serveur utilise YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})", + "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment !)", + "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_description_apps": "Applications", + "diagnosis_description_basesystem": "Système de base", + "diagnosis_description_dnsrecords": "Enregistrements DNS", + "diagnosis_description_ip": "Connectivité Internet", + "diagnosis_description_mail": "Email", + "diagnosis_description_ports": "Exposition des ports", + "diagnosis_description_regenconf": "Configurations système", + "diagnosis_description_services": "État des services", + "diagnosis_description_systemresources": "Ressources système", + "diagnosis_description_web": "Web", + "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_diskusage_ok": "L'espace de stockage {mountpoint} (sur le périphérique {device}) a encore {free} ({free_percent}%) d'espace restant (sur {total}) !", + "diagnosis_diskusage_verylow": "L'espace de stockage {mountpoint} (sur l'appareil {device}) ne dispose que de {free} ({free_percent}%) d'espace restant (sur {total}). Vous devriez vraiment envisager de nettoyer de l'espace !", + "diagnosis_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_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 : {content}", + "diagnosis_dns_good_conf": "Les enregistrements DNS sont correctement configurés pour le domaine {domain} (catégorie {category})", + "diagnosis_dns_missing_record": "Selon la configuration DNS recommandée, vous devez ajouter un enregistrement DNS
Type : {type}
Nom : {name}
Valeur : {content}", + "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation disponible ici https://doc.yunohost.org/dns_config si vous avez besoin d'aide pour configurer les enregistrements DNS.", + "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.", + "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.", + "diagnosis_domain_expiration_error": "Certains domaines vont expirer TRÈS PROCHAINEMENT !", + "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_expiration_success": "Vos domaines sont enregistrés et ne vont pas expirer prochainement.", + "diagnosis_domain_expiration_warning": "Certains domaines vont expirer prochainement !", + "diagnosis_domain_expires_in": "{domain} expire dans {days} jours.", + "diagnosis_domain_not_found_details": "Le domaine {domain} n'existe pas dans la base de donnée WHOIS ou est expiré !", + "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_failed_for_category": "Échec du diagnostic pour la catégorie '{category}' : {error}", + "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_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.", + "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://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Il semble qu'une autre machine (peut-être votre routeur Internet) ait répondu à la place de votre serveur.
1. La cause la plus courante de ce problème est que les ports 80 (et 443) ne sont pas correctement redirigés vers votre serveur.
2. Dans les configurations plus complexes : assurez-vous qu'aucun pare-feu ou proxy inverse n'interfère.", + "diagnosis_http_connection_error": "Erreur de connexion : impossible de se connecter au domaine demandé, il est probablement injoignable.", + "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_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 qui se trouvent sur le 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://doc.yunohost.org/dns_local_network", + "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.", + "diagnosis_http_ok": "Le domaine {domain} est accessible en HTTP depuis l'extérieur.", + "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_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.", + "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.", + "diagnosis_http_unreachable": "Le domaine {domain} est inaccessible en HTTP depuis l'extérieur.", + "diagnosis_ignore_already_filtered": "(Il y a déjà un filtre de diagnostic {category} qui correspond à ces critères)", + "diagnosis_ignore_criteria_error": "Les critères doivent être sous la forme de clé=valeur (ex. domain=yolo.test)", + "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_no_filter_found": "(Il n'y pas de filtre de diagnostic pour la catégorie {category} qui correspond à ces critères)", + "diagnosis_ignore_no_issue_found": "Aucun problème correspondant au critère donné n'a été trouvé.", + "diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))", + "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_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !", + "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6 !", + "diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !", + "diagnosis_ip_global": "IP globale : {global}", + "diagnosis_ip_local": "IP locale : {local}", + "diagnosis_ip_no_ipv4": "Le serveur ne dispose pas d'une adresse IPv4.", + "diagnosis_ip_no_ipv6": "Le serveur ne dispose pas d'une adresse IPv6.", + "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://doc.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_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://doc.yunohost.org/ipv6.", + "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_mail_blocklist_listed_by": "Votre IP ou domaine {item} est sur liste noire sur {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Les adresses IP et les domaines utilisés par ce serveur ne semblent pas être sur liste noire", + "diagnosis_mail_blocklist_reason": "La raison de la liste noire est : {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Il semble que la raison mentionne 'open resolver'.
Cela signifie généralement que votre serveur n'utilise pas son DNS local, mais un DNS public et ouvert.
Vérifiez le contenu de /etc/resolv.conf, il devrait contenir nameserver 127.0.0.1.
Comme ce fichier est généralement généré automatiquement, ne le modifiez pas manuellement. Vérifiez vos paramètres DHCP ou vos paramètres VPN si vous en utilisez un, ou si vous avez utilisé une image Debian créée, par exemple, par un fournisseur de VPS, recherchez une configuration cloudinit.
N'hésitez pas à vous rendre sur les canaux d'assistance YunoHost pour obtenir de l'aide sur ce problème.
La raison exacte de la liste noire est : {reason}", + "diagnosis_mail_blocklist_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 {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Un service non SMTP a répondu sur le port 25 en IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond à la place de votre serveur.", + "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_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": "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_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_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_fcrdns_different_from_ehlo_domain_details": "DNS inverse actuel : {rdns_domain}
Valeur attendue : {ehlo_domain}", + "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_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://doc.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_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_ok": "Votre DNS inverse est correctement configuré !", + "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}.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devriez d'abord essayer d'ouvrir le port 25 dans l'interface de votre routeur, box Internet ou interface d'hébergement. (Certains hébergeurs peuvent vous demander d'ouvrir un ticket sur leur support d'assistance pour cela).", + "diagnosis_mail_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://doc.yunohost.org/vpn_advantage
- Vous pouvez également envisager de passer à un fournisseur plus respectueux de la neutralité du net", + "diagnosis_mail_outgoing_port_25_ok": "Le serveur de messagerie SMTP peut envoyer des emails (le port sortant 25 n'est pas bloqué).", + "diagnosis_mail_queue_ok": "{nb_pending} emails en attente dans les files d'attente de messagerie", + "diagnosis_mail_queue_too_big": "Trop d'emails en attente dans la file d'attente ({nb_pending} emails)", + "diagnosis_mail_queue_unavailable": "Impossible de consulter le nombre d'emails en attente dans la file d'attente", + "diagnosis_mail_queue_unavailable_details": "Erreur : {error}", + "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_no_cache": "Pas encore de cache de diagnostique pour la catégorie '{category}'", + "diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version", + "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}", + "diagnosis_package_security_issue_error": "Le paquet système '{package}' est actuellement dans la version '{current_version}', qui est vulnérable à un problème de sécurité MAJEUR : {title}. Il est recommandé de passer dès que possible à la version '{fixed_in_version}'. Plus d'informations : {more_infos_list}", + "diagnosis_package_security_issue_warning": "Le package système '{package}' est actuellement dans la version '{current_version}', qui est vulnérable à un problème de sécurité modéré : {title}. Il est recommandé de le mettre à niveau vers '{fixed_in_version}'. Plus d'informations : {more_infos_list}", + "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}", + "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://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Rendre ce port accessible est nécessaire pour les fonctionnalités de type {category} (service {service})", + "diagnosis_ports_ok": "Le port {port} est accessible depuis l'extérieur.", + "diagnosis_ports_partially_unreachable": "Le port {port} n'est pas accessible depuis l'extérieur en IPv{failed}.", + "diagnosis_ports_unreachable": "Le port {port} n'est pas accessible depuis l'extérieur.", + "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}", + "diagnosis_ram_low": "Le système n'a plus de {available} ({available_percent}%) RAM sur {total}. Faites attention.", + "diagnosis_ram_ok": "Le système dispose encore 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_regenconf_allgood": "Tous les fichiers de configuration sont conformes aux préconisations !", + "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", + "diagnosis_rfkill_wifi": "La carte Wi-Fi est désactivée et un avertissement du système pourrait empêcher d'installer des applications", + "diagnosis_rfkill_wifi_details": "Cet avertissement se glisse dans beaucoup de retours de commandes, cassant certaines applications. Il s'agit généralement de spécifier votre code pays avec la commande sudo raspi-config. Voici l'erreur:
{rfkill_wifi_error}", + "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.", + "diagnosis_security_vulnerable_to_meltdown": "Vous semblez vulnérable à la faille de sécurité majeure qu'est Meltdown", + "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_services_bad_status": "Le service {service} est {status} :-(", + "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_services_conf_broken": "La configuration est cassée pour le service {service} !", + "diagnosis_services_running": "Le service {service} est en cours de fonctionnement !", + "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_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_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.", + "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 de manque de mémoire.", + "diagnosis_swap_ok": "Le système dispose de {total} de swap !", + "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.", + "diagnosis_unknown_categories": "Les catégories suivantes sont inconnues : {categories}", + "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 (Bookworm).", + "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 bookworm. Le fichier de configuration correspondant doit être /etc/apt/sources.list, ou un fichier dans /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (le gestionnaire de paquets du système) est actuellement configuré pour installer toutes les mises à jour dites 'testing' de votre instance YunoHost.", + "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 à jour 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.", + "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_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}", + "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}'.", + "domain_cert_gen_failed": "Impossible de générer le 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_api_protocol": "Protocole API", + "domain_config_auth_application_key": "Clé d'application", + "domain_config_auth_application_secret": "Clé secrète de l'application", + "domain_config_auth_consumer_key": "Clé d'identification", + "domain_config_auth_entrypoint": "Point d'entrée API", + "domain_config_auth_key": "Clé d'authentification", + "domain_config_auth_secret": "Secret d'authentification", + "domain_config_auth_token": "Jeton d'authentification", + "domain_config_cert_install": "Installer un certificat Let's Encrypt", + "domain_config_cert_issuer": "Autorité de certification", + "domain_config_cert_name": "Certificat", + "domain_config_cert_no_checks": "Ignorer les tests et autres vérifications du diagnostic", + "domain_config_cert_renew": "Renouvellement du certificat Let's Encrypt", + "domain_config_cert_renew_help": "Le certificat sera automatiquement renouvelé dans les 15 derniers jours précédant sa fin de validité. Vous pouvez le renouveler manuellement si vous le souhaitez (non recommandé).", + "domain_config_cert_summary": "État du certificat", + "domain_config_cert_summary_abouttoexpire": "Le certificat actuel est sur le point d'expirer. Il devrait bientôt être renouvelé automatiquement.", + "domain_config_cert_summary_expired": "ATTENTION : Le certificat actuel n'est pas valide ! HTTPS ne fonctionnera pas du tout !", + "domain_config_cert_summary_letsencrypt": "Bravo ! Vous utilisez un certificat Let's Encrypt valide !", + "domain_config_cert_summary_ok": "Bien, le certificat actuel semble bon !", + "domain_config_cert_summary_selfsigned": "AVERTISSEMENT : Le certificat actuel est auto-signé. Les navigateurs afficheront un avertissement alarmiste aux nouveaux visiteurs !", + "domain_config_cert_validity": "Validité", + "domain_config_custom_css": "Feuille de style CSS personnalisée", + "domain_config_custom_css_help": "Réservé aux administrateur⋅ice⋅s avancé⋅e⋅s souhaitant personnaliser l'apparence du portail", + "domain_config_default_app": "Application par défaut", + "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_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Afficher la liste des applications publiques aux visiteurs", + "domain_config_enable_public_apps_page_help": "Les visiteurs verront une page 'applications publiques' lorsqu'ils arriveront sur le portail au lieu du simple formulaire de connexion.", + "domain_config_feature_name": "Fonctionnalités", + "domain_config_mail_in": "Emails entrants", + "domain_config_mail_out": "Emails sortants", + "domain_config_portal_logo": "Logo personnalisé", + "domain_config_portal_logo_help": "Sont acceptés les formats .svg, .png et .jpeg. Préférez un .svg monochrome avec fill: currentColor pour que le logo s'adapte aux thèmes.", + "domain_config_portal_name": "Personnalisation du portail", + "domain_config_portal_public_intro": "Présentation publique personnalisée", + "domain_config_portal_public_intro_help": "Vous pouvez utiliser HTML. Les styles de base seront appliqués aux éléments génériques.", + "domain_config_portal_theme": "Thème de couleur par défaut", + "domain_config_portal_theme_help": "Les utilisateurs sont autorisés à en choisir un autre dans leurs paramètres.", + "domain_config_portal_tile_theme": "Thème d'affichage des tuiles d'application", + "domain_config_portal_title": "Titre personnalisé", + "domain_config_portal_user_intro": "Introduction personnalisée pour les utilisateur⋅ice⋅s", + "domain_config_portal_user_intro_help": "Vous pouvez utiliser HTML. Les styles de base seront appliqués aux éléments génériques.", + "domain_config_search_engine": "URL du moteur de recherche", + "domain_config_search_engine_help": "Il s'agit d'une fonctionnalité facultative, permettant d'afficher une barre de recherche dans le portail (par exemple si vous souhaitez utiliser votre portail YunoHost comme page d'accueil de votre navigateur). Il doit s'agir d'une URL avec une chaîne de requête vide telle que `https://duckduckgo.com/?q=`, avec `q=` comme paramètre de requête vide de duckduckgo", + "domain_config_search_engine_name": "Nom du moteur de recherche", + "domain_config_show_other_domains_apps": "Afficher les applications d'autres domaines", + "domain_created": "Le domaine a été créé", + "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_dns_conf_is_just_a_recommendation": "Cette section 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 fournisseur de nom de domaine (registrar) conformément à cette recommandation.", + "domain_dns_conf_special_use_tld": "Ce domaine est basé sur un domaine de premier niveau (TLD) à usage spécial tel que .local ou .test et ne devrait donc pas avoir d'enregistrements DNS réels.", + "domain_dns_push_already_up_to_date": "Dossiers déjà à jour.", + "domain_dns_push_failed": "La mise à jour des enregistrements DNS a échoué.", + "domain_dns_push_failed_to_list": "Échec de la liste des enregistrements actuels à l'aide de l'API du registraire : {error}", + "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_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://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "Enregistrements DNS partiellement mis à jour : certains avertissements/erreurs ont été signalés.", + "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_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_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://doc.yunohost.org/dns_config.", + "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://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Vous pouvez également configurer manuellement vos enregistrements DNS en suivant la documentation sur https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_use_auto": "Utiliser la fonction DNS automatique", + "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_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS", + "domain_exists": "Le domaine existe déjà", + "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).", + "domain_registrar_is_not_configured": "Le registrar n'est pas encore configuré pour le domaine {domain}.", + "domain_remove_confirm_apps_removal": "Le retrait de ce domaine retirera aussi ces applications :\n{apps}\n\nVoulez-vous vraiment faire cela ? [{answers}]", + "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nVeuillez les désinstaller avec la commande 'yunohost app remove nom-de-l-application' ou les déplacer vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application' avant de procéder à la suppression du domaine", + "domain_unknown": "Domaine '{domain}' inconnu", + "domains_available": "Domaines disponibles :", + "done": "Terminé", + "download_bad_status_code": "{url} renvoie le code d'état {code}", + "download_ssl_error": "Erreur SSL lors de la connexion à {url}", + "download_timeout": "{url} a pris trop de temps pour répondre : abandon.", + "download_unknown_error": "Erreur lors du téléchargement des données à partir de {url} : {error}", + "downloading": "Téléchargement en cours…", + "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`.", + "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)", + "dyndns_could_not_check_available": "Impossible de vérifier si {domain} est disponible chez {provider}.", + "dyndns_domain_not_provided": "Le fournisseur DynDNS {provider} ne peut pas fournir le domaine {domain}.", + "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_not_found": "Clé DNS introuvable pour le domaine", + "dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec 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_provider_unreachable": "Impossible d'atteindre le fournisseur DynDNS {provider} : votre YunoHost n'est pas correctement connecté à Internet ou le serveur Dynette est en panne.", + "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_failed": "Échec de la mise en place du mot de passe de récupération : {error}", + "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_success": "Mot de passe de récupération changé !", + "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_subscribe_failed": "Le nom de domaine DynDNS n'a pas pu être enregistré : {error}", + "dyndns_subscribed": "Domaine DynDNS enregistré", + "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.", + "dyndns_unavailable": "Le domaine {domain} est indisponible.", + "dyndns_unsubscribe_already_unsubscribed": "Le domaine a déjà été résilié", + "dyndns_unsubscribe_denied": "Échec de la résiliation du domaine : informations d'identification non valides", + "dyndns_unsubscribe_failed": "Le nom de domaine DynDNS n'a pas pu être résilié : {error}", + "dyndns_unsubscribed": "Domaine DynDNS résilié", + "error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path} : {error}", + "error_removing": "Erreur lors de la suppression {path} : {error}", + "error_writing_file": "Erreur en écrivant le fichier {file} : {error}", + "extracting": "Extraction en cours…", + "field_invalid": "Champ incorrect : '{field}'", + "file_does_not_exist": "Le fichier dont le chemin est {path} n'existe pas.", + "file_not_exist": "Le fichier '{path}' n'existe pas", + "firewall_reload_failed": "Impossible de recharger le pare-feu. Plus d'informations dans le journal.", + "firewall_reloaded": "Pare-feu rechargé", + "global_settings_reset_success": "Réinitialisation des paramètres généraux", + "global_settings_setting_admin_strength": "Critères pour les mots de passe de comptes administrateurs", + "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_antispam_name": "Antispam", + "global_settings_setting_backup_compress_tar_archives": "Compresser les sauvegardes", + "global_settings_setting_backup_compress_tar_archives_help": "Lors de la création de nouvelles sauvegardes, compresser automatiquement les archives (.tar.gz) au lieu des archives non compressées (.tar). N.B. : activer cette option permet de créer des archives plus légères, mais la procédure de sauvegarde initiale sera significativement plus longues et plus gourmandes en CPU.", + "global_settings_setting_backup_name": "Sauvegarde", + "global_settings_setting_dns_custom_resolvers_enabled": "Utiliser des résolveurs DNS personnalisés", + "global_settings_setting_dns_custom_resolvers_enabled_help": "Par défaut, YunoHost utilise une liste de résolveurs fiables situés en Europe. Les administrateurices avancées peuvent choisir de spécifier des résolveurs personnalisés à la place.", + "global_settings_setting_dns_custom_resolvers_list": "Adresses des résolveurs personnalisés", + "global_settings_setting_dns_custom_resolvers_list_help": "Une liste d'au moins 2 résolveurs DNS par protocole IP utilisé (IPv4/IPv6). Exemple : 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "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.", + "global_settings_setting_email_name": "Email", + "global_settings_setting_enable_blocklists": "Activer les listes de blocages pour le traffic entrant", + "global_settings_setting_enable_blocklists_help": "Bloque le traffic mail provenant des serveurs listés par spamcop.net, spamhaus.org et abuseat.org pour prévenir les spams. Cependant, ceci peut engendrer des problèmes de distributions pour certains serveurs de mails inoffensifs enregistré dans ces listes, et les mails qu'ils enverront ne seront pas reçu.", + "global_settings_setting_experimental_name": "Expérimentale", + "global_settings_setting_misc_name": "Autre", + "global_settings_setting_network_name": "Réseau", + "global_settings_setting_nginx_compatibility": "Compatibilité NGINX", + "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_name": "NGINX (serveur Web)", + "global_settings_setting_nginx_redirect_to_https": "Forcer HTTPS", + "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_password_name": "Mots de passe", + "global_settings_setting_passwordless_sudo": "Permettre aux comptes administrateurs d'utiliser 'sudo' sans retaper leur mot de passe", + "global_settings_setting_pop3_enabled": "Activer POP3", + "global_settings_setting_pop3_enabled_help": "Activer le protocole POP3 pour le serveur de messagerie. POP3 est un protocole plus ancien permettant d'accéder aux boîtes aux lettres à partir des clients de messagerie. Il est plus léger, mais possède moins de fonctionnalités que IMAP (activé par défaut)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Autoriser les utilisateur·ice·s à modifier leur adresse mail principale", + "global_settings_setting_portal_allow_edit_email_alias": "Autoriser les utilisateur·ice·s à ajouter, retirer et modifier leurs alias mails", + "global_settings_setting_portal_allow_edit_email_alias_help": "Si désactivé, les personnes souhaitant changer leurs alias mails devront demander aux administrateur·ice·s.", + "global_settings_setting_portal_allow_edit_email_forward": "Autoriser les utilisateur·ice·s à ajouter, retirer et modifier leur adresse mail de transfert", + "global_settings_setting_portal_allow_edit_email_forward_help": "Si désactivé, les personnes souhaitant changer leurs adresses de transfert devront demander aux administrateur·ice·s.", + "global_settings_setting_portal_allow_edit_email_help": "Si désactivé, les personnes souhaitant changer leur adresse mail devront demander aux administrateur·ice·s.", + "global_settings_setting_portal_name": "Portail", + "global_settings_setting_postfix_compatibility": "Compatibilité Postfix", + "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_postfix_name": "Postfix (serveur de messagerie SMTP)", + "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_access_name": "Changer le mot de passe root", + "global_settings_setting_root_password": "Nouveau mot de passe root", + "global_settings_setting_root_password_confirm": "Nouveau mot de passe root (confirmer)", + "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_name": "Sécurité", + "global_settings_setting_smtp_allow_ipv6": "Autoriser l'IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Autoriser l'utilisation d'IPv6 pour recevoir et envoyer du courrier", + "global_settings_setting_smtp_backup_mx_domains": "Domaines à utiliser comme MX secondaire pour", + "global_settings_setting_smtp_backup_mx_domains_help": "Autoriser ce serveur à agir en tant que domaine MX *secondaire* de secours pour le domaine listé. Cela signifie que si le MX principal pour le domaine n'est pas accessible (par exemple à cause d'une panne), les courriers électroniques seront quand même envoyés à ce serveur, qui les conservera pendant un maximum de 20 jours et essaiera de les relayer vers la destination réelle une fois qu'il sera rétabli. Plusieurs domaines peuvent être fournis, séparés par des virgules.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Sauvegarde SMTP des emails sur liste blanche du MX", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Dans le cas d'un MX secondaire, la liste exhaustive des adresses électroniques des destinataires autorisés doit être fournie (sinon les courriers seront refusés et rejetés). Plusieurs entrées peuvent être fournies, séparées par des virgules.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Activer le relais SMTP", + "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.", + "global_settings_setting_smtp_relay_host": "Adresse du relais SMTP", + "global_settings_setting_smtp_relay_password": "Mot de passe du relais SMTP", + "global_settings_setting_smtp_relay_port": "Port du relais SMTP", + "global_settings_setting_smtp_relay_user": "Compte du relais SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilité SSH", + "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_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Authentification par mot de passe", + "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_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.", + "global_settings_setting_tls_passthrough_enabled": "Activer le flux TLS-passthrough/transfert basé sur les SNI", + "global_settings_setting_tls_passthrough_enabled_help": "Il s'agit d'une fonctionnalité avancée qui permet d'effectuer un reverse-proxy d'un domaine entier vers une autre machine *sans* avoir à déchiffrer le trafic. Elle est utile lorsque vous souhaitez exposer plusieurs machines derrière la même IP, tout en permettant à chaque machine de gérer la partie terminale de SSL.", + "global_settings_setting_tls_passthrough_explain": "Cette fonctionnalité est AVANCÉE et EXPÉRIMENTALE et entraînera des changements majeurs dans la configuration NGINX de votre serveur. Veuillez NE PAS l'utiliser si vous ne savez pas ce que vous faites ! En particulier, vous devez savoir que Fail2Ban ne peut pas être mis en œuvre sur le serveur proxy (nftables ne peut pas bannir le trafic malveillant car tous les paquets IP semblent provenir du serveur principal). De plus, pour l'instant, la configuration NGINX du serveur proxy doit être modifiée manuellement pour accepter le `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "Liste des transferts", + "global_settings_setting_tls_passthrough_list_help": "Doit être une liste de ce type DOMAIN;DESTINATION;PORT, telle que domaine.tld;192.168.1.42;443 ou domaine.tld;serveur.local;8123", + "global_settings_setting_tls_passthrough_name": "Transfert TLS/SNI", + "global_settings_setting_user_strength": "Critères pour les mots de passe des comptes", + "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", + "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", + "global_settings_setting_webadmin_allowlist_enabled_help": "Autoriser seulement certaines IP à accéder à la webadmin.", + "global_settings_setting_webadmin_allowlist_help": "Adresses IP autorisées à accéder à la webadmin. La notation CIDR est autorisée.", + "global_settings_setting_webadmin_name": "Webadmin", + "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).", + "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_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_cannot_be_deleted": "Le groupe {group} ne peut pas être supprimé manuellement.", + "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_primary_group": "Le groupe '{group}' ne peut pas être édité manuellement. C'est le groupe principal destiné à ne contenir qu'un compte spécifique.", + "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_remove_last_admin": "Le compte '{user}' est le dernier membre du groupe 'admins' et ne sera donc pas supprimé de celui-ci.", + "group_created": "Le groupe '{group}' a été créé", + "group_creation_failed": "Échec de la création du groupe '{group}' : {error}", + "group_deleted": "Suppression du groupe '{group}'", + "group_deletion_failed": "Échec de la suppression du groupe '{group}' : {error}", + "group_mailalias_add": "L'alias de courrier électronique '{mail}' sera ajouté au groupe '{group}'", + "group_mailalias_remove": "L'alias de courrier électronique '{mail}' sera supprimé du groupe '{group}'", + "group_no_change": "Rien à mettre à jour pour le groupe '{group}'", + "group_unknown": "Le groupe {group} est inconnu", + "group_update_aliases": "Mise à jour des alias du groupe '{group}'", + "group_update_failed": "La mise à jour du groupe '{group}' a échoué : {error}", + "group_updated": "Le groupe '{group}' a été mis à jour", + "group_user_add": "Le compte '{user}' sera ajouté au 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}", + "group_user_remove": "Le compte '{user}' sera retiré du groupe '{group}'", + "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_json_return_error": "Échec de la lecture au retour du script {path}. Erreur : {msg}. Contenu brut : {raw_content}", + "hook_list_by_invalid": "Propriété invalide pour lister les actions par celle-ci", + "hook_name_unknown": "Nom de l'action '{name}' inconnu", + "installation_complete": "Installation terminée", + "invalid_credentials": "Mot de passe ou nom de compte incorrect", + "invalid_number": "Doit être un nombre", + "invalid_password": "Mot de passe invalide", + "invalid_regex": "Regex non valide : '{regex}'", + "invalid_shell": "Shell invalide : {shell}", + "invalid_url": "Impossible de se connecter à {url}… peut-être que le service est en panne ou que vous n'êtes pas correctement connecté à Internet en IPv4/IPv6.", + "ldap_attribute_already_exists": "L'attribut LDAP '{attribute}' existe déjà avec la valeur '{value}'", + "ldap_server_down": "Impossible d'atteindre le serveur LDAP", + "ldap_server_is_down_restart_it": "Le service LDAP est arrêté, tentative de redémarrage…", + "log_app_action_run": "Lancer l'action de l'application '{}'", + "log_app_change_url": "Changer l'URL de l'application '{}'", + "log_app_config_set": "Appliquer la configuration à l'application '{}'", + "log_app_install": "Installer l'application '{}'", + "log_app_makedefault": "Faire de '{}' l'application par défaut", + "log_app_remove": "Désinstaller l'application '{}'", + "log_app_upgrade": "Mettre à jour l'application '{}'", + "log_available_on_yunopaste": "Le journal est désormais disponible via {url}", + "log_backup_create": "Créer une archive de sauvegarde", + "log_backup_restore_app": "Restaurer '{}' depuis une sauvegarde", + "log_backup_restore_system": "Restaurer le système depuis une archive de sauvegarde", + "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}", + "log_diagnosis_run": "Exécuter le diagnostic", + "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_domain_add": "Ajouter le domaine '{}'", + "log_domain_config_set": "Mettre à jour la configuration du domaine '{}'", + "log_domain_dns_push": "Pousser les enregistrements DNS pour le domaine '{}'", + "log_domain_main_domain": "Faire de '{}' le domaine principal", + "log_domain_remove": "Supprimer le domaine '{}'", + "log_dyndns_subscribe": "Enregistrer un sous-domaine YunoHost '{}'", + "log_dyndns_unsubscribe": "Supprimer un sous-domaine YunoHost '{}'", + "log_dyndns_update": "Mettre à jour l'adresse IP associée à votre sous-domaine YunoHost '{}'", + "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_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Installer le certificat Let's Encrypt sur le domaine '{}'", + "log_letsencrypt_cert_renew": "Renouveler le certificat Let's Encrypt de '{}'", + "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_link_to_log": "Journal complet de cette opération : ' {desc} '", + "log_operation_unit_unclosed_properly": "L'opération ne s'est pas terminée correctement", + "log_regen_conf": "Régénérer les configurations du système '{}'", + "log_remove_on_failed_install": "Enlever '{}' après une installation échouée", + "log_resource_snippet": "Allocation/retrait/mise à jour d'une ressource", + "log_selfsigned_cert_install": "Installer un certificat auto-signé sur le domaine '{}'", + "log_settings_reset": "Réinitialisation des paramètres", + "log_settings_reset_all": "Réinitialisation de tous les paramètres", + "log_settings_set": "Application des paramètres", + "log_tools_migrations_migrate_forward": "Exécuter les migrations", + "log_tools_postinstall": "Faire la post-installation de votre serveur YunoHost", + "log_tools_reboot": "Redémarrer votre serveur", + "log_tools_shutdown": "Éteindre votre serveur", + "log_tools_update": "Recherche des mises à jour système et du catalogue d'applications en cours", + "log_tools_upgrade": "Mettre à jour les paquets du système", + "log_user_create": "Ajouter le compte '{}'", + "log_user_delete": "Supprimer le compte '{}'", + "log_user_group_create": "Créer le groupe '{}'", + "log_user_group_delete": "Supprimer le groupe '{}'", + "log_user_group_update": "Mettre à jour '{}' pour le groupe", + "log_user_import": "Importer des comptes", + "log_user_update": "Mettre à jour les informations du compte '{}'", + "mail_alias_remove_failed": "Impossible de supprimer l'alias mail '{mail}'", + "mail_alias_unauthorized": "Vous n'êtes pas autorisé à ajouter des alias liés au domaine '{domain}'", + "mail_already_exists": "L'adresse mail '{mail}' existe déjà", + "mail_domain_unknown": "Le domaine '{domain}' de cette adresse email n'est pas valide. Merci d'utiliser un domaine administré par ce serveur.", + "mail_edit_operation_unauthorized": "Vous n'êtes pas autorisé·e à effectuer cette modification pour votre propre compte.", + "mail_forward_remove_failed": "Impossible de supprimer l'email de transfert '{mail}'", + "mail_unavailable": "Cette adresse email est réservée au groupe des comptes d'administration", + "mailbox_disabled": "La boîte aux lettres est désactivée pour le compte {user}", + "mailbox_used_space_dovecot_down": "Le service Dovecot doit être démarré si vous souhaitez voir l'espace disque occupé par la messagerie", + "main_domain_change_failed": "Impossible de modifier le domaine principal", + "main_domain_changed": "Le domaine principal a été modifié", + "migration_0027_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus utiles…", + "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": "Finalement, 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 des sauvegardes** de toutes les données ou applications critiques. Plus d'informations sur https://doc.yunohost.org/admin/backups/ ;\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 à jour peut prendre jusqu'à une heure pour s'effectuer correctement;\n - n'hésitez pas à **contacter la communauté** sur le forum si vous avez besoin d'aide pour régler des problèmes.", + "migration_0027_main_upgrade": "Démarrage de la mise à jour 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 à jour : {manually_modified_files}", + "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_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 Bookworm.", + "migration_0027_yunohost_upgrade": "Démarrage de la mise à jour du cœur de YunoHost…", + "migration_not_enough_space": "Libérez suffisamment d'espace dans {path} pour exécuter la migration.", + "migration_postgresql_previous_not_installed": "PostgreSQL n'a pas été installé sur votre système. Rien à faire.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 est installé, mais pas PostgreSQL 15 !? Quelque chose d'étrange s'est peut-être produit sur votre système :(…", + "migration_python_venv_rebuild_broken_app": "{app} est ignoré car virtualenv ne peut pas être facilement reconstruit pour cette application. Vous devriez résoudre la situation en forçant la mise à jour de cette application à l'aide de `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_disclaimer_base": "Suite à la mise à jour vers Debian Bookworm, certaines applications Python doivent être partiellement reconstruites pour être converties vers la nouvelle version Python livrée avec 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 certaines d'entre elles, comme indiqué ci-dessous. Pour d'autres applications, ou si la tentative de reconstruction échoue, vous devrez forcer manuellement une mise à jour pour ces applications.", + "migration_python_venv_rebuild_disclaimer_ignored": "Les environnements virtuels ne peuvent pas être reconstruits automatiquement pour ces applications. Vous devez forcer une mise à jour pour celles-ci, ce qui peut être fait à partir de la ligne de commande avec : `yunohost app upgrade --force APP` : {ignored_apps}", + "migration_python_venv_rebuild_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_python_venv_rebuild_failed": "Impossible de reconstruire l'environnement virtuel Python pour {app}. L'application risque de ne pas fonctionner tant que ce problème n'est pas résolu. Vous devez résoudre le problème en forçant la mise à jour de cette application à l'aide de 'yunohost app upgrade --force {app}'.", + "migration_python_venv_rebuild_in_progress": "Nous essayons maintenant de reconstruire le virtualenv Python pour `{app}`", + "migration_0031_terms_of_services": "Cette migration est un message purement informatif sur le fait que le projet YunoHost publie désormais des conditions d'utilisation liées aux services techniques et communautaires.", + "migration_0036_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus utiles …", + "migration_0036_delayed_api_restart": "L'API YunoHost redémarrera automatiquement dans 15 secondes. Elle pourrait être indisponible pendant quelques secondes, après quoi vous devrez vous reconnecter.", + "migration_0036_general_warning": "Enfin, veuillez noter que cette migration est **une opération délicate**. L'équipe YunoHost a fait de son mieux pour la vérifier et la tester, mais la migration pourrait tout de même endommager certaines parties du système ou de ses applications.\n\nIl est donc recommandé de :\n - **Effectuer des sauvegardes** de toutes les données ou applications critiques. Plus d'informations sur https://doc.yunohost.org/admin/backups/ ;\n - **Faire preuve de patience** après le lancement de la migration : en fonction de votre connexion Internet et de votre matériel, la mise à niveau complète peut prendre jusqu'à une heure ;\n - **Contacter la communauté** sur le forum si vous avez besoin d'aide pour résoudre des problèmes.", + "migration_0036_main_upgrade": "Démarrage de la mise à niveau principale …", + "migration_0036_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés après la mise à niveau :", + "migration_0036_not_bullseye": "La distribution Debian actuelle n'est pas Bookworm ! Si vous avez déjà effectué la migration Bookworm -> Trixie, cette erreur indique que la procédure de migration n'a pas été entièrement réussie (sinon, YunoHost l'aurait signalée comme terminée). Il est recommandé d'examiner ce qui s'est passé avec l'équipe du support, qui aura besoin du journal **complet** de la migration (log), disponible dans Outils > Journaux dans l'interface d'administration web.", + "migration_0036_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devez disposer d'au moins 1 Go d'espace libre pour effectuer cette migration.", + "migration_0036_patch_yunohost_dpkg": "Application d'un correctif à la base de données dpkg pour contourner les problèmes de conflit …", + "migration_0036_patching_sources_list": "Patch des fichiers sources.lists …", + "migration_0036_problematic_apps_warning": "Veuillez noter que les applications installées suivantes, potentiellement problématiques, ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost ou qu'elles ne soient pas signalées comme 'fonctionnelles'. Par conséquent, leur fonctionnement après la mise à niveau ne peut être garanti :", + "migration_0036_start": "Début de la migration vers Trixie …", + "migration_0036_still_on_bookworm_after_main_upgrade": "Une chose s'est mal passée pendant la mise à niveau principale, le système semble toujours être sur Debian Bookworm.", + "migration_0036_system_not_fully_up_to_date": "Votre système n'est pas entièrement à jour. Veuillez effectuer une mise à jour standard avant de lancer la migration vers Trixie.", + "migration_0036_yunohost_upgrade": "Lancement de la mise à niveau du système/cœur de YunoHost …", + "migration_description_0027_migrate_to_bookworm": "Mettre à jour le système vers Debian Bookworm et YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Suppression des anciennes autorisations XMPP. Metronome est désormais une application", + "migration_description_0029_postgresql_13_to_15": "Migrer les bases de données de PostgreSQL 13 vers 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Réparer l'application Python après la migration Bookworm", + "migration_description_0031_terms_of_services": "Conditions d'utilisation", + "migration_description_0032_firewall_config": "Migration du fichier de configuration du pare-feu interne", + "migration_description_0033_rework_permission_infos": "Retravailler la façon dont les autorisations des applications sont enregistrées", + "migration_description_0034_fix_missing_admins_aliases": "Correction des alias de messagerie manquants pour le groupe admins", + "migration_description_0035_fix_apps_nodejs_version": "Correction des versions nodejs dans les configurations systemd de l'application", + "migration_description_0036_migrate_to_trixie": "Mise à niveau du système vers Debian Trixie et YunoHost 13", + "migration_ldap_backup_before_migration": "Création d'une sauvegarde de la base de données LDAP et des paramètres des applications avant la migration proprement dite.", + "migration_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_migration_failed_trying_to_rollback": "Impossible de migrer… tentative de restauration du système.", + "migration_ldap_rollback_success": "Système rétabli dans son état initial.", + "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_exclusive_options": "'auto', '--skip' et '--force-rerun' sont des options mutuellement exclusives.", + "migrations_failed_to_load_migration": "Impossible de charger la migration {id} : {error}", + "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", + "migrations_loading_migration": "Chargement de la migration {id}…", + "migrations_migration_has_failed": "La migration {id} a échoué, abandon. Erreur : {exception}", + "migrations_must_provide_explicit_targets": "Vous devez fournir des cibles explicites lorsque vous utilisez '--skip' ou '--force-rerun'", + "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_no_migrations_to_run": "Aucune migration à lancer", + "migrations_no_such_migration": "Il n'y a pas de migration appelée '{id}'", + "migrations_not_pending_cant_skip": "Ces migrations ne sont pas en attente et ne peuvent donc pas être ignorées : {ids}", + "migrations_pending_cant_rerun": "Ces migrations étant toujours en attente, vous ne pouvez pas les exécuter à nouveau : {ids}", + "migrations_running_forward": "Exécution de la migration {id}…", + "migrations_skip_migration": "Ignorer et passer la migration {id}…", + "migrations_success_forward": "Migration {id} terminée", + "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`.", + "nftables_unavailable": "Vous ne pouvez pas jouer avec nftables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge", + "noninteractive_task": "Tâche non interactive", + "not_enough_disk_space": "L'espace disque est insuffisant sur '{path}'", + "operation_interrupted": "L'opération a-t-elle été interrompue manuellement ?", + "other_available_options": "… et {n} autres options disponibles non affichées", + "password_confirmation_not_the_same": "Le mot de passe et la confirmation de ce dernier ne correspondent pas", + "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_long": "Veuillez choisir un mot de passe de moins de 127 caractères", + "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", + "pattern_backup_archive_name": "Doit être un nom de fichier valide avec un maximum de 30 caractères, et composé de caractères alphanumériques et -_. uniquement", + "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.fr)", + "pattern_email": "Il faut une adresse électronique valide, sans le symbole '+' (par exemple johndoe@exemple.com)", + "pattern_email_forward": "L'adresse électronique doit être valide, le symbole '+' étant accepté (par exemple : johndoe+yunohost@exemple.com)", + "pattern_fullname": "Doit être un nom complet 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_password_app": "Désolé, les mots de passe ne peuvent pas contenir les caractères suivants : {forbidden_chars}", + "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": "Ne doit contenir que des caractères alphanumériques en minuscules, des points, des tirets (aussi appelé tiret du 6) et des traits de soulignement (aussi appelé tiret du 8 ou underscore)", + "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é", + "permission_cant_add_to_all_users": "L'autorisation {permission} ne peut pas être ajoutée à tous les comptes.", + "permission_created": "Permission '{permission}' créée", + "permission_creation_failed": "Impossible de créer l'autorisation '{permission}' : {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é.", + "permission_deleted": "Permission '{permission}' supprimée", + "permission_deletion_failed": "Impossible de supprimer la permission '{permission}' : {error}", + "permission_not_found": "Permission '{permission}' introuvable", + "permission_protected": "L'autorisation {permission} est protégée. Vous ne pouvez pas ajouter ou supprimer le groupe visiteurs à/de cette autorisation.", + "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.", + "permission_update_failed": "Impossible de mettre à jour la permission '{permission}' : {error}", + "permission_updated": "Permission '{permission}' mise à jour", + "port_already_closed": "Le port {port} est déjà fermé", + "port_already_opened": "Le port {port} est déjà ouvert", + "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", + "pydantic_type_error": "Type non valide.", + "pydantic_type_error_none_not_allowed": "La valeur est requise.", + "pydantic_type_error_str": "Type non valide, chaîne attendue.", + "pydantic_value_error_color": "Ce n'est pas une couleur valide, la valeur doit être une couleur nommée ou hexadécimale.", + "pydantic_value_error_const": "Valeur inattendue ; choisissez entre {permitted}", + "pydantic_value_error_date": "Format de date non valide", + "pydantic_value_error_email": "La valeur n'est pas une adresse email valide", + "pydantic_value_error_number_not_ge": "La valeur doit être supérieure ou égale à {limit_value}.", + "pydantic_value_error_number_not_le": "La valeur doit être inférieure ou égale à {limit_value}.", + "pydantic_value_error_str_regex": "Chaîne non valide ; la valeur ne respecte pas le modèle '{pattern}'", + "pydantic_value_error_time": "Format d'heure invalide", + "pydantic_value_error_url_extra": "URL non valide, caractères supplémentaires trouvés après une URL valide : '{extra}'", + "pydantic_value_error_url_host": "URL hôte invalide", + "pydantic_value_error_url_port": "Port URL non valide, le port ne peut pas dépasser 65535", + "pydantic_value_error_url_scheme": "Schéma d'URL non invalide ou manquant", + "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_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}'", + "regenconf_file_kept_back": "Le fichier de configuration '{conf}' devait être supprimé par 'regen-conf' (catégorie {category}) mais a été conservé.", + "regenconf_file_manually_modified": "Le fichier de configuration '{conf}' a été modifié manuellement et ne sera pas mis à jour", + "regenconf_file_manually_removed": "Le fichier de configuration '{conf}' a été supprimé manuellement et ne sera pas créé", + "regenconf_file_remove_failed": "Impossible de supprimer le fichier de configuration '{conf}'", + "regenconf_file_removed": "Le fichier de configuration '{conf}' a été supprimé", + "regenconf_file_updated": "Le fichier de configuration '{conf}' a été mis à jour", + "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.", + "regenconf_now_managed_by_yunohost": "Le fichier de configuration '{conf}' est maintenant géré par YunoHost (catégorie {category}).", + "regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}'…", + "regenconf_up_to_date": "La configuration est déjà à jour pour la catégorie '{category}'", + "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}'", + "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_with_only_domain": "Vous ne pouvez pas utiliser une expression régulière pour le domaine, uniquement pour le chemin", + "registrar_infos": "Infos du Registrar (fournisseur du nom de domaine)", + "restore_already_installed_app": "Une application est déjà installée avec l'identifiant '{app}'", + "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", + "restore_backup_too_old": "Cette sauvegarde ne peut pas être restaurée car elle provient d'une version de YunoHost trop ancienne.", + "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_extracting": "Extraction des fichiers nécessaires depuis l'archive…", + "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_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_nothings_done": "Rien n'a été restauré", + "restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire", + "restore_running_app_script": "Exécution du script de restauration de l'application '{app}'…", + "restore_running_hooks": "Exécution des scripts de restauration…", + "restore_system_part_failed": "Impossible de restaurer la partie '{part}' du système", + "root_password_changed": "Le mot de passe de root a été changé", + "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 !", + "server_reboot": "Le serveur va redémarrer", + "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers}]", + "server_shutdown": "Le serveur va s'éteindre", + "server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers}]", + "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_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)", + "service_description_fail2ban": "Protège contre les attaques brute-force et autres types d'attaques venant d'Internet", + "service_description_mysql": "Stocke les données des applications (bases de données SQL)", + "service_description_nftables": "Gère l'ouverture et la fermeture des ports de connexion aux services", + "service_description_nginx": "Sert ou permet l'accès à tous les sites web hébergés sur votre serveur", + "service_description_opendkim": "Signe les emails sortants à l'aide de DKIM afin qu'ils soient moins susceptibles d'être signalés comme spam", + "service_description_postfix": "Utilisé pour envoyer et recevoir des emails", + "service_description_postgresql": "Stocke les données d'application (base de données SQL)", + "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_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-portal-api": "Gère les interactions entre les différentes interfaces Web du portail et le système", + "service_description_yunomdns": "Vous permet d'atteindre votre serveur en utilisant 'yunohost.local' sur votre réseau local", + "service_disable_failed": "Impossible de ne pas lancer le service '{service}' au démarrage.", + "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.", + "service_enabled": "Le service '{service}' sera désormais lancé automatiquement au démarrage du système.", + "service_not_reloading_because_conf_broken": "Le service '{name}' n'a pas été rechargé/redémarré car sa configuration est cassée : {errors}", + "service_reload_failed": "Impossible de recharger le service '{service}'", + "service_reload_or_restart_failed": "Impossible de recharger ou de redémarrer le service '{service}'", + "service_reloaded": "Le service '{service}' a été rechargé", + "service_reloaded_or_restarted": "Le service '{service}' a été rechargé ou redémarré", + "service_remove_failed": "Impossible de supprimer le service '{service}'", + "service_removed": "Le service '{service}' a été supprimé", + "service_restart_failed": "Impossible de redémarrer le service '{service}'", + "service_restarted": "Le service '{service}' a été redémarré", + "service_start_failed": "Impossible de démarrer le service '{service}'", + "service_started": "Le service '{service}' a été démarré", + "service_stop_failed": "Impossible d'arrêter le service '{service}'", + "service_stopped": "Le service '{service}' a été arrêté", + "service_unknown": "Le service '{service}' est inconnu", + "session_expired": "Session expirée", + "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}'", + "ssowat_conf_generated": "La configuration de SSOwat a été regénérée", + "system_upgraded": "Système mis à jour", + "system_username_exists": "Ce nom de compte existe déjà dans les comptes système", + "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`.", + "tools_upgrade": "Mise à jour des paquets du système", + "tools_upgrade_failed": "Impossible de mettre à jour les paquets : {packages_list}", + "tos_dyndns_acknowledgement": "Vous avez choisi d'enregistrer un domaine DynDNS qui est un service fourni par le projet YunoHost. Considérant que les noms de domaine sont un aspect important des services numériques à long terme, nous vous rappelons de lire attentivement les Conditions Générales d'Utilisation, en particulier la section concernant ces noms de domaine gratuits : .", + "tos_postinstall_acknowledgement": "Le projet YunoHost est une équipe de bénévoles qui ont fait cause commune pour créer un système d'exploitation libre pour serveurs, appelé YunoHost. Le logiciel YunoHost est publié sous la licence AGPLv3 (). En relation avec ce logiciel, le projet administre et met à disposition plusieurs services techniques et communautaires à des fins diverses. En utilisant ces services, vous acceptez d'être lié par les conditions d'utilisation suivantes : .", + "unable_authenticate": "Échec de l'authentification de la session", + "unbackup_app": "'{app}' ne sera pas sauvegardée", + "unexpected_error": "Une erreur inattendue est survenue : {error}", + "unknown_error_reading_file": "Erreur inconnue en essayant de lire le fichier {file} (raison :{error})", + "unknown_group": "Le groupe '{group}' est inconnu", + "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.", + "unknown_user": "Le compte '{user}' est inconnu", + "unlimit": "Pas de quota", + "unrestore_app": "'{app}' ne sera pas restaurée", + "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}", + "updating_apt_cache": "Récupération des mises à jour disponibles pour les paquets du système…", + "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_already_exists": "Le compte '{user}' existe déjà", + "user_cannot_delete_last_admin": "Le compte '{user}' est le seul 'admins' et ne sera donc pas supprimé.", + "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_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}", + "user_import_cannot_edit_or_delete_admins": "Impossible de modifier ou de supprimer '{user}' via la fonction d'importation car il s'agit d'un compte admin", + "user_import_failed": "L'opération d'importation des comptes a totalement échoué", + "user_import_missing_columns": "Les colonnes suivantes sont manquantes : {columns}", + "user_import_nothing_to_do": "Aucun compte n'a besoin d'être importé", + "user_import_partial_failed": "L'opération d'importation des comptes a partiellement échoué", + "user_import_success": "Comptes importés avec succès", + "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é", + "visitors": "Visiteurs", + "yunohost_already_installed": "YunoHost est déjà installé", + "yunohost_api": "API YunoHost", + "yunohost_configured": "YunoHost est maintenant configuré", + "yunohost_installing": "L'installation de YunoHost est en cours…", + "yunohost_not_installed": "YunoHost n'est pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'", + "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://doc.yunohost.org/admin.", + "migration_0036_apt_lists_file_still_exists": "Le fichier hérité '{file}' existe toujours alors qu'il ne devrait plus être présent. Il sera renommé '{file}.legacy_bookworm'.", + "app_db_prompt_no_app_database": "Cette application ne semble pas avoir de base de données déclarée dans son fichier manifest", + "app_db_prompt_type_not_supported": "La commande ne prend pas en charge ce type de base de données : {type}", + "migration_0037_upgrade_dkim_keys_disclaimer": "Cette migration vise à mettre à niveau les anciennes clés DKIM de 1 024 bits vers des clés de 2 048 bits afin d'améliorer la délivrabilité des e-mails. Les domaines suivants sont concernés et certains d'entre eux pourraient nécessiter une mise à jour des clés DKIM dans votre zone DNS immédiatement après la migration : {domains}\nIMPORTANT : Afin d'éviter tout risque de mise sur liste noire et de problème de délivrabilité, cette migration doit être effectuée à un moment où votre serveur n'envoie pas d'e-mails. Par mesure de sécurité, vous pouvez arrêter le service Postfix avant la migration et le redémarrer 1 heure après la mise à jour de vos clés DKIM dans vos zones DNS.", + "migration_0037_upgrade_dkim_keys_pending_mails": "{pending_mails} messages se trouvent dans votre file d'attente de courrier. Afin d'éviter tout risque de mise sur liste noire et tout problème de délivrabilité, cette migration doit être effectuée à un moment où votre serveur n'envoie pas d'e-mails. Vous pouvez arrêter temporairement Postfix et utiliser postsuper -d ALL pour vider votre file d'attente de courrier. Pour voir quels e-mails se trouvent dans la file d'attente, utilisez postqueue -p", + "migration_0037_upgrade_dkim_keys_failed": "Impossible de générer une nouvelle clé de 2048 bits pour {domains}.", + "migration_0037_upgrade_dkim_keys_manual_action": "Pour finaliser le processus de migration, vous devez mettre à jour les clés publiques DKIM dans les zones DNS suivantes : {domains}\nLancez le diagnostic ou utilisez l'onglet DNS dans 'Webadmin > Domaines', ou encore via la commande yunohost domain dns suggest DOMAINE. Si vous avez décidé d'arrêter Postfix, n'oubliez pas de le redémarrer une heure après avoir modifié votre dernière zone DNS.", + "migration_description_0037_upgrade_dkim_keys": "Mettez à jour vos clés DKIM pour améliorer la délivrabilité de vos e-mails" +} diff --git a/locales/gl.json b/locales/gl.json new file mode 100644 index 0000000..2568a8e --- /dev/null +++ b/locales/gl.json @@ -0,0 +1,921 @@ +{ + "aborting": "Abortando.", + "action_invalid": "Acción non válida '{action}'", + "additional_urls_already_added": "URL adicional '{url}' xa fora engadida ás URL adicionais para o permiso '{permission}'", + "additional_urls_already_removed": "URL adicional '{url}' xa foi eliminada das URL adicionais para o permiso '{permission}'", + "admin_password": "Contrasinal de administración", + "admins": "Admins", + "all_users": "Todas as contas YunoHost", + "already_up_to_date": "Nada que facer. Todo está ao día.", + "app_action_broke_system": "Esta acción semella que estragou estos servizos importantes: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Estos servizos requeridos deberían estar en execución para realizar esta acción: {services}. Intenta reinicialos para continuar (e tamén intenta saber por que están apagados).", + "app_action_failed": "Fallou a execución da acción {action} da app {app}", + "app_already_installed": "{app} xa está instalada", + "app_already_installed_cant_change_url": "Esta app xa está instalada. O URL non pode cambiarse só con esta acción. Miran en `app changeurl` se está dispoñible.", + "app_arch_not_supported": "Esta app só pode ser instalada e arquitecturas {required} pero a arquitectura do teu servidor é {current}", + "app_argument_choice_invalid": "Elixe un valor válido para o argumento '{name}': '{value}' non está entre as opcións dispoñibles ({choices})", + "app_argument_invalid": "Elixe un valor válido para o argumento '{name}': {error}", + "app_change_url_failed": "Non se cambiou o url para {app}: {error}", + "app_change_url_identical_domains": "O antigo e o novo dominio/url_path son idénticos ('{domain}{path}'), nada que facer.", + "app_change_url_no_script": "A app '{app_name}' non soporta o cambio de URL. Pode que debas actualizala.", + "app_change_url_require_full_domain": "{app} non se pode mover a este novo URL porque require un dominio completo propio (ex. con ruta = /)", + "app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url", + "app_change_url_success": "A URL de {app} agora é {domain}{path}", + "app_config__core_name": "Teselas e permisos", + "app_config_permission_allowed": "Grupos/contas con acceso", + "app_config_permission_allowed_warn_protected": "Nota: este permiso está 'proxtexido' e por tanto o grupo 'visitantes' non pode engadirse/retirarse dos grupos con autorización.", + "app_config_permission_description": "Descrición", + "app_config_permission_description_help": "Isto só é útil se estás a usar o modo de portal 'descritivo'", + "app_config_permission_extraperm_section_name": "Permiso '{perm}'", + "app_config_permission_label": "Etiqueta", + "app_config_permission_location": "Corresponde a [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Logo personal para usar", + "app_config_permission_logo_help": "Só se permite PNG", + "app_config_permission_show_tile": "Mostrar tesela no portal", + "app_config_unable_to_apply": "Fallou a aplicación dos valores de configuración.", + "app_config_unable_to_read": "Fallou a lectura dos valores de configuración.", + "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}", + "app_extraction_failed": "Non se puideron extraer os ficheiros de instalación", + "app_failed_to_download_asset": "Fallou a descarga do recurso '{source_id}' ({url}) para {app}: {out}", + "app_full_domain_unavailable": "Lamentámolo, esta app ten que ser instalada nun dominio propio, pero xa tes outras apps instaladas no dominio '{domain}'. Podes usar un subdominio dedicado para esta app.", + "app_id_invalid": "ID da app non válido", + "app_install_failed": "Non se pode instalar {app}: {error}", + "app_install_files_invalid": "Non se poden instalar estos ficheiros", + "app_install_script_failed": "Houbo un fallo interno do script de instalación da app", + "app_location_unavailable": "Este URL ou ben non está dispoñible ou entra en conflito cunha app(s) xa instalada:\n{apps}", + "app_make_default_location_already_used": "Non se puido establecer a '{app}' como app por defecto no dominio, '{domain}' xa está utilizado por '{other_app}'", + "app_manifest_install_ask_admin": "Elixe unha usuaria administradora para esta app", + "app_manifest_install_ask_domain": "Elixe o dominio onde queres instalar esta 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)", + "app_manifest_install_ask_is_public": "Debería esta app estar exposta ante visitantes anónimas?", + "app_manifest_install_ask_password": "Elixe un contrasinal de administración para esta app", + "app_manifest_install_ask_path": "Elixe a ruta URL (após o dominio) onde será instalada esta app", + "app_not_correctly_installed": "{app} semella que non está instalada correctamente", + "app_not_enough_disk": "Esta app precisa {required} de espazo libre.", + "app_not_enough_ram": "Esta app require {required} de RAM para instalar/actualizar pero só hai {current} dispoñible.", + "app_not_installed": "Non se puido atopar {app} na lista de apps instaladas: {all_apps}", + "app_not_properly_removed": "{app} non se eliminou de xeito correcto", + "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_remove_after_failed_install": "Eliminando a app debido ao fallo na instalación…", + "app_removed": "{app} desinstalada", + "app_requirements_checking": "Comprobando os requisitos de {app}…", + "app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}", + "app_restore_failed": "Non se puido restablecer {app}: {error}", + "app_restore_script_failed": "Houbo un erro interno do script de restablecemento da app", + "app_sources_fetch_failed": "Non se puideron obter os ficheiros fonte, é o URL correcto?", + "app_start_backup": "Xuntando os ficheiros para a copia de apoio de {app}…", + "app_start_install": "A instalar {app}…", + "app_start_remove": "A eliminar {app}…", + "app_start_restore": "A restablecer {app}…", + "app_unknown": "App descoñecida", + "app_unsupported_remote_type": "Tipo remoto non soportado para a app", + "app_upgrade_app_name": "A actualizar {app}…", + "app_upgrade_bad_quality": "Esta aplicación está actualmente marcada como estragada no catálogo de YunoHost. Pode ser un problema temporal mentras intentamos arranxar a incidencia. Mentras tanto temos desactivada a actualización de esta aplicación.", + "app_upgrade_broke_the_system": "Aparentmente a actualización de {app} foi correcta, pero deixou o sistema nun estado defectuoso e isto considérase un fallo.", + "app_upgrade_cli_bad_quality": "Omitindo as actualizacións de {app} porque actualmente está marcada como estragada no catálogo de YunoHost.", + "app_upgrade_cli_up_to_date": "{app} xa está ao día {current_version}", + "app_upgrade_cli_url_required": "{app} xa non está no catálogo (nunca máis?) e por tanto non se pode actualizar automáticamente. Deberías usar `yunohost app upgrade {app}`e engadir a opción `-u` co URL do repositorio.", + "app_upgrade_cli_will_force_upgrade": "Vaise forzar a actualización de {app} a {current_version}", + "app_upgrade_cli_will_upgrade": "Vaise actualizar {app} desde {current_version} a {new_version}", + "app_upgrade_continuing_with_other_apps": "Fallou a actualización de {app}, pero continúa actualización das outras aplicacións (xa que se usou `--continue-on-failure`)", + "app_upgrade_fail_requirements": "Hai unha nova versión dispoñible para esta aplicación ({new_version}), pero faltan algúns requerimentos:\n{failed_requirements}", + "app_upgrade_failed": "Fallou a actualización {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "Fallou a actualización de '{app}', e estragou o sistema.", + "app_upgrade_script_failed": "Houbo un fallo interno no script de actualización da app", + "app_upgrade_several_apps": "Vanse actualizar as seguintes apps: {apps}", + "app_upgrade_some_app_failed": "Algunhas apps non se puideron actualizar", + "app_upgrade_specific_channel_msg": "Ten en conta que estás usando `{channel}` como orixe das actualizacións. Mira [aquí]({pr_url}) a conversa en curso.", + "app_upgrade_up_to_date": "Forzar a actualización da aplicación (á mesma versión) a veces pode ser útil para reconstruir a aplicación e a súa configuración.", + "app_upgrade_upgradable": "A aplicación pódese actualizar desde a versión {current_version} a {new_version}", + "app_upgrade_url_required": "Esta aplicación non está no catálogo (nunca máis?), deberías ocuparte de actualizala manualmente.
Desde a liña de ordes, podes usar `yunohost app upgrade ` e engadirlle o URL do repositorio a utilizar usando a opción `-u`.", + "app_upgraded": "{app} actualizadas", + "app_yunohost_version_not_supported": "Esta app require YunoHost >= {required} pero a versión actual instalada é {current}.", + "apps_already_up_to_date": "Xa tes todas as apps ao día", + "apps_catalog_failed_to_download": "Non se puido descargar o catálogo de apps {apps_catalog}: {error}", + "apps_catalog_obsolete_cache": "A caché do catálogo de apps está baleiro ou obsoleto.", + "apps_catalog_update_success": "O catálogo de aplicacións foi actualizado!", + "apps_catalog_updating": "Actualizando o catálogo de aplicacións…", + "apps_confirm_partial_upgrade": "Algunha das aplicacións indicadas xa non se poden actualizar. Aínda así, queres actualizar o resto de aplicacións?", + "apps_no_target_can_be_upgraded": "Non se pode actualizar ningunha app", + "apps_upgrade_cancelled": "Aínda quedan actualizacións que realizar para algunhas aplicacións pero detívose o proceso (usa `--continue-on-failure`para continuar igualmente): {apps}", + "ask_admin_fullname": "Nome completo de Admin", + "ask_admin_username": "Identificador da Admin", + "ask_dyndns_recovery_password": "Contrasinal de recuperación DynDNS", + "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_explain_during_unsubscribe": "Escribe o contrasinal de recuperación para este dominio DynDNS.", + "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.", + "ask_fullname": "Nome completo", + "ask_main_domain": "Dominio principal", + "ask_new_admin_password": "Novo contrasinal de administración", + "ask_new_domain": "Novo dominio", + "ask_new_path": "Nova ruta", + "ask_password": "Contrasinal", + "ask_user_domain": "Dominio a utilizar como enderezo de email", + "automatic_task": "Tarefa automatizada", + "backup_abstract_method": "Este método de copia de apoio aínda non foi implementado", + "backup_actually_backuping": "Creando o arquivo de copia cos ficheiros recollidos…", + "backup_app_script_failed": "Fallou a recollida de ficheiros para a copia de apoio de {app}.", + "backup_applying_method_copy": "Gardando os ficheiros na copia…", + "backup_applying_method_custom": "A requerir o método de copia de apoio personalizado '{method}'…", + "backup_applying_method_tar": "Creando o arquivo TAR da copia…", + "backup_archive_app_not_found": "Non se atopa {app} no arquivo da copia", + "backup_archive_broken_link": "Non se puido acceder ao arquivo da copia (ligazón rota a {path})", + "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_corrupted": "Semella que o arquivo de copia '{archive}' está estragado : {error}", + "backup_archive_name_exists": "Xa existe un ficheiro de copia con nome '{name}'.", + "backup_archive_name_unknown": "Arquivo local de copia de apoio descoñecido con nome '{name}'", + "backup_archive_open_failed": "Non se puido abrir o arquivo de copia de apoio", + "backup_archive_system_part_not_available": "A parte do sistema '{part}' non está dispoñible nesta copia", + "backup_archive_writing_error": "Non se puideron engadir os ficheiros '{source}' (chamados no arquivo '{dest}' para ser copiados dentro do arquivo comprimido '{archive}'", + "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_before_upgrade_deleted_because_replaced_by_newer_backup": "Eliminouse a copia {name} porque foi substituída pola nova copia {newname}", + "backup_cant_mount_uncompress_archive": "Non se puido montar o arquivo sen comprimir porque está protexido contra escritura", + "backup_cleaning_failed": "Non se puido baleirar o cartafol temporal para a copia", + "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", + "backup_couldnt_bind": "Non se puido ligar {src} a {dest}.", + "backup_create_size_estimation": "O arquivo vai conter arredor de {size} de datos.", + "backup_created": "Copia creada: {name}", + "backup_creation_failed": "Non se puido crear o arquivo de copia de apoio", + "backup_csv_addition_failed": "Non se engadiron os ficheiros a copiar ao ficheiro CSV", + "backup_csv_creation_failed": "Non se creou o ficheiro CSV necesario para restablecer a copia", + "backup_custom_backup_error": "O método personalizado da copia non superou o paso 'backup'", + "backup_custom_mount_error": "O método personalizado de copia non superou o paso 'mount'", + "backup_delete_error": "Non se eliminou '{path}'", + "backup_deleted": "Copia eliminada: {name}", + "backup_hook_unknown": "O gancho da copia '{hook}' é descoñecido", + "backup_method_copy_finished": "Rematou o copiado dos ficheiros", + "backup_method_custom_finished": "O método de copia personalizado '{method}' rematou", + "backup_method_tar_finished": "Creouse o arquivo de copia TAR", + "backup_mount_archive_for_restore": "Preparando o arquivo a restablecer…", + "backup_no_file_collected": "Fallou a recollida de ficheiros para a copia de apoio", + "backup_no_uncompress_archive_dir": "Non hai tal directorio do arquivo descomprimido", + "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_output_directory_not_empty": "Debes elixir un directorio de saída baleiro", + "backup_output_directory_required": "Debes proporcionar un directorio de saída para a copia", + "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_running_hooks": "Executando os ganchos da copia…", + "backup_system_part_failed": "Non se puido facer copia da parte do sistema '{part}'", + "backup_unable_to_organize_files": "Non se puido usar o método rápido para organizar ficheiros no arquivo", + "backup_with_no_backup_script_for_app": "A app '{app}' non ten script para a copia. Ignorada.", + "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.", + "cannot_open_file": "Non se puido abrir o ficheiro {file} (razón: {error})", + "cannot_write_file": "Non se puido escribir o ficheiro {file} (razón: {error})", + "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_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_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_replace_valid_cert": "Estás intentando sobrescribir un certificado correcto e en bo estado para o dominio {domain}! (Usa --force para obviar)", + "certmanager_cannot_read_cert": "Algo fallou ao intentar abrir o certificado actual para o dominio {domain} (ficheiro: {file}), razón: {reason}", + "certmanager_cert_install_failed": "Fallou a instalación do certificado Let's Encrypt para {domains}", + "certmanager_cert_install_failed_selfsigned": "Fallou a instalación do certificado auto-asinado para {domains}", + "certmanager_cert_install_success": "O certificado Let's Encrypt está instalado para o dominio '{domain}'", + "certmanager_cert_install_success_selfsigned": "O certificado auto-asinado está instalado para o dominio '{domain}'", + "certmanager_cert_renew_failed": "Fallou a renovación do certificado Let's Encrypt para {domains}", + "certmanager_cert_renew_success": "Certificado Let's Encrypt renovado para o dominio '{domain}'", + "certmanager_cert_signing_failed": "Non se puido asinar o novo certificado", + "certmanager_certificate_fetching_or_enabling_failed": "Fallou o intento de usar o novo certificado para '{domain}'…", + "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_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.)", + "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_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_hit_rate_limit": "Recentemente crearonse demasiados certificados para este mesmo grupo de dominios {domain}. Inténtao máis tarde. Podes ler https://letsencrypt.org/docs/rate-limits/ para máis info", + "certmanager_no_cert_file": "Non se puido ler o ficheiro do certificado para o dominio {domain} (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})", + "certmanager_unable_to_parse_self_CA_name": "Non se puido obter o nome da autoridade do auto-asinado (ficheiro: {file})", + "config_action_disabled": "Non se executou a accción '{action}' porque está desactivada, comproba os seus requerimentos. Axuda: {help}", + "config_action_failed": "Fallou a execución da acción '{action}': {error}", + "config_apply_failed": "Fallou a aplicación da nova configuración: {error}", + "config_cant_set_value_on_section": "Non podes establecer un valor único na sección completa de configuración.", + "config_forbidden_keyword": "O palabra chave '{keyword}' está reservada, non podes crear ou usar un panel de configuración cunha pregunta con este id.", + "config_forbidden_readonly_type": "O tipo '{type}' non pode establecerse como só lectura, usa outro tipo para mostrar este valor (id relevante: '{id}').", + "config_no_panel": "Non se atopa panel configurado.", + "config_unknown_filter_key": "A chave do filtro '{filter_key}' non é correcta.", + "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_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}'", + "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}] ", + "confirm_app_insufficient_ram": "Esta app precisa máis RAM para instalar/actualizar da dispoñible actualmente. Incluso se a app funcionase, o seu proceso de instalación/actualización require gran cantidade de RAM e o teu servidor podería colgarse e fallar. Se queres asumir o risco, escribe '{answers}'", + "confirm_notifications_read": "AVISO: Deberías comprobar as notificacións da app antes de continuar, poderías ter información importante que revisar. [{answers}]", + "confirm_tos_acknowledgement": "Lin e comprendo os Termos dos Servizos [{answers}]", + "corrupted_json": "Lectura corrupta dos datos JSON de {ressource} (razón: {error})", + "corrupted_toml": "Lectura corrupta de datos TOML de {ressource} (razón: {error})", + "corrupted_yaml": "Lectura corrupta dos datos YAML de {ressource} (razón: {error})", + "danger": "Perigo:", + "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.", + "diagnosis_apps_broken": "Actualmente esta aplicación está marcada como estragada no catálogo de aplicacións de YunoHost. Podería tratarse dun problema temporal mentras as mantedoras intentan arraxala. Entanto así a actualización da app está desactivada.", + "diagnosis_apps_deprecated_practices": "A versión instalada desta app aínda utiliza algunha das antigas prácticas de empaquetado xa abandonadas. Deberías considerar actualizala.", + "diagnosis_apps_issue": "Atopouse un problema na app {app}", + "diagnosis_apps_not_in_app_catalog": "Esta aplicación non está no catálgo de aplicacións de YunoHost. Se estivo no pasado e foi eliminada, deberías considerar desinstalala porque non recibirá actualizacións, e podería comprometer a integridade e seguridade do teu sistema.", + "diagnosis_apps_outdated_packaging_format": "Esta aplicación usa un sistema de empaquetado que está en desuso e deixará de estar mantida por YunoHost. Deberías considerar actualizala.", + "diagnosis_apps_outdated_ynh_requirement": "A versión instalada desta app só require yunohost >= 2.x , 3.x ou 4.x, isto normalmente indica que non está ao día coas prácticas recomendadas de empaquetado e asistentes. Deberías considerar actualizala.", + "diagnosis_apps_security_issue_error": "A aplicación {app} actualmente está na versión '{current_version}', que é vulnerable a unha incidencia IMPORTANTE de seguridade: {title}. É recomendable actualizala O MÁIS AXIÑA POSIBLE á versión '{fixed_in_version}'. Máis info: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "A aplicación {app} actualmente está na versión '{current_version}', que é vulnerable a unha incidencia moderada de seguridade: {title}. É recomendable actualizala á versión '{fixed_in_version}'. Máis info: {more_infos_list}", + "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_hardware": "A arquitectura do hardware do servidor é {virt} {arch}", + "diagnosis_basesystem_hardware_model": "O modelo de servidor é {model}", + "diagnosis_basesystem_host": "O servidor está a executar Debian {debian_version}", + "diagnosis_basesystem_kernel": "O servidor está a executar o kernel Linux {kernel_version}", + "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_cache_still_valid": "(A caché aínda é válida para o diagnóstico {category}. Non o repetiremos polo de agora!)", + "diagnosis_cant_run_because_of_dep": "Non é posible facer o diganóstico para {category} cando aínda hai importantes problemas con {dep}.", + "diagnosis_description_apps": "Aplicacións", + "diagnosis_description_basesystem": "Sistema base", + "diagnosis_description_dnsrecords": "Rexistros DNS", + "diagnosis_description_ip": "Conectividade a internet", + "diagnosis_description_mail": "Correo electrónico", + "diagnosis_description_ports": "Exposición de portos", + "diagnosis_description_regenconf": "Configuracións do sistema", + "diagnosis_description_services": "Comprobación do estado dos servizos", + "diagnosis_description_systemresources": "Recursos do sistema", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "A almacenaxe {mountpoint} (no dispositivo {device}) só lle queda {free} ({free_percent}%) de espazo libre (de {total}). Ten coidado.", + "diagnosis_diskusage_ok": "A almacenaxe {mountpoint} (no dispositivo {device}) aínda ten {free} ({free_percent}%) de espazo restante (de {total})!", + "diagnosis_diskusage_verylow": "A almacenaxe {mountpoint} (no dispositivo {device}) só lle queda {free} ({free_percent}%) de espazo libre (de {total}). Deberías considerar liberar algún espazo!", + "diagnosis_display_tip": "Para ver os problemas atopados, podes ir á sección de Diagnóstico na administración web, ou executa 'yunohost diagnosis show --issues --human-readable' desde a liña de comandos.", + "diagnosis_dns_bad_conf": "Faltan algúns rexistros DNS ou están mal configurados para o dominio {domain} (categoría {category})", + "diagnosis_dns_discrepancy": "O seguinte rexistro DNS non segue a configuración recomendada:
Tipo: {type}
Nome: {name}
Valor actual: {current}
Valor agardado: {content}", + "diagnosis_dns_good_conf": "Os rexistros DNS están correctamente configurados para o dominio {domain} (categoría {category})", + "diagnosis_dns_missing_record": "Facendo caso á configuración DNS recomendada, deberías engadir un rexistro DNS coa seguinte info.
Tipo: {type}
Nome: {name}
Valor: {content}", + "diagnosis_dns_point_to_doc": "Revisa a documentación en https://doc.yunohost.org/dns_config se precisas axuda para configurar os rexistros DNS.", + "diagnosis_dns_specialusedomain": "O dominio {domain} baséase un dominio de nivel alto e uso especial (TLD) como .local ou .test polo que non é de agardar que realmente teña rexistros DNS.", + "diagnosis_dns_try_dyndns_update_force": "A xestión DNS deste dominio debería estar xestionada directamente por YunoHost. Se non fose o caso, podes intentar forzar unha actualización executando yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Algúns dominios van caducan MOI PRONTO!", + "diagnosis_domain_expiration_not_found": "Non se puido comprobar a data de caducidade para algúns dominios", + "diagnosis_domain_expiration_not_found_details": "A información WHOIS para o dominio {domain} non semella conter información acerca da data de caducidade?", + "diagnosis_domain_expiration_success": "Os teus dominios están rexistrados e non van caducar pronto.", + "diagnosis_domain_expiration_warning": "Algúns dominios van caducar pronto!", + "diagnosis_domain_expires_in": "{domain} caduca en {days} días.", + "diagnosis_domain_not_found_details": "O dominio {domain} non existe na base de datos de WHOIS ou está caducado!", + "diagnosis_everything_ok": "Semella todo correcto en {category}!", + "diagnosis_failed": "Non se puido obter o resultado do diagnóstico para '{category}': {error}", + "diagnosis_failed_for_category": "O diagnóstico fallou para a categoría '{category}': {error}", + "diagnosis_found_errors": "Atopado(s) {errors} problema significativo(s) relacionado con {category}!", + "diagnosis_found_errors_and_warnings": "Atopado(s) {errors} problema(s) significativo(s) (e {warnings} avisos(s)) en relación a {category}!", + "diagnosis_found_warnings": "Atoparonse {warnings} elemento(s) que poderían optimizarse en {category}.", + "diagnosis_high_number_auth_failures": "Hai un alto número sospeitoso de intentos fallidos de autenticación. Deberías comprobar que fail2ban está a executarse e que está correctamente configurado, ou utiliza un porto personalizado para SSH tal como se explica en https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Semella que outra máquina (podería ser o rúter de internet) respondeu no lugar do teu servidor.
1. A razón máis habitual para este problema é que o porto 80 (e 443) non están correctamente redirixidos ao teu servidor.
2. En configuracións avanzadas: revisa que nin o cortalumes nin o proxy-inverso están interferindo.", + "diagnosis_http_connection_error": "Erro de conexión: non se puido conectar co dominio solicitado, moi probablemente non sexa accesible.", + "diagnosis_http_could_not_diagnose": "Non se puido comprobar se os dominios son accesibles desde o exterior en IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Erro: {error}", + "diagnosis_http_hairpinning_issue": "A túa rede local semella que non ten hairpinning activado.", + "diagnosis_http_hairpinning_issue_details": "Isto acontece probablemente debido ao rúter do teu ISP. Como resultado, as persoas externas á túa rede local poderán acceder ao teu servidor tal como se espera, pero non as usuarias na rede local (como ti, probablemente?) cando usan o nome de dominio ou IP global. Podes mellorar a situación lendo https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "A configuración nginx deste dominio semella foi modificada manualmente, e está evitando que YunoHost comprobe se é accesible a través de HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Para arranxar a situación, revisa as diferenzas na liña de comandos usando yunohost tools regen-conf nginx --dry-run --with-diff e se todo está ben, aplica os cambios con yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "O dominio {domain} é accesible a través de HTTP desde o exterior da rede local.", + "diagnosis_http_partially_unreachable": "O dominio {domain} non semella accesible a través de HTTP desde o exterior da rede local en IPv{failed}, pero funciona en IPv{passed}.", + "diagnosis_http_special_use_tld": "O dominio {domain} baséase nun dominio de alto-nivel (TLD) especial como .local ou .test e por isto non é de agardar que esté exposto fóra da rede local.", + "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.", + "diagnosis_http_unreachable": "O dominio {domain} non semella accesible a través de HTTP desde o exterior da rede local.", + "diagnosis_ignore_already_filtered": "(Xa existe un filtro de diagnóstico de {category} con estes criterios)", + "diagnosis_ignore_criteria_error": "Os criterios deben ter o formato key=value (ex. domain=yolo.test)", + "diagnosis_ignore_filter_added": "Engadiuse o filtro do diagnóstico para {category}", + "diagnosis_ignore_filter_removed": "Eliminouse 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á", + "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_ignored_issues": "(+ {nb_ignored} problema ignorado(s))", + "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_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_connected_ipv4": "O servidor está conectado a internet a través de IPv4!", + "diagnosis_ip_connected_ipv6": "O servidor está conectado a internet a través de IPv6!", + "diagnosis_ip_dnsresolution_working": "A resolución de nomes de dominio está a funcionar!", + "diagnosis_ip_global": "IP global: {global}", + "diagnosis_ip_local": "IP local: {local}", + "diagnosis_ip_no_ipv4": "O servidor non ten conexión IPv4.", + "diagnosis_ip_no_ipv6": "O servidor non ten conexión IPv6.", + "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://doc.yunohost.org/ipv6. Se non podes activar IPv6 ou é moi complicado para ti, podes ignorar tranquilamente esta mensaxe.", + "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://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "O servidor semella non ter ningún tipo de conexión a internet!?", + "diagnosis_ip_weird_resolvconf": "A resolución DNS semella funcionar, mais parecese que estás a utilizar un /etc/resolv.conf personalizado.", + "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_mail_blocklist_listed_by": "O teu dominio ou IP {item} está na lista de bloqueo {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Os IPs e dominios utilizados neste servidor non parecen estar en listas de bloqueo", + "diagnosis_mail_blocklist_reason": "A razón do bloqueo é: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Semella que a razón menciona «open resolver».
Normalmente isto significa que o teu servidor non está usando o seu DNS local, se non un público (aberto).
Mira o contido de /etc/resolv.conf, debería conter nameserver 127.0.0.1.
Xa que este ficheiro se crea automaticamente non o edites de xeito manual. Comproba os axustes DHCP, ou os axustes VPN se estás a usar unha, ou se usaches unha imaxe Debian creada, por exemplo, por un provedor VPS, busca a configuración cloudinit.
Podes consultar as canles de axuda de YunoHost onde intentaremos axudarche coa incidencia.
A razón literal do bloqueo é: {reason}", + "diagnosis_mail_blocklist_website": "Tras ver a razón do bloqueo e arranxalo, considera solicitar que o teu dominio ou IP sexan eliminados de {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Un servizo non-SMTP respondeu no porto 25 en IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Podería deberse a que outro servidor está a responder no lugar do teu.", + "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_could_not_diagnose_details": "Erro: {error}", + "diagnosis_mail_ehlo_ok": "O servidor de email SMTP é accesible desde o exterior e por tanto pode recibir emails!", + "diagnosis_mail_ehlo_unreachable": "O servidor de email SMTP non é accesible desde o exterior en IPv{ipversion}. Non poderá recibir emails.", + "diagnosis_mail_ehlo_unreachable_details": "Non se puido abrir unha conexión no porto 25 do teu servidor en IPv{ipversion}. Non semella accesible.
1. A causa máis habitual é que o porto 25 non está correctamente redirixido no servidor.
2. Asegúrate tamén de que o servizo postfix está a funcionar.
3. En configuracións máis complexas: asegúrate de que o cortalumes ou reverse-proxy non están interferindo.", + "diagnosis_mail_ehlo_wrong": "Un servidor de email SMPT diferente responde en IPv{ipversion}. O teu servidor probablemente non poida recibir emails.", + "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_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_different_from_ehlo_domain_details": "DNS inverso actual: {rdns_domain}
Valor agardado: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "Non hai DNS inverso definido en IPv{ipversion}. Algúns emails poderían non ser entregados ou ser marcados como spam.", + "diagnosis_mail_fcrdns_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://doc.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_details": "Deberías intentar configurar o DNS inverso con {ehlo_domain} na interface do teu rúter de internet ou na interface do teu provedor de hospedaxe. (Algúns provedores de hospedaxe poderían pedirche que lle fagas unha solicitude por escrito para isto).", + "diagnosis_mail_fcrdns_ok": "O DNS inverso está correctamente configurado!", + "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_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_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://doc.yunohost.org/vpn_advantage
- Tamén podes considerar cambiar a un provedor máis amigable coa neutralidade da rede", + "diagnosis_mail_outgoing_port_25_ok": "O servidor de email SMTP pode enviar emails (porto 25 de saída non está bloqueado).", + "diagnosis_mail_queue_ok": "{nb_pending} emails pendentes na cola de correo", + "diagnosis_mail_queue_too_big": "Hai demasiados emails pendentes na cola de correo ({nb_pending} emails)", + "diagnosis_mail_queue_unavailable": "Non se pode consultar o número de emails pendentes na cola", + "diagnosis_mail_queue_unavailable_details": "Erro: {error}", + "diagnosis_never_ran_yet": "Semella que o servidor foi configurado recentemente e aínda non hai informes diagnósticos. Deberías iniciar un diagnóstico completo, ben desde a administración web ou usando 'yunohost diagnosis run' desde a liña de comandos.", + "diagnosis_no_cache": "Aínda non hai datos na caché para '{category}'", + "diagnosis_package_installed_from_sury": "Algúns paquetes do sistema deberían ser baixados de versión", + "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_security_issue_error": "O paquete do sistema '{package}' está actualmente na versión '{current_version}', que é vulnerable a unha incidencia IMPORTANTE de seguridade: {title}. É recomendable actualizalo O MÁIS AXIÑA POSIBLE á versión '{fixed_in_version}'. Máis info: {more_infos_list}", + "diagnosis_package_security_issue_warning": "O paquete do sistema '{package}' está actualmente na versión '{current_version}', que é vulnerable a unha incidencia moderada de seguridade: {title}. É recomendable actualizalo á versión '{fixed_in_version}'. Máis info: {more_infos_list}", + "diagnosis_ports_could_not_diagnose": "Non se puido comprobar se os portos son accesibles desde o exterior en IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Erro: {error}", + "diagnosis_ports_forwarding_tip": "Para arranxar isto, probablemente tes que configurar o reenvío do porto no teu rúter de internet tal como se di en https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "A apertura deste porto é precisa para {category} (servizo {service})", + "diagnosis_ports_ok": "O porto {port} é accesible desde o exterior.", + "diagnosis_ports_partially_unreachable": "O porto {port} non é accesible desde o exterior en IPv{failed}.", + "diagnosis_ports_unreachable": "O porto {port} non é accesible desde o exterior.", + "diagnosis_processes_killed_by_oom_reaper": "Algúns procesos foron apagados recentemente polo sistema porque quedou sen memoria dispoñible. Isto acontece normalmente porque o sistema quedou sen memoria ou un proceso consumía demasiada. Resumo cos procesos apagados:\n{kills_summary}", + "diagnosis_ram_low": "O sistema ten {available} ({available_percent}%) da RAM dispoñible (total {total}). Ten coidado.", + "diagnosis_ram_ok": "Ao sistema aínda lle queda {available} ({available_percent}%) de RAM dispoñible dun total de {total}.", + "diagnosis_ram_verylow": "Ao sistema só lle queda {available} ({available_percent}%) de RAM dispoñible! (total {total})", + "diagnosis_regenconf_allgood": "Todos os ficheiros de configuración seguen a configuración recomendada!", + "diagnosis_regenconf_manually_modified": "O ficheiro de configuración {file} semella que foi modificado manualmente.", + "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_rfkill_wifi": "A tarxeta Wi-Fi está desactivada e un aviso do sistema podería previr a instalación de apps", + "diagnosis_rfkill_wifi_details": "Este aviso aparece na saída de varias ordes, estragando algunhas apps. Normalmente require indicar o código de país coa orde sudo raspi-config. Aquí tes o erro:
{rfkill_wifi_error}", + "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 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.", + "diagnosis_security_vulnerable_to_meltdown": "Semella que es vulnerable á vulnerabilidade crítica de seguridade Meltdown", + "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_services_bad_status": "O servizo {service} está {status} :(", + "diagnosis_services_bad_status_tip": "Podes intentar reiniciar o servizo, e se isto non funciona, mira os rexistros do servizo na webadmin (desde a liña de comandos con yunohost service restart {service} e yunohost service log {service}).", + "diagnosis_services_conf_broken": "A configuración do {service} está estragada!", + "diagnosis_services_running": "O servizo {service} está en execución!", + "diagnosis_sshd_config_inconsistent": "Semella que o porto SSH foi modificado manualmente en /etc/ssh/sshd_config. Desde YunoHost 4.2, un novo axuste global 'security.ssh.ssh_port' está dispoñible para evitar a edición manual da configuración.", + "diagnosis_sshd_config_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_insecure": "Semella que a configuración SSH modificouse manualmente, e é insegura porque non contén unha directiva 'AllowGroups' ou 'AllowUsers' para limitar o acceso ás contas autorizadas.", + "diagnosis_swap_none": "O sistema non ten partición swap. Deberías considerar engadir polo menos {recommended} de swap para evitar situación onde o sistema esgote a memoria.", + "diagnosis_swap_notsomuch": "O sistema só ten {total} de swap. Deberías considerar ter polo menos {recommended} para evitar situacións onde o sistema esgote a memoria.", + "diagnosis_swap_ok": "O sistema ten {total} de swap!", + "diagnosis_swap_tip": "Por favor ten en conta que se o servidor ten a swap instalada nunha tarxeta SD ou almacenaxe SSD podería reducir drásticamente a expectativa de vida do dispositivo.", + "diagnosis_unknown_categories": "As seguintes categorías son descoñecidas: {categories}", + "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 (bookworm).", + "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 bookworm. 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.", + "disk_space_not_sufficient_install": "Non queda espazo suficiente no disco para instalar esta aplicación", + "disk_space_not_sufficient_update": "Non hai espazo suficiente no disco para actualizar esta aplicación", + "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}", + "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_cert_gen_failed": "Non se puido crear o certificado", + "domain_config_acme_eligible": "Elixibilidade ACME", + "domain_config_acme_eligible_explain": "Este dominio non semella estar preparado para un certificado Let's Encrypt. Comproba a configuración DNS e que é accesible por HTTP. A sección 'Rexistros DNS' e 'Web' na páxina de diagnóstico pode axudarche a entender o que está a fallar.", + "domain_config_api_protocol": "Protocolo API", + "domain_config_auth_application_key": "Chave da aplicación", + "domain_config_auth_application_secret": "Chave segreda da aplicación", + "domain_config_auth_consumer_key": "Chave consumidora", + "domain_config_auth_entrypoint": "Punto de entrada da API", + "domain_config_auth_key": "Chave de autenticación", + "domain_config_auth_secret": "Segreda de autenticación", + "domain_config_auth_token": "Token de autenticación", + "domain_config_cert_install": "Instalar certificado Let's Encrypt", + "domain_config_cert_issuer": "Autoridade certificadora", + "domain_config_cert_name": "Certificado", + "domain_config_cert_no_checks": "Ignorar comprobacións de diagnóstico", + "domain_config_cert_renew": "Anovar certificado Let's Encrypt", + "domain_config_cert_renew_help": "O certificado anovarase automáticamente nos últimos 15 días de validez. Podes anovalo automáticamente se queres. (Non é recomendable).", + "domain_config_cert_summary": "Estado do certificado", + "domain_config_cert_summary_abouttoexpire": "O certificado actual vai caducar. Debería anovarse automáticamente..", + "domain_config_cert_summary_expired": "CRÍTICO: O certificado actual non é válido! HTTPS non funcionará!!", + "domain_config_cert_summary_letsencrypt": "Ben! Estás a usar un certificado Let's Encrypt válido!", + "domain_config_cert_summary_ok": "Correcto, o certificado ten boa pinta!", + "domain_config_cert_summary_selfsigned": "AVISO: O certificado actual está auto-asinado. Os navegadores van mostrar un aviso que mete medo a quen te visite!", + "domain_config_cert_validity": "Validez", + "domain_config_custom_css": "Folla de estilos CSS persoal", + "domain_config_custom_css_help": "Isto é para admins con coñecemento que queiran personalizar a aparencia do portal", + "domain_config_default_app": "App por defecto", + "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.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Mostrar ás visitantes a lista de apps públicas", + "domain_config_enable_public_apps_page_help": "As persoas visitantes poderán ver a páxina 'apps públicas' cando accedan ao portal no lugar do formulario de acceso.", + "domain_config_feature_name": "Características", + "domain_config_mail_in": "Emails entrantes", + "domain_config_mail_out": "Emails saíntes", + "domain_config_portal_logo": "Logo persoal", + "domain_config_portal_logo_help": "Acepta .svg, .png e .jpeg. É preferible un logo monocromático .svg con fill: currentColor para que o logo se adapte ao decorado.", + "domain_config_portal_name": "Personalización do portal", + "domain_config_portal_public_intro": "Presentación pública persoal", + "domain_config_portal_public_intro_help": "Podes usar HTML, vaiselle aplicar o estilo básico aos elementos xenéricos.", + "domain_config_portal_theme": "Cor predeterminada do decorado", + "domain_config_portal_theme_help": "As usuarias poden elixir outra cor nos seus axustes.", + "domain_config_portal_tile_theme": "Estilo das teselas das apps", + "domain_config_portal_title": "Título persoal", + "domain_config_portal_user_intro": "Presentación persoal da usuaria", + "domain_config_portal_user_intro_help": "Podes usar HTML, vanse aplicar estilos básicos aos elementos xenéricos.", + "domain_config_search_engine": "URL do motor de busca", + "domain_config_search_engine_help": "Esta é unha ferramenta opcional, que permite mostrar unha barra de busca no portal (por exemplo se queres usar o teu portal YunoHost como páxina de inicio do navegador). Isto debería ser un URL cun valor de busca baleiro tal que `https://duckduckgo.com/?q=`, con `q=` como o parámetro sen valor de duckduckgo", + "domain_config_search_engine_name": "Nome do motor de busca", + "domain_config_show_other_domains_apps": "Mostrar apps de outros dominios", + "domain_created": "Dominio creado", + "domain_creation_failed": "Non se puido crear o dominio {domain}: {error}", + "domain_deleted": "Dominio eliminado", + "domain_deletion_failed": "Non se puido eliminar o dominio {domain}: {error}", + "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_dns_conf_special_use_tld": "Este dominio baséase nun dominio de alto-nivel (TLD) de uso especial como .local ou .test e por isto non é de agardar que teña rexistros DNS asociados.", + "domain_dns_push_already_up_to_date": "Rexistros ao día, nada que facer.", + "domain_dns_push_failed": "Fallou completamente a actualización dos rexistros DNS.", + "domain_dns_push_failed_to_list": "Non se pode mostrar a lista actual de rexistros na API da rexistradora: {error}", + "domain_dns_push_managed_in_parent_domain": "A función de rexistro DNS automático está xestionada polo dominio nai {parent_domain}.", + "domain_dns_push_not_applicable": "A función de rexistro DNS automático non é aplicable ao dominio {domain}. Debes configurar manualmente os teus rexistros DNS seguindo a documentación de https://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "Actualización parcial dos rexistros DNS: informouse dalgúns avisos/erros.", + "domain_dns_push_record_failed": "Fallou {action} do rexistro {type}/{name}: {error}", + "domain_dns_push_success": "Rexistros DNS actualizados!", + "domain_dns_pushing": "A enviar os rexistros DNS…", + "domain_dns_registrar_experimental": "Ata o momento, a interface coa API de **{registrar}** aínda non foi comprobada e revisada pola comunidade YunoHost. O soporte é **moi experimental** - ten coidado!", + "domain_dns_registrar_managed_in_parent_domain": "Este dominio é un subdominio de {parent_domain_link}. A configuración DNS debe xestionarse no panel de configuración de {parent_domain}'s.", + "domain_dns_registrar_not_supported": "YunoHost non é quen de detectar a rexistradora que xestiona o dominio. Debes configurar manualmente os seus rexistros DNS seguindo a documentación en https://doc.yunohost.org/dns_config.", + "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://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Tamén podes configurar manualmente os rexistros DNS seguindo a documentación en https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_use_auto": "Usar ferramenta de DNS automático", + "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_dyndns_already_subscribed": "Xa tes unha subscrición a un dominio DynDNS", + "domain_exists": "Xa existe o dominio", + "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_registrar_is_not_configured": "A rexistradora non aínda non está configurada para o dominio {domain}.", + "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_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_unknown": "Dominio '{domain}' descoñecido", + "domains_available": "Dominios dispoñibles:", + "done": "Feito", + "download_bad_status_code": "{url} devolveu o código de estado {code}", + "download_ssl_error": "Erro SSL ao conectar con {url}", + "download_timeout": "{url} está tardando en responder, deixámolo.", + "download_unknown_error": "Erro ao descargar os datos desde {url}: {error}", + "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`.", + "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)", + "dyndns_could_not_check_available": "Non se comprobou se {domain} está dispoñible en {provider}.", + "dyndns_domain_not_provided": "O provedor DynDNS {provider} non pode proporcionar o dominio {domain}.", + "dyndns_ip_update_failed": "Non se actualizou o enderezo IP en DynDNS", + "dyndns_ip_updated": "Actualizouse o IP en DynDNS", + "dyndns_key_not_found": "Non se atopou a chave DNS para o dominio", + "dyndns_no_domain_registered": "Non hai dominio rexistrado con 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_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_set_recovery_password_denied": "Fallou o establecemento do contrasinal de recuperación: chave non válida", + "dyndns_set_recovery_password_failed": "Fallo ao establecer o contrasinal de recuperación: {error}", + "dyndns_set_recovery_password_invalid_password": "Fallo ao establecer contrasinal de recuperación: o contrasinal non é suficientemente forte", + "dyndns_set_recovery_password_success": "Estableceuse o contrasinal de recuperación!", + "dyndns_set_recovery_password_unknown_domain": "Fallo ao establecer o contrasinal de recuperación: dominio non rexistrado", + "dyndns_subscribe_failed": "Non te subscribiches ao dominio DynDNS: {error}", + "dyndns_subscribed": "Tes unha subscrición a un dominio DynDNS", + "dyndns_too_many_requests": "O servicio dyndns de YunoHost recibeu demasiadas peticións do teu sistema, agarda 1 hora e volve intentalo.", + "dyndns_unavailable": "O dominio '{domain}' non está dispoñible.", + "dyndns_unsubscribe_already_unsubscribed": "Non tes unha subscrición ao dominio", + "dyndns_unsubscribe_denied": "Fallo ao intentar retirar subscrición: credenciais incorrectas", + "dyndns_unsubscribe_failed": "Non se retirou a subscrición ao dominio DynDNS: {error}", + "dyndns_unsubscribed": "Retirada a subscrición ao dominio DynDNS", + "error_changing_file_permissions": "Erro ao cambiar os permisos de {path}: {error}", + "error_removing": "Erro ao eliminar {path}: {error}", + "error_writing_file": "Erro ao escribir o ficheiro {file}: {error}", + "extracting": "A extraer…", + "field_invalid": "Campo non válido '{field}'", + "file_does_not_exist": "O ficheiro {path} non existe.", + "file_not_exist": "Non existe o ficheiro: '{path}'", + "firewall_reload_failed": "Non se puido recargar o cortalumes. Máis info no rexistro.", + "firewall_reloaded": "Recargouse o cortalumes", + "global_settings_reset_success": "Restablecer axustes globais", + "global_settings_setting_admin_strength": "Fortaleza do contrasinal de Admin", + "global_settings_setting_admin_strength_help": "Estos requerimentos só se esixen ao inicializar ou cambiar o contrasinal", + "global_settings_setting_antispam_name": "Antispam", + "global_settings_setting_backup_compress_tar_archives": "Comprimir copias de apoio", + "global_settings_setting_backup_compress_tar_archives_help": "Ao crear novas copias de apoio, comprime os arquivos (.tar.gz) en lugar de non facelo (.tar). Nota: activando esta opción creas arquivos máis lixeiros, mais o procedemento da primeira copia será significativamente máis longo e esixente coa CPU.", + "global_settings_setting_backup_name": "Copia de apoio", + "global_settings_setting_dns_custom_resolvers_enabled": "Usar a resolución DNS persoal", + "global_settings_setting_dns_custom_resolvers_enabled_help": "De xeito predeterminado YunoHost usa unha lista de resolvedores de confianza situados en Europa. As usuarias con experiencia poden utilizar os que prefiran.", + "global_settings_setting_dns_custom_resolvers_list": "Enderezos dos resolvedores persoais", + "global_settings_setting_dns_custom_resolvers_list_help": "Lista de polo menos 2 resolvedores DNS para cada protocolo IP utilizado (IPv4/IPv6). Ex.: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "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.", + "global_settings_setting_email_name": "Correo electrónico", + "global_settings_setting_enable_blocklists": "Activar listas de bloqueo para o tráfico entrante", + "global_settings_setting_enable_blocklists_help": "Lista de servidores bloqueados procedente de spamcop.net, spamhaus.org e abuseat.org. Ten en conta que podería causar problemas para entregar o correo a algúns servidores que poderían aparecer nesas listas pero ser inofensivos, nese caso tampouco vas recibir correo procedente deses servidores.", + "global_settings_setting_experimental_name": "Experimental", + "global_settings_setting_misc_name": "Outros", + "global_settings_setting_network_name": "Rede", + "global_settings_setting_nginx_compatibility": "Compatibilidade NGINX", + "global_settings_setting_nginx_compatibility_help": "Compromiso entre compatiblidade e seguridade para o servidor NGINX. Afecta á cifraxe (e outros aspectos relacionados coa seguridade)", + "global_settings_setting_nginx_name": "NGINX (servidor web)", + "global_settings_setting_nginx_redirect_to_https": "Forzar HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Redirixir peticións HTTP a HTTPs por defecto (NON DESACTIVAR ISTO a non ser que realmente saibas o que fas!)", + "global_settings_setting_password_name": "Contrasinais", + "global_settings_setting_passwordless_sudo": "Permitir a Admins usar 'sudo' sen ter que volver a escribir o contrasinal", + "global_settings_setting_pop3_enabled": "Activar POP3", + "global_settings_setting_pop3_enabled_help": "Activar o protocolo POP3 no servidor de email. POP3 é un protocolo antigo para acceder a caixar de correo que é máis lixeiro, pero ten menos funcións que IMAP (activado por defecto)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Permitir ás usuarias editar o seu enderezo de correo principal", + "global_settings_setting_portal_allow_edit_email_alias": "Permitir ás usuarias engadir, retirar, editar alias de correo", + "global_settings_setting_portal_allow_edit_email_alias_help": "Se está desactivado, precisas pedirlle a Admin que o faga.", + "global_settings_setting_portal_allow_edit_email_forward": "Permitir ás usuarias engadir, retirar, editar reenvío de correo", + "global_settings_setting_portal_allow_edit_email_forward_help": "Se está desactivado, precisas pedirlle a Admin que o faga.", + "global_settings_setting_portal_allow_edit_email_help": "Se está desactivado, precisas pedirlle a Admin que o faga.", + "global_settings_setting_portal_name": "Portal", + "global_settings_setting_postfix_compatibility": "Compatibilidade Postfix", + "global_settings_setting_postfix_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor Postfix. Aféctalle á cifraxe (e outros aspectos da seguridade)", + "global_settings_setting_postfix_name": "Postfix (servidor de correo SMTP)", + "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.", + "global_settings_setting_root_access_name": "Cambiar contrasinal root", + "global_settings_setting_root_password": "Novo contrasinal root", + "global_settings_setting_root_password_confirm": "Novo contrasinal root (confirmar)", + "global_settings_setting_security_experimental_enabled": "Ferramentas experimentais de seguridade", + "global_settings_setting_security_experimental_enabled_help": "Activar características de seguridade experimentais (non actives isto se non sabes o que estás a facer!)", + "global_settings_setting_security_name": "Seguridade", + "global_settings_setting_smtp_allow_ipv6": "Permitir IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Permitir o uso de IPv6 para recibir e enviar emais", + "global_settings_setting_smtp_backup_mx_domains": "Dominios que actúan como MX secundario para", + "global_settings_setting_smtp_backup_mx_domains_help": "Permitir a este servidor actuar como un dominio MX *secundario* de apoio para o dominio da lista. Así se o MX principal para o dominio non está accesible (por exemplo por quedar ser electricidade), os correos seguirán enviándose ao servidor, que os gardará un máximo de 20 días e intentará entregalos ao destino real unha vez volva ser accesible. Pódense indicar varios dominios, separados por comas.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Lista de enderezos para apoio MX de SMTP", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Para actuar como MX secundario, hai que proporcionar unha lista detallada de enderezos de correspondentes permitidos (doutro xeito os correos serán rexeitados e desbotados). Pódense indicar varias entradas, separadas por comas.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Activar repetidor SMTP", + "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.", + "global_settings_setting_smtp_relay_host": "Sevidor repetidor SMTP", + "global_settings_setting_smtp_relay_password": "Contrasinal do repetidor SMTP", + "global_settings_setting_smtp_relay_port": "Porto do repetidor SMTP", + "global_settings_setting_smtp_relay_user": "Usuaria no repetidor SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilidade SSH", + "global_settings_setting_ssh_compatibility_help": "Compromiso entre compatibilidade e seguridade para o servidor SSH. Aféctalle á cifraxe (e outros aspectos da seguridade). Máis info en https://infosec.mozilla.org/guidelines/openssh", + "global_settings_setting_ssh_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Autenticación con contrasinal", + "global_settings_setting_ssh_password_authentication_help": "Permitir autenticación con contrasinal para SSH", + "global_settings_setting_ssh_port": "Porto SSH", + "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 elixir un porto que xa está sendo utilizado, como 80 ou 443.", + "global_settings_setting_tls_passthrough_enabled": "Activar TLS-passthrough / reenvío SNI-based", + "global_settings_setting_tls_passthrough_enabled_help": "Esta característica avanzada para facer proxy-inverso a un dominio completo cara outra máquina *sen* descifrar o tráfico. É útil cando queres expoñer varias máquinas detrás do mesmo IP pero permitir a cada máquina xestionar a súa conexión SSL.", + "global_settings_setting_tls_passthrough_explain": "Esta ferramenta é AVANZADA e EXPERIMENTAL e provoca grandes cambios na configuración de nginx neste servidor. NON A USES se non sabes o que estás a facer! En particular, tes que ter en conta que fail2ban non se pode implementar no servidor adicional (nftables non pode vetar tráfico daniño xa que todos os paquetes parecen proceder do servidor principal). Adicionalmente, por agora o nginx do servidor engadido ten que modificarse manualmente para aceptar o `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "Lista de reenvío", + "global_settings_setting_tls_passthrough_list_help": "Debería ser unha lista tipo DOMINIO;DESTINO;PORTO, como domain.tld;192.168.1.42;443 ou dominio.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "TLS-passthrough / reenvío SNI-based", + "global_settings_setting_user_strength": "Fortaleza do contrasinal da usuaria", + "global_settings_setting_user_strength_help": "Estos requerimentos só se esixen ao inicializar ou cambiar o contrasinal", + "global_settings_setting_webadmin_allowlist": "Lista IP autorizados para Webadmin", + "global_settings_setting_webadmin_allowlist_enabled": "Activar a lista de IP autorizados", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permitir que só algúns IPs accedan á webadmin.", + "global_settings_setting_webadmin_allowlist_help": "Enderezos IP con permiso para acceder á webadmin. Permítese a notación CIDR.", + "global_settings_setting_webadmin_name": "Web Admin", + "good_practices_about_admin_password": "Vas definir o novo contrasinal de administración. O contrasinal debe ter 8 caracteres como mínimo—aínda que se recomenda utilizar un máis longo (ex. unha frase de paso) e/ou utilizar caracteres variados (maiúsculas, minúsculas, números e caracteres especiais).", + "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).", + "group_already_exist": "Xa existe o grupo {group}", + "group_already_exist_on_system": "O grupo {group} xa é un dos grupos do sistema", + "group_already_exist_on_system_but_removing_it": "O grupo {group} xa é un dos grupos do sistema, pero YunoHost vaino eliminar…", + "group_cannot_be_deleted": "O grupo {group} non se pode eliminar manualmente.", + "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", + "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_remove_last_admin": "A usuaria '{user}' é a última usuaria do grupo 'admins' e non se vai retirar del.", + "group_created": "Creouse o grupo '{group}'", + "group_creation_failed": "Non se puido crear o grupo '{group}': {error}", + "group_deleted": "Grupo '{group}' eliminado", + "group_deletion_failed": "Non se eliminou o grupo '{group}': {error}", + "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_no_change": "Nada que cambiar para o grupo '{group}'", + "group_unknown": "Grupo descoñecido '{group}'", + "group_update_aliases": "Actualizando os alias do grupo '{group}'", + "group_update_failed": "Non se actualizou o grupo '{group}': {error}", + "group_updated": "Grupo '{group}' actualizado", + "group_user_add": "Vaise engadir a '{user}' ao grupo '{group}'", + "group_user_already_in_group": "A usuaria {user} xa está no grupo {group}", + "group_user_not_in_group": "A usuaria {user} non está no grupo {group}", + "group_user_remove": "Vaise quitar a '{user}' do grupo '{group}'", + "hook_exec_failed": "Non se executou o script: {path}", + "hook_exec_not_terminated": "O script non rematou correctamente: {path}", + "hook_json_return_error": "Non se puido ler a info de retorno do gancho {path}. Erro: {msg}. Contido en bruto: {raw_content}", + "hook_list_by_invalid": "Esta propiedade non se pode usar para enumerar os ganchos", + "hook_name_unknown": "Nome descoñecido do gancho '{name}'", + "installation_complete": "Instalación completa", + "invalid_credentials": "Credenciais non válidas", + "invalid_number": "Ten que ser un número", + "invalid_password": "Contrasinal non válido", + "invalid_regex": "Regex non válido: '{regex}'", + "invalid_shell": "Intérprete de ordes non válido: {shell}", + "invalid_url": "Fallou a conexión con {url}… pode que o servizo estea caído, ou que non teñas conexión a Internet con IPv4/IPv6.", + "ldap_attribute_already_exists": "Xa existe o atributo LDAP '{attribute}' con valor '{value}'", + "ldap_server_down": "Non se chegou ao servidor LDAP", + "ldap_server_is_down_restart_it": "O servidor LDAP está caído, intenta reinicialo…", + "log_app_action_run": "Executar acción da app '{}'", + "log_app_change_url": "Cambiar o URL da app '{}'", + "log_app_config_set": "Aplicar a configuración á app '{}'", + "log_app_install": "Instalar a app '{}'", + "log_app_makedefault": "Converter '{}' na app por defecto", + "log_app_remove": "Eliminar a app '{}'", + "log_app_upgrade": "Actualizar a app '{}'", + "log_available_on_yunopaste": "Este rexistro está dispoñible en {url}", + "log_backup_create": "Crear copia de apoio", + "log_backup_restore_app": "Restablecer '{}' desde unha copia de apoio", + "log_backup_restore_system": "Restablecer o sistema desde unha copia de apoio", + "log_corrupted_md_file": "O ficheiro YAML con metadatos asociado aos rexistros está danado: '{md_file}\nErro: {error}'", + "log_diagnosis_run": "Realizar diagnóstico", + "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_domain_add": "Engadir o doninio '{}'", + "log_domain_config_set": "Actualizar configuración para o dominio '{}'", + "log_domain_dns_push": "Enviar rexistros DNS para o dominio '{}'", + "log_domain_main_domain": "Facer que '{}' sexa o dominio principal", + "log_domain_remove": "Retirar o dominio '{}'", + "log_dyndns_subscribe": "Rexistrar un subdominio YunoHost '{}'", + "log_dyndns_unsubscribe": "Retirar rexistro do subdominio YunoHost '{}'", + "log_dyndns_update": "Actualizar o IP asociado ao teu subdominio YunoHost '{}'", + "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_help_to_get_log": "Para ver o rexistro completo da operación '{desc}', usa o comando 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Instalar un certificado Let's Encrypt para o dominio '{}'", + "log_letsencrypt_cert_renew": "Anovar certificado Let's Encrypt para '{}'", + "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", + "log_link_to_log": "Rexistro completo desta operación: '{desc}'", + "log_operation_unit_unclosed_properly": "Non se pechou correctamente a unidade da operación", + "log_regen_conf": "Rexerar configuración do sistema '{}'", + "log_remove_on_failed_install": "Eliminar '{}' tras unha instalación fallida", + "log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso", + "log_selfsigned_cert_install": "Instalar certificado auto-asinado para o dominio '{}'", + "log_settings_reset": "Restablecer axuste", + "log_settings_reset_all": "Restablecer todos os axustes", + "log_settings_set": "Aplicar axustes", + "log_tools_migrations_migrate_forward": "Executar migracións", + "log_tools_postinstall": "Postinstalación do servidor YunoHost", + "log_tools_reboot": "Reiniciar o servidor", + "log_tools_shutdown": "Apagar o servidor", + "log_tools_update": "Obtendo as actualizacións dispoñibles e actualizando o catálogo de aplicacións", + "log_tools_upgrade": "Actualizar paquetes do sistema", + "log_user_create": "Engadir usuaria '{}'", + "log_user_delete": "Eliminar usuaria '{}'", + "log_user_group_create": "Crear grupo '{}'", + "log_user_group_delete": "Eliminar grupo '{}'", + "log_user_group_update": "Actualizar grupo '{}'", + "log_user_import": "Importar usuarias", + "log_user_update": "Actualizar info da usuaria '{}'", + "mail_alias_remove_failed": "Non se puido eliminar o alias de email '{mail}'", + "mail_alias_unauthorized": "Non tes autorización para engadir alias para o dominio '{domain}'", + "mail_already_exists": "Xa existe o enderezo '{mail}'", + "mail_domain_unknown": "Enderezo de email non válido para o dominio '{domain}'. Usa un dominio administrado por este servidor.", + "mail_edit_operation_unauthorized": "Non tes permiso para facer este cambio na túa conta.", + "mail_forward_remove_failed": "Non se eliminou o reenvío de email '{mail}'", + "mail_unavailable": "Este enderezo de email está reservado para o grupo de admins", + "mailbox_disabled": "Desactivado email para usuaria {user}", + "mailbox_used_space_dovecot_down": "O servizo de caixa de correo Dovecot ten que estar activo se queres obter o espazo utilizado polo correo", + "main_domain_change_failed": "Non se pode cambiar o dominio principal", + "main_domain_changed": "Foi cambiado o dominio principal", + "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_general_warning": "Finalmente, ten en conta que a migración é **unha operación delicada**. O equipo YunoHost fixo todo o que puido para revisalo e probalo, pero aínda así podería haber partes do teu sistema ou apps que non funcionen.\n\nAsí, é recomendable:\n - **Facer copias de apoio** de todos os datos importantes. Máis información en https://doc.yunohost.org/backup;\n - **Ser paciente** despois de iniciar a migración: dependendo da túa conexión a internet e hardware podería levarlle mais dunha hora completar todo o proceso;\n - **Contacta coa comunidade** no foro se precisas axuda para solucionar algún problema.", + "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_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_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_patching_sources_list": "A configurar o ficheiro sources.list…", + "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_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_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 Bookworm.", + "migration_0027_yunohost_upgrade": "A iniciar a actualización do núcleo de YunoHost…", + "migration_not_enough_space": "Ter espazo suficiente en {path} para realizar a migración.", + "migration_postgresql_previous_not_installed": "PostgreSQL non estaba instalado no teu sistema. Nada que facer.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 está instalado, pero non PostgreSQL 15!? Isto é algo raro que pasa no teu sistema :(…", + "migration_python_venv_rebuild_broken_app": "Omitindo {app} porque o virtualenv non se pode reconstruir facilmente para esta app. Deberás arranxar a situación forzando a actualización desta app con `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_disclaimer_base": "Debido á actualización a Debian Bookworm, algunhas aplicacións Python teñen que ser parcialmente reconstruídas para convertelas á nova versión de Python que inclúe Debian (en termos técnicos: hai que recrear o que se chama 'virtualenv'). Mentrar tanto algunhas aplicacións Python poderían non funcionar. YunoHost pode intentar reconstruír o virtualenv dalgunha delas, como se indica aquí abaixo. Para outras, o se o intento de reconstrución falla, deberás forzar manualmente a actualización desas apps.", + "migration_python_venv_rebuild_disclaimer_ignored": "Non se pode reconstruir automáticamente o virtualenv destas apps. Tes que forzar a actualización, que se pode facer desde a liña de ordes con: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "Pódese intentar a reconstrución do virtualenv das seguintes apps (Nota: podería levarlle algún tempo!): {rebuild_apps}", + "migration_python_venv_rebuild_failed": "Fallou a reconstrución do virtualenv Python para {app}. A app podería non funcionar ata que o resolvas. Deberías arranxar a situación forzando a actualización desta app usando `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_in_progress": "Intentando reconstruir o virtualenv Python para `{app}`", + "migration_0031_terms_of_services": "Esta migración é só unha mensaxe informativa sobre o feito de que agora o proxecto YunoHost publica uns Termos dos Servizos en relación aos servizos técnicos e da comunidade.", + "migration_0036_cleaning_up": "Limpando a memoria tobo e paquetes que xa non se usan…", + "migration_0036_delayed_api_restart": "A API de YunoHost vai reiniciar en 15 segundos. Pode deixar de estar dispoñible durante uns segundos, e terás que volver a iniciar sesión.", + "migration_0036_general_warning": "Finalmente, ten en conta que a migración é **unha operación delicada**. O equipo de YunoHost revisou e probou o procedemento pero aínda así poderían fallar partes do sistema ou aplicacións.\n\nDito isto, é recomendable:\n - **Facer copias de apoio** dos datos críticos e aplicacións. Máis info en https://doc.yunohost.org/backup ;\n - **Ter paciencia** despois de iniciar a migración: dependendo da conexión a internet e hardware podería levarlle máis dunha hora finalizar o procedemento de xeito correcto;\n - **Contactar coa comunidade** no foro se precisas axuda para resolver as posibles incidencias.", + "migration_0036_main_upgrade": "Iniciando a actualización principal…", + "migration_0036_modified_files": "Detectouse que os seguintes ficheiros foron modificados manualmente e poderían sobreescribirse ao facer a actualización:", + "migration_0036_not_bullseye": "A versión actual de Debian non é Bookworm! Se xa realizaches a migración Bookworm -> Trixie entón este erro é síntoma de que o procedemento non foi correcto ao 100% (se non YunoHost debería telo marcado como completo). Recoméndase investigar o acontecido e contactar co equipo de axuda, que precisará o rexistro **completo** da migración que podes atopar na web de administración en Ferramentas > Rexistros.", + "migration_0036_not_enough_free_space": "Queda pouco espazo libre en /var/! Deberías ter polo menos 1GB libre para iniciar a migración.", + "migration_0036_patch_yunohost_dpkg": "Aplicando os cambios na base de datos dpkg para resolver os conflitos…", + "migration_0036_patching_sources_list": "Cambiando o ficheiro sources.list…", + "migration_0036_problematic_apps_warning": "Detectamos as seguintes aplicacións que poderían ser problemáticas. Semella que non se instalaron desde o catálogo de YunoHost, ou non están marcadas como 'funcional'. Xa que así, non se pode garantir que seguirán funcionando despois de facer a migración:", + "migration_0036_start": "Iniciando a migración a Trixie…", + "migration_0036_still_on_bookworm_after_main_upgrade": "Houbo un fallo durante a migración principal, o sistema semella estar aínda en Debian Bookworm.", + "migration_0036_system_not_fully_up_to_date": "O sistema non está totalmente ao día. Fai unha actualización corrente antes de iniciar a migración a Trixie.", + "migration_0036_yunohost_upgrade": "Iniciando a actualización do núcleo YunoHost…", + "migration_description_0027_migrate_to_bookworm": "Actualiza o sistema a Debian Bookworm e YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Eliminar os antigos permisos XMPP, agora Metronome é unha app", + "migration_description_0029_postgresql_13_to_15": "Migrar as bases de datos desde PostgreSQL 13 a 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Reparar app Python despois da migración bookworm", + "migration_description_0031_terms_of_services": "Termos dos servizos", + "migration_description_0032_firewall_config": "Migración do ficheiro de configuración do cortalumes interno", + "migration_description_0033_rework_permission_infos": "Modifica o xeito en que se gardan os permisos da app", + "migration_description_0034_fix_missing_admins_aliases": "Arranxar os alias de correo que faltan para o grupo de admins", + "migration_description_0035_fix_apps_nodejs_version": "Arranxar as versións de nodejs na configuración de systemd da app", + "migration_description_0036_migrate_to_trixie": "Actualizar o sistema a Debian Trixie e YunoHost 13", + "migration_ldap_backup_before_migration": "Crear copia de apoio da base de datos LDAP e axustes de apps antes de realizar a migración.", + "migration_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_migration_failed_trying_to_rollback": "Non se puido migrar… intentando volver á versión anterior do sistema.", + "migration_ldap_rollback_success": "Sistema restablecido.", + "migrations_already_ran": "Xa se realizaron estas migracións: {ids}", + "migrations_dependencies_not_satisfied": "Executar estas migracións: '{dependencies_id}', antes da migración {id}.", + "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' son opcións que se exclúen unhas a outras.", + "migrations_failed_to_load_migration": "Non se cargou a migración {id}: {error}", + "migrations_list_conflict_pending_done": "Non podes usar ao mesmo tempo '--previous' e '--done'.", + "migrations_loading_migration": "A cargar a migración {id}…", + "migrations_migration_has_failed": "A migración {id} non se completou, abortando. Erro: {exception}", + "migrations_must_provide_explicit_targets": "Debes proporcionar obxectivos explícitos ao utilizar '--skip' ou '--force-rerun'", + "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_no_migrations_to_run": "Sen migracións a executar", + "migrations_no_such_migration": "Non hai migración co nome '{id}'", + "migrations_not_pending_cant_skip": "Estas migracións non están pendentes, polo que non poden ser omitidas: {ids}", + "migrations_pending_cant_rerun": "Estas migracións están pendentes, polo que non ser realizadas outra vez: {ids}", + "migrations_running_forward": "Realizando a migración {id}…", + "migrations_skip_migration": "Omitindo migración {id}…", + "migrations_success_forward": "Migración {id} completada", + "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`.", + "nftables_unavailable": "Non podes andar remexendo en nftables aquí. Ou ben estás nun contedor ou o teu kernel non ten soporte para isto", + "noninteractive_task": "Tarefa non-interactiva", + "not_enough_disk_space": "Non hai espazo libre abondo en '{path}'", + "operation_interrupted": "Foi interrumpida manualmente a operación?", + "other_available_options": "… e outras {n} opcións dispoñibles non mostradas", + "password_confirmation_not_the_same": "Non concordan os contrasinais escritos", + "password_listed": "Este contrasinal está entre os máis utilizados no mundo. Por favor elixe outro que sexa máis orixinal.", + "password_too_long": "Elixe un contrasinal menor de 127 caracteres", + "password_too_simple_1": "O contrasinal ten que ter 8 caracteres como mínimo", + "password_too_simple_2": "O contrasinal ten que ter 8 caracteres como mínimo e conter un díxito, maiúsculas e minúsculas", + "password_too_simple_3": "O contrasinal ten que ter 8 caracteres como mínimo e conter un díxito, maiúsculas, minúsculas e caracteres especiais", + "password_too_simple_4": "O contrasinal ten que ter 12 caracteres como mínimo e conter un díxito, maiúsculas, minúsculas e caracteres especiais", + "pattern_backup_archive_name": "Ten que ser un nome de ficheiro válido con 30 caracteres como máximo, alfanuméricos ou só caracteres -_.", + "pattern_domain": "Ten que ser un nome de dominio válido (ex. dominiopropio.org)", + "pattern_email": "Ten que ser un enderezo de email válido, sen o símbolo '+' (ex. persoa@exemplo.com)", + "pattern_email_forward": "Ten que ser un enderezo de email válido, está aceptado o símbolo '+' (ex. persoa+etiqueta@exemplo.com)", + "pattern_fullname": "Ten que ser un nome completo válido (min. 3 caract.)", + "pattern_mailbox_quota": "Ten que ser un tamaño co sufixo b/k/M/G/T ou 0 para non ter unha cota", + "pattern_password": "Ten que ter polo menos 3 caracteres", + "pattern_password_app": "Lamentámolo, os contrasinais non poden conter os seguintes caracteres: {forbidden_chars}", + "pattern_port_or_range": "Debe ser un número válido de porto (entre 0-65535) ou rango de portos (ex. 100:200)", + "pattern_username": "Só admite caracteres alfanuméricos en minúscula, punto, trazo e trazo baixo", + "permission_already_allowed": "O grupo '{group}' xa ten o permiso '{permission}' activado", + "permission_already_disallowed": "O grupo '{group}' xa ten o permiso '{permission}' desactivado", + "permission_cannot_remove_main": "Non está permitido eliminar un permiso principal", + "permission_cant_add_to_all_users": "O permiso {permission} non se pode conceder a todas as usuarias.", + "permission_created": "Creado o permiso '{permission}'", + "permission_creation_failed": "Non se creou o permiso '{permission}': {error}", + "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.", + "permission_deleted": "O permiso '{permission}' foi eliminado", + "permission_deletion_failed": "Non se puido eliminar o permiso '{permission}': {error}", + "permission_not_found": "Non se atopa o permiso '{permission}'", + "permission_protected": "O permiso {permission} está protexido. Non podes engadir ou eliminar o grupo visitantes a/de este permiso.", + "permission_require_account": "O permiso {permission} só ten sentido para usuarias cunha conta, e por tanto non pode concederse a visitantes.", + "permission_update_failed": "Non se actualizou o permiso '{permission}': {error}", + "permission_updated": "Permiso '{permission}' actualizado", + "port_already_closed": "O porto {port} xa está pechado", + "port_already_opened": "O porto {port} xa está aberto", + "postinstall_low_rootfsspace": "O sistema de ficheiros raiz ten un espazo total menor de 10GB, que é pouco! Probablemente vas quedar sen espazo moi pronto! É recomendable ter polo menos 16GB para o sistema raíz. Se queres instalar YunoHost obviando este aviso, volve a executar a postinstalación con --force-diskspace", + "pydantic_type_error": "Tipo non válido.", + "pydantic_type_error_none_not_allowed": "Valor requerido.", + "pydantic_type_error_str": "Tipo non válido, agárdase unha cadea.", + "pydantic_value_error_color": "Cor non válida, ten que ser un valor hex ou nome de cor.", + "pydantic_value_error_const": "Valor non agardado; elixe entre {permitted}", + "pydantic_value_error_date": "Formato de data non válido", + "pydantic_value_error_email": "O valor non é un enderezo de correo válido", + "pydantic_value_error_number_not_ge": "O valor debe ser mais grande ou igual a {limit_value}.", + "pydantic_value_error_number_not_le": "O valor debe ser menor ou igual a {limit_value}.", + "pydantic_value_error_str_regex": "Cadea non válida; o valor non respecta o patrón '{pattern}'", + "pydantic_value_error_time": "Formato de hora non válido", + "pydantic_value_error_url_extra": "URL non válido, atopáronse caracteres extra despois do URL: '{extra}'", + "pydantic_value_error_url_host": "URL do servidor non válido", + "pydantic_value_error_url_port": "Porto do URL non válido, o porto non pode superar 65535", + "pydantic_value_error_url_scheme": "Falta ou non é válido o esquema URL", + "regenconf_dry_pending_applying": "Comprobando as configuracións pendentes que deberían aplicarse á categoría '{category}'…", + "regenconf_failed": "Non se rexenerou a configuración para a categoría(s): {categories}", + "regenconf_file_backed_up": "Ficheiro de configuración '{conf}' copiado a '{backup}'", + "regenconf_file_copy_failed": "Non se puido copiar o novo ficheiro de configuración '{new}' a '{conf}'", + "regenconf_file_kept_back": "Era de agardar que o ficheiro de configuración '{conf}' fose eliminado por regen-conf (categoría {category}) mais foi mantido.", + "regenconf_file_manually_modified": "O ficheiro de configuración '{conf}' foi modificado manualmente e non vai ser actualizado", + "regenconf_file_manually_removed": "O ficheiro de configuración '{conf}' foi eliminado manualmente e non será creado", + "regenconf_file_remove_failed": "Non se puido eliminar o ficheiro de configuración '{conf}'", + "regenconf_file_removed": "Eliminado o ficheiro de configuración '{conf}'", + "regenconf_file_updated": "Actualizado o ficheiro de configuración '{conf}'", + "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_now_managed_by_yunohost": "O ficheiro de configuración '{conf}' agora está xestionado por YunoHost (categoría {category}).", + "regenconf_pending_applying": "Aplicando a configuración pendente para categoría '{category}'…", + "regenconf_up_to_date": "A configuración xa está ao día para a categoría '{category}'", + "regenconf_updated": "Configuración actualizada para '{category}'", + "regenconf_would_be_updated": "A configuración debería ser actualizada para a categoría '{category}'", + "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", + "regex_with_only_domain": "Agora xa non podes usar un regex para o dominio, só para ruta", + "registrar_infos": "Info da rexistradora", + "restore_already_installed_app": "Unha app con ID '{app}' xa está instalada", + "restore_already_installed_apps": "As seguintes apps non se poden restablecer porque xa están instaladas: {apps}", + "restore_backup_too_old": "Este arquivo de apoio non pode ser restaurado porque procede dunha versión YunoHost demasiado antiga.", + "restore_cleaning_failed": "Non se puido despexar o directorio temporal de restablecemento", + "restore_complete": "Restablecemento completado", + "restore_confirm_yunohost_installed": "Tes a certeza de querer restablecer un sistema xa instalado? [{answers}]", + "restore_extracting": "A extraer os ficheiros necesarios desde o arquivo…", + "restore_failed": "Non se puido restablecer o sistema", + "restore_hook_unavailable": "O script de restablecemento para '{part}' non está dispoñible no teu sistema nin no arquivo", + "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_not_enough_disk_space": "Non hai espazo abondo (espazo: {free_space} B, espazo necesario: {needed_space} B, marxe de seguridade: {margin} B)", + "restore_nothings_done": "Nada foi restablecido", + "restore_removing_tmp_dir_failed": "Non se puido eliminar o directorio temporal antigo", + "restore_running_app_script": "A restablecer a app '{app}'…", + "restore_running_hooks": "Executando os ganchos do restablecemento…", + "restore_system_part_failed": "Non se restableceu a parte do sistema '{part}'", + "root_password_changed": "cambiouse o contrasinal de root", + "root_password_desynchronized": "Mudou o contrasinal de administración, pero YunoHost non puido transferir este cambio ao contrasinal root!", + "server_reboot": "Vaise reiniciar o servidor", + "server_reboot_confirm": "Queres reiniciar o servidor inmediatamente? [{answers}]", + "server_shutdown": "Vaise apagar o servidor", + "server_shutdown_confirm": "Queres apagar o servidor inmediatamente? [{answers}]", + "service_add_failed": "Non se puido engadir o servizo '{service}'", + "service_added": "Foi engadido o servizo '{service}'", + "service_already_started": "O servizo '{service}' xa se está a executar", + "service_already_stopped": "O servizo '{service}' xa está detido", + "service_cmd_exec_failed": "Non se puido executar o comando '{command}'", + "service_description_dnsmasq": "Xestiona a resolución de nomes de dominio (DNS)", + "service_description_dovecot": "Permite aos clientes de email acceder/obter o correo (vía IMAP e POP3)", + "service_description_fail2ban": "Protexe contra ataques de forza bruta e outro tipo de ataques desde internet", + "service_description_mysql": "Almacena datos da app (base de datos SQL)", + "service_description_nftables": "Xestiona, abre e pecha a conexións dos portos aos servizos", + "service_description_nginx": "Serve ou proporciona acceso a todos os sitios web hospedados no teu servidor", + "service_description_opendkim": "Asina os correos saíntes usando DKIM para que sexa menos probable que o marquen como spam", + "service_description_postfix": "Utilizado para enviar e recibir emails", + "service_description_postgresql": "Almacena datos da app (Base datos SQL)", + "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_slapd": "Almacena usuarias, dominios e info relacionada", + "service_description_ssh": "Permíteche acceder de xeito remoto ao teu servidor a través dun terminal (protocolo SSH)", + "service_description_yunohost-api": "Xestiona as interaccións entre a interface web de YunoHost e o sistema", + "service_description_yunohost-portal-api": "Xestiona as interaccións entre as diferentes interfaces do portal web e o sistema", + "service_description_yunomdns": "Permíteche chegar ao teu servidor utilizando 'yunohost.local' na túa rede local", + "service_disable_failed": "Non se puido iniciar o servizo '{service}' ao inicio.", + "service_disabled": "O servizo '{service}' xa non vai volver a ser iniciado ao inicio do sistema.", + "service_enable_failed": "Non se puido facer que o servizo '{service}' se inicie automáticamente no inicio.", + "service_enabled": "O servizo '{service}' vai ser iniciado automáticamente no inicio do sistema.", + "service_not_reloading_because_conf_broken": "Non se recargou/reiniciou o servizo '{name}' porque a súa configuración está estragada: {errors}", + "service_reload_failed": "Non se recargou o servizo '{service}'", + "service_reload_or_restart_failed": "Non se recargou ou reiniciou o servizo '{service}'", + "service_reloaded": "Recargado o servizo '{service}'", + "service_reloaded_or_restarted": "O servizo '{service}' foi recargado ou reiniciado", + "service_remove_failed": "Non se eliminou o servizo '{service}'", + "service_removed": "Eliminado o servizo '{service}'", + "service_restart_failed": "Non se reiniciou o servizo '{service}'", + "service_restarted": "Reiniciado o servizo '{service}'", + "service_start_failed": "Non se puido iniciar o servizo '{service}'", + "service_started": "Iniciado o servizo '{service}'", + "service_stop_failed": "Non se puido deter o servizo '{service}'", + "service_stopped": "Detívose o servizo '{service}'", + "service_unknown": "Servizo descoñecido '{service}'", + "session_expired": "Sesión caducada", + "show_tile_cant_be_enabled_for_regex": "Non podes activar 'show_tile' neste intre, porque o URL para o permiso '{permission}' é un regex", + "show_tile_cant_be_enabled_for_url_not_defined": "Non podes activar 'show_tile' neste intre, primeiro tes que definir un URL para o permiso '{permission}'", + "ssowat_conf_generated": "Recreadas as configuracións de SSO e portal", + "system_upgraded": "Sistema actualizado", + "system_username_exists": "Xa existe este nome de usuaria na lista de usuarias do sistema", + "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`.", + "tools_upgrade": "Actualizando paquetes do sistema", + "tools_upgrade_failed": "Non se actualizaron os paquetes: {packages_list}", + "tos_dyndns_acknowledgement": "Elixes rexistrar un dominio DynDNS que é un servizo proporcionado polo proxecto YunoHost. Tendo en conta que os nomes de dominio son un aspecto importante a longo prazo, lembrámosche que leas con atención os Termos dos Servizos, en particular a sección relativa aos nomes de dominio gratuítos: .", + "tos_postinstall_acknowledgement": "O proxecto YunoHost é un equipo de persoas voluntarias que teñen en común a causa de crear un sistema operativo libre para servidores, con nome YunoHost. O software YunoHost publicase con licenza AGPLv3 (). Relacionado con este software a administración do proxecto pon a disposición varios servizos técnicos e da comunidade para varios propósitos. Ao usar estes servizos aceptas rexirte polos Termos dos Servizos: .", + "unable_authenticate": "Fallou a autenticación da sesión", + "unbackup_app": "{app} non vai ser gardada", + "unexpected_error": "Aconteceu un fallo non agardado: {error}", + "unknown_error_reading_file": "Erro descoñecido ao intentar ler o ficheiro {file} (razón: {error})", + "unknown_group": "Grupo '{group}' descoñecido", + "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.", + "unknown_user": "Usuaria '{user}' descoñecida", + "unlimit": "Sen cota", + "unrestore_app": "{app} non vai ser restablecida", + "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}", + "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}", + "updating_apt_cache": "A obter as actualizacións dispoñibles para os paquetes do sistema…", + "upgrading_packages": "Actualizando paquetes…", + "upnp_dev_not_found": "Non se atopa dispositivo UPnP", + "upnp_disabled": "UPnP desactivado", + "upnp_enabled": "UPnP activado", + "upnp_port_open_failed": "Non se puido abrir porto a través de UPnP", + "user_already_exists": "A usuaria '{user}' xa existe", + "user_cannot_delete_last_admin": "A usuaria '{user}' é a última usuaria do grupo 'admins' e non será eliminada.", + "user_created": "Usuaria creada", + "user_creation_failed": "Non se puido crear a usuaria {user}: {error}", + "user_deleted": "Usuaria eliminada", + "user_deletion_failed": "Non se puido eliminar a usuaria {user}: {error}", + "user_home_creation_failed": "Non se puido crear cartafol home '{home}' para a usuaria", + "user_import_bad_file": "O ficheiro CSV non ten o formato correcto e será ignorado para evitar unha potencial perda de datos", + "user_import_bad_line": "Liña incorrecta {line}: {details}", + "user_import_cannot_edit_or_delete_admins": "Non se pode editar ou eliminar '{user}' cunha importación porque é administradora", + "user_import_failed": "A operación de importación de usuarias fracasou", + "user_import_missing_columns": "Faltan as seguintes columnas: {columns}", + "user_import_nothing_to_do": "Ningunha usuaria precisa ser importada", + "user_import_partial_failed": "A operación de importación de usuarias fallou parcialmente", + "user_import_success": "Usuarias importadas correctamente", + "user_unknown": "Usuaria descoñecida: {user}", + "user_update_failed": "Non se actualizou usuaria {user}: {error}", + "user_updated": "Cambiada a info da usuaria", + "visitors": "Visitantes", + "yunohost_already_installed": "YunoHost xa está instalado", + "yunohost_api": "API de YunoHost", + "yunohost_configured": "YunoHost está configurado", + "yunohost_installing": "A instalar YunoHost…", + "yunohost_not_installed": "YunoHost non está instalado correctamente. Executa 'yunohost tools postinstall'", + "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://doc.yunohost.org/admin.", + "migration_0036_apt_lists_file_still_exists": "O ficheiro '{file}' procedente de versións antigas non debería existir. Vaise renomear como '{file}.legacy_bookworm'.", + "app_db_prompt_no_app_database": "Semella que esta app non ten declarada unha base de datos no seu manifesto", + "app_db_prompt_type_not_supported": "A orde non é compatible con este tipo de base de datos: {type}" +} diff --git a/locales/he.json b/locales/he.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/he.json @@ -0,0 +1 @@ +{} diff --git a/locales/hi.json b/locales/hi.json new file mode 100644 index 0000000..d781fba --- /dev/null +++ b/locales/hi.json @@ -0,0 +1,47 @@ +{ + "action_invalid": "अवैध कार्रवाई '{action}'", + "admin_password": "व्यवस्थापक पासवर्ड", + "app_already_installed": "'{app}' पहले से ही इंस्टाल्ड है", + "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name}' , तर्क इन विकल्पों में से होने चाहिए {choices}", + "app_argument_invalid": "तर्क के लिए अमान्य मान '{name}': {error}", + "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_removed": "{app} को अनइन्सटॉल कर दिया गया", + "app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है…", + "app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ?", + "app_unknown": "अनजान एप्लीकेशन", + "app_unsupported_remote_type": "एप्लीकेशन के लिए उन्सुपपोर्टेड रिमोट टाइप इस्तेमाल किया गया", + "app_upgrade_failed": "{app} अपडेट करने में असमर्थ", + "app_upgraded": "{app} अपडेट हो गयी हैं", + "ask_main_domain": "मुख्य डोमेन", + "ask_new_admin_password": "नया व्यवस्थापक पासवर्ड", + "ask_password": "पासवर्ड", + "backup_archive_app_not_found": "'{app}' बैकअप आरचिव में नहीं मिला", + "backup_archive_name_exists": "इस बैकअप आरचिव का नाम पहले से ही मौजूद है", + "backup_archive_name_unknown": "'{name}' इस नाम की लोकल बैकअप आरचिव मौजूद नहीं", + "backup_archive_open_failed": "बैकअप आरचिव को खोलने में असमर्थ", + "backup_cleaning_failed": "टेम्पोरेरी बैकअप डायरेक्टरी को उड़ने में असमर्थ", + "backup_created": "बैकअप सफलतापूर्वक किया गया", + "backup_creation_failed": "बैकअप बनाने में विफल", + "backup_delete_error": "'{path}' डिलीट करने में असमर्थ", + "backup_deleted": "इस बैकअप को डिलीट दिया गया है", + "backup_hook_unknown": "'{hook}' यह बैकअप हुक नहीं मिला", + "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": "बैकअप हुक्स चल रहे है…", + "domain_cert_gen_failed": "सर्टिफिकेट उत्पन करने में असमर्थ", + "domain_created": "डोमेन बनाया गया", + "domain_creation_failed": "डोमेन बनाने में असमर्थ", + "domain_deleted": "डोमेन डिलीट कर दिया गया है", + "domain_deletion_failed": "डोमेन डिलीट करने में असमर्थ", + "domain_dyndns_already_subscribed": "DynDNS डोमेन पहले ही सब्स्क्राइड है", + "file_not_exist": "फ़ाइल मौजूद नहीं है: '{path}'", + "password_too_simple_1": "पासवर्ड को कम से कम 8 वर्ण लंबा होना चाहिए", + "unknown_group": "अज्ञात ग्रुप: '{group}'", + "unknown_user": "अज्ञात उपयोगकर्ता: '{user}'" +} diff --git a/locales/hu.json b/locales/hu.json new file mode 100644 index 0000000..bdd265f --- /dev/null +++ b/locales/hu.json @@ -0,0 +1,16 @@ +{ + "aborting": "Megszakítás.", + "action_invalid": "Érvénytelen művelet '{action}'", + "admin_password": "Adminisztrátori jelszó", + "app_already_installed": "{app} már telepítve van", + "app_already_installed_cant_change_url": "Ez az app már telepítve van. Ezzel a funkcióval az url nem változtatható. Javaslat 'app url változtatás' ha lehetséges.", + "app_argument_choice_invalid": "{name} érvénytelen választás, csak egyike lehet {choices} közül", + "app_argument_invalid": "'{name}' hibás paraméter érték :{error}", + "cannot_open_file": "{file} megnyitása sikertelen (Oka: {error})", + "download_timeout": "{url} régóta nem válaszol, folyamat megszakítva.", + "file_not_exist": "A fájl nem létezik: '{path}'", + "invalid_url": "Helytelen URL: {url} (biztos létezik az oldal?)", + "password_too_simple_1": "A jelszónak legalább 8 karakter hosszúnak kell lennie", + "unknown_group": "Ismeretlen csoport: '{group}'", + "unknown_user": "Ismeretlen felhasználó: '{user}'" +} diff --git a/locales/id.json b/locales/id.json new file mode 100644 index 0000000..6656017 --- /dev/null +++ b/locales/id.json @@ -0,0 +1,757 @@ +{ + "aborting": "Membatalkan.", + "action_invalid": "Tindakan tidak valid '{action}'", + "additional_urls_already_added": "URL tambahan '{url}' sudah ditambahkan pada URL tambahan untuk perizinan '{permission}'", + "additional_urls_already_removed": "URL tambahan '{url}' sudah disingkirkan pada URL tambahan untuk perizinan '{permission}'", + "admin_password": "Kata sandi administrasi", + "admins": "Admin", + "all_users": "Semua pengguna YunoHost", + "already_up_to_date": "Tak ada yang harus dilakukan. Semuanya sudah mutakhir.", + "app_action_broke_system": "Tindakan ini sepertinya telah merusak layanan-layanan penting ini: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Layanan yang dibutuhkan ini harus aktif untuk menjalankan tindakan ini: {services}. Coba memulai ulang layanan tersebut untuk melanjutkan (dan mungkin melakukan penyelidikan mengapa layanan tersebut nonaktif).", + "app_action_failed": "Gagal menjalankan tindakan {action} untuk aplikasi {app}", + "app_already_installed": "{app} sudah terpasang", + "app_already_installed_cant_change_url": "Aplikasi ini sudah terpasang. URL tidak dapat diubah hanya dengan ini. Periksa `app changeurl` jika tersedia.", + "app_arch_not_supported": "Aplikasi ini hanya bisa dipasang pada arsitektur {required}, tapi arsitektur peladen Anda adalah {current}", + "app_argument_choice_invalid": "Pilih yang valid untuk argumen '{name}': '{value}' tidak termasuk pada pilihan yang tersedia ({choices})", + "app_argument_invalid": "Pilih yang valid untuk argumen '{name}': {error}", + "app_change_url_failed": "Tidak dapat mengubah URL untuk {app}: {error}", + "app_change_url_identical_domains": "Domain/url_path yang lama dan baru identik ('{domain}{path}'), tak ada yang perlu dilakukan.", + "app_change_url_no_script": "Aplikasi '{app_name}' belum mendukung pengubahan URL. Mungkin Anda harus memperbaruinya.", + "app_change_url_require_full_domain": "{app} tidak dapat dipindah ke URL baru ini karena ini memerlukan domain penuh (tanpa jalur = /)", + "app_change_url_script_failed": "Galat terjadi di skrip pengubahan URL", + "app_change_url_success": "URL {app} sekarang adalah {domain}{path}", + "app_config_unable_to_apply": "Gagal menerapkan nilai-nilai panel konfigurasi.", + "app_config_unable_to_read": "Gagal membaca nilai-nilai panel konfigurasi.", + "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_extraction_failed": "Tidak dapat mengekstrak berkas pemasangan", + "app_failed_to_download_asset": "Gagal mengunduh aset '{source_id}' ({url}) untuk {app}: {out}", + "app_full_domain_unavailable": "Maaf, aplikasi ini harus dipasang pada domain sendiri, namun aplikasi lain sudah terpasang pada domain '{domain}'. Anda dapat menggunakan subdomain hanya untuk aplikasi ini.", + "app_id_invalid": "ID aplikasi tidak sah", + "app_install_failed": "Tidak dapat memasang {app}: {error}", + "app_install_files_invalid": "Berkas ini tidak dapat dipasang", + "app_install_script_failed": "Sebuah kesalahan terjadi pada skrip pemasangan aplikasi", + "app_location_unavailable": "URL ini mungkin tidak tersedia atau terjadi konflik dengan aplikasi yang telah terpasang:\n{apps}", + "app_make_default_location_already_used": "Tidak dapat membuat '{app}' menjadi aplikasi baku untuk domain, '{domain}' telah dipakai oleh '{other_app}'", + "app_manifest_install_ask_admin": "Pilih seorang administrator untuk aplikasi ini", + "app_manifest_install_ask_domain": "Pilih di domain mana aplikasi ini harus dipasang", + "app_manifest_install_ask_init_admin_permission": "Siapa yang boleh mengakses fitur admin untuk aplikasi ini? (Ini bisa diubah nanti)", + "app_manifest_install_ask_init_main_permission": "Siapa yang boleh mengakses aplikasi ini? (Ini bisa diubah nanti)", + "app_manifest_install_ask_is_public": "Bolehkan aplikasi ini dibuka untuk pengunjung awanama?", + "app_manifest_install_ask_password": "Pilih kata sandi administrasi untuk aplikasi ini", + "app_manifest_install_ask_path": "Pilih jalur URL (setelah domain) dimana aplikasi ini harus dipasang", + "app_not_correctly_installed": "{app} kelihatannya terpasang dengan salah", + "app_not_enough_disk": "Aplikasi ini memerlukan {required} ruang kosong.", + "app_not_enough_ram": "Aplikasi ini memerlukan {required} RAM untuk pemasangan/pembaruan, tapi sekarang hanya tersedia {current} saja.", + "app_not_installed": "Tidak dapat menemukan {app} di daftar aplikasi yang terpasang: {all_apps}", + "app_not_properly_removed": "{app} belum dilepas dengan benar", + "app_packaging_format_not_supported": "Aplikasi ini tidak dapat dipasang karena format pengemasan tidak didukung oleh YunoHost versi Anda. Anda sebaiknya memperbarui sistem Anda.", + "app_remove_after_failed_install": "Menyingkirkan aplikasi setelah kegagalan pemasangan…", + "app_removed": "{app} dilepas", + "app_requirements_checking": "Memeriksa persyaratan pada {app}…", + "app_resource_failed": "Menyediakan, membatalkan penyediaan, atau memperbarui sumber daya pada {app} telah gagal: {error}", + "app_restore_failed": "Tidak dapat memulihkan {app}: {error}", + "app_restore_script_failed": "Galat terjadi di skrip pemulihan aplikasi", + "app_sources_fetch_failed": "Tidak dapat mengambil berkas sumber, apakah URL-nya benar?", + "app_start_backup": "Mengumpulkan berkas untuk dicadangkan pada {app}…", + "app_start_install": "Memasang {app}…", + "app_start_remove": "Menyingkirkan {app}…", + "app_start_restore": "Memulihkan {app}…", + "app_unknown": "Aplikasi tak dikenal", + "app_unsupported_remote_type": "Tidak mendukung type remot yang digunakan pada aplikasi", + "app_upgrade_app_name": "Sedang meningkatkan {app}…", + "app_upgrade_failed": "Tidak dapat memperbarui {app}: {error}", + "app_upgrade_script_failed": "Galat terjadi di skrip pembaruan aplikasi", + "app_upgrade_several_apps": "Aplikasi berikut akan diperbarui: {apps}", + "app_upgrade_some_app_failed": "Beberapa aplikasi tidak dapat diperbarui", + "app_upgraded": "{app} diperbarui", + "app_yunohost_version_not_supported": "Aplikasi ini memerlukan YunoHost >= {required}, tapi versi yang terpasang adalah {current}", + "apps_already_up_to_date": "Semua aplikasi sudah pada versi mutakhir", + "apps_catalog_failed_to_download": "Tidak dapat mengunduh katalog aplikasi {apps_catalog}: {error}", + "apps_catalog_obsolete_cache": "Tembolok katalog aplikasi kosong atau sudah tua.", + "apps_catalog_update_success": "Katalog aplikasi telah diperbarui!", + "apps_catalog_updating": "Memperbarui katalog aplikasi…", + "ask_admin_fullname": "Nama lengkap admin", + "ask_admin_username": "Nama pengguna admin", + "ask_dyndns_recovery_password": "Kata sandi pemulihan DynDNS", + "ask_dyndns_recovery_password_explain": "Pilih kata sandi pemulihan untuk domain DynDNS Anda.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Masukkan kata sandi pemulihan untuk domain DynDNS ini.", + "ask_dyndns_recovery_password_explain_unavailable": "Domain DynDNS ini sudah terdaftar. Jika Anda adalah orang yang pertama kali mendaftarkan domain ini, Anda dapat memasukkan kata sandi pemulihan untuk mengeklaim kembali domain ini.", + "ask_fullname": "Nama lengkap", + "ask_main_domain": "Domain utama", + "ask_new_admin_password": "Kata sandi administrasi baru", + "ask_new_domain": "Domain baru", + "ask_new_path": "Jalur baru", + "ask_password": "Kata sandi", + "ask_user_domain": "Domain yang digunakan untuk alamat surel dan akun XMPP pengguna", + "backup_abstract_method": "Metode pencadangan ini belum diimplementasikan", + "backup_actually_backuping": "Membuat arsip cadangan dari berkas yang dikumpulkan…", + "backup_applying_method_copy": "Menyalin semua berkas ke cadangan…", + "backup_applying_method_custom": "Memanggil metode pencadangan khusus '{method}'…", + "backup_applying_method_tar": "Membuat arsip TAR cadangan…", + "backup_archive_app_not_found": "Tidak dapat menemukan {app} di arsip cadangan", + "backup_archive_broken_link": "Tidak dapat mengakses arsip cadangan (tautan rusak pada {path})", + "backup_archive_cant_retrieve_info_json": "Tidak dapat memuat info pada arsip '{archive}'… Berkas info.json tidak dapat dipulihkan (atau bukan json yang valid).", + "backup_archive_corrupted": "Sepertinya arsip cadangan '{archive}' rusak: {error}", + "backup_archive_name_exists": "Arsip cadangan dengan nama '{name}' ini sudah ada.", + "backup_archive_name_unknown": "Arsip cadangan lokal dengan nama '{name}' tidak diketahui", + "backup_archive_open_failed": "Tidak dapat membuka arsip cadangan", + "backup_archive_system_part_not_available": "Segmen '{part}' tidak tersedia di cadangan ini", + "backup_archive_writing_error": "Tidak bisa menambahkan berkas '{source}' (disebutkan dalam arsip '{dest}') untuk dicadangkan ke dalam arsip yang terkompres '{archive}'", + "backup_ask_for_copying_if_needed": "Apakah Anda ingin melakukan pencadangan menggunakan {size}MB untuk sementara? (Cara ini digunakan karena beberapa berkas tidak dapat disiapkan menggunakan metode yang lebih efisien.)", + "backup_cant_mount_uncompress_archive": "Tidak dapat memasang arsip yang tidak terkompres sebagai proteksi penulisan", + "backup_cleaning_failed": "Tidak dapat menghapus folder cadangan sementara", + "backup_copying_to_organize_the_archive": "Menyalin {size}MB untuk menyusun arsip", + "backup_couldnt_bind": "Tidak dapat memaut {src} ke {dest}.", + "backup_create_size_estimation": "Arsip ini akan mengandung data dengan ukuran {size}.", + "backup_created": "Cadangan dibuat: {name}", + "backup_creation_failed": "Tidak dapat membuat arsip cadangan", + "backup_csv_addition_failed": "Tidak dapat menambahkan berkas ke cadangan dengan berkas CSV", + "backup_csv_creation_failed": "Tidak dapat membuat berkas CSV yang dibutuhkan untuk pemulihan", + "backup_custom_backup_error": "Metode pencadangan khusus tidak dapat melewati langkah 'backup'", + "backup_custom_mount_error": "Metode pencadangan khusus tidak dapat melewati langkah 'mount'", + "backup_delete_error": "Tidak dapat menghapus '{path}'", + "backup_deleted": "Cadangan dihapus: {name}", + "backup_hook_unknown": "Kait cadangan '{hook}' tidak diketahui", + "backup_method_copy_finished": "Salinan cadangan telah selesai", + "backup_method_custom_finished": "Metode pencadangan khusus '{method}' selesai", + "backup_method_tar_finished": "Arsip TAR cadangan dibuat", + "backup_mount_archive_for_restore": "Menyiapkan arsip untuk pemulihan…", + "backup_no_uncompress_archive_dir": "Tidak ada direktori arsip yang tidak terkompres", + "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", + "backup_output_directory_not_empty": "Anda harus memilih direktori yang kosong", + "backup_output_directory_required": "Anda harus menyediakan direktori keluaran untuk cadangan tersebut", + "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.", + "backup_running_hooks": "Menjalankan kait cadangan…", + "backup_system_part_failed": "Tidak dapat mencadangkan bagian sistem '{part}'", + "backup_unable_to_organize_files": "Tidak dapat menggunakan metode cepat untuk mengatur berkas dalam arsip", + "backup_with_no_backup_script_for_app": "Aplikasi '{app}' tidak memiliki skrip pencadangan. Mengabaikan.", + "backup_with_no_restore_script_for_app": "{app} tidak memiliki skrip pemulihan, Anda tidak akan bisa secara otomatis memulihkan cadangan aplikasi ini.", + "cannot_open_file": "Tidak dapat membuka berkas {file} (alasan: {error})", + "cannot_write_file": "Tidak dapat menyimpan berkas {file} (alasan: {error})", + "certmanager_acme_not_configured_for_domain": "Tantangan ACME tidak dapat dijalankan untuk {domain} saat ini karena konfigurasi pada nginx tidak memiliki potongan kode yang sesuai… Pastikan konfigurasi nginx Anda mutakhir menggunakan `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonLE_cert": "Sertifikat untuk domain '{domain}' tidak diterbitkan oleh Let's Encrypt. Tidak dapat memperbarui secara otomatis!", + "certmanager_attempt_to_renew_valid_cert": "Sertifikat untuk domain '{domain}' belum akan kedaluwarsa! (Anda bisa menggunakan --force jika Anda tahu apa yang Anda lakukan)", + "certmanager_attempt_to_replace_valid_cert": "Anda sedang mencoba untuk menimpa sertifikat yang valid untuk domain {domain}! (Gunakan --force untuk melewati ini)", + "certmanager_cannot_read_cert": "Terjadi kesalahan saat mencoba membuka sertifikat saat ini untuk domain {domain} (berkas: {file}), alasan: {reason}", + "certmanager_cert_install_failed": "Pemasangan sertifikat Let's Encrypt gagal untuk {domains}", + "certmanager_cert_install_failed_selfsigned": "Pemasangan sertifikat ditandai sendiri (self-signed) gagal untuk {domains}", + "certmanager_cert_install_success": "Sertifikat Let's Encrypt sekarang sudah terpasang pada domain '{domain}'", + "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}'", + "certmanager_cert_signing_failed": "Tidak dapat memverifikasi sertifikat baru", + "certmanager_certificate_fetching_or_enabling_failed": "Mencoba sertifikat baru pada {domain} tidak dapat digunakan…", + "certmanager_domain_cert_not_selfsigned": "Sertifikat untuk domain {domain} bukan disertifikasi sendiri. Apakah Anda yakin ingin mengubahnya? (Gunakan '--force' jika iya)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Rekaman DNS untuk domain '{domain}' berbeda dengan IP server ini. Silakan periksa kategori 'Catatan DNS' (dasar) dalam diagnosis untuk info lebih lanjut. Jika Anda baru saja memodifikasi rekaman A, silakan menunggu hingga rekaman tersebut disebarkan (beberapa pemeriksa sebaran DNS tersedia online). (Jika Anda tahu apa yang Anda lakukan, gunakan '--no-checks' untuk mematikan pemeriksaan ini.)", + "certmanager_domain_http_not_working": "Domain {domain} sepertinya tidak dapat diakses melalui HTTP. Silakan periksa kategori 'Web' dalam diagnosis untuk info lebih lanjut. (Jika Anda tahu apa yang Anda lakukan, gunakan '--no-checks' untuk mematikan pemeriksaan ini.)", + "certmanager_domain_not_diagnosed_yet": "Belum ada hasil diagnosis untuk domain {domain}. Silakan jalankan kembali diagnosis untuk kategori 'DNS records' dan 'Web' di bagian diagnosis untuk memeriksa apakah domain siap untuk Let's Encrypt. (Atau jika Anda tahu apa yang Anda lakukan, gunakan '--no-checks' untuk mematikan pemeriksaan ini.)", + "certmanager_hit_rate_limit": "Terlalu banyak sertifikat yang telah diterbitkan untuk kumpulan domain {domain} ini baru-baru ini. Silakan coba lagi nanti. Lihat https://letsencrypt.org/docs/rate-limits/ untuk detail lebih lanjut", + "certmanager_no_cert_file": "Tidak dapat membuka berkas sertifikat untuk domain {domain} (berkas: {file})", + "certmanager_self_ca_conf_file_not_found": "Tidak dapat menemukan berkas konfigurasi untuk otoritas teken mandiri (berkas: {file})", + "certmanager_unable_to_parse_self_CA_name": "Tidak dapat menguraikan nama otoritas teken mandiri (berkas: {file})", + "config_action_disabled": "Tidak dapat menjalankan aksi '{action}' karena dinonaktifkan, pastikan untuk memenuhi batasannya. bantuan: {help}", + "config_action_failed": "Gagal menjalankan tindakan '{action}': {error}", + "config_apply_failed": "Gagal menerapkan konfigurasi baru: {error}", + "config_cant_set_value_on_section": "Anda tidak dapat menetapkan satu nilai pun di seluruh bagian konfigurasi.", + "config_forbidden_keyword": "Kata kunci '{keyword}' sudah ada, Anda tidak dapat membuat atau menggunakan panel konfigurasi disertai pertanyaan dengan id ini.", + "config_forbidden_readonly_type": "Tipe '{type}' tidak dapat disetel sebagai hanya baca, gunakan tipe lain untuk mengubah nilai ini (id argumen yang relevan: '{id}').", + "config_no_panel": "Panel konfigurasi tidak ditemukan.", + "config_unknown_filter_key": "Kunci filter '{filter_key}' tidak sesuai.", + "confirm_app_install_danger": "BAHAYA! Aplikasi ini diketahui masih eksperimental (jika tidak secara eksplisit tidak berfungsi)! Anda mungkin TIDAK boleh menginstalnya kecuali Anda tahu apa yang Anda lakukan. TIDAK ADA DUKUNGAN yang akan diberikan jika aplikasi ini tidak berfungsi atau merusak sistem Anda… Jika Anda tetap bersedia mengambil risiko tersebut, ketik '{answers}'", + "confirm_app_install_thirdparty": "BAHAYA! Aplikasi ini bukan bagian dari katalog aplikasi YunoHost. Memasang aplikasi pihak ketiga dapat membahayakan integritas dan keamanan sistem Anda. Mungkin Anda TIDAK boleh menginstalnya kecuali Anda tahu apa yang Anda lakukan. TIDAK ADA DUKUNGAN yang akan diberikan jika aplikasi ini tidak berfungsi atau merusak sistem Anda… Jika Anda tetap bersedia mengambil risiko tersebut, ketik '{answers}'", + "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}] ", + "confirm_app_insufficient_ram": "Aplikasi ini memerlukan lebih banyak RAM untuk diinstal daripada yang saat ini tersedia. Meskipun aplikasi ini dapat berjalan, proses instalasi/peningkatannya memerlukan RAM dalam jumlah besar sehingga server Anda mungkin macet dan gagal total. Jika Anda tetap bersedia mengambil risiko tersebut, ketik '{answers}'", + "confirm_notifications_read": "PERINGATAN: Anda harus memeriksa notifikasi pada aplikasi di atas sebelum melanjutkan, mungkin terdapat hal yang penting untuk diketahui. [{answers}]", + "corrupted_json": "Pembacaan rusak untuk JSON {ressource} (alasan: {error})", + "corrupted_toml": "Pembacaan rusak untuk TOML {ressource} (alasan: {error})", + "corrupted_yaml": "Pembacaan rusak untuk YAML {ressource} (alasan: {error})", + "danger": "Peringatan:", + "diagnosis_apps_allgood": "Semua aplikasi yang dipasang mengikuti panduan pemaketan yang baik", + "diagnosis_apps_bad_quality": "Aplikasi tersebut saat ini ditandai sebagai rusak pada katalog aplikasi YunoHost. Ini mungkin hanya isu sementara ketika pengelola berupaya memperbaiki masalah tersebut. Untuk sementara, peningkatan versi aplikasi ini dinonaktifkan.", + "diagnosis_apps_broken": "Aplikasi tersebut saat ini ditandai sebagai rusak pada katalog aplikasi YunoHost. Ini mungkin hanya isu sementara ketika pengelola berupaya memperbaiki masalah tersebut. Untuk sementara, peningkatan versi aplikasi ini dinonaktifkan.", + "diagnosis_apps_deprecated_practices": "Versi aplikasi yang dipasang ini masih menggunakan praktik pengemasan yang lama. Anda lebih baik untuk memperbarui aplikasi tersebut.", + "diagnosis_apps_issue": "Sebuah masalah ditemukan pada aplikasi {app}", + "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.", + "diagnosis_apps_outdated_ynh_requirement": "Versi terinstal aplikasi ini hanya memerlukan yunohost >= 2.x atau 3.x, yang cenderung menunjukkan bahwa versi tersebut tidak mutakhir dengan praktik pengemasan dan bantuan yang direkomendasikan. Anda harus benar-benar mempertimbangkan untuk memutakhirkannya.", + "diagnosis_backports_in_sources_list": "Sepertinya apt (manajer paket) dikonfigurasi untuk menggunakan depot backports. Kecuali Anda benar-benar tahu apa yang Anda lakukan, kami sangat tidak menyarankan memasang paket dari backport, karena kemungkinan besar akan menjadi labil atau konflik pada sistem Anda.", + "diagnosis_basesystem_hardware": "Arsitektur perangkat keras peladen adalah {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Model server adalah {model}", + "diagnosis_basesystem_host": "Peladen memakai Debian {debian_version}", + "diagnosis_basesystem_kernel": "Peladen memakai kernel Linux {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Anda menjalankan versi paket YunoHost yang tidak konsisten… sepertinya karena kegagalan atau sebagian pembaruan.", + "diagnosis_basesystem_ynh_main_version": "Peladen memakai YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "versi {package}: {version} ({repo})", + "diagnosis_cache_still_valid": "(Tembolok masih valid untuk diagnosis {category}. Belum akan didiagnosis ulang!)", + "diagnosis_cant_run_because_of_dep": "Tidak dapat menjalankan diagnosis pada {category} ketika ada masalah utama yang terkait dengan {dep}.", + "diagnosis_description_apps": "Aplikasi", + "diagnosis_description_basesystem": "Sistem basis", + "diagnosis_description_dnsrecords": "Rekaman DNS", + "diagnosis_description_ip": "Konektivitas internet", + "diagnosis_description_mail": "Surel", + "diagnosis_description_ports": "Penyingkapan porta", + "diagnosis_description_regenconf": "Konfigurasi sistem", + "diagnosis_description_services": "Status layanan", + "diagnosis_description_systemresources": "Sumber daya sistem", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Penyimpanan {mountpoint} (di perangkat {device}) hanya tinggal memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total}).", + "diagnosis_diskusage_ok": "Penyimpanan {mountpoint} (di perangkat {device}) masih memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total})!", + "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!", + "diagnosis_display_tip": "Untuk melihat masalah yang ditemukan, Anda bisa ke bagian Diagnosis di administrasi web atau jalankan 'yunohost diagnosis show --issues --human-readable'.", + "diagnosis_dns_bad_conf": "Beberapa rekaman DNS untuk domain {domain} ada yang tidak ada atau salah (kategori {category})", + "diagnosis_dns_discrepancy": "Data DNS berikut tampaknya tidak mengikuti konfigurasi yang disarankan:
Tipe: {type}
Nama: {name}
Nilai saat ini: < code>{current}
Nilai yang diharapkan: {content}", + "diagnosis_dns_good_conf": "Rekaman DNS untuk domain {domain} sudah diatur dengan benar (kategori {category})", + "diagnosis_dns_missing_record": "Sesuai dengan konfigurasi DNS yang direkomendasikan, Anda harus menambahkan data DNS dengan info berikut.
Jenis: {type}
Nama: {name}
Nilai: {content}", + "diagnosis_dns_point_to_doc": "Silakan periksa dokumentasi di https://doc.yunohost.org/dns_config jika Anda masih membutuhkan bantuan untuk mengatur rekaman DNS.", + "diagnosis_dns_specialusedomain": "Domain {domain} didasarkan pada domain tingkat atas (TLD) penggunaan khusus seperti .local atau .test dan oleh karena itu tidak diharapkan memiliki data DNS yang sebenarnya.", + "diagnosis_dns_try_dyndns_update_force": "Konfigurasi DNS domain ini secara otomatis dikelola oleh YunoHost. Jika tidak, Anda dapat mencoba memaksa pembaruan menggunakan yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Beberapa domain akan SEGERA kedaluwarsa!", + "diagnosis_domain_expiration_not_found": "Tidak dapat memeriksa tanggal kedaluwarsa untuk beberapa domain", + "diagnosis_domain_expiration_not_found_details": "Informasi WHOIS untuk domain {domain} sepertinya tidak mengandung informasi tentang tanggal kedaluwarsa?", + "diagnosis_domain_expiration_success": "Domain Anda sudah terdaftar dan belum akan kedaluwarsa dalam waktu dekat.", + "diagnosis_domain_expiration_warning": "Beberapa domain akan kedaluwarsa!", + "diagnosis_domain_expires_in": "{domain} kedaluwarsa dalam {days} hari.", + "diagnosis_domain_not_found_details": "Domain {domain} tidak ada di basis data WHOIS atau sudah kedaluwarsa!", + "diagnosis_everything_ok": "Sepertinya semuanya bagus untuk {category}!", + "diagnosis_failed": "Gagal mengambil hasil diagnosis untuk kategori '{category}': {error}", + "diagnosis_failed_for_category": "Diagnosis gagal untuk kategori '{category}': {error}", + "diagnosis_found_errors": "{errors} masalah signifikan ditemukan terkait dengan {category}!", + "diagnosis_found_errors_and_warnings": "Ditemukan {errors} isu penting (dan {warnings} peringatan) terkait dengan {category}!", + "diagnosis_found_warnings": "Ditemukan {warnings} bagian yang dapat ditingkatkan pada {category}.", + "diagnosis_high_number_auth_failures": "Ada sejumlah besar kegagalan autentikasi yang mencurigakan baru-baru ini. Anda mungkin ingin memastikan bahwa fail2ban berjalan dan dikonfigurasi dengan benar, atau menggunakan port khusus untuk SSH seperti yang dijelaskan di https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Sepertinya komputer lain (mungkin router internet Anda) yang menjawab bukannya server Anda.
1. Penyebab paling umum dari isu ini adalah port 80 (dan 443) tidak diteruskan dengan benar ke server Anda.
2. Pada pengaturan yang lebih rumit: pastikan tidak ada firewall atau reverse-proxy yang mengganggu.", + "diagnosis_http_connection_error": "Masalah jaringan: tidak dapat terhubung dengan domain yang diminta, sangat mungkin terputus.", + "diagnosis_http_could_not_diagnose": "Tidak dapat mendiagnosis apakah domain dapat dijangkau dari luar pada IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Galat: {error}", + "diagnosis_http_hairpinning_issue": "Jaringan lokal Anda sepertinya tidak mengaktifkan hairpinning.", + "diagnosis_http_hairpinning_issue_details": "Ini mungkin karena kotak / router ISP Anda. Akibatnya, orang dari luar jaringan lokal Anda akan dapat mengakses server Anda seperti yang diharapkan, tetapi bukan orang dari dalam jaringan lokal (seperti Anda, mungkin?) saat menggunakan nama domain atau IP global. Anda mungkin dapat memperbaiki situasi ini dengan melihat https://doc.yunohost.org/dns_local_network", + "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.", + "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 menurut Anda sudah sesuai, terapkan perubahannya menggunakan yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "Domain {domain} bisa dicapai dengan HTTP dari luar jaringan lokal.", + "diagnosis_http_partially_unreachable": "Domain {domain} tampaknya tidak dapat dijangkau melalui HTTP dari luar jaringan lokal di IPv{failed}, meskipun berfungsi di IPv{passed}.", + "diagnosis_http_special_use_tld": "Domain {domain} berdasarkan pada domain tingkat atas (TLD) penggunaan khusus seperti .local atau .test dan oleh karena itu tidak diharapkan untuk diekspos di luar jaringan lokal.", + "diagnosis_http_timeout": "Waktu habis saat mencoba menghubungi server Anda dari luar. Tampaknya tidak dapat dijangkau.
1. Penyebab paling umum dari masalah ini adalah port 80 (dan 443) tidak diteruskan dengan benar ke server Anda.
2. Anda juga harus memastikan bahwa layanan nginx berjalan
3. Pada pengaturan yang lebih rumit: pastikan tidak ada firewall atau reverse-proxy yang mengganggu.", + "diagnosis_http_unreachable": "Domain {domain} tampaknya tidak dapat dijangkau melalui HTTP dari luar jaringan lokal.", + "diagnosis_ignore_already_filtered": "(Sudah ada filter diagnosis {category} dengan kriteria ini)", + "diagnosis_ignore_criteria_error": "Kriteria harus dalam bentuk key=value (cth. domain=yolo.test)", + "diagnosis_ignore_filter_added": "Menambahkan filter diagnosis {category}", + "diagnosis_ignore_filter_removed": "Menyingkirkan filter diagnosis {category}", + "diagnosis_ignore_missing_criteria": "Anda harus memberikan setidaknya satu kriteria sebagai kategori diagnosis untuk diabaikan", + "diagnosis_ignore_no_filter_found": "(Tidak ada filter {category} diagnosis dengan kriteria ini yang harus disingkirkan)", + "diagnosis_ignore_no_issue_found": "Tidak menemukan isu yang sesuai dengan kriteria tersebut.", + "diagnosis_ignored_issues": "(+ {nb_ignored} isu yang diabaikan)", + "diagnosis_ip_broken_dnsresolution": "Resolusi nama domain tampaknya rusak karena beberapa alasan… Apakah firewall memblokir permintaan DNS?", + "diagnosis_ip_broken_resolvconf": "Resolusi nama domain tampaknya rusak pada server Anda, yang tampaknya terkait dengan /etc/resolv.conf tidak mengarah ke 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "Peladen ini terhubung ke internet lewat IPv4!", + "diagnosis_ip_connected_ipv6": "Peladen ini terhubung ke internet lewat IPv6!", + "diagnosis_ip_dnsresolution_working": "Resolusi nama domain bisa digunakan!", + "diagnosis_ip_global": "IP Global: {global}", + "diagnosis_ip_local": "IP Lokal: {local}", + "diagnosis_ip_no_ipv4": "Peladen ini sepertinya tidak memiliki IPv4.", + "diagnosis_ip_no_ipv6": "Peladen ini sepertinya tidak memiliki 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://doc.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://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "Peladen ini sepertinya tidak terhubung dengan internet sama sekali?", + "diagnosis_ip_weird_resolvconf": "Resolusi DNS tampaknya berfungsi, namun sepertinya Anda menggunakan /etc/resolv.conf khusus.", + "diagnosis_ip_weird_resolvconf_details": "Berkas /etc/resolv.conf harus berupa symlink ke /etc/resolvconf/run/resolv.conf itu sendiri yang menunjuk ke 127.0.0.1 (dnsmasq). Jika Anda ingin mengatur DNS resolver secara manual, silakan edit /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blocklist_listed_by": "IP atau domain Anda {item} masuk daftar hitam pada {blocklist_name}", + "diagnosis_mail_blocklist_ok": "IP dan domain yang digunakan oleh peladen ini sepertinya tidak didaftarhitamkan", + "diagnosis_mail_blocklist_reason": "Alasan pendaftarhitaman adalah: {reason}", + "diagnosis_mail_blocklist_website": "Setelah mengidentifikasi alasan Anda terdaftar dan memperbaikinya, silakan meminta IP atau domain Anda agar disingkirkan di {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Layanan non-SMTP dijawab pada port 25 di IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Ini mungkin disebabkan oleh mesin lain yang menjawab bukannya server Anda.", + "diagnosis_mail_ehlo_could_not_diagnose": "Tidak dapat mendiagnosis apakah server pos postfix dapat dijangkau dari luar pada IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Galat: {error}", + "diagnosis_mail_ehlo_ok": "Server pos SMTP dapat dijangkau dari luar sehingga dapat menerima email!", + "diagnosis_mail_ehlo_unreachable": "Server pos SMTP tidak dapat dijangkau dari luar pada IPv{ipversion}. Itu tidak akan dapat menerima email.", + "diagnosis_mail_ehlo_unreachable_details": "Tidak dapat membuka koneksi pada port 25 ke server Anda di IPv{ipversion}. Tampaknya tidak dapat dijangkau.
1. Penyebab paling umum dari masalah ini adalah port 25 tidak diteruskan dengan benar ke server Anda.
2. Anda juga harus memastikan bahwa layanan postfix berjalan.
3. Pada pengaturan yang lebih rumit: pastikan tidak ada firewall atau proxy terbalik yang mengganggu.", + "diagnosis_mail_ehlo_wrong": "Server pos SMTP yang berbeda menjawab pada IPv{ipversion}. Server Anda mungkin tidak dapat menerima email.", + "diagnosis_mail_ehlo_wrong_details": "EHLO yang diterima oleh pemeriksa jarak jauh di IPv{ipversion} berbeda dengan domain server Anda.
EHLO yang diterima: {wrong_ehlo}
Diharapkan: {right_ehlo}
Penyebab paling umum dari masalah ini adalah port 25 tidak diteruskan dengan benar ke server Anda. Alternatifnya, pastikan tidak ada firewall atau reverse-proxy yang mengganggu.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Reverse-DNS tidak dikonfigurasi dengan benar pada IPv{ipversion}. Beberapa surel mungkin gagal terkirim atau ditandai sebagai spam.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Reverse-DNS saat ini: {rdns_domain}
Nilai yang diharapkan: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "Tidak ada reverse-DNS yang ditentukan dalam IPv{ipversion}. Beberapa surel mungkin gagal terkirim atau ditandai sebagai spam.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Beberapa penyedia tidak mengizinkan Anda mengonfigurasi reverse-DNS (atau fitur mereka mungkin rusak…). Jika Anda mengalami isu karena hal ini, pertimbangkan solusi berikut:
- Beberapa ISP menyediakan alternatif menggunakan server pos relai ini menyiratkan bahwa relai akan dapat memata-matai lalu lintas surel Anda.
- Alternatif ramah privasi adalah menggunakan VPN *dengan IP publik khusus* untuk melewati batasan semacam ini. Lihat https://doc.yunohost.org/vpn_advantage
- Atau bisa juga ke beralih ke penyedia lain", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Beberapa penyedia tidak mengizinkan Anda mengonfigurasi reverse-DNS (atau fitur mereka mungkin rusak…). Jika reverse-DNS Anda dikonfigurasi dengan benar untuk IPv4, Anda dapat mencoba menonaktifkan penggunaan IPv6 saat mengirim surel dengan menjalankan yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Catatan: dengan solusi tersebut berarti Anda tidak akan bisa mengirim atau menerima surel dari beberapa server khusus IPv6 di luar sana.", + "diagnosis_mail_fcrdns_nok_details": "Anda harus terlebih dahulu mencoba mengonfigurasi reverse-DNS dengan {ehlo_domain} di antarmuka router internet atau antarmuka penyedia hosting Anda. (Beberapa penyedia hosting mungkin meminta Anda mengirimi mereka tiket dukungan untuk ini).", + "diagnosis_mail_fcrdns_ok": "Reverse-DNS Anda telah dikonfigurasi dengan benar!", + "diagnosis_mail_outgoing_port_25_blocked": "Server pos SMTP tidak dapat mengirim email ke server lain karena port keluar 25 diblokir pada IPv{ipversion}.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Anda harus terlebih dahulu mencoba membuka blokir port keluar 25 di antarmuka router internet atau antarmuka penyedia hosting Anda. (Beberapa penyedia hosting mungkin meminta Anda mengirimi mereka tiket dukungan untuk ini).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Beberapa penyedia tidak mengizinkan Anda membuka blokir port keluar 25 karena mereka tidak peduli dengan Netralitas Net.
- Beberapa dari mereka memberikan alternatif menggunakan server pos relai meskipun hal ini menyiratkan bahwa relai akan dapat memata-matai lalu lintas surel Anda.
- Alternatif yang lebih ramah privasi adalah menggunakan VPN *dengan IP publik khusus* untuk melewati batasan semacam ini . Lihat https://doc.yunohost.org/vpn_advantage
- Anda juga dapat mempertimbangkan untuk beralih ke penyedia yang lebih ramah terhadap netralitas", + "diagnosis_mail_outgoing_port_25_ok": "Server pos SMTP dapat mengirim surel (port keluar 25 tidak diblokir).", + "diagnosis_mail_queue_ok": "{nb_pending} surel tertunda di antrean pos", + "diagnosis_mail_queue_too_big": "Terlalu banyak surel yang tertunda dalam antrean pos ({nb_pending} surel)", + "diagnosis_mail_queue_unavailable": "Tidak dapat melihat jumlah surel yang tertunda dalam antrean", + "diagnosis_mail_queue_unavailable_details": "Galat: {error}", + "diagnosis_never_ran_yet": "Sepertinya server ini baru saja tertata dan belum ada laporan diagnosis yang ditampilkan. Anda harus memulai dengan menjalankan diagnosis lengkap, baik dari webadmin atau menggunakan 'yunohost diagnosis run' dari baris perintah.", + "diagnosis_no_cache": "Belum ada cache diagnosis untuk kategori '{category}'", + "diagnosis_package_installed_from_sury": "Beberapa paket sistem harus diturunkan versinya", + "diagnosis_package_installed_from_sury_details": "Beberapa paket secara tidak sengaja dipasang dari depot pihak ketiga bernama Sury. Tim YunoHost meningkatkan strategi dalam menangani paket tersebut, namun diperkirakan bahwa beberapa pengaturan aplikasi yang terpasang PHP7.3 saat masih dalam Stretch masih memiliki beberapa inkonsistensi. Untuk memperbaiki situasi ini, Anda harus mencoba menjalankan perintah berikut: {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "Tidak dapat mendiagnosis apakah port dapat dijangkau dari luar pada IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Galat: {error}", + "diagnosis_ports_forwarding_tip": "Untuk memperbaiki masalah ini, kemungkinan besar Anda perlu mengonfigurasi penerusan port pada router internet Anda seperti yang dijelaskan di https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Mengekspos port ini diperlukan untuk fitur {category} (layanan {service})", + "diagnosis_ports_ok": "Porta {port} tercapai dari luar.", + "diagnosis_ports_partially_unreachable": "Porta {port} tidak tercapai dari luar lewat IPv{failed}.", + "diagnosis_ports_unreachable": "Porta {port} tidak tercapai dari luar.", + "diagnosis_processes_killed_by_oom_reaper": "Beberapa proses baru-baru ini dihentikan oleh sistem karena kehabisan memori. Hal ini biasanya merupakan gejala dari kurangnya memori pada sistem atau proses yang memakan terlalu banyak memori. Ringkasan proses yang dihentikan:\n{kills_summary}", + "diagnosis_ram_low": "Sistem memiliki {available} ({available_percent}%) RAM yang tersedia (dari {total}). Hati-hati.", + "diagnosis_ram_ok": "Sistem masih memiliki {available} ({available_percent}%) RAM yang tersedia dari {total}.", + "diagnosis_ram_verylow": "Sistem hanya memiliki {available} ({available_percent}%) RAM yang tersedia! (dari {total})", + "diagnosis_regenconf_allgood": "Semua berkas konfigurasi sesuai dengan rekomendasi konfigurasi!", + "diagnosis_regenconf_manually_modified": "Berkas konfigurasi {file} sepertinya telah diubah manual.", + "diagnosis_regenconf_manually_modified_details": "Ini mungkin OK jika Anda tahu apa yang Anda lakukan! YunoHost akan berhenti memperbarui file ini secara otomatis… Namun berhati-hatilah karena pemutakhiran YunoHost mungkin berisi perubahan penting yang disarankan. Jika mau, Anda dapat memeriksa perbedaannya dengan yunohost tools regen-conf {category} --dry-run --with-diff dan memaksa reset ke konfigurasi yang disarankan dengan yunohost tools regen-conf {category} --force", + "diagnosis_rfkill_wifi": "Kartu Wi-Fi dinonaktifkan dan peringatan sistem mungkin akan mencegah pemasangan aplikasi", + "diagnosis_rfkill_wifi_details": "Peringatan ini muncul di banyak keluaran perintah, sehingga merusak beberapa aplikasi. Biasanya Anda diminta untuk menentukan kode negara dengan perintah sudo raspi-config. Galat yang muncul:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "Sistem berkas root hanya memiliki total {space} yang cukup mengkhawatirkan! Kemungkinan besar Anda akan kehabisan ruang disket dengan sangat cepat! Disarankan untuk memiliki setidaknya 16 GB untuk sistem berkas root.", + "diagnosis_rootfstotalspace_warning": "Sistem file root hanya memiliki total {space}. Ini mungkin oke, tapi hati-hati karena pada akhirnya Anda mungkin akan kehabisan ruang disket dengan cepat… Disarankan untuk memiliki setidaknya 16 GB untuk sistem file root.", + "diagnosis_security_vulnerable_to_meltdown": "Sepertinya sistem Anda rentan terhadap kerentanan keamanan Meltdown", + "diagnosis_security_vulnerable_to_meltdown_details": "Untuk memperbaiki ini, sebaiknya perbarui sistem Anda dan mulai ulang untuk memuat kernel linux yang baru (atau hubungi penyedia peladen Anda jika itu tidak bekerja). Kunjungi https://meltdownattack.com/ untuk informasi lebih lanjut.", + "diagnosis_services_bad_status": "Layanan {service} {status} :(", + "diagnosis_services_bad_status_tip": "Anda dapat mencoba mengulang layanan, dan jika tidak berhasil, lihat log layanan pada webadmin (dari baris perintah, Anda dapat melakukannya dengan yunohost service restart {service} dan yunohost service log {service} ).", + "diagnosis_services_conf_broken": "Konfigurasi rusak untuk layanan {service}!", + "diagnosis_services_running": "Layanan {service} berjalan!", + "diagnosis_sshd_config_inconsistent": "Sepertinya port SSH telah dimodifikasi secara manual di /etc/ssh/sshd_config. Sejak YunoHost 4.2, pengaturan global baru 'security.ssh.ssh_port' tersedia untuk menghindari pengeditan konfigurasi secara manual.", + "diagnosis_sshd_config_inconsistent_details": "Silakan jalankan yunohost settings set security.ssh.ssh_port -v PORT_SSH_ANDA untuk menentukan port SSH, dan periksa yunohost tools regen-conf ssh --dry-run --with-diff dan yunohost tools regen-conf ssh --force untuk mengatur ulang konfigurasi Anda sesuai rekomendasi YunoHost.", + "diagnosis_sshd_config_insecure": "Konfigurasi SSH tampaknya telah dimodifikasi secara manual, dan tidak aman karena tidak berisi pedoman 'AllowGroups' atau 'AllowUsers' untuk membatasi akses kepada pengguna yang berwenang.", + "diagnosis_swap_none": "Sistem tidak memiliki swap sama sekali. Anda harus mempertimbangkan untuk menambahkan setidaknya {recommended} swap untuk menghindari situasi di mana sistem kehabisan memori.", + "diagnosis_swap_notsomuch": "Sistem hanya memiliki {total} swap. Anda harus mempertimbangkan untuk memiliki setidaknya {recommended} untuk menghindari situasi di mana sistem kehabisan memori.", + "diagnosis_swap_ok": "Sistem ini memiliki {total} swap!", + "diagnosis_swap_tip": "Harap berhati-hati dan sadari bahwa jika server adalah hosting swap pada kartu SD atau penyimpanan SSD, hal ini dapat mengurangi masa pakai perangkat secara drastis.", + "diagnosis_unknown_categories": "Kategori berikut tidak diketahui: {categories}", + "diagnosis_using_stable_codename": "apt (pengelola paket sistem) saat ini dikonfigurasi untuk memasang paket dari nama kode 'stable', bukan nama kode versi Debian saat ini (bullseye).", + "diagnosis_using_stable_codename_details": "Biasanya hal ini disebabkan oleh kesalahan konfigurasi dari penyedia hosting Anda. Ini berbahaya, karena segera setelah versi Debian berikutnya menjadi 'stable' yang baru, apt akan melakukan upgrade pada semua paket sistem tanpa melalui prosedur migrasi yang benar. Disarankan untuk memperbaikinya dengan mengedit sumber apt untuk depot dasar Debian, dan mengganti kata kunci stable dengan bullseye. Berkas konfigurasi yang sesuai harus /etc/apt/sources.list, atau berkas dalam /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (manajer paket sistem) saat ini dikonfigurasi agar memasang pemutakhiran 'testing' apa pun untuk inti YunoHost.", + "diagnosis_using_yunohost_testing_details": "Ini mungkin OK jika Anda tahu apa yang Anda lakukan, tapi perhatikan catatan rilis sebelum memasang pemutakhiran YunoHost! Jika Anda ingin menonaktifkan peningkatan 'testing', Anda harus menghapus kata kunci testing dari /etc/apt/sources.list.d/yunohost.list.", + "disk_space_not_sufficient_install": "Ruang disket yang tersisa tidak cukup untuk memasang aplikasi ini", + "disk_space_not_sufficient_update": "Ruang disket yang tersisa tidak cukup untuk memperbarui aplikasi ini", + "domain_cannot_remove_main": "Anda tidak dapat menyingkirkan '{domain}' karena ini adalah domain utama, Anda harus terlebih dahulu menetapkan domain lain sebagai domain utama menggunakan 'yunohost domain main-domain -n '; berikut daftar kandidat domain: {other_domains}", + "domain_cannot_remove_main_add_new_one": "Anda tidak dapat menyingkirkan '{domain}' karena ini adalah domain utama dan satu-satunya domain Anda, Anda harus menambahkan domain lain terlebih dahulu menggunakan 'yunohost domain add ', kemudian menetapkannya sebagai domain utama menggunakan 'yunohost domain main-domain -n ' kemudian Anda dapat menyingkirkan domain '{domain}' menggunakan 'yunohost domain remove {domain}'.", + "domain_cert_gen_failed": "Tidak dapat membuat sertifikat", + "domain_config_acme_eligible": "Kelayakan ACME", + "domain_config_acme_eligible_explain": "Sepertinya domain ini belum siap untuk sertifikat Let's Encrypt. Silakan periksa konfigurasi DNS dan jangkauan server HTTP Anda. Bagian 'Rekaman DNS' dan 'Web' di laman diagnosis dapat membantu Anda memahami apa yang salah dalam konfigurasi.", + "domain_config_api_protocol": "Protokol API", + "domain_config_auth_application_key": "Kunci aplikasi", + "domain_config_auth_application_secret": "Kunci rahasia aplikasi", + "domain_config_auth_consumer_key": "Kunci konsumen", + "domain_config_auth_entrypoint": "Titik entri API", + "domain_config_auth_key": "Kunci otentikasi", + "domain_config_auth_secret": "Rahasia otentikasi", + "domain_config_auth_token": "Token autentikasi", + "domain_config_cert_install": "Pasang sertifikat Let's Encrypt", + "domain_config_cert_issuer": "Otoritas sertifikasi", + "domain_config_cert_no_checks": "Abaikan pemeriksaan diagnosis", + "domain_config_cert_renew": "Perbarui sertifikat Let's Encrypt", + "domain_config_cert_renew_help": "Sertifikat akan diperpanjang secara otomatis selama 15 hari validitas terakhir. Anda dapat memperbaruinya secara manual jika Anda mau. (Tidak direkomendasikan).", + "domain_config_cert_summary": "Status sertifikat", + "domain_config_cert_summary_abouttoexpire": "Sertifikat saat ini akan kedaluwarsa. Akan secara otomatis diperbarui secepatnya.", + "domain_config_cert_summary_expired": "PENTING: Sertifikat saat ini tidak valid! HTTPS tidak akan bekerja sama sekali!", + "domain_config_cert_summary_letsencrypt": "Bagus! Anda menggunakan sertifikat Let's Encrypt yang valid!", + "domain_config_cert_summary_ok": "Oke, sertifikat saat ini terlihat bagus!", + "domain_config_cert_summary_selfsigned": "PERINGATAN: Sertifikat saat ini ditandatangani sendiri. Browser akan menampilkan peringatan seram kepada pengunjung baru!", + "domain_config_cert_validity": "Validitas", + "domain_config_default_app": "Aplikasi baku", + "domain_config_default_app_help": "Orang-orang akan secara otomatis diarahkan ke aplikasi ini ketika membuka domain ini. Jika tidak ada aplikasi yang ditentukan, orang-orang akan diarahkan ke formulir login portal pengguna.", + "domain_config_mail_in": "Surel datang", + "domain_config_mail_out": "Surel keluar", + "domain_created": "Domain dibuat", + "domain_creation_failed": "Tidak dapat membuat domain {domain}: {error}", + "domain_deleted": "Domain dihapus", + "domain_deletion_failed": "Tidak dapat menghapus domain {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "Perintah ini menunjukkan kepada Anda konfigurasi yang *direkomendasikan*. Ini sebenarnya tidak mengatur konfigurasi DNS untuk Anda. Anda bertanggung jawab untuk mengonfigurasi zona DNS di registrar Anda sesuai dengan rekomendasi ini.", + "domain_dns_conf_special_use_tld": "Domain ini berdasarkan pada domain tingkat atas (TLD) penggunaan khusus seperti .local atau .test dan oleh karena itu tidak diharapkan memiliki rekaman DNS yang sesungguhnya.", + "domain_dns_push_already_up_to_date": "Rekaman sudah diperbarui, biarkan saja.", + "domain_dns_push_failed": "Gagal total dalam memperbarui rekaman DNS.", + "domain_dns_push_failed_to_list": "Gagal mencantumkan rekaman saat ini menggunakan API registrar: {error}", + "domain_dns_push_managed_in_parent_domain": "Fitur konfigurasi DNS otomatis dikelola di domain induk {parent_domain}.", + "domain_dns_push_not_applicable": "Fitur konfigurasi DNS otomatis tidak berlaku untuk domain {domain}. Anda harus mengonfigurasi rekaman DNS Anda secara manual dengan mengikuti dokumentasi di https://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "Rekaman DNS diperbarui sebagian: beberapa peringatan/galat dilaporkan.", + "domain_dns_push_record_failed": "Gagal mencatat {action} {type}/{name} : {error}", + "domain_dns_push_success": "Rekaman DNS diperbarui!", + "domain_dns_pushing": "Mendorong rekaman DNS…", + "domain_dns_registrar_experimental": "Sejauh ini, antarmuka dengan API **{registrar}** belum diuji dan ditinjau dengan benar oleh komunitas YunoHost. Dukungan masih **sangat eksperimental** - waspadalah!", + "domain_dns_registrar_managed_in_parent_domain": "Domain ini adalah subdomain dari {parent_domain_link}. Konfigurasi registrar DNS harus dikelola di panel konfigurasi {parent_domain}.", + "domain_dns_registrar_not_supported": "YunoHost tidak dapat secara otomatis mendeteksi registrar yang menangani domain ini. Anda harus mengonfigurasi rekaman DNS Anda secara manual dengan mengikuti dokumentasi di https://doc.yunohost.org/dns_config.", + "domain_dns_registrar_supported": "YunoHost secara otomatis mendeteksi bahwa domain ini ditangani oleh registrar **{registrar}**. Jika Anda mau, YunoHost akan secara otomatis mengonfigurasi zona DNS ini, jika Anda memberikan kredensial API yang sesuai. Anda dapat menemukan dokumentasi mengenai cara mendapatkan kredensial API Anda di halaman ini: https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Anda juga dapat mengonfigurasi rekaman DNS Anda secara manual dengan mengikuti dokumentasi di https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_yunohost": "Domain ini adalah nohost.me / nohost.st / ynh.fr dan oleh karena itu konfigurasi DNS tersebut secara otomatis akan ditangani oleh YunoHost tanpa konfigurasi lebih lanjut. (lihat perintah 'yunohost dyndns update')", + "domain_dyndns_already_subscribed": "Anda sudah berlangganan domain pada DynDNS", + "domain_exists": "Domain telah ada", + "domain_hostname_failed": "Tidak dapat menetapkan nama host baru. Ini mungkin menimbulkan masalah di kemudian hari (mungkin baik-baik saja).", + "domain_registrar_is_not_configured": "Registrar belum dikonfigurasi untuk domain {domain}.", + "domain_remove_confirm_apps_removal": "Menghapus domain ini akan melepas aplikasi berikut:\n{apps}\n\nApakah Anda yakin? [{answers}]", + "domain_uninstall_app_first": "Aplikasi berikut masih terpasang di domain Anda:\n{apps}\n\nSilakan lepas mereka menggunakan 'yunohost app remove id_aplikasi' atau pindahkan ke domain lain menggunakan 'yunohost app change-url id_aplikasi' sebelum melanjutkan ke penghapusan domain", + "domain_unknown": "Domain '{domain}' tidak diketahui", + "domains_available": "Domain yang tersedia:", + "done": "Selesai", + "download_bad_status_code": "{url} menjawab dengan kode status {code}", + "download_ssl_error": "Galat SSL ketika menghubungi {url}", + "download_timeout": "{url} memakan waktu yang lama untuk menjawab, menyerah.", + "download_unknown_error": "Galat ketika mengunduh data dari {url}: {error}", + "downloading": "Mengunduh…", + "dpkg_is_broken": "Anda tidak dapat melakukan ini sekarang karena dpkg/APT (manajer paket sistem) sepertinya dalam keadaan rusak… Anda dapat mencoba menyelesaikan masalah ini melalui koneksi SSH dan menjalankan `sudo apt install --fix-broken` dan/atau `sudo dpkg --configure -a` dan/atau `sudo dpkg --audit`.", + "dpkg_lock_not_available": "Perintah ini tidak dapat dijalankan sekarang karena program lain sepertinya menggunakan kunci pada dpkg (manajer paket sistem)", + "dyndns_could_not_check_available": "Tidak dapat memeriksa apakah {domain} tersedia di {provider}.", + "dyndns_domain_not_provided": "Penyedia DynDNS {provider} tidak dapat menyediakan domain {domain}.", + "dyndns_ip_update_failed": "Tidak dapat memperbarui IP Anda di DynDNS", + "dyndns_ip_updated": "IP Anda diperbarui pada DynDNS", + "dyndns_key_not_found": "Kunci DNS tidak ditemukan di domain tersebut", + "dyndns_no_domain_registered": "Tidak ada domain yang terdaftar pada DynDNS", + "dyndns_no_recovery_password": "Tidak ada kata sandi pemulihan yang ditentukan! Jika Anda kehilangan kendali atas domain ini, Anda perlu menghubungi administrator di tim YunoHost!", + "dyndns_provider_unreachable": "Tidak dapat menghubungi penyedia DynDNS {provider}: YunoHost Anda tidak terhubung dengan benar ke internet atau server dynette sedang kolaps.", + "dyndns_set_recovery_password_denied": "Tidak dapat menyetel kata sandi pemulihan: tidak valid", + "dyndns_set_recovery_password_failed": "Tidak dapat menyetel kata sandi pemulihan: {error}", + "dyndns_set_recovery_password_invalid_password": "Tidak dapat menyetel kata sandi pemulihan: kata sandi tidak cukup kuat", + "dyndns_set_recovery_password_success": "Kata sandi pemulihan berhasil disetel!", + "dyndns_set_recovery_password_unknown_domain": "Tidak dapat menyetel kata sandi pemulihan: domain belum terdaftar", + "dyndns_subscribe_failed": "Tidak dapat berlangganan domain DynDNS: {error}", + "dyndns_subscribed": "Berlangganan domain di DynDNS", + "dyndns_too_many_requests": "Layanan dyndns YunoHost menerima terlalu banyak permintaan dari Anda, tunggu sekitar 1 jam sebelum mencoba lagi.", + "dyndns_unavailable": "Domain '{domain}' tidak tersedia.", + "dyndns_unsubscribe_already_unsubscribed": "Domain sudah berhenti berlangganan", + "dyndns_unsubscribe_denied": "Gagal berhenti berlangganan domain: kredensial tidak valid", + "dyndns_unsubscribe_failed": "Tidak dapat berhenti berlangganan domain DynDNS: {error}", + "dyndns_unsubscribed": "Berhenti berlangganan domain DynDNS", + "error_changing_file_permissions": "Galat ketika mengubah izin untuk {path}: {error}", + "error_removing": "Terjadi galat ketika menghapus {path}: {error}", + "error_writing_file": "Galat ketika menyimpan berkas {file}: {error}", + "extracting": "Mengekstrak…", + "field_invalid": "Bidang '{field}' tidak valid", + "file_does_not_exist": "Berkas {path} tidak ada.", + "file_not_exist": "Berkas tidak ada: '{path}'", + "firewall_reload_failed": "Tidak dapat memuat ulang tembok api. Info lebih lanjut di dalam log.", + "firewall_reloaded": "Tembok api dimuat ulang", + "global_settings_reset_success": "Atur ulang pengaturan global", + "global_settings_setting_admin_strength": "Persyaratan kualitas kata sandi admin", + "global_settings_setting_admin_strength_help": "Persyaratan ini hanya diterapkan saat mengawali atau mengubah kata sandi", + "global_settings_setting_backup_compress_tar_archives": "Memadatkan cadangan", + "global_settings_setting_backup_compress_tar_archives_help": "Ketika membuat cadangan baru, padatkan arsip (.tar.gz) dan bukannya arsip yang tidak dipadatkan (.tar). Catatan : mengizinkan opsi ini berarti membuat cadangan yang telah dipadatkan lebih ringan, namun prosedur pencadangan awal akan jauh lebih lama dan membebani CPU.", + "global_settings_setting_dns_exposure": "Versi IP yang perlu dipertimbangkan pada konfigurasi dan diagnosis DNS", + "global_settings_setting_dns_exposure_help": "NB: Ini hanya mempengaruhi konfigurasi DNS yang disarankan dan pemeriksaan diagnosis. Ini tidak mempengaruhi konfigurasi sistem.", + "global_settings_setting_nginx_compatibility": "Kompatibilitas NGINX", + "global_settings_setting_nginx_compatibility_help": "Kompatibilas versus kompromi keamanan untuk server web NGINX. Mempengaruhi kerahasian (dan aspek terkait keamanan lainnya)", + "global_settings_setting_nginx_redirect_to_https": "Paksa HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Alihkan permintaan HTTP ke HTTPs bawaan (JANGAN MATIKAN kecuali Anda benar-benar tahu apa yang Anda lakukan!)", + "global_settings_setting_passwordless_sudo": "Izinkan pengelola menggunakan 'sudo' tanpa mengetik ulang kata sandinya", + "global_settings_setting_pop3_enabled": "Aktifkan POP3", + "global_settings_setting_pop3_enabled_help": "Aktifkan protokol POP3 untuk peladen surel", + "global_settings_setting_postfix_compatibility": "Kompatibilitas Postfix", + "global_settings_setting_postfix_compatibility_help": "Kompatibilas versus kompromi keamanan untuk server Postfix. Mempengaruhi kerahasian (dan aspek terkait keamanan lainnya)", + "global_settings_setting_root_access_explain": "Pada sistem Linux, 'root' adalah admin mutlak. Dalam konteks YunoHost, masuk SSH sebagai 'root' secara langsung dinonaktifkan sesuai bawaan - kecuali dari jaringan lokal pada server. Anggota grup 'admin' dapat menggunakan perintah sudo untuk bertindak sebagai root dari baris perintah. Namun, akan sangat membantu jika memiliki kata sandi root (yang kuat) untuk melakukan debug pada sistem apabila dengan alasan tertentu admin biasa tidak dapat masuk lagi.", + "global_settings_setting_root_password": "Kata sandi root baru", + "global_settings_setting_root_password_confirm": "Kata sandi root baru (konfirmasi)", + "global_settings_setting_security_experimental_enabled": "Fitur keamanan eksperimental", + "global_settings_setting_security_experimental_enabled_help": "Aktifkan fitur keamanan eksperimental (jangan mengaktifkan ini jika Anda tidak tahu apa yang Anda lakukan!)", + "global_settings_setting_smtp_allow_ipv6": "Perbolehkan IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Perbolehkan penggunaan IPv6 untuk menerima dan mengirim surel", + "global_settings_setting_smtp_backup_mx_domains": "Domain yang digunakan sebagai MX sekunder", + "global_settings_setting_smtp_backup_mx_domains_help": "Izinkan server ini digunakan sebagai domain MX *sekunder* cadangan pada domain yang terdaftar. Ini berarti bahwa jika MX utama untuk domain tersebut tidak dapat dijangkau (misalnya karena gangguan), surel akan tetap dikirim ke server ini, yang akan menyimpannya selama maksimal 20 hari dan mencoba meneruskannya ke tujuan yang sebenarnya setelah kembali aktif. Beberapa domain dapat disediakan, dipisahkan dengan koma.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Daftar surel yang diperbolehkan sebagai MX cadangan SMTP", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Bila digunakan sebagai MX sekunder, daftar lengkap alamat surel penerima yang diizinkan harus disediakan (sebaliknya surel akan ditolak dan dibuang). Beberapa alamat dapat diberikan, dipisahkan dengan koma.", + "global_settings_setting_smtp_relay_enabled": "Aktifkan relai SMTP", + "global_settings_setting_smtp_relay_enabled_help": "Aktifkan relai SMTP yang akan digunakan untuk mengirim surel selain instansi yunohost ini. Berguna jika Anda berada dalam salah satu situasi ini: port 25 Anda diblokir oleh ISP atau penyedia VPS Anda, Anda memiliki IP residental yang terdaftar di DUHL, Anda tidak dapat mengkonfigurasi reverse DNS atau server ini tidak terekspos secara langsung di internet dan Anda ingin menggunakan yang lain untuk mengirim surel.", + "global_settings_setting_smtp_relay_host": "Host relai SMTP", + "global_settings_setting_smtp_relay_password": "Kata sandi relai SMTP", + "global_settings_setting_smtp_relay_port": "Porta relai SMTP", + "global_settings_setting_smtp_relay_user": "Pengguna relai SMTP", + "global_settings_setting_ssh_compatibility": "Kompatibilitas SSH", + "global_settings_setting_ssh_compatibility_help": "Kompatibilitas versus kompromi keamanan untuk server SSH. Mempengaruhi kerahasiaan (dan aspek terkait keamanan lainnya). Lihat https://infosec.mozilla.org/guidelines/openssh untuk informasi lebih lanjut.", + "global_settings_setting_ssh_password_authentication": "Autentikasi kata sandi", + "global_settings_setting_ssh_password_authentication_help": "Izinkan autentikasi kata sandi untuk SSH", + "global_settings_setting_ssh_port": "Porta SSH", + "global_settings_setting_ssh_port_help": "Port kurang dari 1024 lebih dianjurkan untuk mencegah upaya kudeta oleh layanan non-administrator pada mesin jarak jauh. Anda juga sebaiknya menghindari penggunaan port yang sudah digunakan, seperti 80 atau 443.", + "global_settings_setting_user_strength": "Persyaratan kualitas kata sandi pengguna", + "global_settings_setting_user_strength_help": "Persyaratan ini hanya diterapkan saat mengawali atau mengubah kata sandi", + "global_settings_setting_webadmin_allowlist": "Daftar IP pengelola web yang diizinkan", + "global_settings_setting_webadmin_allowlist_enabled": "Aktifkan daftar IP Pengelelola web yang diizinkan", + "global_settings_setting_webadmin_allowlist_enabled_help": "Izinkan hanya beberapa IP untuk mengakses webadmin.", + "global_settings_setting_webadmin_allowlist_help": "Alamat IP diizinkan untuk mengakses webadmin. Notasi CIDR diperbolehkan.", + "good_practices_about_admin_password": "Sekarang Anda akan menentukan kata sandi administrasi baru. Kata sandi harus terdiri dari minimal 8 karakter—meskipun sebaiknya menggunakan kata sandi yang lebih panjang (misalnya parafrasa) dan/atau menggunakan beragam karakter (huruf besar, huruf kecil, angka, dan karakter khusus).", + "good_practices_about_user_password": "Sekarang Anda akan menentukan kata sandi pengguna baru. Kata sandi harus terdiri dari minimal 8 karakter—meskipun sebaiknya menggunakan kata sandi yang lebih panjang (misalnya parafrasa) dan/atau menggunakan beragam karakter (huruf besar, huruf kecil, angka, dan karakter khusus).", + "group_already_exist": "Grup {group} sudah ada", + "group_already_exist_on_system": "Grup {group} sudah ada di dalam grup sistem", + "group_already_exist_on_system_but_removing_it": "Grup {group} sudah ada di dalam grup sistem, tetapi YunoHost akan menyingkirkannya…", + "group_cannot_be_deleted": "Grup {group} tidak dapat dihapus secara manual.", + "group_cannot_edit_all_users": "Grup 'all_users' tidak dapat diedit secara manual. Ini adalah grup khusus yang dimaksudkan untuk menampung semua pengguna yang terdaftar di YunoHost", + "group_cannot_edit_primary_group": "Grup '{group}' tidak dapat diedit secara manual. Ini adalah grup utama yang dimaksudkan untuk menampung hanya satu pengguna tertentu.", + "group_cannot_edit_visitors": "Grup 'pengunjung' tidak dapat diedit secara manual. Ini adalah grup khusus yang mewakili pengunjung anonim", + "group_created": "Kelompok '{group}' dibuat", + "group_creation_failed": "Tidak dapat membuat kelompok '{group}': {error}", + "group_deleted": "Kelompok '{group}' dihapus", + "group_deletion_failed": "Tidak dapat menghapus kelompok '{group}': {error}", + "group_mailalias_add": "Alias email '{mail}' akan ditambahkan ke grup '{group}'", + "group_mailalias_remove": "Alias surel '{mail}' akan dihapus dari kelompok '{group}'", + "group_no_change": "Tidak ada yang perlu dirubah pada grup '{group}'", + "group_unknown": "Grup '{group}' tidak diketahui", + "group_update_aliases": "Memperbarui alias untuk grup '{group}'", + "group_update_failed": "Tidak dapat memperbarui grup '{group}': {error}", + "group_updated": "Kelompok '{group}' diperbarui", + "group_user_add": "Pengguna '{user}' akan ditambahkan ke grup '{group}'", + "group_user_already_in_group": "Pengguna {user} sudah ada di grup {group}", + "group_user_not_in_group": "Pengguna {user} tidak ada dalam grup {group}", + "group_user_remove": "Pengguna '{user}' akan disingkirkan dari grup '{group}'", + "hook_exec_failed": "Tidak dapat menjalankan skrip: {path}", + "hook_exec_not_terminated": "Skrip tidak selesai dengan tepat: {path}", + "hook_json_return_error": "Tidak dapat membaca jawaban dari pengait {path}. Galat: {msg}. Konten mentah: {raw_content}", + "hook_list_by_invalid": "Properti ini tidak dapat digunakan untuk mencantumkan pengait", + "hook_name_unknown": "Nama pengait '{name}' tidak diketahui", + "installation_complete": "Pemasangan selesai", + "invalid_credentials": "Nama pengguna atau kata sandi salah", + "invalid_number": "Harus berupa angka", + "invalid_regex": "Regex tidak valid:'{regex}'", + "invalid_shell": "Shell tidak valid: {shell}", + "invalid_url": "Gagal terhubung dengan {url}... mungkin layanan tersebut sedang turun, atau Anda tidak terhubung dengan benar ke internet di IPv4/IPv6.", + "ldap_attribute_already_exists": "Atribut LDAP '{attribute}' sudah ada dengan nilai '{value}'", + "ldap_server_down": "Tidak dapat menjangkau server LDAP", + "ldap_server_is_down_restart_it": "Layanan LDAP tidak aktif, mencoba memulai ulang…", + "log_app_action_run": "Menjalankan tindakan pada aplikasi '{}'", + "log_app_change_url": "Mengubah URL untuk aplikasi '{}'", + "log_app_config_set": "Menerapkan konfigurasi untuk aplikasi '{}'", + "log_app_install": "Memasang aplikasi '{}'", + "log_app_makedefault": "Membuat '{}' sebagai aplikasi baku", + "log_app_remove": "Melepas aplikasi '{}'", + "log_app_upgrade": "Memperbarui aplikasi '{}'", + "log_available_on_yunopaste": "Log ini sekarang sudah tersedia di {url}", + "log_backup_create": "Membuat arsip cadangan", + "log_backup_restore_app": "Memulihkan '{}' dari arsip cadangan", + "log_backup_restore_system": "Memulihkan sistem dari arsip cadangan", + "log_corrupted_md_file": "Berkas metadata YAML yang terkait dengan log rusak: '{md_file}\nGalat: {error}'", + "log_does_exists": "Tidak ada log operasi dengan nama '{log}', gunakan 'yunohost log list' untuk melihat semua log operasi yang tersedia", + "log_domain_add": "Menambahkan domain '{}' ke konfigurasi sistem", + "log_domain_config_set": "Memperbarui konfigurasi untuk domain '{}'", + "log_domain_dns_push": "Mendorong rekaman DNS untuk domain '{}'", + "log_domain_main_domain": "Atur '{}' sebagai domain utama", + "log_domain_remove": "Hapus domain '{}' dari konfigurasi sistem", + "log_dyndns_subscribe": "Berlangganan ke subdomain YunoHost '{}'", + "log_dyndns_unsubscribe": "Berhenti berlangganan subdomain YunoHost '{}'", + "log_dyndns_update": "Perbarui IP yang terkait dengan subdomain YunoHost Anda '{}'", + "log_help_to_get_failed_log": "Operasi '{desc}' tidak dapat diselesaikan. Silakan memberi log lengkap operasi ini menggunakan perintah 'yunohost log share {name}' untuk mendapatkan bantuan", + "log_help_to_get_log": "Untuk melihat log operasi '{desc}', gunakan perintah 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Memasang sertifikat Let's Encrypt di domain '{}'", + "log_letsencrypt_cert_renew": "Memperbarui sertifikat Let's Encrypt '{}'", + "log_link_to_failed_log": "Tidak dapat menyelesaikan operasi '{desc}'. Silakan memberi log lengkap operasi ini dengan cara gklik di sini agar mendapatkan bantuan", + "log_link_to_log": "Log penuh untuk tindakan ini: '{desc}'", + "log_operation_unit_unclosed_properly": "Unit operasi belum ditutup dengan tepat", + "log_regen_conf": "Membuat ulang konfigurasi sistem '{}'", + "log_remove_on_failed_install": "Menyingkirkan '{}' setelah instalasi gagal", + "log_resource_snippet": "Menyediakan/Meniadakan/memperbarui sumber daya", + "log_selfsigned_cert_install": "Memasang sertifikat ditandai sendiri pada domain '{}'", + "log_settings_reset": "Atur ulang pengaturan", + "log_settings_reset_all": "Atur ulang semua pengaturan", + "log_settings_set": "Terapkan pengaturan", + "log_tools_migrations_migrate_forward": "Menjalankan migrasi", + "log_tools_postinstall": "Pasca pemasangan server YunoHost Anda", + "log_tools_reboot": "Mulai ulang peladen Anda", + "log_tools_shutdown": "Matikan peladen Anda", + "log_tools_upgrade": "Perbarui paket sistem", + "log_user_create": "Menambahkan pengguna '{}'", + "log_user_delete": "Menghapus pengguna '{}'", + "log_user_group_create": "Membuat kelompok '{}'", + "log_user_group_delete": "Menghapus kelompok '{}'", + "log_user_group_update": "Memperbarui kelompok '{}'", + "log_user_import": "Mengimpor pengguna", + "log_user_update": "Memperbarui informasi untuk pengguna '{}'", + "mail_alias_remove_failed": "Tidak dapat menghapus alias surel '{mail}'", + "mail_domain_unknown": "Alamat surel tidak valid untuk domain '{domain}'. Silakan, menggunakan domain yang dikelola oleh server ini.", + "mail_forward_remove_failed": "tidak dapat menyingkirkan penerus surel '{mail}'", + "mail_unavailable": "Alamat surel ini hanya untuk kelompok admin", + "mailbox_disabled": "Surel dimatikan untuk pengguna {user}", + "mailbox_used_space_dovecot_down": "Layanan kotak pos Dovecot harus aktif jika Anda ingin mengambil ruang kotak pos yang telah digunakan", + "main_domain_change_failed": "Tidak dapat mengubah domain utama", + "main_domain_changed": "Domain utama telah diubah", + "migration_0027_cleaning_up": "Membersihkan cache dan paket yang sudah tidak berguna…", + "migration_0027_delayed_api_restart": "API YunoHost akan otomatis diulangi dalam 15 detik. Ini mungkin tidak tersedia selama beberapa detik, dan kemudian Anda harus masuk lagi.", + "migration_0027_general_warning": "Harap diingat bahwa migrasi ini adalah operasi yang rumit. Tim YunoHost melakukan yang terbaik untuk meninjau dan mengujinya, namun migrasi tersebut mungkin bisa merusak suatu bagian dari sistem atau aplikasinya.\n\nOleh karena itu, disarankan untuk:\n - Lakukan pencadangan data atau aplikasi penting apa pun. Informasi lebih lanjut di https://doc.yunohost.org/backup;\n - Bersabarlah setelah meluncurkan migrasi: Tergantung pada koneksi Internet dan perangkat keras Anda, mungkin diperlukan waktu hingga beberapa jam agar semuanya dapat dimutakhirkan dengan tepat.", + "migration_0027_main_upgrade": "Memulai pemutakhiran utama…", + "migration_0027_modified_files": "Harap diperhatikan bahwa berkas berikut ternyata dimodifikasi secara manual dan mungkin ditimpa setelah pemutakhiran: {manually_modified_files}", + "migration_0027_not_bullseye": "Distribusi Debian saat ini bukanlah Bullseye! Jika Anda sudah menjalankan migrasi Bullseye -> Bookworm, maka galat ini merupakan gejala dari fakta bahwa prosedur migrasi tidak 100% berhasil (selain itu YunoHost akan menandainya sebagai komplet). Disarankan agar menyelidiki apa yang terjadi bersama dengan tim bantuan, yang memerlukan log migrasi **lengkap**, yang dapat ditemukan di Alat > Log pada webadmin.", + "migration_0027_not_enough_free_space": "Ruang kosong cukup sedikit di /var/! Anda harus memiliki setidaknya 1 GB ruang kosong untuk menjalankan migrasi ini.", + "migration_0027_patch_yunohost_conflicts": "Menerapkan tambalan untuk menanggulangi isu konflik…", + "migration_0027_patching_sources_list": "Menambal berkas source.lists…", + "migration_0027_problematic_apps_warning": "Harap diperhatikan bahwa aplikasi terpasang yang mungkin bermasalah telah terdeteksi. Sepertinya aplikasi tersebut tidak dipasang dari katalog aplikasi YunoHost, atau tidak ditandai sebagai 'berfungsi'. Oleh karena itu, tidak ada jaminan bahwa aplikasi tersebut akan tetap berfungsi setelah pemutakhiran: {problematic_apps}", + "migration_0027_start": "Memulai migrasi ke Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Ada yang tidak sesuai ketika menjalankan pemutakhiran utama, sistem tampaknya masih menggunakan Debian Bullseye.", + "migration_0027_system_not_fully_up_to_date": "Sistem Anda belum sepenuhnya mutakhir. Harap melakukan pemutakhiran rutin sebelum menjalankan migrasi ke Bookworm.", + "migration_0027_yunohost_upgrade": "Memulai pemutakhiran inti YunoHost…", + "migration_description_0027_migrate_to_bookworm": "pemutakhiran sistem ke Debian Bookworm dan YunoHost 12", + "migration_ldap_backup_before_migration": "Membuat cadangan basis data LDAP dan pengaturan aplikasi sebelum migrasi yang sebenarnya.", + "migration_ldap_can_not_backup_before_migration": "Pencadangan sistem tidak dapat diselesaikan sebelum migrasi gagal. Galat: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Tidak dapat bermigrasi… mencoba mengembalikan sistem seperti semula.", + "migration_ldap_rollback_success": "Mengembalikan sistem seperti semula.", + "migrations_already_ran": "Migrasi tersebut sudah selesai: {ids}", + "migrations_dependencies_not_satisfied": "Jalankan migrasi berikut: '{dependencies_id}', sebelum migrasi {id}.", + "migrations_exclusive_options": "'--auto', '--skip', dan '--force-rerun' adalah opsi khusus yang saling berkaitan.", + "migrations_failed_to_load_migration": "Tidak dapat memuat migrasi {id}: {error}", + "migrations_list_conflict_pending_done": "Anda tidak dapat menggunakan '--previous' dan '--done' secara bersamaan.", + "migrations_loading_migration": "Memuat migrasi {id}…", + "migrations_migration_has_failed": "Migrasi {id} tidak lengkap, dibatalkan. Galat: {exception}", + "migrations_must_provide_explicit_targets": "Anda harus memberikan target yang jelas saat menggunakan '--skip' atau '--force-rerun'", + "migrations_need_to_accept_disclaimer": "Untuk menjalankan migrasi {id}, Anda harus menerima pernyataan berikut:\n---\n{disclaimer}\n---\nJika Anda setuju untuk menjalankan migrasi, silakan jalankan kembali perintah dengan opsi '--accept-disclaimer'.", + "migrations_no_migrations_to_run": "Tidak ada migrasi yang harus dijalankan", + "migrations_no_such_migration": "Tidak ada migrasi yang disebut '{id}'", + "migrations_not_pending_cant_skip": "Migrasi ini tidak tertunda, sehingga tidak dapat dilewati: {ids}", + "migrations_pending_cant_rerun": "Migrasi ini masih tertunda, sehingga belum bisa dijalankan lagi: {ids}", + "migrations_running_forward": "Menjalankan migrasi {id}…", + "migrations_skip_migration": "Melewatkan migrasi {id}…", + "migrations_success_forward": "Migrasi {id} selesai", + "migrations_to_be_ran_manually": "Migrasi {id} harus dijalankan secara manual. Silakan buka Alat → Migrasi di halaman webadmin, atau jalankan `yunohost tools migrans run`.", + "nftables_unavailable": "Anda tidak dapat menggunakan nftables di sini. Anda berada dalam sebuah penampungan atau kernel Anda yang tidak mendukungnya", + "not_enough_disk_space": "Ruang kosong tidak cukup di '{path}'", + "operation_interrupted": "Operasinya dihentikan secara manual?", + "other_available_options": "… dan {n} opsi lain yang tersedia tidak ditampilkan", + "password_confirmation_not_the_same": "Kata sandi dan untuk konfirmasinya tidak sama", + "password_listed": "Kata sandi ini termasuk dalam daftar kata sandi yang sering digunakan di dunia. Silakan untuk memilih yang lebih unik.", + "password_too_long": "Pilih kata sandi yang lebih pendek dari 127 karakter", + "password_too_simple_1": "Panjang kata sandi harus paling tidak 8 karakter", + "password_too_simple_2": "Kata sandi harus terdiri dari minimal 8 karakter dan berisi karakter angka, besar, dan kecil", + "password_too_simple_3": "Kata sandi harus terdiri dari minimal 8 karakter dan berisi karakter angka, besar, kecil dan khusus", + "password_too_simple_4": "Panjang kata sandi harus paling tidak 12 karakter dan mengandung digit, huruf kapital, huruf kecil, dan karakter khusus", + "pattern_backup_archive_name": "Harus berupa nama berkas yang valid dengan maksimal 30 karakter, alfanumerik dan karakter -_. saja", + "pattern_domain": "Harus berupa nama domain yang valid (misalnya domain-saya.org)", + "pattern_email": "Harus berupa alamat surel yang valid, tanpa simbol '+' (misalnya seseorang@example.com)", + "pattern_email_forward": "Harus berupa alamatsurel yang valid, simbol '+' masih diperbolehkan (misalnya seseorang+tag@example.com)", + "pattern_fullname": "Harus berupa nama lengkap yang valid (minimal 3 karakter)", + "pattern_mailbox_quota": "Harus seukuran dengan akhiran b/k/M/G/T atau 0 agar tidak memiliki kuota", + "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)", + "pattern_username": "Harus berupa karakter huruf alfanumerik kecil dan garis bawah saja", + "permission_already_allowed": "Grup '{group}' sudah mengaktifkan izin '{permission}'", + "permission_already_disallowed": "Grup '{group}' sudah menonaktifkan izin '{permission}'", + "permission_cannot_remove_main": "Menyingkirkan izin utama tidak diperbolehkan", + "permission_cant_add_to_all_users": "Izin '{permission}' tidak dapat ditambahkan ke semua pengguna.", + "permission_created": "Izin '{permission}' dibuat", + "permission_creation_failed": "Tidak dapat membuat izin '{permission}': {error}", + "permission_currently_allowed_for_all_users": "Izin ini sekarang diberikan kepada semua pengguna selain grup yang lain. Anda mungkin ingin menyingkirkan izin 'all_users' atau menyingkirkan grup lain yang saat ini diberikan izin tersebut.", + "permission_deleted": "Izin '{permission}' dihapus", + "permission_deletion_failed": "Tidak dapat menghapus izin '{permission}': {error}", + "permission_not_found": "Izin '{permission}' tidak ditemukan", + "permission_protected": "Izin {permission} dilindungi. Anda tidak dapat menambahkan atau menyingkirkan grup pengunjung ke/dari izin ini.", + "permission_require_account": "Izin {permission} hanya masuk akal untuk pengguna yang memiliki akun, maka ini tidak dapat diaktifkan untuk pengunjung.", + "permission_update_failed": "Tidak dapat memperbarui izin '{permission}': {error}", + "permission_updated": "Izin '{permission}' diperbarui", + "port_already_closed": "Porta {port} telah ditutup", + "port_already_opened": "Porta {port} telah dibuka", + "postinstall_low_rootfsspace": "Sistem pemberkasan root memiliki total ruang kurang dari 10 GB, yang cukup mengkhawatirkan! Kemungkinan besar Anda akan kehabisan ruang disket dengan sangat cepat! Disarankan agar memiliki setidaknya 16GB untuk sistem pemberkasan root. Jika Anda ingin memasang YunoHost meskipun ada peringatan ini, jalankan kembali pasca pemasangan dengan --force-diskspace", + "regenconf_dry_pending_applying": "Memeriksa konfigurasi yang tertunda yang akan diterapkan pada kategori '{category}'…", + "regenconf_failed": "Tidak dapat membuat ulang konfigurasi pada kategori: {categories}", + "regenconf_file_backed_up": "Berkas konfigurasi '{conf}' dicadangkan ke '{backup}'", + "regenconf_file_copy_failed": "Tidak dapat menyalin berkas konfigurasi baru '{new}' ke '{conf}'", + "regenconf_file_kept_back": "Berkas konfigurasi '{conf}' seharusnya dihapus oleh regen-conf (kategori {category}) tapi tidak jadi.", + "regenconf_file_manually_modified": "Berkas konfigurasi '{conf}' telah diubah secara manual dan tidak akan diperbarui", + "regenconf_file_manually_removed": "Berkas konfigurasi '{conf}' telah dihapus secara manual dan tidak akan dibikin", + "regenconf_file_remove_failed": "Tidak dapat menghapus berkas konfigurasi '{conf}'", + "regenconf_file_removed": "Berkas konfigurasi '{conf}' dihapus", + "regenconf_file_updated": "Berkas konfigurasi '{conf}' diperbarui", + "regenconf_need_to_explicitly_specify_ssh": "Konfigurasi ssh telah dimodifikasi secara manual, namun Anda perlu secara eksplisit menentukan kategori 'ssh' dengan --force agar menerapkan perubahan yang sebenarnya.", + "regenconf_now_managed_by_yunohost": "Berkas konfigurasi '{conf}' sekarang dikelola oleh YunoHost (kategori {category}).", + "regenconf_pending_applying": "Menerapkan konfigurasi yang tertunda pada kategori '{category}'…", + "regenconf_up_to_date": "Konfigurasi sudah yang terbaru pada kategori '{category}'", + "regenconf_updated": "Konfigurasi diperbarui untuk '{category}'", + "regenconf_would_be_updated": "Konfigurasi akan diperbarui pada kategori '{category}'", + "regex_incompatible_with_tile": "/!\\ Pemaket! Perizinan '{permission}' memiliki show_tile yang diatur menjadi 'true' dan oleh karena itu Anda tidak dapat menentukan URL regex sebagai URL utama", + "regex_with_only_domain": "Anda tidak dapat menggunakan regex untuk domain, hanya untuk jalur", + "registrar_infos": "Info registrar", + "restore_already_installed_app": "Aplikasi dengan ID '{app}' telah terpasang", + "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_cleaning_failed": "Tidak dapat membersihkan direktori restorasi sementara", + "restore_complete": "Pemulihan selesai", + "restore_confirm_yunohost_installed": "Apakah Anda benar-benar ingin memulihkan sistem yang sudah terpasang? [{answers}]", + "restore_extracting": "Mengekstrak berkas yang diperlukan dari arsip…", + "restore_failed": "Tidak dapat memulihkan sistem", + "restore_hook_unavailable": "Skrip pemulihan pada '{part}' tidak tersedia pada sistem Anda dan juga tidak ada di dalam arsip", + "restore_may_be_not_enough_disk_space": "Sistem Anda tampaknya tidak memiliki cukup ruang (bebas: {free_space} B, ruang yang diperlukan: {needed_space} B, batas keamanan: {margin} B)", + "restore_not_enough_disk_space": "Ruang tidak cukup (ruang: {free_space} B, ruang yang dibutuhkan: {needed_space} B, margin aman: {margin} B)", + "restore_nothings_done": "Tidak ada yang dipulihkan", + "restore_removing_tmp_dir_failed": "Tidak dapat menghapus direktori sementara yang dulu", + "restore_running_app_script": "Memulihkan aplikasi {app}…", + "restore_running_hooks": "Menjalankan kait restorasi…", + "restore_system_part_failed": "Tidak dapat memulihkan segmen '{part}'", + "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": "Peladen akan dimulai ulang", + "server_reboot_confirm": "Peladen akan dimulai ulang segera, apakan Anda yakin [{answers}]", + "server_shutdown": "Peladen akan dimatikan", + "server_shutdown_confirm": "Peladen akan dimatikan segera, apakah Anda yakin? [{answers}]", + "service_add_failed": "Tidak dapat menambahkan layanan '{service}'", + "service_added": "Layanan '{service}' ditambahkan", + "service_already_started": "Layanan '{service}' telah berjalan", + "service_already_stopped": "Layanan '{service}' telah dihentikan", + "service_cmd_exec_failed": "Tidak dapat menjalankan perintah '{command}'", + "service_description_dnsmasq": "Mengurus DNS", + "service_description_dovecot": "Digunakan untuk memperbolehkan klien surel mengakses surel (via IMAP dan POP3)", + "service_description_fail2ban": "Melindungi dari serangan kotor dan berbagai macam serangan dari Internet", + "service_description_mysql": "Menyimpan data aplikasi (basis data SQL)", + "service_description_nftables": "Mengelola pembukaan dan penutupan porta koneksi ke layanan", + "service_description_nginx": "Menyediakan akses untuk semua situs yang dihos di peladen Anda", + "service_description_postfix": "Digunakan untuk mengirim dan menerima surel", + "service_description_postgresql": "Menyimpan data aplikasi (basis data SQL)", + "service_description_redis-server": "Basis data khusus yang digunakan untuk akses data cepat, antrian tugas, dan komunikasi antar program", + "service_description_slapd": "Menyimpan info terkait pengguna, domain, dan sejenisnya", + "service_description_ssh": "Memperbolehkan Anda untuk terhubung secara jarak jauh dengan peladen Anda via terminal (protokol SSH)", + "service_description_yunohost-api": "Mengelola interaksi antara antarmuka web YunoHost dengan sistem", + "service_description_yunomdns": "Membuat Anda bisa menemukan peladen Anda menggunakan 'yunohost.local' di jaringan lokal Anda", + "service_disable_failed": "Tidak dapat membuat layanan '{service}' dimulai saat pemulaian.", + "service_disabled": "Layanan '{service}' tidak akan dimulai kembali saat pemulaian.", + "service_enable_failed": "Tidak dapat membuat layanan '{service}' dimulai mandiri saat pemulaian.", + "service_enabled": "Layanan '{service}' akan secara mandiri dimulai saat pemulaian.", + "service_not_reloading_because_conf_broken": "Tidak memuat atau memulai ulang layanan '{name}' karena konfigurasinya rusak: {errors}", + "service_reload_failed": "Tidak dapat memuat ulang layanan '{service}'", + "service_reload_or_restart_failed": "Tidak dapat memuat atau memulai ulang layanan '{service}'", + "service_reloaded": "Layanan {service} dimuat ulang", + "service_reloaded_or_restarted": "Layanan {service} dimuat atau dimulai ulang", + "service_remove_failed": "Tidak dapat menghapus layanan '{service}'", + "service_removed": "Layanan '{service}' dihapus", + "service_restart_failed": "Tidak dapat memulai ulang layanan '{service}'", + "service_restarted": "Layanan {service} dimulai ulang", + "service_start_failed": "Tidak dapat memulai layanan '{service}'", + "service_started": "Layanan '{service}' dimulai", + "service_stop_failed": "Tidak dapat menghentikan layanan '{service}'", + "service_stopped": "Layanan '{service}' diberhentikan", + "service_unknown": "Layanan yang tidak diketahui: '{service}'", + "show_tile_cant_be_enabled_for_regex": "Anda tidak dapat mengaktifkan 'show_tile' saat ini, karena URL untuk perizinan '{permission}' adalah regex", + "show_tile_cant_be_enabled_for_url_not_defined": "Anda tidak dapat mengaktifkan 'show_tile' saat ini, karena Anda harus terlebih dahulu menentukan URL pada perizinan '{permission}'", + "ssowat_conf_generated": "Konfigurasi SSOwat diperbarui", + "system_upgraded": "Sistem diperbarui", + "system_username_exists": "Nama pengguna telah ada di daftar pengguna 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`.", + "tools_upgrade": "Memperbarui paket sistem", + "tools_upgrade_failed": "Tidak dapat memperbarui paket: {packages_list}", + "unbackup_app": "{app} tidak akan disimpan", + "unexpected_error": "Terjadi kesalahan yang tidak terduga: {error}", + "unknown_error_reading_file": "Galat yang tidak diketahui ketika membaca berkas {file} (alasan: {error})", + "unknown_group": "Kelompok '{group}' tidak diketahui", + "unknown_main_domain_path": "Domain atau jalur pada '{app}' tidak diketahui. Anda perlu menentukan domain dan jalur agar dapat menentukan URL untuk perizinan.", + "unknown_user": "Pengguna '{user}' tidak diketahui", + "unlimit": "Tidak ada kuota", + "unrestore_app": "{app} akan dipulihkan", + "update_apt_cache_failed": "Tidak dapat memperbarui cache APT (manajer paket Debian). Berikut ini adalah kumpulan baris source.list, yang mungkin dapat membantu mengidentifikasi baris yang bermasalah:\n{sourceslist}", + "update_apt_cache_warning": "Ada yang tidak sesuai saat memperbarui cache APT (manajer paket Debian). Berikut ini adalah kumpulan baris source.list, yang mungkin membantu mengidentifikasi baris yang bermasalah:\n{sourceslist}", + "updating_apt_cache": "Mengambil pembaruan yang tersedia untuk paket sistem…", + "upgrading_packages": "Memperbarui paket…", + "upnp_dev_not_found": "Tidak ada perangkat UPnP yang ditemukan", + "upnp_disabled": "UPnP dimatikan", + "upnp_enabled": "UPnP dinyalakan", + "upnp_port_open_failed": "Tidak dapat membuka porta lewat UPnP", + "user_already_exists": "Pengguna '{user}' telah ada", + "user_created": "Pengguna dibuat", + "user_creation_failed": "Tidak dapat membuat pengguna {user}: {error}", + "user_deleted": "Pengguna dihapus", + "user_deletion_failed": "Tidak dapat menghapus pengguna {user}: {error}", + "user_home_creation_failed": "Tidak dapat membuat folder home '{home}' untuk pengguna", + "user_import_bad_file": "Berkas CSV Anda tidak secara benar diformat, akan diabaikan untuk menghindari potensi data hilang", + "user_import_bad_line": "Baris yang salah {line}: {details}", + "user_import_failed": "Operasi impor pengguna gagal total", + "user_import_missing_columns": "Kehilangan kolom berikut: {columns}", + "user_import_nothing_to_do": "Tidak ada pengguna yang perlu diimpor", + "user_import_partial_failed": "Operasi impor pengguna gagal sebagian", + "user_import_success": "Pengguna berhasil diimpor", + "user_unknown": "Pengguna tidak diketahui: {user}", + "user_update_failed": "Tidak dapat memperbarui pengguna {user}: {error}", + "user_updated": "Informasi pengguna diubah", + "visitors": "Pengunjung", + "yunohost_already_installed": "YunoHost sudah terpasang", + "yunohost_configured": "YunoHost sudah terkonfigurasi", + "yunohost_installing": "Memasang YunoHost…", + "yunohost_not_installed": "YunoHost tidak terpasang dengan benar. Jalankan 'yunohost tools postinstall'", + "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://doc.yunohost.org/admin." +} diff --git a/locales/it.json b/locales/it.json new file mode 100644 index 0000000..988bf8a --- /dev/null +++ b/locales/it.json @@ -0,0 +1,647 @@ +{ + "aborting": "Annullamento.", + "action_invalid": "L'azione '{action}' non è valida", + "additional_urls_already_added": "L’URL aggiuntivo ‘{url}’ è già utilizzato 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}’", + "admin_password": "Password dell'amministrazione", + "admins": "Amministratori", + "all_users": "Tutti gli utenti di YunoHost", + "already_up_to_date": "Niente da fare. Tutto è già aggiornato.", + "app_action_broke_system": "Questa azione sembra avere rotto questi servizi importanti: {services}", + "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ù).", + "app_action_failed": "L’esecuzione dell’azione {action} per l’app {app} è fallita", + "app_already_installed": "{app} è già installata", + "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_arch_not_supported": "Quest’app può essere installata su architetture {required} ma l’architettura del tuo server è {current}", + "app_argument_choice_invalid": "Scegli un opzione valida per il parametro '{name}': '{value}' non è fra le opzioni disponibili ('{choices}')", + "app_argument_invalid": "Scegli un valore valido per il parametro '{name}': {error}", + "app_change_url_failed": "Non è possibile cambiare l'URL per {app}:{error}", + "app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain}{path}'), nessuna operazione necessaria.", + "app_change_url_no_script": "L'applicazione '{app_name}' non supporta ancora la modifica dell'URL. Forse dovresti aggiornarla.", + "app_change_url_require_full_domain": "{app} non può essere spostato su questo nuovo URL, poiché richiede un dominio intero (ovvero con percorso /)", + "app_change_url_script_failed": "È stato registrato un errore eseguendo lo script per la modifica dell’URL", + "app_change_url_success": "L'URL dell'applicazione {app} è stato cambiato in {domain}{path}", + "app_config_unable_to_apply": "Applicazione dei valori nel pannello di configurazione non riuscita.", + "app_config_unable_to_read": "Lettura dei valori nel pannello di configurazione non riuscita.", + "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 forse aggiornare il “manifest” dell’app per considerare questo cambiamento.\n Checksum sha256 previsto: {expected_sha256}\n Checksum sha256 scaricato: {computed_sha256}\n Dimensioni del file scaricato: {size}", + "app_extraction_failed": "Impossibile estrarre i file di installazione", + "app_failed_to_download_asset": "Lo scaricamento della risorsa ‘{source_id}’ ({url}) per l’app {app} è fallito: {out}", + "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.", + "app_id_invalid": "Identificativo dell'applicazione non valido", + "app_install_failed": "Impossibile installare {app}:{error}", + "app_install_files_invalid": "Questi file non possono essere installati", + "app_install_script_failed": "Si è verificato un errore nello script di installazione dell'applicazione", + "app_location_unavailable": "Questo URL non è più disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps}", + "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_manifest_install_ask_admin": "Scegli un utente amministratore per quest'applicazione", + "app_manifest_install_ask_domain": "Scegli il dominio dove installare quest'app", + "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_manifest_install_ask_init_main_permission": "Chi dovrebbe aver accesso a quest’app? (Questa scelta potrà esser cambiata in seguito)", + "app_manifest_install_ask_is_public": "Quest'applicazione dovrà essere visibile ai visitatori anonimi?", + "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_not_correctly_installed": "{app} sembra di non essere installata correttamente", + "app_not_enough_disk": "Quest’app richiede {required} di spazio libero.", + "app_not_enough_ram": "Quest’app richiede {required} di RAM per essere installata/aggiornata, ma solo {current} sono disponibili al momento.", + "app_not_installed": "Impossibile trovare l'applicazione {app} nell'elenco delle applicazioni installate: {all_apps}", + "app_not_properly_removed": "{app} non è stata correttamente rimossa", + "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.", + "app_remove_after_failed_install": "Rimozione dell’applicazione dopo del fallimento della sua installazione…", + "app_removed": "{app} disinstallata", + "app_requirements_checking": "Controllo dei requisiti per {app}…", + "app_resource_failed": "Fallimento della fornitura, della rimozione o dell’aggiornamento di risorse per {app}: {error}", + "app_restore_failed": "Impossibile ripristinare l'applicazione '{app}': {error}", + "app_restore_script_failed": "C'è stato un errore all'interno dello script di recupero", + "app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l’URL è corretto?", + "app_start_backup": "Raccogliendo file da salvare nel backup per '{app}'…", + "app_start_install": "Installando '{app}'…", + "app_start_remove": "Rimozione di {app}…", + "app_start_restore": "Ripristino di '{app}'…", + "app_unknown": "Applicazione sconosciuta", + "app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato", + "app_upgrade_app_name": "Aggiornamento di {app}…", + "app_upgrade_failed": "Impossibile aggiornare {app}: {error}", + "app_upgrade_script_failed": "È stato trovato un errore nello script di aggiornamento dell'applicazione", + "app_upgrade_several_apps": "Le seguenti applicazioni saranno aggiornate : {apps}", + "app_upgrade_some_app_failed": "Alcune applicazioni non possono essere aggiornate", + "app_upgraded": "{app} aggiornata", + "app_yunohost_version_not_supported": "Quest’app richiede YunoHost ≥ {required}, ma la versione installata ora è {current}", + "apps_already_up_to_date": "Tutte le applicazioni sono aggiornate", + "apps_catalog_failed_to_download": "Impossibile scaricare il catalogo delle applicazioni {apps_catalog} : {error}", + "apps_catalog_obsolete_cache": "La cache del catalogo della applicazioni è vuoto o obsoleto.", + "apps_catalog_update_success": "Il catalogo delle applicazioni è stato aggiornato!", + "apps_catalog_updating": "Aggiornamento del catalogo delle applicazioni…", + "ask_admin_fullname": "Nome completo dell’utente amministratore", + "ask_admin_username": "Username dell’utente amministratore", + "ask_dyndns_recovery_password": "Password di recupero per DynDNS", + "ask_dyndns_recovery_password_explain": "Scegli una password di recupero per il tuo dominio DynDNS, in caso dovessi ripristinarlo successivamente.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Digita la password di recupero per questo dominio DynDNS.", + "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.", + "ask_fullname": "Nome completo", + "ask_main_domain": "Dominio principale", + "ask_new_admin_password": "Nuova password dell'amministrazione", + "ask_new_domain": "Nuovo dominio", + "ask_new_path": "Nuovo percorso", + "ask_password": "Password", + "ask_user_domain": "Dominio da usare per l'indirizzo email", + "backup_abstract_method": "Questo metodo di backup deve essere ancora implementato", + "backup_actually_backuping": "Creazione di un archivio di backup con i file raccolti…", + "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_app_not_found": "{app} non è stata trovata nel archivio di backup", + "backup_archive_broken_link": "Non è possibile accedere all'archivio di backup (link rotto verso {path})", + "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).", + "backup_archive_corrupted": "Sembra che l'archivio di backup '{archive}' sia corrotto: {error}", + "backup_archive_name_exists": "Esiste già un archivio di backup con il nome ‘{name}’.", + "backup_archive_name_unknown": "Archivio di backup locale chiamato '{name}' sconosciuto", + "backup_archive_open_failed": "Impossibile aprire l'archivio di 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)", + "backup_cant_mount_uncompress_archive": "Impossibile montare in modalità sola lettura la cartella di archivio non compressa", + "backup_cleaning_failed": "Non è possibile pulire la directory temporanea di backup", + "backup_copying_to_organize_the_archive": "Copiando {size}MB per organizzare l'archivio", + "backup_couldnt_bind": "Impossibile legare {src} a {dest}.", + "backup_create_size_estimation": "L'archivio conterrà circa {size} di dati.", + "backup_created": "Backup creato: {name}", + "backup_creation_failed": "Impossibile creare l'archivio di backup", + "backup_csv_addition_failed": "Impossibile aggiungere file del backup nel file CSV", + "backup_csv_creation_failed": "Impossibile creare il file CVS richiesto per le operazioni di ripristino", + "backup_custom_backup_error": "Il metodo di backup personalizzato è fallito allo step 'backup'", + "backup_custom_mount_error": "Il metodo di backup personalizzato è fallito allo step 'mount'", + "backup_delete_error": "Impossibile cancellare '{path}'", + "backup_deleted": "Backup eliminato: {name}", + "backup_hook_unknown": "Hook di backup '{hook}' sconosciuto", + "backup_method_copy_finished": "Copia di backup terminata", + "backup_method_custom_finished": "Metodo di backup personalizzato '{method}' terminato", + "backup_method_tar_finished": "Archivio TAR di backup creato", + "backup_mount_archive_for_restore": "Preparazione dell'archivio per il ripristino…", + "backup_no_uncompress_archive_dir": "La cartella di archivio non compressa non esiste", + "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", + "backup_output_directory_not_empty": "Dovresti scegliere una cartella di output vuota", + "backup_output_directory_required": "Devi fornire una directory di output per il backup", + "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.", + "backup_running_hooks": "Esecuzione degli hook di backup…", + "backup_system_part_failed": "Impossibile creare il backup della parte di sistema '{part}'", + "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.", + "cannot_open_file": "Impossibile aprire il file {file} (motivo: {error})", + "cannot_write_file": "Impossibile scrivere il file {file} (motivo: {error})", + "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_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_attempt_to_replace_valid_cert": "Stai provando a sovrascrivere un certificato buono e valido per il dominio {domain}! (Usa --force per ignorare)", + "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_failed": "L’installazione del certificato Let’s Encrypt è fallita per {domains}", + "certmanager_cert_install_failed_selfsigned": "L’installazione di un certificato auto-firmato è fallita per {domains}", + "certmanager_cert_install_success": "Certificato Let's Encrypt per il dominio {domain} installato", + "certmanager_cert_install_success_selfsigned": "Certificato autofirmato installato con successo per il dominio {domain}", + "certmanager_cert_renew_failed": "Il rinnovo del certificato Let’s Encrypt è fallito per {domains}", + "certmanager_cert_renew_success": "Certificato di Let's Encrypt rinnovato con successo per il dominio {domain}", + "certmanager_cert_signing_failed": "Impossibile firmare il nuovo certificato", + "certmanager_certificate_fetching_or_enabling_failed": "Il tentativo di usare il nuovo certificato per {domain} non funziona…", + "certmanager_domain_cert_not_selfsigned": "Il certificato per il dominio {domain} non è auto-firmato. Sei sicuro di volere sostituirlo? (Usa '--force')", + "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_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.)", + "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_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})", + "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}", + "config_apply_failed": "L’applicazione della nuova configurazione è fallita: {error}", + "config_cant_set_value_on_section": "Non puoi impostare un unico parametro in un’intera sezione della configurazione.", + "config_forbidden_keyword": "La parola chiave '{keyword}' è riservata, non puoi creare o utilizzare un pannello di configurazione con una domanda con questo id.", + "config_no_panel": "Nessun panello di configurazione trovato.", + "config_unknown_filter_key": "Il valore del filtro '{filter_key}' non è corretto.", + "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}'", + "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_insufficient_ram": "Quest’app richiede più RAM di quella attualmente disponibile per l'installazione. 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}’", + "confirm_notifications_read": "ATTENZIONE: Dovresti controllare le notifiche dell’app qui sopra prima di continuare, potrebbero esserci cose importanti da sapere. [{answers}]", + "corrupted_json": "Lettura JSON corrotta da {ressource} (motivo: {error})", + "corrupted_toml": "TOML corrotto da {ressource} (motivo: {error})", + "corrupted_yaml": "Lettura YAML corrotta da {ressource} (motivo: {error})", + "danger": "Attenzione:", + "diagnosis_apps_allgood": "Tutte le applicazioni installate rispettano le pratiche di packaging di base", + "diagnosis_apps_bad_quality": "Sul catalogo delle applicazioni di YunoHost, questa applicazione è momentaneamente segnalata come non funzionante. Potrebbe trattarsi di un problema temporaneo, mentre i manutentori provano a risolverlo. Nel frattempo, l’aggiornamento di quest’app è disabilitato.", + "diagnosis_apps_broken": "Sul catalogo delle applicazioni di YunoHost, questa applicazione è momentaneamente segnalata come non funzionante. Potrebbe trattarsi di un problema temporaneo, mentre i manutentori provano a risolverlo. Nel frattempo, l’aggiornamento di quest’app è disabilitato.", + "diagnosis_apps_deprecated_practices": "La versione installata di questa app usa ancora delle pratiche di packaging super-vecchie oppure deprecate. Dovresti proprio considerare di aggiornarla.", + "diagnosis_apps_issue": "È stato rilevato un errore per l’app {app}", + "diagnosis_apps_not_in_app_catalog": "Questa applicazione non è nel catalogo delle applicazioni di YunoHost. Se precedentemente lo era ed è stata rimossa, dovresti considerare di disinstallare l’app, dato che non riceverà aggiornamenti e potrebbe compromettere l’integrità e la sicurezza del tuo sistema.", + "diagnosis_apps_outdated_ynh_requirement": "La versione installata di quest’app richiede esclusivamente YunoHost >= 2.x, che tendenzialmente significa che non è aggiornata secondo le pratiche di packaging raccomandate. Dovresti proprio considerare di aggiornarla.", + "diagnosis_backports_in_sources_list": "Sembra che apt (il package manager) sia configurato per utilizzare le backport del repository. A meno che tu non sappia quello che stai facendo, scoraggiamo fortemente di installare pacchetti tramite esse, perché ci sono alte probabilità di creare conflitti con il tuo sistema.", + "diagnosis_basesystem_hardware": "L'architettura hardware del server è {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Modello server: {model}", + "diagnosis_basesystem_host": "Il server sta eseguendo Debian {debian_version}", + "diagnosis_basesystem_kernel": "Il server sta eseguendo Linux kernel {kernel_version}", + "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_cache_still_valid": "(La cache della diagnosi di {category} è ancora valida. Non la ricontrollo di nuovo per ora!)", + "diagnosis_cant_run_because_of_dep": "Impossibile lanciare la diagnosi per {category} mentre ci sono problemi importanti collegati a {dep}.", + "diagnosis_description_apps": "Applicazioni", + "diagnosis_description_basesystem": "Sistema base", + "diagnosis_description_dnsrecords": "Record DNS", + "diagnosis_description_ip": "Connettività internet", + "diagnosis_description_mail": "Email", + "diagnosis_description_ports": "Esposizione porte", + "diagnosis_description_regenconf": "Configurazioni sistema", + "diagnosis_description_services": "Check stato servizi", + "diagnosis_description_systemresources": "Risorse di sistema", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Fa attenzione.", + "diagnosis_diskusage_ok": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total})!", + "diagnosis_diskusage_verylow": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Dovresti seriamente considerare di fare un po' di pulizia!", + "diagnosis_display_tip": "Per vedere i problemi rilevati, puoi andare alla sezione Diagnosi del amministratore, o eseguire 'yunohost diagnosis show --issues --human-readable' dalla riga di comando.", + "diagnosis_dns_bad_conf": "Alcuni record DNS sono mancanti o incorretti per il dominio {domain} (categoria {category})", + "diagnosis_dns_discrepancy": "Il record DNS non sembra seguire la configurazione DNS raccomandata:
Type: {type}
Name: {name}
Current value: {current}
Expected value: {content}", + "diagnosis_dns_good_conf": "I recordDNS sono configurati correttamente per il dominio {domain} (categoria {category})", + "diagnosis_dns_missing_record": "Stando alla configurazione DNS raccomandata, dovresti aggiungere un record DNS con le seguenti informazioni.
Type: {type}
Name: {name}
Value: {content}", + "diagnosis_dns_point_to_doc": "Controlla la documentazione a https://doc.yunohost.org/dns_config se hai bisogno di aiuto nel configurare i record DNS.", + "diagnosis_dns_specialusedomain": "Il dominio {domain} è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto abbia reali record DNS.", + "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_domain_expiration_error": "Alcuni domini scadranno MOLTO PRESTO!", + "diagnosis_domain_expiration_not_found": "Non riesco a controllare la data di scadenza di alcuni domini", + "diagnosis_domain_expiration_not_found_details": "Le informazioni WHOIS per il dominio {domain} non sembrano contenere la data di scadenza, giusto?", + "diagnosis_domain_expiration_success": "I tuoi domini sono registrati e non scadranno a breve.", + "diagnosis_domain_expiration_warning": "Alcuni domini scadranno a breve!", + "diagnosis_domain_expires_in": "{domain} scadrà tra {days} giorni.", + "diagnosis_domain_not_found_details": "Il dominio {domain} non esiste nel database WHOIS o è scaduto!", + "diagnosis_everything_ok": "Tutto ok per {category}!", + "diagnosis_failed": "Recupero dei risultati della diagnosi per la categoria '{category}' fallito: {error}", + "diagnosis_failed_for_category": "Diagnosi fallita per la categoria '{category}:{error}", + "diagnosis_found_errors": "Trovato {errors} problemi significativi collegati a {category}!", + "diagnosis_found_errors_and_warnings": "Trovato {errors} problemi (e {warnings} alerts) significativi collegati a {category}!", + "diagnosis_found_warnings": "Trovato {warnings} oggetti che potrebbero essere migliorati per {category}.", + "diagnosis_high_number_auth_failures": "Recentemente c’è stato un numero insolitamente alto di autenticazioni fallite. Potresti assicurarti che fail2ban stia funzionando e che sia configurato correttamente, oppure usare una differente porta SSH, come spiegato in https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Sembra che un altro dispositivo (forse il tuo router internet) abbia risposto al posto del tuo server
1. La causa più comune è la porta 80 (e 443) non correttamente inoltrata al tuo server.
2. Su setup più complessi: assicurati che nessun firewall o reverse-proxy stia interferendo.", + "diagnosis_http_connection_error": "Errore connessione: impossibile connettersi al dominio richiesto, probabilmente è irraggiungibile.", + "diagnosis_http_could_not_diagnose": "Non posso controllare se i domini sono raggiungibili dall'esterno su IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Errore: {error}", + "diagnosis_http_hairpinning_issue": "La tua rete locale sembra non avere \"hairpinning\" abilitato.", + "diagnosis_http_hairpinning_issue_details": "Questo probabilmente è causato dal tuo ISP router. Come conseguenza, persone al di fuori della tua LAN saranno in grado di accedere al tuo server come atteso, ma non le persone all'interno della LAN (tipo te, immagino) utilizzando il dominio internet o l'IP globale. Dovresti essere in grado di migliorare la situazione visitando https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "La configurazione nginx di questo dominio sembra esser stato modificato manualmente, e impedisce a YunoHost di controlalre se è raggiungibile su HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Per sistemare, ispeziona le differenze nel terminale eseguendo yunohost tools regen-conf nginx --dry-run --with-diff e se ti va bene, applica le modifiche con yunohost tools regen-conf ngix --force.", + "diagnosis_http_ok": "Il dominio {domain} è raggiungibile attraverso HTTP al di fuori della tua LAN.", + "diagnosis_http_partially_unreachable": "Il dominio {domain} sembra irraggiungibile attraverso HTTP dall'esterno della tua LAN su IPv{failed}, anche se funziona su IPv{passed}.", + "diagnosis_http_special_use_tld": "Il dominio {domain} è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto che sia esposto al di fuori della rete locale.", + "diagnosis_http_timeout": "Andato in time-out cercando di contattare il server dall'esterno. Sembra essere irraggiungibile.
1. La causa più comune è la porta 80 (e 443) non correttamente inoltrata al tuo server.
2. Dovresti accertarti che il servizio nginx sia attivo.
3. Su setup più complessi: assicurati che nessun firewall o reverse-proxy stia interferendo.", + "diagnosis_http_unreachable": "Il dominio {domain} sembra irraggiungibile attraverso HTTP dall'esterno della tua LAN.", + "diagnosis_ignored_issues": "(+ {nb_ignored} problemi ignorati)", + "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_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_connected_ipv4": "Il server è connesso ad Internet tramite IPv4!", + "diagnosis_ip_connected_ipv6": "Il server è connesso ad Internet tramite IPv6!", + "diagnosis_ip_dnsresolution_working": "Risoluzione dei nomi di rete funzionante!", + "diagnosis_ip_global": "IP globale: {global}", + "diagnosis_ip_local": "IP locale: {local}", + "diagnosis_ip_no_ipv4": "Il server non ha IPv4 funzionante.", + "diagnosis_ip_no_ipv6": "Il server non ha IPv6 funzionante.", + "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://doc.yunohost.org/ipv6. Se non puoi abilitare IPv6 o se ti sembra troppo complicato per te, puoi tranquillamente ignorare questo avvertimento.", + "diagnosis_ip_not_connected_at_all": "Sei sicuro che il server sia collegato ad Internet!?", + "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_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_mail_blocklist_listed_by": "Il tuo IP o dominio {item} è nella blocklist {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Gli IP e i domini utilizzati da questo server non sembrano essere nelle blocklist", + "diagnosis_mail_blocklist_reason": "Il motivo della blocklist è: {reason}", + "diagnosis_mail_blocklist_website": "Dopo aver identificato il motivo e averlo risolto, sentiti libero di chiedere di rimuovere il tuo IP o dominio da {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Un servizio diverso da SMTP ha risposto sulla porta 25 su IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server.", + "diagnosis_mail_ehlo_could_not_diagnose": "Non è possibile verificare se il server mail postfix è raggiungibile dall'esterno su IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Errore: {error}", + "diagnosis_mail_ehlo_ok": "Il server SMTP è raggiungibile dall'esterno e quindi può ricevere email!", + "diagnosis_mail_ehlo_unreachable": "Il server SMTP non è raggiungibile dall'esterno su IPv{ipversion}. Non potrà ricevere email.", + "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_wrong": "Un server mail SMTP diverso sta rispondendo su IPv{ipversion}. Probabilmente il tuo server non può ricevere email.", + "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_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_different_from_ehlo_domain_details": "DNS invero corrente: {rdns_domain}
Valore atteso: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "Nessun DNS inverso è configurato per IPv{ipversion}. Alcune email potrebbero non essere inviate o segnalate come spam.", + "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://doc.yunohost.org/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider", + "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 email.smtp.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.", + "diagnosis_mail_fcrdns_nok_details": "Dovresti prima configurare il DNS inverso con {ehlo_domain} nell'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_fcrdns_ok": "Il tuo DNS inverso è configurato correttamente!", + "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_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_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://doc.yunohost.org/vpn_advantage
- Puoi anche prendere in considerazione di cambiare per un provider pro Net Neutrality", + "diagnosis_mail_outgoing_port_25_ok": "Il server SMTP è abile all'invio delle email (porta 25 in uscita non bloccata).", + "diagnosis_mail_queue_ok": "{nb_pending} emails in attesa nelle code", + "diagnosis_mail_queue_too_big": "Troppe email in attesa nella coda ({nb_pending} emails)", + "diagnosis_mail_queue_unavailable": "Impossibile consultare il numero di email in attesa", + "diagnosis_mail_queue_unavailable_details": "Errore: {error}", + "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'.", + "diagnosis_no_cache": "Nessuna diagnosi nella cache per la categoria '{category}'", + "diagnosis_package_installed_from_sury": "Alcuni pacchetti di sistema dovrebbero fare il downgrade", + "diagnosis_package_installed_from_sury_details": "Alcuni pacchetti sono stati inavvertitamente installati da un repository di terze parti chiamato Sury. Il team di YunoHost ha migliorato la gestione di tali pacchetti, ma ci si aspetta che alcuni setup di app PHP7.3 abbiano delle incompatibilità anche se sono ancora in Stretch. Per sistemare questa situazione, dovresti provare a lanciare il seguente comando: {cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "Impossibile diagnosticare se le porte sono raggiungibili dall'esterno su IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Errore: {error}", + "diagnosis_ports_forwarding_tip": "Per sistemare questo problema, probabilmente dovresti configurare l'inoltro della porta sul tuo router internet come descritto qui https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Esporre questa porta è necessario per le feature di {category} (servizio {service})", + "diagnosis_ports_ok": "La porta {port} è raggiungibile dall'esterno.", + "diagnosis_ports_partially_unreachable": "La porta {port} non è raggiungibile dall'esterno su IPv{failed}.", + "diagnosis_ports_unreachable": "La porta {port} non è raggiungibile dall'esterno.", + "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_ram_low": "Il sistema ha solo {available} ({available_percent}%) di RAM disponibile (su {total}). Fa attenzione.", + "diagnosis_ram_ok": "Il sistema ha ancora {available} ({available_percent}%) di RAM disponibile su {total}.", + "diagnosis_ram_verylow": "Il sistema ha solo {available} ({available_percent}%) di RAM disponibile (su {total})", + "diagnosis_regenconf_allgood": "Tutti i file di configurazione sono allineati con le configurazioni raccomandate!", + "diagnosis_regenconf_manually_modified": "Il file di configurazione {file} sembra esser stato modificato manualmente.", + "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_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_security_vulnerable_to_meltdown": "Sembra che tu sia vulnerabile alla vulnerabilità di sicurezza critica \"Meltdown\"", + "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_services_bad_status": "Il servizio {service} è {status} :(", + "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_conf_broken": "Il servizio {service} è mal-configurato!", + "diagnosis_services_running": "Il servizio {service} sta funzionando!", + "diagnosis_sshd_config_inconsistent": "Sembra che la porta SSH sia stata modificata manualmente in /etc/ssh/sshd_config: A partire da YunoHost 4.2, una nuova configurazione globale 'security.ssh.port' è disponibile per evitare di modificare manualmente la configurazione.", + "diagnosis_sshd_config_inconsistent_details": "Esegui yunohost settings set security.ssh.port -v PORTA_SSH per definire la porta SSH, e controlla con yunohost tools regen-conf ssh --dry-run --with-diff, poi yunohost tools regen-conf ssh --force per resettare la tua configurazione con le raccomandazioni YunoHost.", + "diagnosis_sshd_config_insecure": "Sembra che la configurazione SSH sia stata modificata manualmente, ed non è sicuro dato che non contiene le direttive 'AllowGroups' o 'Allowusers' che limitano l'accesso agli utenti autorizzati.", + "diagnosis_swap_none": "Il sistema non ha lo swap. Dovresti considerare almeno di aggiungere {recommended} di memoria swap per evitare situazioni dove il sistema esaurisce la memoria.", + "diagnosis_swap_notsomuch": "Il sistema ha solo {total} di swap. Dovresti considerare almeno di aggiungere {recommended} di memoria swap per evitare situazioni dove il sistema esaurisce la memoria.", + "diagnosis_swap_ok": "Il sistema ha {total} di memoria swap!", + "diagnosis_swap_tip": "Attenzione. Sii consapevole che se il server ha lo swap su di una memoria SD o un disco SSD, potrebbe drasticamente ridurre la durata di vita del dispositivo.", + "diagnosis_unknown_categories": "Le seguenti categorie sono sconosciute: {categories}", + "disk_space_not_sufficient_install": "Non c'è abbastanza spazio libero per installare questa applicazione", + "disk_space_not_sufficient_update": "Non c'è abbastanza spazio libero per aggiornare questa applicazione", + "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_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_cert_gen_failed": "Impossibile generare il certificato", + "domain_config_api_protocol": "Protocollo API", + "domain_config_auth_application_key": "Chiave applicazione", + "domain_config_auth_application_secret": "Chiave segreta applicazione", + "domain_config_auth_consumer_key": "Chiave consumatore", + "domain_config_auth_entrypoint": "API entry point", + "domain_config_auth_key": "Chiave di autenticazione", + "domain_config_auth_secret": "Autenticazione segreta", + "domain_config_auth_token": "Token di autenticazione", + "domain_config_default_app": "Applicazione di default", + "domain_config_mail_in": "Email in arrivo", + "domain_config_mail_out": "Email in uscita", + "domain_created": "Dominio creato", + "domain_creation_failed": "Impossibile creare il dominio {domain}: {error}", + "domain_deleted": "Dominio cancellato", + "domain_deletion_failed": "Impossibile cancellare il dominio {domain}: {error}", + "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.", + "domain_dns_conf_special_use_tld": "Questo dominio è basato su un dominio di primo livello (TLD) dall’uso speciale, come .local o .test, perciò non è previsto abbia reali record DNS.", + "domain_dns_push_already_up_to_date": "I record sono aggiornati, nulla da fare.", + "domain_dns_push_failed": "L’aggiornamento dei record DNS è miseramente fallito.", + "domain_dns_push_failed_to_list": "Il reperimento dei record attuali usando le API del registrar è fallito: {error}", + "domain_dns_push_managed_in_parent_domain": "La configurazione automatica del DNS è gestita nel dominio genitore {parent_domain}.", + "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://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "Record DNS parzialmente aggiornati: alcuni segnali/errori sono stati riportati.", + "domain_dns_push_success": "Record DNS aggiornati!", + "domain_dns_pushing": "Sincronizzando i record DNS…", + "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_registrar_managed_in_parent_domain": "Questo dominio è un sotto-dominio di {parent_domain_link}. La configurazione del registrar DNS dovrebbe essere gestita dal pannello di configurazione di {parent_domain}.", + "domain_dns_registrar_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_supported": "YunoHost ha automaticamente riconosciuto che questo dominio è gestito dal registrar **{registrar}**. Se vuoi e se fornirai le credenziali API appropriate, YunoHost può configurare automaticamente questa zona DNS. Puoi trovare la documentazione su come ottenere le tue credenziali API su questa pagina. (Puoi anche configurare i tuoi record DNS manualmente, seguendo la documentazione)", + "domain_dns_registrar_yunohost": "Questo dominio è un nohost.me / nohost.st / ynh.fr, perciò la sua configurazione DNS è gestita automaticamente da YunoHost, senza alcuna ulteriore configurazione. (vedi il comando yunohost dyndns update)", + "domain_dyndns_already_subscribed": "Hai già sottoscritto un dominio DynDNS", + "domain_exists": "Il dominio esiste già", + "domain_hostname_failed": "Impossibile impostare il nuovo hostname. Potrebbe causare problemi in futuro (o anche no).", + "domain_registrar_is_not_configured": "Il registrar non è ancora configurato per il dominio {domain}.", + "domain_remove_confirm_apps_removal": "Rimuovere questo dominio rimuoverà anche le seguenti applicazioni:\n{apps}\n\nSei sicuro di voler continuare? [{answers}]", + "domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio:\n{apps}\n\nDisinstallale eseguendo 'yunohost app remove app_id' o spostale in un altro dominio eseguendo 'yunohost app change-url app_id' prima di procedere alla cancellazione del dominio", + "domain_unknown": "Il dominio '{domain}' è sconosciuto", + "domains_available": "Domini disponibili:", + "done": "Terminato", + "download_bad_status_code": "{url} ha restituito il codice di stato {code}", + "download_ssl_error": "Errore SSL durante la connessione a {url}", + "download_timeout": "{url} ci ha messo troppo a rispondere, abbandonato.", + "download_unknown_error": "Errore durante il download di dati da {url} : {error}", + "downloading": "Scaricamento…", + "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`.", + "dpkg_lock_not_available": "Impossibile eseguire il comando in questo momento perché un altro programma sta bloccando dpkg (il package manager di sistema)", + "dyndns_could_not_check_available": "Impossibile controllare se {domain} è disponibile su {provider}.", + "dyndns_domain_not_provided": "Il fornitore DynDNS {provider} non può fornire il dominio {domain}.", + "dyndns_ip_update_failed": "Impossibile aggiornare l'indirizzo IP in DynDNS", + "dyndns_ip_updated": "Il tuo indirizzo IP è stato aggiornato su DynDNS", + "dyndns_key_not_found": "La chiave DNS non è stata trovata per il dominio", + "dyndns_no_domain_registered": "Nessuno dominio registrato con DynDNS", + "dyndns_provider_unreachable": "Incapace di raggiungere il provider DynDNS {provider}: o il tuo YunoHost non è connesso ad internet o il server dynette è down.", + "dyndns_unavailable": "Il dominio {domain} non disponibile.", + "error_changing_file_permissions": "Errore durante il cambio di permessi per {path}: {error}", + "error_removing": "Errore durante la rimozione {path}: {error}", + "error_writing_file": "Errore durante la scrittura del file {file}: {error}", + "extracting": "Estrazione…", + "field_invalid": "Campo '{field}' non valido", + "file_does_not_exist": "Il file {path} non esiste.", + "file_not_exist": "Il file non esiste: '{path}'", + "firewall_reload_failed": "Impossibile ricaricare il firewall. Per ulteriori informazioni, vedi il registro.", + "firewall_reloaded": "Firewall ricaricato", + "global_settings_setting_admin_strength": "Complessità della password di amministratore", + "global_settings_setting_backup_compress_tar_archives_help": "Quando creo nuovi backup, usa un archivio (.tar.gz) al posto di un archivio non compresso (.tar). NB: abilitare quest'opzione significa create backup più leggeri, ma la procedura durerà di più e il carico CPU sarà maggiore.", + "global_settings_setting_nginx_compatibility_help": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_nginx_redirect_to_https_help": "Reindirizza richieste HTTP a HTTPs di default (NON DISABILITARE a meno che tu non sappia veramente bene cosa stai facendo!)", + "global_settings_setting_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_security_experimental_enabled_help": "Abilita funzionalità di sicurezza sperimentali (non abilitare se non sai cosa stai facendo!)", + "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.", + "global_settings_setting_smtp_relay_password": "Password del relay SMTP", + "global_settings_setting_smtp_relay_port": "Porta del relay SMTP", + "global_settings_setting_smtp_relay_user": "User account del relay SMTP", + "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_user_strength": "Complessità della password utente", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permetti solo ad alcuni IP di accedere al webadmin.", + "global_settings_setting_webadmin_allowlist_help": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.", + "good_practices_about_admin_password": "Stai per impostare una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).", + "good_practices_about_user_password": "Ora stai per impostare una nuova password utente. La password dovrebbe essere di almeno 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una sequenza di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).", + "group_already_exist": "Il gruppo {group} esiste già", + "group_already_exist_on_system": "Il gruppo {group} esiste già tra i gruppi di sistema", + "group_already_exist_on_system_but_removing_it": "Il gruppo {group} esiste già tra i gruppi di sistema, ma YunoHost lo cancellerà…", + "group_cannot_be_deleted": "Il gruppo {group} non può essere eliminato manualmente.", + "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_cannot_edit_primary_group": "Il gruppo '{group}' non può essere modificato manualmente. È il gruppo principale con lo scopo di contenere solamente uno specifico utente.", + "group_cannot_edit_visitors": "Il gruppo 'visitatori' non può essere modificato manualmente. È un gruppo speciale che rappresenta i visitatori anonimi", + "group_created": "Gruppo '{group}' creato", + "group_creation_failed": "Impossibile creare il gruppo '{group}': {error}", + "group_deleted": "Gruppo '{group}' cancellato", + "group_deletion_failed": "Impossibile cancellare il gruppo '{group}': {error}", + "group_unknown": "Gruppo '{group}' sconosciuto", + "group_update_failed": "Impossibile aggiornare il gruppo '{group}': {error}", + "group_updated": "Gruppo '{group}' aggiornato", + "group_user_already_in_group": "L'utente {user} è già nel gruppo {group}", + "group_user_not_in_group": "L'utente {user} non è nel gruppo {group}", + "hook_exec_failed": "Impossibile eseguire lo script: {path}", + "hook_exec_not_terminated": "Los script non è stato eseguito correttamente: {path}", + "hook_json_return_error": "Impossibile leggere la risposta del hook {path}. Errore: {msg}. Contenuto raw: {raw_content}", + "hook_list_by_invalid": "Questa proprietà non può essere usata per listare gli hooks", + "hook_name_unknown": "Nome di hook '{name}' sconosciuto", + "installation_complete": "Installazione completata", + "invalid_number": "Dev'essere un numero", + "invalid_regex": "Regex invalida:'{regex}'", + "invalid_url": "Fallita connessione a {url}… magari il servizio è down, o non sei connesso correttamente a internet con IPv4/IPv6.", + "ldap_attribute_already_exists": "L’attributo LDAP '{attribute}' esiste già con il valore '{value}'", + "ldap_server_down": "Impossibile raggiungere il server LDAP", + "ldap_server_is_down_restart_it": "Il servizio LDAP è down, prova a riavviarlo…", + "log_app_action_run": "Esegui l'azione dell'app '{}'", + "log_app_change_url": "Cambia l'URL dell'app '{}'", + "log_app_config_set": "Applica la configurazione all’app '{}'", + "log_app_install": "Installa l'app '{}'", + "log_app_makedefault": "Rendi '{}' l'app predefinita", + "log_app_remove": "Rimuovi l'app '{}'", + "log_app_upgrade": "Aggiorna l'app '{}'", + "log_available_on_yunopaste": "Questo registro è ora disponibile via {url}", + "log_backup_create": "Crea un archivio backup", + "log_backup_restore_app": "Ripristina '{}' da un archivio di backup", + "log_backup_restore_system": "Ripristina sistema da un archivio di backup", + "log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}", + "log_does_exists": "Non esiste nessun registro delle operazioni chiamato '{log}', usa 'yunohost log list' per vedere tutti i registri delle operazioni disponibili", + "log_domain_add": "Aggiungi il dominio '{}' nella configurazione di sistema", + "log_domain_config_set": "Aggiorna la configurazione per il dominio '{}'", + "log_domain_dns_push": "Sincronizza i record DNS per il dominio '{}'", + "log_domain_main_domain": "Rendi '{}' il dominio principale", + "log_domain_remove": "Rimuovi il dominio '{}' dalla configurazione di sistema", + "log_dyndns_subscribe": "Sottoscrivi un sottodominio YunoHost '{}'", + "log_dyndns_update": "Aggiorna l'IP associato con il tuo sottodominio YunoHost '{}'", + "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'", + "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Installa un certificato Let's encrypt sul dominio '{}'", + "log_letsencrypt_cert_renew": "Rinnova il certificato Let's Encrypt sul dominio '{}'", + "log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", + "log_link_to_log": "Registro completo di questa operazione: '{desc}'", + "log_operation_unit_unclosed_properly": "Operazion unit non è stata chiusa correttamente", + "log_regen_conf": "Rigenera configurazioni di sistema '{}'", + "log_remove_on_failed_install": "Rimuovi '{}' dopo un'installazione fallita", + "log_selfsigned_cert_install": "Installa un certificato autofirmato sul dominio '{}'", + "log_tools_migrations_migrate_forward": "Esegui le migrazioni", + "log_tools_postinstall": "Postinstallazione del tuo server YunoHost", + "log_tools_reboot": "Riavvia il tuo server", + "log_tools_shutdown": "Spegni il tuo server", + "log_tools_upgrade": "Aggiornamento dei pacchetti di sistema", + "log_user_create": "Aggiungi l'utente '{}'", + "log_user_delete": "Elimina l'utente '{}'", + "log_user_group_create": "Crea il gruppo '{}'", + "log_user_group_delete": "Cancella il gruppo '{}'", + "log_user_group_update": "Aggiorna il gruppo '{}'", + "log_user_import": "Importa utenti", + "log_user_update": "Aggiorna le informazioni dell'utente '{}'", + "mail_alias_remove_failed": "Impossibile rimuovere l'alias mail '{mail}'", + "mail_domain_unknown": "Indirizzo mail non valido per il dominio '{domain}'. Usa un dominio gestito da questo server.", + "mail_forward_remove_failed": "Impossibile rimuovere la mail inoltrata '{mail}'", + "mail_unavailable": "Questo indirizzo email è riservato e dovrebbe essere automaticamente assegnato al primo utente", + "mailbox_disabled": "E-mail disabilitate per l'utente {user}", + "mailbox_used_space_dovecot_down": "La casella di posta elettronica Dovecot deve essere attivato se vuoi recuperare lo spazio usato dalla posta elettronica", + "main_domain_change_failed": "Impossibile cambiare il dominio principale", + "main_domain_changed": "Il dominio principale è stato cambiato", + "migration_ldap_backup_before_migration": "Sto generando il backup del database LDAP e delle impostazioni delle app prima di effettuare la migrazione.", + "migration_ldap_can_not_backup_before_migration": "Il backup del sistema non è stato completato prima che la migrazione fallisse. Errore: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Impossibile migrare… provo a ripristinare il sistema.", + "migration_ldap_rollback_success": "Sistema ripristinato allo stato precedente.", + "migrations_already_ran": "Migrazioni già effettuate: {ids}", + "migrations_dependencies_not_satisfied": "Esegui queste migrazioni: '{dependencies_id}', prima di {id}.", + "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' sono opzioni che si escludono a vicenda.", + "migrations_failed_to_load_migration": "Impossibile caricare la migrazione {id}: {error}", + "migrations_list_conflict_pending_done": "Non puoi usare sia '--previous' e '--done' allo stesso tempo.", + "migrations_loading_migration": "Caricamento migrazione {id}…", + "migrations_migration_has_failed": "Migrazione {id} non completata, annullamento. Errore: {exception}", + "migrations_must_provide_explicit_targets": "Devi specificare i target quando utilizzi '--skip' o '--force-rerun'", + "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_no_migrations_to_run": "Nessuna migrazione da eseguire", + "migrations_no_such_migration": "Non esiste una migrazione chiamata '{id}'", + "migrations_not_pending_cant_skip": "Queste migrazioni non sono in attesa, quindi non possono essere saltate: {ids}", + "migrations_pending_cant_rerun": "Queste migrazioni sono ancora in attesa, quindi non possono essere eseguite nuovamente: {ids}", + "migrations_running_forward": "Eseguo migrazione {id}…", + "migrations_skip_migration": "Salto migrazione {id}…", + "migrations_success_forward": "Migrazione {id} completata", + "migrations_to_be_ran_manually": "Migrazione {id} dev'essere eseguita manualmente. Vai in Strumenti → Migrazioni nella pagina webadmin, o esegui `yunohost tools migrations run`.", + "nftables_unavailable": "Non puoi giocare con nftables qui. O sei in un container o il tuo kernel non lo supporta", + "not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path}'", + "operation_interrupted": "L'operazione è stata interrotta manualmente?", + "other_available_options": "… e {n} altre opzioni di variabili non mostrate", + "password_listed": "Questa password è una tra le più utilizzate al mondo. Per favore scegline una più unica.", + "password_too_simple_1": "La password deve contenere almeno 8 caratteri", + "password_too_simple_2": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole", + "password_too_simple_3": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole e simboli", + "password_too_simple_4": "La password deve essere lunga almeno 12 caratteri e contenere numeri, maiuscole e minuscole", + "pattern_backup_archive_name": "Deve essere un nome di file valido di massimo 30 caratteri di lunghezza, con caratteri alfanumerici e \"-_.\" come unica punteggiatura", + "pattern_domain": "Deve essere un nome di dominio valido (es. il-mio-dominio.org)", + "pattern_email": "L'indirizzo email deve essere valido, senza simboli '+' (es. tizio@dominio.com)", + "pattern_email_forward": "Dev'essere un indirizzo mail valido, simbolo '+' accettato (es: tizio+tag@example.com)", + "pattern_mailbox_quota": "La dimensione deve avere un suffisso b/k/M/G/T o 0 per disattivare la quota", + "pattern_password": "Deve contenere almeno 3 caratteri", + "pattern_password_app": "Mi spiace, le password non possono contenere i seguenti caratteri: {forbidden_chars}", + "pattern_port_or_range": "Deve essere un numero di porta valido (es. 0-65535) o una fascia di porte valida (es. 100:200)", + "pattern_username": "Caratteri minuscoli alfanumerici o trattini bassi soli", + "permission_already_allowed": "Il gruppo '{group}' ha già il permesso '{permission}' abilitato", + "permission_already_disallowed": "Il gruppo '{group}' ha già il permesso '{permission}' disabilitato", + "permission_cannot_remove_main": "Non è possibile rimuovere un permesso principale", + "permission_cant_add_to_all_users": "Il permesso {permission} non può essere aggiunto a tutto gli utenti.", + "permission_created": "Permesso '{permission}' creato", + "permission_creation_failed": "Impossibile creare i permesso '{permission}': {error}", + "permission_currently_allowed_for_all_users": "Il permesso è attualmente garantito a tutti gli utenti oltre gli altri gruppi. Probabilmente vuoi o rimuovere il permesso 'all_user' o rimuovere gli altri gruppi per cui è garantito attualmente.", + "permission_deleted": "Permesso '{permission}' cancellato", + "permission_deletion_failed": "Impossibile cancellare il permesso '{permission}': {error}", + "permission_not_found": "Permesso '{permission}' non trovato", + "permission_protected": "Il permesso {permission} è protetto. Non puoi aggiungere o rimuovere il gruppo visitatori dal permesso.", + "permission_require_account": "Il permesso {permission} ha senso solo per gli utenti con un account, quindi non può essere attivato per i visitatori.", + "permission_update_failed": "Impossibile aggiornare il permesso '{permission}': {error}", + "permission_updated": "Permesso '{permission}' aggiornato", + "port_already_closed": "La porta {port} è già chiusa", + "port_already_opened": "La porta {port} è già aperta", + "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", + "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'…", + "regenconf_failed": "Impossibile rigenerare la configurazione per le categorie: {categories}", + "regenconf_file_backed_up": "File di configurazione '{conf}' salvato in '{backup}'", + "regenconf_file_copy_failed": "Impossibile copiare il nuovo file di configurazione da '{new}' a '{conf}'", + "regenconf_file_kept_back": "Il file di configurazione '{conf}' dovrebbe esser stato cancellato da regen-conf (categoria {category}), ma non è così.", + "regenconf_file_manually_modified": "Il file di configurazione '{conf}' è stato modificato manualmente e non sarà aggiornato", + "regenconf_file_manually_removed": "Il file di configurazione '{conf}' è stato rimosso manualmente, e non sarà generato", + "regenconf_file_remove_failed": "Impossibile rimuovere il file di configurazione '{conf}'", + "regenconf_file_removed": "File di configurazione '{conf}' rimosso", + "regenconf_file_updated": "File di configurazione '{conf}' aggiornato", + "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_now_managed_by_yunohost": "Il file di configurazione '{conf}' da adesso è gestito da YunoHost (categoria {category}).", + "regenconf_pending_applying": "Applico le configurazioni in attesa per la categoria '{category}'…", + "regenconf_up_to_date": "Il file di configurazione è già aggiornato per la categoria '{category}'", + "regenconf_updated": "Configurazione aggiornata per '{category}'", + "regenconf_would_be_updated": "La configurazione sarebbe stata aggiornata per la categoria '{category}'", + "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", + "regex_with_only_domain": "Non puoi usare una regex per il dominio, solo per i percorsi", + "restore_already_installed_app": "Un'applicazione con l'ID '{app}' è già installata", + "restore_already_installed_apps": "Le seguenti app non possono essere ripristinate perché sono già installate: {apps}", + "restore_backup_too_old": "Questo archivio backup non può essere ripristinato perché è stato generato da una versione troppo vecchia di YunoHost.", + "restore_cleaning_failed": "Impossibile pulire la directory temporanea di ripristino", + "restore_complete": "Ripristino completo", + "restore_confirm_yunohost_installed": "Sei sicuro di volere ripristinare un sistema già installato? {answers}", + "restore_extracting": "Sto estraendo i file necessari dall'archivio…", + "restore_failed": "Impossibile ripristinare il sistema", + "restore_hook_unavailable": "Lo script di ripristino per '{part}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio", + "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_not_enough_disk_space": "Spazio libero insufficiente (spazio: {free_space}B, necessario: {needed_space}B, margine di sicurezza: {margin}B)", + "restore_nothings_done": "Nulla è stato ripristinato", + "restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea", + "restore_running_app_script": "Ripristino dell'app '{app}'…", + "restore_running_hooks": "Esecuzione degli hook di ripristino…", + "restore_system_part_failed": "Impossibile ripristinare la sezione di sistema '{part}'", + "root_password_desynchronized": "La password d'amministratore è stata cambiata, ma YunoHost non ha potuto propagarla alla password di root!", + "server_reboot": "Il server si riavvierà", + "server_reboot_confirm": "Il server si riavvierà immediatamente, sei sicuro? [{answers}]", + "server_shutdown": "Il server si spegnerà", + "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers}]", + "service_add_failed": "Impossibile aggiungere il servizio '{service}'", + "service_added": "Il servizio '{service}' è stato aggiunto", + "service_already_started": "Il servizio '{service}' è già avviato", + "service_already_stopped": "Il servizio '{service}' è già stato fermato", + "service_cmd_exec_failed": "Impossibile eseguire il comando '{command}'", + "service_description_dnsmasq": "Gestisce la risoluzione dei domini (DNS)", + "service_description_dovecot": "Consente ai client mail di accedere/recuperare le email (via IMAP e POP3)", + "service_description_fail2ban": "Ti protegge dal brute-force e altri tipi di attacchi da Internet", + "service_description_mysql": "Memorizza i dati delle app (database SQL)", + "service_description_nftables": "Gestisce l'apertura e la chiusura delle porte ai servizi", + "service_description_nginx": "Serve o permette l'accesso a tutti i siti pubblicati sul tuo server", + "service_description_postfix": "Usato per inviare e ricevere email", + "service_description_redis-server": "Un database specializzato usato per un veloce accesso ai dati, task queue, e comunicazioni tra programmi", + "service_description_slapd": "Memorizza utenti, domini e info correlate", + "service_description_ssh": "Ti consente di accedere da remoto al tuo server attraverso il terminale (protocollo SSH)", + "service_description_yunohost-api": "Gestisce l'interazione tra l'interfaccia web YunoHost ed il sistema", + "service_description_yunomdns": "Ti permette di raggiungere il tuo server usando 'yunohost.local' all’interno della tua rete locale", + "service_disable_failed": "Impossibile disabilitare l'avvio al boot del servizio '{service}'", + "service_disabled": "Il servizio '{service}' non partirà più al boot di sistema.", + "service_enable_failed": "Impossibile eseguire il servizio '{service}' al boot di sistema.", + "service_enabled": "Il servizio '{service}' si avvierà automaticamente al boot di sistema.", + "service_not_reloading_because_conf_broken": "Non sto ricaricando/riavviando il servizio '{name}' perché la sua configurazione è rotta: {errors}", + "service_reload_failed": "Impossibile ricaricare il servizio '{service}'", + "service_reload_or_restart_failed": "Impossibile ricaricare o riavviare il servizio '{service}'", + "service_reloaded": "Servizio '{service}' ricaricato", + "service_reloaded_or_restarted": "Il servizio '{service}' è stato ricaricato o riavviato", + "service_remove_failed": "Impossibile rimuovere il servizio '{service}'", + "service_removed": "Servizio '{service}' rimosso", + "service_restart_failed": "Impossibile riavviare il servizio '{service}'", + "service_restarted": "Servizio '{service}' riavviato", + "service_start_failed": "Impossibile eseguire il servizio '{service}'", + "service_started": "Servizio '{service}' avviato", + "service_stop_failed": "Impossibile fermare il servizio '{service}'", + "service_stopped": "Servizio '{service}' fermato", + "service_unknown": "Servizio '{service}' sconosciuto", + "show_tile_cant_be_enabled_for_regex": "Non puoi abilitare 'show_tile' in questo momento, perché l'URL del permesso '{permission}' è una regex", + "show_tile_cant_be_enabled_for_url_not_defined": "Non puoi abilitare 'show_tile' in questo momento, devi prima definire un URL per il permesso '{permission}'", + "ssowat_conf_generated": "La configurazione SSOwat rigenerata", + "system_upgraded": "Sistema aggiornato", + "system_username_exists": "Il nome utente esiste già negli utenti del sistema", + "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`.", + "unbackup_app": "{app} non verrà salvata", + "unexpected_error": "È successo qualcosa di inatteso: {error}", + "unknown_error_reading_file": "Errore sconosciuto durante il tentativo di leggere il file {file} (motivo: {error})", + "unknown_group": "Gruppo '{group}' sconosciuto", + "unknown_main_domain_path": "Percorso o dominio sconosciuto per '{app}'. Devi specificare un dominio e un percorso per poter specificare un URL per il permesso.", + "unknown_user": "Utente '{user}' sconosciuto", + "unlimit": "Nessuna quota", + "unrestore_app": "{app} non verrà ripristinata", + "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}", + "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}", + "updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema…", + "upgrading_packages": "Aggiornamento dei pacchetti…", + "upnp_dev_not_found": "Nessuno supporto UPnP trovato", + "upnp_disabled": "UPnP è disattivato", + "upnp_enabled": "UPnP è attivato", + "upnp_port_open_failed": "Impossibile aprire le porte attraverso UPnP", + "user_already_exists": "L'utente '{user}' esiste già", + "user_created": "Utente creato", + "user_creation_failed": "Impossibile creare l'utente {user}: {error}", + "user_deleted": "Utente cancellato", + "user_deletion_failed": "Impossibile cancellare l'utente {user}: {error}", + "user_home_creation_failed": "Impossibile creare la home directory '{home}' del utente", + "user_import_bad_file": "Il tuo file CSV non è formattato correttamente e sarà ignorato per evitare potenziali perdite di dati", + "user_import_bad_line": "Linea errata {line}: {details}", + "user_import_failed": "L’operazione di importazione è completamente fallita", + "user_import_missing_columns": "Mancano le seguenti colonne: {columns}", + "user_import_nothing_to_do": "Nessun utente deve essere importato", + "user_import_partial_failed": "L’importazione degli utenti è parzialmente fallita", + "user_import_success": "Utenti importati con successo", + "user_unknown": "Utente sconosciuto: {user}", + "user_update_failed": "Impossibile aggiornare l'utente {user}: {error}", + "user_updated": "Info dell'utente cambiate", + "yunohost_already_installed": "YunoHost è già installato", + "yunohost_configured": "YunoHost ora è configurato", + "yunohost_installing": "Installazione di YunoHost…", + "yunohost_not_installed": "YunoHost non è correttamente installato. Esegui 'yunohost tools postinstall'", + "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://doc.yunohost.org/admin." +} diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 0000000..9ec0db9 --- /dev/null +++ b/locales/ja.json @@ -0,0 +1,707 @@ +{ + "aborting": "中止します。", + "action_invalid": "不正なアクション ’ {action}’", + "additional_urls_already_added": "アクセス許可 '{permission}' に対する追加URLには ‘{url}’ が既に追加されています", + "additional_urls_already_removed": "アクセス許可 ‘{permission}’ に対する追加URLで ‘{url}’ は既に削除されています", + "admin_password": "管理者パスワード", + "admins": "管理者", + "all_users": "YunoHostの全ユーザー", + "already_up_to_date": "何もすることはありません。すべてが最新です。", + "app_action_broke_system": "このアクションは、これらの重要なサービスを壊したようです: {services}", + "app_action_cannot_be_ran_because_required_services_down": "このアクションを実行するには、次の必要なサービスが実行されている必要があります: {services} 。続行するには再起動してみてください (そして何故実行されていないのか調査してください)。", + "app_action_failed": "アプリ{app}のアクション{action}の実行に失敗しました", + "app_already_installed": "{app}は既にインストールされています", + "app_already_installed_cant_change_url": "このアプリは既にインストールされています。この機能だけではURLを変更することはできません。利用可能な場合は、`app changeurl`を確認してください。", + "app_arch_not_supported": "このアプリはアーキテクチャ {required} にのみインストールできますが、サーバーのアーキテクチャは{current} です", + "app_argument_choice_invalid": "引数 '{name}' に有効な値を選択してください: '{value}' は使用可能な選択肢に含まれていません ({choices})", + "app_argument_invalid": "引数 '{name}' の有効な値を選択してください: {error}", + "app_change_url_failed": "{app}のURLを変更できませんでした:{error}", + "app_change_url_identical_domains": "古いドメインと新しいドメイン/url_pathは同一であるため( '{domain}{path}')、何もしません。", + "app_change_url_no_script": "アプリ ‘{app_name}’ はまだURLの変更をサポートしていません。おそらく、あなたはそれをアップグレードする必要があります。", + "app_change_url_require_full_domain": "{app}は完全なドメイン(つまり、path = /)を必要とするため、この新しいURLに移動できません。", + "app_change_url_script_failed": "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_full_domain_unavailable": "申し訳ありませんが、このアプリは独自のドメインにインストールする必要がありますが、他のアプリは既にドメイン '{domain}' にインストールされています。代わりに、このアプリ専用のサブドメインを使用できます。", + "app_id_invalid": "不正なアプリID", + "app_install_failed": "インストールできません {app}:{error}", + "app_install_files_invalid": "これらのファイルはインストールできません", + "app_install_script_failed": "アプリのインストールスクリプト内部でエラーが発生しました", + "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_manifest_install_ask_password": "このアプリの管理パスワードを選択してください", + "app_manifest_install_ask_path": "このアプリをインストールするURLパス(ドメインの後)を選択します", + "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_properly_removed": "{app}が正しく削除されていません", + "app_packaging_format_not_supported": "このアプリは、パッケージ形式がこのYunoHostバージョンではサポートされていないため、インストールできません。おそらく、システムのアップグレードを検討する必要があります。", + "app_remove_after_failed_install": "インストールの失敗後にアプリを削除しています…", + "app_removed": "'{app}' はアンインストール済", + "app_requirements_checking": "{app} の依存関係を確認しています…", + "app_resource_failed": "{app}のリソースのプロビジョニング、プロビジョニング解除、または更新に失敗しました: {error}", + "app_restore_failed": "{app}を復元できませんでした: {error}", + "app_restore_script_failed": "アプリのリストアスクリプト内でエラーが発生しました", + "app_sources_fetch_failed": "ソースファイルをフェッチできませんでしたが、URLは正しいですか?", + "app_start_backup": "{app}用にバックアップするファイルを収集しています…", + "app_start_install": "‘{app}’ をインストールしています…", + "app_start_remove": "‘{app}’ を削除しています…", + "app_start_restore": "‘{app}’ をリストアしています…", + "app_unknown": "未知のアプリ", + "app_unsupported_remote_type": "アプリで使用されている、サポートされないリモートの種類", + "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_catalog_obsolete_cache": "アプリケーションカタログキャッシュが空であるか、古くなっています。", + "apps_catalog_update_success": "アプリケーションカタログを更新しました!", + "apps_catalog_updating": "アプリケーションカタログを更新しています…", + "ask_admin_fullname": "管理者 フルネーム", + "ask_admin_username": "管理者ユーザー名", + "ask_fullname": "フルネーム", + "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_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_corrupted": "バックアップアーカイブ ’{archive}’ は破損しているようです: {error}", + "backup_archive_name_exists": "この名前のバックアップアーカイブはすでに存在します。", + "backup_archive_name_unknown": "‘{name}’ という不明なローカルバックアップアーカイブ", + "backup_archive_open_failed": "バックアップアーカイブを開けませんでした", + "backup_archive_system_part_not_available": "このバックアップでは、システム部分 '{part}' を使用できません", + "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_hook_unknown": "バックアップ フック '{hook}' が不明です", + "backup_method_copy_finished": "バックアップコピーがファイナライズされました", + "backup_method_custom_finished": "カスタム バックアップ方法 '{method}' が完了しました", + "backup_method_tar_finished": "TARバックアップアーカイブが作成されました", + "backup_mount_archive_for_restore": "復元のためにアーカイブを準備しています…", + "backup_no_uncompress_archive_dir": "そのような圧縮されていないアーカイブディレクトリはありません", + "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_output_symlink_dir_broken": "アーカイブディレクトリ '{path}' は壊れたシンボリックリンクです。おそらく、リンク先の記憶媒体をマウント/再マウントし忘れたか、差し込むのを忘れたのではないかと。", + "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}には復元スクリプトがないため、このアプリのバックアップを自動的に復元することはできません。", + "cannot_open_file": "ファイル{file}を開けませんでした(理由:{error})", + "cannot_write_file": "ファイル {file}を書き込めませんでした (理由: {error})", + "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_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_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_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_domain_not_diagnosed_yet": "ドメイン{domain}の診断結果はまだありません。診断セクションのカテゴリ'DNSレコード'と'Web'の診断を再実行して、ドメインの暗号化が準備できているかどうかを確認してください。(または、何をしているかがわかっている場合は、'--no-checks'を使用してこれらのチェックをオフにします。", + "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})", + "certmanager_unable_to_parse_self_CA_name": "自己署名機関の名前をパースできませんでした (ファイル: {file})", + "config_action_disabled": "アクション '{action}' は無効になっているため実行できませんでした。制約を満たしていることを確認してください。ヘルプ: {help}", + "config_action_failed": "アクション '{action}' の実行に失敗しました: {error}", + "config_apply_failed": "新しい設定の適用に失敗しました: {error}", + "config_cant_set_value_on_section": "設定セクション全体に 1 つの値を設定することはできません。", + "config_forbidden_keyword": "キーワード '{keyword}' は予約されており、この ID を持つ質問を含む設定パネルを作成または使用することはできません。", + "config_forbidden_readonly_type": "型 '{type}' は読み取り専用として設定できず、別の型を使用してこの値をレンダリングしてください (関連する引数 ID: '{id}')。", + "config_no_panel": "設定パネルが見つかりません。", + "config_unknown_filter_key": "フィルター キー '{filter_key}' が正しくありません。", + "confirm_app_install_danger": "危険!このアプリはまだ実験的であることが知られています(明示的に動作しないとされていない場合)! 自分で何をしているのかわからない限り、それをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません… それでも、とにかくそのリスクを冒しても構わないと思っているなら、'{answers}'と入力してください", + "confirm_app_install_thirdparty": "危険!このアプリはYunoHostのアプリカタログの一部ではありません。サードパーティのアプリをインストールすると、システムの整合性とセキュリティが損なわれる可能性があります。あなたが何をしているのかわからない限り、それをインストールしないでください。このアプリが機能しないか、システムを壊した場合、サポートは提供されません… それでもとにかくそのリスクを冒しても構わないと思っているなら、'{answers}'と入力してください", + "confirm_app_install_warning": "警告:このアプリは動作する可能性がありますが、YunoHostにうまく統合されていません。シングル サインオンやバックアップ/復元などの一部の機能は使用できない場合があります。とにかくインストールしますか? [{answers}] ", + "confirm_app_insufficient_ram": "このアプリのインストールには、現在利用可能なRAM容量を超えるメモリが必要です。このアプリを実行できたとしても、インストール/アップグレードには大量のRAMが必要なため、サーバーがフリーズして惨めに失敗する可能性があります。とにかく、そのリスクを冒しても構わないと思っているなら'{answers}'と入力してください", + "confirm_notifications_read": "警告: 続行する前に、上記のアプリ通知を確認する必要があります。知っておくべき重要なことがあるかもしれません。[{answers}]", + "corrupted_json": "{ressource}から読み取られたJSONは破損していました(理由:{error})", + "corrupted_toml": "破損した TOML が{ressource}から読み取られました (理由: {error})", + "corrupted_yaml": "破損した YAML が{ressource}から読み取られました (理由: {error})", + "danger": "危険:", + "diagnosis_apps_allgood": "インストールされているすべてのアプリは、基本的なパッケージ化プラクティスを尊重します", + "diagnosis_apps_bad_quality": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_broken": "このアプリケーションは現在、YunoHostのアプリケーションカタログで壊れているとフラグが付けられています。これは、メンテナが問題を修正しようとしている間の一時的な問題である可能性があります。それまでの間、このアプリのアップグレードは無効になります。", + "diagnosis_apps_deprecated_practices": "このアプリのインストール済みバージョンでは、非常に古い非推奨のパッケージ化プラクティスがまだ使用されています。あなたは本当にそれをアップグレードすることを検討する必要があります。", + "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_hardware": "サーバーのハードウェア アーキテクチャが{virt} {arch}", + "diagnosis_basesystem_hardware_model": "サーバーモデルが{model}", + "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_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_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}
期待値: {content}", + "diagnosis_dns_good_conf": "DNS レコードがドメイン {domain} (カテゴリ {category}) 用に正しく構成されている", + "diagnosis_dns_missing_record": "推奨される DNS 構成に従って、次の情報を含む DNS レコードを追加する必要があります。
種類: {type}
名前: {name}
価値: {content}", + "diagnosis_dns_point_to_doc": "DNS レコードの構成についてサポートが必要な場合は 、https://doc.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_domain_expiration_not_found": "一部のドメインの有効期限を確認できない", + "diagnosis_domain_expiration_not_found_details": "ドメイン{domain}のWHOIS情報に有効期限に関する情報が含まれていないようですね?", + "diagnosis_domain_expiration_success": "ドメインは登録されており、すぐに期限切れになることはありません。", + "diagnosis_domain_expiration_warning": "一部のドメインはまもなく期限切れになります!", + "diagnosis_domain_expires_in": "{domain} の有効期限は {days}日です。", + "diagnosis_domain_not_found_details": "ドメイン{domain}がWHOISデータベースに存在しないか、有効期限が切れています!", + "diagnosis_everything_ok": "{category}はすべて大丈夫そうです!", + "diagnosis_failed": "カテゴリ '{category}' の診断結果を取得できませんでした: {error}", + "diagnosis_failed_for_category": "カテゴリ '{category}' の診断に失敗しました: {error}", + "diagnosis_found_errors": "{category}に関連する{errors}重大な問題が見つかりました!", + "diagnosis_found_errors_and_warnings": "{category}に関連する重大な問題が{errors}(および{warnings}の警告)見つかりました!", + "diagnosis_found_warnings": "{category}{warnings}改善できるアイテムが見つかりました。", + "diagnosis_high_number_auth_failures": "最近、疑わしいほど多くの認証失敗が発生しています。fail2banが実行されていて正しく構成されていることを確認するか、https://doc.yunohost.org/security で説明されているようにSSHにカスタムポートを使用することをお勧めします。", + "diagnosis_http_bad_status_code": "サーバーの代わりに別のマシン(おそらくインターネットルーター)が応答したようです。
1.この問題の最も一般的な原因は、ポート80(および443)が サーバーに正しく転送されていないことです。
2.より複雑なセットアップでは、ファイアウォールまたはリバースプロキシが干渉していないことを確認します。", + "diagnosis_http_connection_error": "接続エラー: 要求されたドメインに接続できませんでした。到達できない可能性が非常に高いです。", + "diagnosis_http_could_not_diagnose": "ドメインが IPv{ipversion} の外部から到達可能かどうかを診断できませんでした。", + "diagnosis_http_could_not_diagnose_details": "エラー: {error}", + "diagnosis_http_hairpinning_issue": "ローカルネットワークでヘアピニングが有効になっていないようです。", + "diagnosis_http_hairpinning_issue_details": "これはおそらくISPボックス/ルーターが原因です。その結果、ローカルネットワークの外部の人々は期待どおりにサーバーにアクセスできますが、ドメイン名またはグローバルIPを使用する場合、ローカルネットワーク内の人々(おそらくあなたのような人)はアクセスできません。https://doc.yunohost.org/dns_local_network を見ることによって状況を改善できるかもしれません", + "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_ignored_issues": "(+{nb_ignored}無視された問題)", + "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_dnsresolution_working": "ドメイン名前解決は機能しています!", + "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://doc.yunohost.org/ipv6。IPv6を有効にできない場合、または技術的に難しすぎると思われる場合は、この警告を無視しても問題ありません。", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 は通常、システムまたはプロバイダー (使用可能な場合) によって自動的に構成されます。それ以外の場合は、こちらのドキュメントで説明されているように、いくつかのことを手動で構成する必要があります: https://doc.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_blocklist_listed_by": "あなたのIPまたはドメイン {item} はブラックリスト {blocklist_name} に登録されています", + "diagnosis_mail_blocklist_ok": "このサーバーが使用するIPとドメインはブラックリストに登録されていないようです", + "diagnosis_mail_blocklist_reason": "ブラックリストの登録理由は次のとおりです: {reason}", + "diagnosis_mail_blocklist_website": "リストされている理由を特定して修正した後、IPまたはドメインを削除するように依頼してください: {blocklist_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_could_not_diagnose_details": "エラー: {error}", + "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_4": "一部のプロバイダーでは、逆引きDNSを構成できません(または機能が壊れている可能性があります…)。そのせいで問題が発生している場合は、次の解決策を検討してください。
- 一部のISPが提供するメールサーバーリレーを使用する ことで代替できますが、ISPが電子メールトラフィックを盗み見る可能性があることを意味します。
- プライバシーに配慮した代替手段は、この種の制限を回避するために*専用のパブリックIP*を持つVPNを使用することです。https://doc.yunohost.org/vpn_advantage を見る
-または、別のプロバイダーに切り替えることが可能です", + "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_fcrdns_ok": "逆引きDNSが正しく構成されています!", + "diagnosis_mail_outgoing_port_25_blocked": "送信ポート 25 が IPv{ipversion} でブロックされているため、SMTP メール サーバーは他のサーバーに電子メールを送信できません。", + "diagnosis_mail_outgoing_port_25_blocked_details": "まず、インターネットルーターインターフェイスまたはホスティングプロバイダーインターフェイスの送信ポート25のブロックを解除する必要があります。(一部のホスティングプロバイダーでは、このために問い合わせを行う必要がある場合があります)。", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一部のプロバイダーは、ネット中立性を気にしないため、送信ポート25のブロックを解除することを許可しません。
-それらのいくつかは 、メールサーバーリレーを使用する 代替手段を提供しますが、リレーが電子メールトラフィックをスパイできることを意味します。
- プライバシーに配慮した代替手段は、*専用のパブリックIP*を持つVPNを使用して、これらの種類の制限を回避することです。https://doc.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_never_ran_yet": "このサーバーは最近セットアップされたようで、表示する診断レポートはまだありません。Web管理画面またはコマンドラインから ’yunohost diagnosis run’ を実行して、完全な診断を実行することから始める必要があります。", + "diagnosis_no_cache": "カテゴリ '{category}' の診断キャッシュがまだありません", + "diagnosis_package_installed_from_sury": "一部のシステムパッケージはダウングレードする必要があります", + "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_forwarding_tip": "この問題を解決するには、ほとんどの場合、https://doc.yunohost.org/admin/get_started/post_install/dns_config/ で説明されているように、インターネットルーターでポート転送を構成する必要があります", + "diagnosis_ports_needed_by": "このポートの公開は、{category}機能 (サービス {service}) に必要です。", + "diagnosis_ports_ok": "ポート {port} は外部から到達可能です。", + "diagnosis_ports_partially_unreachable": "ポート {port} は、IPv{failed} では外部から到達できません。", + "diagnosis_ports_unreachable": "ポート {port} は外部から到達できません。", + "diagnosis_processes_killed_by_oom_reaper": "一部のプロセスは、メモリが不足したため、最近システムによって強制終了されました。これは通常、システム上のメモリ不足、またはプロセスがメモリを消費しすぎていることを示しています。強制終了されたプロセスの概要:\n{kills_summary}", + "diagnosis_ram_low": "システムには{available}({available_percent}%)の使用可能なRAMがあります({total}のうち)。注意してください。", + "diagnosis_ram_ok": "システムには、{total}のうち{available} ({available_percent}%) の RAM がまだ使用可能です。", + "diagnosis_ram_verylow": "システムには{available}({available_percent}%)のRAMしか使用できません。({total}のうち)", + "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_rootfstotalspace_warning": "ルートファイルシステムには合計{space}しかありません。これは問題ないかもしれませんが、最終的にはディスク容量がすぐに枯渇する可能性があるため、注意してください… ルートファイルシステム用に少なくとも16GBを用意することをお勧めします。", + "diagnosis_security_vulnerable_to_meltdown": "Meltdown(重大なセキュリティの脆弱性)に対して脆弱に見えます", + "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_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_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_notsomuch": "システムにはスワップが {total} しかありません。システムのメモリ不足の状況を回避するために、少なくとも {recommended} のスワップを用意することを検討してください。", + "diagnosis_swap_ok": "システムには {total} のスワップがあります!", + "diagnosis_swap_tip": "サーバーがSDカードまたはSSDストレージでスワップをホストしている場合、デバイスの平均寿命が大幅に短くなる可能性があることに注意してください。", + "diagnosis_unknown_categories": "次のカテゴリは不明です: {categories}", + "diagnosis_using_stable_codename": "apt (システムのパッケージマネージャ) は現在、現在の Debian バージョン (bullseye) のコードネームではなく、コードネーム 'stable' からパッケージをインストールするように設定されています。", + "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_install": "このアプリケーションをインストールするのに十分なディスク領域が残っていません", + "disk_space_not_sufficient_update": "このアプリケーションを更新するのに十分なディスク領域が残っていません", + "domain_cannot_remove_main": "'{domain}'はメインドメインなので削除できないので、まず'yunohost domain main-domain -n'を使用して別のドメインをメインドメインとして設定する必要があります。 候補 ドメインのリストは次のとおりです。 {other_domains}", + "domain_cannot_remove_main_add_new_one": "'{domain}'はメインドメインであり唯一のドメインであるため、最初に'yunohostドメイン追加'を使用して別のドメインを追加し、次に'yunohostドメインメインドメイン-n 'を使用してメインドメインとして設定し、'yunohostドメイン削除{domain}'を使用してドメイン'{domain}'を削除する必要があります。", + "domain_cert_gen_failed": "証明書を生成できませんでした", + "domain_config_acme_eligible": "ACMEの資格", + "domain_config_acme_eligible_explain": "このドメインは、Let's Encrypt証明書の準備ができていないようです。DNS 構成と HTTP サーバーの到達可能性を確認してください。 診断ページの 'DNSレコード'と'Web'セクションは、何が誤って構成されているかを理解するのに役立ちます。", + "domain_config_api_protocol": "API プロトコル", + "domain_config_auth_application_key": "アプリケーションキー", + "domain_config_auth_application_secret": "アプリケーション秘密鍵", + "domain_config_auth_consumer_key": "消費者キー", + "domain_config_auth_entrypoint": "API エントリ ポイント", + "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": "証明書の状態", + "domain_config_cert_summary_abouttoexpire": "現在の証明書の有効期限が近づいています。すぐに自動的に更新されるはずです。", + "domain_config_cert_summary_expired": "クリティカル: 現在の証明書が無効です!HTTPSはまったく機能しません!", + "domain_config_cert_summary_letsencrypt": "やった!有効なLet's Encrypt証明書を使用しています!", + "domain_config_cert_summary_ok": "さて、現在の証明書は良さそうです!", + "domain_config_cert_summary_selfsigned": "警告: 現在の証明書は自己署名です。ブラウザは新しい訪問者に不気味な警告を表示します!", + "domain_config_cert_validity": "データの入力規則", + "domain_config_default_app": "デフォルトのアプリ", + "domain_config_default_app_help": "このドメインを開くと、ユーザーは自動的にこのアプリにリダイレクトされます。アプリが指定されていない場合、ユーザーはユーザーポータルのログインフォームにリダイレクトされます。", + "domain_config_mail_in": "受信メール", + "domain_config_mail_out": "送信メール", + "domain_created": "作成されたドメイン", + "domain_creation_failed": "ドメイン {domain}を作成できません: {error}", + "domain_deleted": "ドメインが削除されました", + "domain_deletion_failed": "ドメイン {domain}を削除できません: {error}", + "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_dns_push_failed_to_list": "レジストラの API を使用して現在のレコードを一覧表示できませんでした: {error}", + "domain_dns_push_managed_in_parent_domain": "自動 DNS 構成機能は、親ドメイン {parent_domain}で管理されます。", + "domain_dns_push_not_applicable": "自動 DNS 構成機能は、ドメイン {domain}には適用されません。https://doc.yunohost.org/dns_config のドキュメントに従って、DNS レコードを手動で構成する必要があります。", + "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://doc.yunohost.org/dns_config のドキュメントに従って手動で構成する必要があります。", + "domain_dns_registrar_supported": "YunoHost は、このドメインがレジストラ **{registrar}** によって処理されていることを自動的に検出しました。必要に応じて適切なAPI資格情報を提供すると、YunoHostはこのDNSゾーンを自動的に構成します。API 資格情報の取得方法に関するドキュメントは、https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/ ページにあります。(https://doc.yunohost.org/dns_config のドキュメントに従ってDNSレコードを手動で構成することもできます)", + "domain_dns_registrar_yunohost": "このドメインは nohost.me / nohost.st / ynh.fr であるため、DNS構成は特別な構成なしでYunoHostによって自動的に処理されます。(‘yunohost dyndns update’ コマンドを参照)", + "domain_dyndns_already_subscribed": "すでに DynDNS ドメインにサブスクライブしています", + "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": "完了", + "download_bad_status_code": "{url}は状態コード {code} を返しました", + "download_ssl_error": "{url}への接続時のSSLエラー", + "download_timeout": "{url}は応答に時間がかかりすぎたため、あきらめました。", + "download_unknown_error": "{url}からデータをダウンロードする際のエラー:{error}", + "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_domain_not_provided": "DynDNS プロバイダー{provider} はドメイン{domain}を提供できません。", + "dyndns_ip_update_failed": "IP アドレスを DynDNS で更新できませんでした", + "dyndns_ip_updated": "DynDNSでIPを更新しました", + "dyndns_key_not_found": "ドメインの DNS キーが見つかりません", + "dyndns_no_domain_registered": "DynDNS に登録されているドメインがありません", + "dyndns_provider_unreachable": "DynDNSプロバイダー {provider} に到達できません: YunoHostがインターネットに正しく接続されていないか、dynetteサーバーがダウンしています。", + "dyndns_unavailable": "ドメイン '{domain}' は使用できません。", + "error_changing_file_permissions": "{path}のアクセス許可変更時のエラー: {error}", + "error_removing": "{path}を削除するときのエラー:{error}", + "error_writing_file": "ファイル{file}書き込み時のエラー:{error}", + "extracting": "抽出中…", + "field_invalid": "無効なフィールド '{field}'", + "file_does_not_exist": "ファイル {path}が存在しません。", + "file_not_exist": "ファイルが存在しません: '{path}'", + "firewall_reload_failed": "ファイアウォールをリロードできませんでした。詳細情報はログに残されています。", + "firewall_reloaded": "ファイアウォールがリロードされました", + "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_pop3_enabled": "POP3 を有効にする", + "global_settings_setting_pop3_enabled_help": "メール サーバーの POP3 プロトコルを有効にする", + "global_settings_setting_postfix_compatibility": "Postfixの互換性", + "global_settings_setting_postfix_compatibility_help": "Postfix サーバーの互換性とセキュリティにはトレードオフがあります。暗号(およびその他のセキュリティ関連の側面)に影響します", + "global_settings_setting_root_access_explain": "Linux システムでは'root'が絶対に管理者です。YunoHost のコンテキストでは、'root'ユーザーでのSSH ログインは(サーバーのローカルネットワークからのSSHである場合を除き)デフォルトで無効になっています。'admins' グループのメンバーは、sudo コマンドを使用することで、root としてコマンドを実行できます。ただし、何らかの理由で通常の管理者がログインできなくなった場合には、システムをデバッグするための(堅牢な)rootパスワードがあると便利です。", + "global_settings_setting_root_password": "新しいルートパスワード", + "global_settings_setting_root_password_confirm": "新しいルートパスワード(確認)", + "global_settings_setting_security_experimental_enabled": "実験的なセキュリティ機能", + "global_settings_setting_security_experimental_enabled_help": "実験的なセキュリティ機能を有効にします(何をしているのかわからない場合は有効にしないでください)", + "global_settings_setting_smtp_allow_ipv6": "IPv6 を許可する", + "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_user_strength": "ユーザー パスワードの強度要件", + "global_settings_setting_user_strength_help": "これらの要件は、パスワードを初期化または変更する場合にのみ適用されます", + "global_settings_setting_webadmin_allowlist": "ウェブ管理者 IP 許可リスト", + "global_settings_setting_webadmin_allowlist_enabled": "ウェブ管理 IP 許可リストを有効にする", + "global_settings_setting_webadmin_allowlist_enabled_help": "一部の IP のみにウェブ管理者へのアクセスを許可します。", + "global_settings_setting_webadmin_allowlist_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_be_deleted": "グループ{group}を手動で削除することはできません。", + "group_cannot_edit_all_users": "グループ 'all_users' は手動で編集できません。これは、YunoHostに登録されているすべてのユーザーを含むことを目的とした特別なグループです", + "group_cannot_edit_primary_group": "グループ '{group}' を手動で編集することはできません。これは、特定のユーザーを 1 人だけ含むためのプライマリ グループです。", + "group_cannot_edit_visitors": "グループの'訪問者'を手動で編集することはできません。匿名の訪問者を代表する特別なグループです", + "group_created": "グループ '{group}' が作成されました", + "group_creation_failed": "グループ '{group}' を作成できませんでした: {error}", + "group_deleted": "グループ '{group}' が削除されました", + "group_deletion_failed": "グループ '{group}' を削除できませんでした: {error}", + "group_mailalias_add": "メール エイリアス '{mail}' がグループ '{group}' に追加されます。", + "group_mailalias_remove": "メール エイリアス '{mail}' がグループ '{group}' から削除されます。", + "group_no_change": "グループ '{group}' に対して変更はありません", + "group_unknown": "グループ '{group}' は不明です", + "group_update_aliases": "グループ '{group}' のエイリアスの更新", + "group_update_failed": "グループ '{group}' を更新できませんでした: {error}", + "group_updated": "グループ '{group}' が更新されました", + "group_user_add": "ユーザー '{user}' がグループ '{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}", + "hook_json_return_error": "フック{path}からリターンを読み取れませんでした。エラー: {msg}. 生のコンテンツ: {raw_content}", + "hook_list_by_invalid": "このプロパティは、フックを一覧表示するために使用することはできません", + "hook_name_unknown": "不明なフック名 '{name}'", + "installation_complete": "インストールが完了しました", + "invalid_credentials": "無効なパスワードまたはユーザー名", + "invalid_number": "数値にする必要があります", + "invalid_regex": "無効な正規表現: '{regex}'", + "invalid_shell": "無効なシェル: {shell}", + "invalid_url": "{url}に接続できませんでした…サービスがダウンしているか、IPv4 / IPv6でインターネットに正しく接続されていない可能性があります。", + "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_install": "‘{}’ アプリをインストールする", + "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_regen_conf": "システム構成 '{}' を再生成する", + "log_remove_on_failed_install": "インストールに失敗した後に'{}'を削除します", + "log_resource_snippet": "リソースのプロビジョニング/プロビジョニング解除/更新", + "log_selfsigned_cert_install": "'{}'ドメインに自己署名証明書をインストールする", + "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_create": "‘{}’ ユーザーを追加", + "log_user_delete": "‘{}’ ユーザーを削除", + "log_user_group_create": "‘{}’ グループを作成", + "log_user_group_delete": "‘{}’ グループを削除", + "log_user_group_update": "‘{}’ グループを更新", + "log_user_import": "ユーザーをインポート", + "mail_alias_remove_failed": "電子メール エイリアス '{mail}' を削除できませんでした", + "mail_domain_unknown": "ドメイン '{domain}' の電子メール アドレスが無効です。このサーバーによって管理されているドメインを使用してください。", + "mail_forward_remove_failed": "電子メール転送 '{mail}' を削除できませんでした", + "mail_unavailable": "この電子メール アドレスは、管理者グループ用に予約されています", + "mailbox_disabled": "ユーザーの{user}に対して電子メールがオフになっている", + "mailbox_used_space_dovecot_down": "使用済みメールボックススペースをフェッチする場合は、Dovecotメールボックスサービスが稼働している必要があります", + "main_domain_change_failed": "メインドメインを変更できません", + "main_domain_changed": "メインドメインが変更されました", + "migration_ldap_backup_before_migration": "実際の移行の前に、LDAP データベースとアプリ設定のバックアップを作成します。", + "migration_ldap_can_not_backup_before_migration": "移行が失敗する前に、システムのバックアップを完了できませんでした。エラー: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "移行できませんでした…システムをロールバックしようとしています。", + "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_no_migrations_to_run": "実行する移行はありません", + "migrations_no_such_migration": "'{id}'と呼ばれる移行はありません", + "migrations_not_pending_cant_skip": "これらの移行は保留中ではないため、スキップすることはできません。 {ids}", + "migrations_pending_cant_rerun": "これらの移行はまだ保留中であるため、再度実行することはできません{ids}", + "migrations_running_forward": "移行{id}を実行しています…", + "migrations_skip_migration": "移行{id}スキップしています…", + "migrations_success_forward": "移行{id}完了しました", + "migrations_to_be_ran_manually": "移行{id}は手動で実行する必要があります。Web管理ページの移行→ツールに移動するか、'yunohost tools migrations run'を実行してください。", + "nftables_unavailable": "ここではnftablesを使うことはできません。あなたはコンテナ内にいるか、カーネルがサポートしていません", + "not_enough_disk_space": "'{path}'に十分な空き容量がありません", + "operation_interrupted": "操作は手動で中断されたようですね?", + "other_available_options": "…および{n}個の表示されない他の使用可能なオプション", + "password_confirmation_not_the_same": "パスワードが一致しません", + "password_listed": "このパスワードは、世界で最も使用されているパスワードの1つです。もっと他の人と被っていないものを選んでください。", + "password_too_long": "127文字未満のパスワードを使用してください", + "password_too_simple_1": "パスワードは8文字以上である必要があります", + "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_fullname": "有効なフルネーム (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_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}", + "permission_updated": "アクセス許可 '{permission}' が更新されました", + "port_already_closed": "ポート {port} はすでに閉じられている", + "port_already_opened": "ポート {port} はすでに開いている", + "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_confirm_yunohost_installed": "すでにインストールされているシステムを復元しますか?[{answers}]", + "restore_extracting": "アーカイブから必要なファイルを抽出しています…", + "restore_failed": "システムを復元できませんでした", + "restore_hook_unavailable": "'{part}'の復元スクリプトは、システムで使用できず、アーカイブでも利用できません", + "restore_may_be_not_enough_disk_space": "システムに十分なスペースがないようです(空き:{free_space} B、必要なスペース:{needed_space} B、セキュリティマージン:{margin} B)", + "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": "ルートのパスワードが変更されました", + "root_password_desynchronized": "管理者パスワードが変更されましたが、YunoHostはこれをrootパスワードに反映できませんでした!", + "server_reboot": "サーバーが再起動します", + "server_reboot_confirm": "サーバーはすぐに再起動しますが、よろしいですか? [{answers}]", + "server_shutdown": "サーバーがシャットダウンします", + "server_shutdown_confirm": "サーバーはすぐにシャットダウンしますが、よろしいですか? [{answers}]", + "service_add_failed": "サービス '{service}' を追加できませんでした", + "service_added": "サービス '{service}' が追加されました", + "service_already_started": "サービス '{service}' は既に実行されています", + "service_already_stopped": "サービス '{service}' は既に停止されています", + "service_cmd_exec_failed": "コマンド '{command}' を実行できませんでした", + "service_description_dnsmasq": "ドメイン名解決 (DNS) を処理します", + "service_description_dovecot": "電子メールクライアントが電子メールにアクセス/フェッチすることを許可します(IMAPおよびPOP3経由)", + "service_description_fail2ban": "インターネットからのブルートフォース攻撃やその他の攻撃から保護します", + "service_description_mysql": "アプリ データの格納 (SQL データベース)", + "service_description_nftables": "サービスへの接続ポートの開閉を管理", + "service_description_nginx": "サーバーでホストされているすべてのWebサイトへのアクセスを提供します", + "service_description_postfix": "電子メールの送受信に使用", + "service_description_postgresql": "アプリ データの格納 (SQL データベース)", + "service_description_redis-server": "高速データ・アクセス、タスク・キュー、およびプログラム間の通信に使用される特殊なデータベース", + "service_description_slapd": "ユーザー、ドメイン、関連情報を格納します", + "service_description_ssh": "ターミナル経由でサーバーにリモート接続できます(SSHプロトコル)", + "service_description_yunohost-api": "YunoHostウェブインターフェイスとシステム間の連携を管理します", + "service_description_yunomdns": "ローカルネットワークで'yunohost.local'を使用してサーバーに到達できます", + "service_disable_failed": "起動時にサービス '{service}' を開始できませんでした。", + "service_disabled": "システムの起動時にサービス '{service}' は自動開始されなくなります。", + "service_enable_failed": "起動時にサービス '{service}' を自動的に開始できませんでした。", + "service_enabled": "サービス '{service}' は、システムの起動時に自動的に開始されるようになりました。", + "service_not_reloading_because_conf_broken": "構成が壊れているため、サービス'{name}'をリロード/再起動しません: {errors}", + "service_reload_failed": "サービス '{service}' をリロードできませんでした", + "service_reload_or_restart_failed": "サービス '{service}' をリロードまたは再起動できませんでした", + "service_reloaded": "サービス '{service}' がリロードされました", + "service_reloaded_or_restarted": "サービス '{service}' が再読み込みまたは再起動されました", + "service_remove_failed": "サービス '{service}' を削除できませんでした", + "service_removed": "サービス '{service}' が削除されました", + "service_restart_failed": "サービス '{service}' を再起動できませんでした", + "service_restarted": "サービス '{service}' が再起動しました", + "service_start_failed": "サービス '{service}' を開始できませんでした", + "service_started": "サービス '{service}' が開始されました", + "service_stop_failed": "サービス '{service}' を停止できません", + "service_stopped": "サービス '{service}' が停止しました", + "service_unknown": "不明なサービス '{service}'", + "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": "システムのアップグレード", + "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_error_reading_file": "ファイル{file}を読み取ろうとしているときに不明なエラーが発生しました(理由:{error})", + "unknown_group": "不明な '{group}' グループ", + "unknown_main_domain_path": "'{app}' のドメインまたはパスが不明です。アクセス許可の URL を指定できるようにするには、ドメインとパスを指定する必要があります。", + "unknown_user": "不明な '{user}' ユーザー", + "unlimit": "クォータなし", + "unrestore_app": "{app}は復元されません", + "update_apt_cache_failed": "APT (Debian のパッケージマネージャ) のキャッシュを更新できません。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "update_apt_cache_warning": "APT(Debianのパッケージマネージャー)のキャッシュを更新中に問題が発生しました。問題のある行を特定するのに役立つ可能性のあるsources.list行のダンプを次に示します。\n{sourceslist}", + "updating_apt_cache": "システムパッケージの利用可能なアップグレードを取得しています…", + "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://doc.yunohost.org/admin。" +} diff --git a/locales/kab.json b/locales/kab.json new file mode 100644 index 0000000..d192ac6 --- /dev/null +++ b/locales/kab.json @@ -0,0 +1,143 @@ +{ + "action_invalid": "Tigawt '{action}' d tarameɣtut", + "admin_password": "Awal uffir n tedbelt", + "admins": "Inedbalen", + "all_users": "Iseqdacen n YunoHost s umata", + "app_already_installed": "{app} yettusebded ya kan", + "app_change_url_success": "URL n usnas {app} tettubeddel tura ɣer {domain}{path}", + "app_config_permission_description": "Aglam", + "app_config_permission_extraperm_section_name": "Tasiregt '{perm}'", + "app_config_permission_label": "Tabzimt", + "app_install_failed": "Ulamek tukksa n usebded n {app}: {error}", + "app_start_install": "Asebded n {app}…", + "app_start_remove": "Tukksa n {app}…", + "app_unknown": "Asnas arussin", + "app_upgrade_app_name": "Amucceḍ n {app}…", + "app_upgraded": "{app} yettwalqem", + "apps_catalog_updating": "Lqem n umucceḍ n ukaram n yesnasen…", + "ask_admin_fullname": "Isem ummid n umiḍan n unedbal", + "ask_admin_username": "Isem n umiḍan n unedbal", + "ask_fullname": "Isem ummid", + "ask_main_domain": "Taɣult tagejdant", + "ask_new_admin_password": "Awal n uɛeddi amaynut n tedbelt", + "ask_new_domain": "Taɣult tamaynut", + "ask_new_path": "Abrid amaynut", + "ask_password": "Awal n uɛeddi", + "ask_user_domain": "Taɣult ara yettwasqedcen i tansiwin imayl n useqdac", + "certmanager_cert_signing_failed": "Ur yessaweḍ ara ad yezmel aselkin-nni amaynut", + "diagnosis_basesystem_hardware_model": "Tamudemt n uqeddac d {model}", + "diagnosis_basesystem_host": "Aseqdac-a isedday Debian {debian_version}", + "diagnosis_basesystem_kernel": "Aqeddac-a isedday iɣes n Linux {kernel_version}", + "diagnosis_basesystem_ynh_main_version": "Aqeddac-a isedday YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} lqem: {version} ({repo})", + "diagnosis_description_apps": "Isnasen", + "diagnosis_description_basesystem": "Anagraw n taffa", + "diagnosis_description_dnsrecords": "Ikalasen DNS", + "diagnosis_description_ip": "Tuqqna ar Internet", + "diagnosis_description_mail": "Imayl", + "diagnosis_description_regenconf": "Tiwilawin n unagraw", + "diagnosis_description_systemresources": "Tiɣbula n unagraw", + "diagnosis_description_web": "Réseau", + "diagnosis_domain_expires_in": "{domain} ad ttfat akka {days} n wussan.", + "diagnosis_everything_ok": "Kra yellan yettban-d yelha i {category}!", + "diagnosis_http_could_not_diagnose_details": "Tuccḍa: {error}", + "diagnosis_ip_local": "IP tadigant: {local}", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Tuccḍa: {error}", + "diagnosis_mail_queue_unavailable_details": "Tuccḍa: {error}", + "diagnosis_ports_could_not_diagnose_details": "Tuccḍa: {error}", + "diagnosis_services_bad_status": "Ameẓlu {service} {status} :(", + "diagnosis_services_running": "Ameẓlu {service} itteddu!", + "domain_cert_gen_failed": "Ulamek asarew n uselkin", + "domain_config_api_protocol": "Aneggaf API", + "domain_config_auth_application_key": "Tasarut n wesnas", + "domain_config_auth_consumer_key": "Tasarut n usulu", + "domain_config_auth_key": "Tasarut n usesteb", + "domain_config_cert_install": "Sebded aselkin n Let's Encrypt", + "domain_config_cert_name": "Aselkin", + "domain_config_cert_renew": "Ɛiwed-d aselkin n Let's Encrypt", + "domain_config_cert_summary": "Addad n uselkin", + "domain_config_cert_summary_letsencrypt": "Igerrez! Aql-ik·ikem tesseqdaceḍ aselkin ameɣtu n Let's Encrypt!", + "domain_config_cert_summary_ok": "Yerbeḥ, aselkin-a yettban-d yelha!", + "domain_config_default_app": "Asnas amezwaru", + "domain_config_dns_name": "DNS", + "domain_config_portal_logo": "Alugu udmawan", + "domain_config_portal_name": "Asagen n wewwur", + "domain_config_portal_title": "Azwel udmawan", + "domain_config_search_engine": "URL n umsedday n unadi", + "domain_config_search_engine_name": "Isem n umsedday n unadi", + "domain_created": "Taɣult tettwarna", + "domain_deleted": "Taɣult tettwakkes", + "domain_exists": "Taɣult-a tella ya kan", + "domain_unknown": "Taɣult '{domain}' d tarussint", + "domains_available": "Tiɣula yellan:", + "done": "Immed", + "file_does_not_exist": "Afaylu {path} ulac-it.", + "file_not_exist": "Afaylu '{path}' ulac-it", + "global_settings_setting_backup_name": "Aḥraz", + "global_settings_setting_email_name": "Imayl", + "global_settings_setting_misc_name": "Ayen nniḍen", + "global_settings_setting_network_name": "Aẓeṭṭa", + "global_settings_setting_nginx_name": "NGINX (aqeddacWeb)", + "global_settings_setting_nginx_redirect_to_https": "Hettem HTTPS", + "global_settings_setting_password_name": "Awalen n uɛeddi", + "global_settings_setting_pop3_enabled": "Sermed POP3", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_name": "Awwur", + "global_settings_setting_root_access_name": "Snifel awal n uɛeddi aẓaran", + "global_settings_setting_root_password": "Awal n uɛeddi aẓaran", + "global_settings_setting_security_name": "Taɣellist", + "global_settings_setting_smtp_allow_ipv6": "Sireg IPv6", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_ssh_name": "SSH", + "global_settings_setting_ssh_password_authentication_help": "Sireg asesteb s wawal n uɛeddi i SSH", + "global_settings_setting_ssh_port": "Tawwurt SSH", + "group_already_exist": "Agraw {group} yella ya kan", + "group_created": "Agraw '{group}' yettwasnulfa-d", + "group_deleted": "Agraw '{group}' yettwakkes", + "group_updated": "Agraw '{group}' yettuleqqem", + "group_user_not_in_group": "Aseqdac {user} ulac-it deg ugraw {group}", + "installation_complete": "Asebded yemmed", + "invalid_password": "Awal n uɛeddi d arameɣtu", + "invalid_shell": "Shell d arameɣtu : {shell}", + "log_app_change_url": "Senfel URL n usnas '{}'", + "log_app_install": "Sebded asnas '{}'", + "log_app_makedefault": "Sbadu '{}' d netta i d asnas amezwer", + "log_app_remove": "Kkes asebded n usnas n '{}'", + "log_app_upgrade": "Sali aswir n usnas n '{}'", + "log_domain_add": "Rnu taɣult '{}'", + "log_domain_main_domain": "Sbadu '{}' d taɣult tagejdant", + "log_domain_remove": "Kkes taɣult '{}'", + "log_letsencrypt_cert_renew": "Ɛiwed-d aselkin n Let's Encrypt n '{}'", + "log_selfsigned_cert_install": "Sebded aselkin i yettwazemlen i yiman-is ɣef taɣult '{}'", + "log_settings_reset_all": "Awennez n iɣewwaren akken ma llan", + "log_settings_set": "Snes iɣewwaren", + "log_tools_reboot": "Ales asekker n uqeddac-ik·im", + "log_tools_shutdown": "Sens aqeddac-ik·im", + "log_tools_upgrade": "Sali aswir n ikemmusen n unagraw", + "log_user_create": "Rnu amiḍan n '{}'", + "log_user_delete": "Kkes amiḍan n '{}'", + "log_user_group_create": "Snulfu-d agraw n '{}'", + "log_user_group_delete": "Kkes agraw n '{}'", + "log_user_group_update": "Aleqqem n '{}' i ugraw", + "migration_description_0031_terms_of_services": "Tiwtilin n useqdec", + "port_already_closed": "Tawwurt {port} temdel ya kan", + "port_already_opened": "Tawwurt {port} teldi ya kan", + "server_reboot": "Aqeddac-a a yalles asekker", + "server_shutdown": "Aqeddac-a ad yexsi", + "service_added": "Ameẓlu '{service}' yettwarna", + "service_already_started": "Ameẓlu n '{service}' iteddu yakan", + "service_removed": "Ameẓlu '{service}' yettwakkes", + "service_started": "Ameẓlu n '{service}' yekker", + "service_stopped": "Ameẓlu '{service}' yettwaḥbes", + "service_unknown": "Ameẓlu arussin '{service}'", + "tools_upgrade": "Amucced n ikemmusen n unagraw", + "unknown_group": "Agraw '{group}' d arussin", + "unknown_user": "Aseqdac '{user}' d arussin", + "updating_apt_cache": "Awway n ileqman n umucceḍ yellan i yikemmusen n unagraw…", + "user_already_exists": "Aseqdac '{user}' yella yakan", + "user_created": "Aseqdac yettwarna", + "user_unknown": "Amiḍan n {user} d arussin", + "yunohost_already_installed": "YunoHost yettusebded yakan", + "yunohost_api": "API n YunoHost", + "yunohost_installing": "Yesebdad YunoHost…" +} diff --git a/locales/ko.json b/locales/ko.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/ko.json @@ -0,0 +1 @@ +{} diff --git a/locales/lt.json b/locales/lt.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/lt.json @@ -0,0 +1 @@ +{} diff --git a/locales/mk.json b/locales/mk.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/mk.json @@ -0,0 +1 @@ +{} diff --git a/locales/nb_NO.json b/locales/nb_NO.json new file mode 100644 index 0000000..9ab74c1 --- /dev/null +++ b/locales/nb_NO.json @@ -0,0 +1,107 @@ +{ + "aborting": "Avbryter.", + "action_invalid": "Ugyldig handling '{action}'", + "admin_password": "Administrasjonspassord", + "already_up_to_date": "Ingenting å gjøre. Alt er oppdatert.", + "app_action_broke_system": "Denne handlingen ser ut til å ha knekt disse viktige tjenestene: {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": "{app} er allerede installert", + "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.", + "app_argument_choice_invalid": "Bruk én av disse valgene '{choices}' for argumentet '{name}'", + "app_argument_invalid": "Velg en gydlig verdi for argumentet '{name}': {error}", + "app_change_url_success": "{app} nettadressen er nå {domain}{path}", + "app_extraction_failed": "Kunne ikke pakke ut installasjonsfilene", + "app_id_invalid": "Ugyldig program-ID", + "app_install_failed": "Kunne ikke installere {app}: {error}", + "app_install_files_invalid": "Disse filene kan ikke installeres", + "app_not_correctly_installed": "{app} ser ikke ut til å ha blitt installert på riktig måte", + "app_not_properly_removed": "{app} har ikke blitt fjernet på riktig måte", + "app_removed": "{app} fjernet", + "app_requirements_checking": "Sjekker påkrevde pakker for {app}…", + "app_start_backup": "Samler inn filer for sikkerhetskopiering for {app}…", + "app_start_install": "Installerer programmet '{app}'…", + "app_start_remove": "Fjerner programmet '{app}'…", + "app_start_restore": "Gjenoppretter programmet '{app}'…", + "app_unknown": "Ukjent program", + "app_upgrade_app_name": "Oppgraderer {app}…", + "app_upgrade_failed": "Kunne ikke oppgradere {app}: {error}", + "app_upgrade_several_apps": "Følgende programmer vil oppgraderes: {apps}", + "app_upgrade_some_app_failed": "Noen programmer kunne ikke oppgraderes", + "app_upgraded": "{app} oppgradert", + "apps_already_up_to_date": "Alle programmer allerede oppdatert", + "ask_main_domain": "Hoveddomene", + "ask_new_admin_password": "Nytt administrasjonspassord", + "ask_new_domain": "Nytt domene", + "ask_new_path": "Ny sti", + "ask_password": "Passord", + "backup_abstract_method": "Denne sikkerhetskopimetoden er ikke implementert enda", + "backup_actually_backuping": "Oppretter sikkerhetskopiarkiv fra innsamlede filer…", + "backup_applying_method_copy": "Kopier alle filer til sikkerhetskopi…", + "backup_applying_method_tar": "Lager TAR-sikkerhetskopiarkiv…", + "backup_archive_app_not_found": "Fant ikke programmet '{app}' i sikkerhetskopiarkivet", + "backup_archive_name_exists": "En sikkerhetskopi med dette navnet '{name}' finnes allerede.", + "backup_archive_name_unknown": "Ukjent lokalt sikkerhetskopiarkiv ved navn '{name}'", + "backup_archive_open_failed": "Kunne ikke åpne sikkerhetskopiarkivet", + "backup_copying_to_organize_the_archive": "Kopierer {size} MB for å organisere arkivet", + "backup_couldnt_bind": "Kunne ikke binde {src} til {dest}.", + "backup_created": "Sikkerhetskopi opprettet: {name}", + "backup_creation_failed": "Kunne ikke opprette sikkerhetskopiarkiv", + "backup_csv_addition_failed": "Kunne ikke legge til filer for sikkerhetskopi inn i CSV-filen", + "backup_delete_error": "Kunne ikke slette '{path}'", + "backup_deleted": "Sikkerhetskopi slettet: {name}", + "backup_method_copy_finished": "Sikkerhetskopi fullført", + "backup_method_tar_finished": "TAR-sikkerhetskopiarkiv opprettet", + "backup_mount_archive_for_restore": "Forbereder arkiv for gjenopprettelse…", + "backup_no_uncompress_archive_dir": "Det finnes ingen slik utpakket arkivmappe", + "certmanager_cert_signing_failed": "Kunne ikke signere det nye sertifikatet", + "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}: {error}", + "domain_deleted": "Domene slettet", + "domain_deletion_failed": "Kunne ikke slette domene {domain}: {error}", + "domain_dyndns_already_subscribed": "Du har allerede abonnement på et DynDNS-domene", + "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}.", + "dyndns_ip_update_failed": "Kunne ikke oppdatere IP-adresse til DynDNS", + "dyndns_ip_updated": "Oppdaterte din IP på DynDNS", + "dyndns_key_not_found": "Fant ikke DNS-nøkkel for domenet", + "dyndns_no_domain_registered": "Inget domene registrert med DynDNS", + "dyndns_provider_unreachable": "Kunne ikke nå DynDNS-tilbyder {provider}: Enten har du ikke satt opp din YunoHost rett, dynette-tjeneren er nede, eller du mangler nett.", + "extracting": "Pakker ut…", + "field_invalid": "Ugyldig felt '{field}'", + "firewall_reloaded": "Brannmur gjeninnlastet", + "global_settings_setting_admin_strength": "Admin-passordets styrke", + "global_settings_setting_user_strength": "Brukerpassordets styrke", + "log_app_change_url": "Endre nettadresse for '{}'-programmet", + "log_app_install": "Installer '{}'-programmet", + "log_app_makedefault": "Gjør '{}' til forvalgt program", + "log_app_remove": "Fjern '{}'-programmet", + "log_app_upgrade": "Oppgrader '{}'-programmet", + "log_available_on_yunopaste": "Denne loggen er nå tilgjengelig via {url}", + "log_backup_restore_app": "Gjenopprett '{}' fra sikkerhetskopiarkiv", + "log_domain_add": "Legg til '{}'-domenet i systemoppsett", + "log_domain_remove": "Fjern '{}'-domenet fra systemoppsett", + "log_dyndns_subscribe": "Abonner på YunoHost-underdomenet '{}'", + "log_dyndns_update": "Oppdater IP-adressen tilknyttet ditt YunoHost-underdomene '{}'", + "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Installer et Let's Encrypt-sertifikat på '{}'-domenet", + "log_letsencrypt_cert_renew": "Forny '{}'-Let's Encrypt-sertifikat", + "log_link_to_log": "Full logg for denne operasjonen: '{desc}'", + "log_remove_on_failed_install": "Fjern '{}' etter mislykket installasjon", + "log_selfsigned_cert_install": "Installer selvsignert sertifikat på '{}'-domenet", + "log_tools_reboot": "Utfør omstart av tjeneren din", + "log_tools_shutdown": "Slå av tjeneren din", + "log_user_create": "Legg til '{}' bruker", + "log_user_delete": "Slett '{}' bruker", + "log_user_group_delete": "Slett '{}' gruppe", + "log_user_group_update": "Oppdater '{}' gruppe", + "log_user_update": "Oppdater brukerinfo for '{}'", + "mail_alias_remove_failed": "Kunne ikke fjerne e-postaliaset '{mail}'", + "mail_domain_unknown": "Ukjent e-postadresse for domenet '{domain}'.", + "unknown_group": "Ukjent '{group}' gruppe", + "unknown_user": "Ukjent '{user}' bruker" +} diff --git a/locales/ne.json b/locales/ne.json new file mode 100644 index 0000000..72c4c85 --- /dev/null +++ b/locales/ne.json @@ -0,0 +1,3 @@ +{ + "password_too_simple_1": "पासवर्ड कम्तिमा characters अक्षर लामो हुनु आवश्यक छ" +} diff --git a/locales/nl.json b/locales/nl.json new file mode 100644 index 0000000..b7e3d65 --- /dev/null +++ b/locales/nl.json @@ -0,0 +1,225 @@ +{ + "aborting": "Annulatie.", + "action_invalid": "Ongeldige actie '{action}'", + "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}'", + "admin_password": "Administrator wachtwoord", + "admins": "Beheerders", + "all_users": "Alle YunoHost gebruikers", + "already_up_to_date": "Er is niets te doen, alles is al up-to-date.", + "app_action_broke_system": "Deze actie lijkt de volgende belangrijke services te hebben kapotgemaakt: {services}", + "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).", + "app_action_failed": "Mislukte actie {action} voor app {app}", + "app_already_installed": "{app} is al geïnstalleerd", + "app_already_installed_cant_change_url": "Deze app is al geïnstalleerd. De URL kan niet veranderd worden met deze functie. Probeer of dat lukt via `app changeurl`.", + "app_arch_not_supported": "Deze applicatie kan alleen geïnstalleerd op architecturen {required} maar jouw server architectuur is {current}", + "app_argument_choice_invalid": "Kiel een geldige waarde voor argument '{name}'; {value}' komt niet voor in de keuzelijst {choices}", + "app_argument_invalid": "Kies een geldige waarde voor '{name}': {error}", + "app_change_url_failed": "Het is niet gelukt de URL voor {app} te wijzigen: {error}", + "app_change_url_identical_domains": "De oude en nieuwe domeinnaam/url_path zijn identiek ('{domain}{path}'), er is niets te doen.", + "app_change_url_no_script": "App '{app_name}' ondersteunt nog geen URL-aanpassingen. Misschien wel na een upgrade.", + "app_change_url_require_full_domain": "{app} kan niet naar deze nieuwe URL verplaatst worden, omdat het een eigen (sub)domein nodig heeft (dat wil zeggen, \"Pad\" = /)", + "app_change_url_script_failed": "Er is een fout opgetreden in het URL-wijzigings-script", + "app_change_url_success": "{app} URL is nu {domain}{path}", + "app_config__core_name": "Tegels en toelatingen", + "app_config_permission_allowed": "Groepen/gebruikers met toegang", + "app_config_permission_allowed_warn_protected": "NB: dit privelege is 'beschermd' en daarom kan de 'bezoekers'-groep niet toegevoegd/verwijderd worden van de geautoriseerde groepen.", + "app_config_permission_description": "Omschrijving", + "app_config_permission_description_help": "Dit is eigenlijk enkel van belang als je de 'beschrijvende' portaal-modus gebruikt", + "app_config_permission_extraperm_section_name": "Privelege '{perm}'", + "app_config_permission_label": "Label", + "app_config_permission_location": "Komt overeen met [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Zelfgekozen logo om te gebruiken", + "app_config_permission_logo_help": "Enkel PNG wordt ondersteund", + "app_config_permission_show_tile": "Toon de tegel in het portaal", + "app_config_unable_to_apply": "De waarden in het configuratiescherm konden niet toegepast worden.", + "app_config_unable_to_read": "Het is niet gelukt de waarden van het configuratiescherm te lezen.", + "app_corrupt_source": "YunoHost kon de hulpbron '{source_id}' ({url}) voor {app} downloaden, maar de hulpbron heeft niet de verwachtte 'checksum'. Dit kan betekenen dat er een tijdelijk netwerk probleem is gebeurd op je server, OF dat de hulpbron op een manier veranderd is door de stroomopwaartse beheerder (of een kwaadwillende speler?) en dat de YunoHost pakketbeheerder dit moet onderzoeken en misschien het app manifest moet bijwerken om dit in rekening te brengen.\n Verwachtte sha256 checksum: {expected_sha256}\n Gedownloade sha256 checksum: {computed_sha256}\n Gedownloade bestand grootte: {size}", + "app_extraction_failed": "Het lukt niet om de installatiebestanden uit te pakken", + "app_failed_to_download_asset": "Mislukte download voor hulpbron '{source_id}' ({url}) voor {app} : {out}", + "app_full_domain_unavailable": "Sorry, deze app moet op haar eigen domein geïnstalleerd worden, maar andere apps zijn al geïnstalleerd op het domein '{domain}'. Een mogelijke oplossing is om een nieuw subdomein toe te voegen, speciaal voor deze app.", + "app_id_invalid": "Ongeldige app-id", + "app_install_failed": "Kan {app} niet installeren: {error}", + "app_install_files_invalid": "Deze bestanden kunnen niet worden geïnstalleerd", + "app_install_script_failed": "Er is een fout opgetreden in het installatiescript van de app", + "app_location_unavailable": "Deze URL is niet beschikbaar of is in conflict met de al geïnstalleerde app(s):\n{apps}", + "app_make_default_location_already_used": "Kan '{app}' niet de standaardapp maken op het domein, '{domain}' wordt al gebruikt door '{other_app}'", + "app_manifest_install_ask_admin": "Kies een administrator voor deze app", + "app_manifest_install_ask_domain": "Kies het domein waar deze app op geïnstalleerd moet worden", + "app_manifest_install_ask_init_admin_permission": "Wie moet er toegang hebben tot de beheerders functies voor deze app? (Dit kan later worden aangepast)", + "app_manifest_install_ask_init_main_permission": "Wie moet toegang hebben tot deze app? (dit kan later worden aangepast)", + "app_manifest_install_ask_is_public": "Moet deze app zichtbaar zijn voor anomieme bezoekers?", + "app_manifest_install_ask_password": "Kies een administratiewachtwoord voor deze app", + "app_manifest_install_ask_path": "Kies het URL-pad (achter het domein) waar deze app geïnstalleerd moet worden", + "app_not_correctly_installed": "{app} schijnt niet juist geïnstalleerd te zijn", + "app_not_enough_disk": "Deze app vereist {required} vrije ruimte.", + "app_not_enough_ram": "Deze app vereist {required} RAM om geïnstalleerd/bijgewerkt te worden terwijl op dit moment maar {current} beschikbaar is.", + "app_not_installed": "Het lukte niet om {app} te vinden in de lijst met geïnstalleerde apps: {all_apps}", + "app_not_properly_removed": "{app} werd niet volledig verwijderd", + "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.", + "app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie…", + "app_removed": "{app} is verwijderd", + "app_requirements_checking": "Vereisten voor {app} aan het controleren…", + "app_resource_failed": "Het toekennen ('provisioning') of afnemen ('deprovisioning') van hulpbronnen, of het bijwerken van hulpbronnen voor {app} is mislukt: {error}", + "app_restore_failed": "De app '{app}' kon niet worden terug gezet: {error}", + "app_restore_script_failed": "Er ging iets mis in het helstelscript van de app", + "app_sources_fetch_failed": "Het is niet gelukt bronbestanden op te halen, klopt de URL?", + "app_start_backup": "Bestanden aan het verzamelen voor de backup van {app}…", + "app_start_install": "Bezig met installeren van {app}…", + "app_start_remove": "Bezig met verwijderen van {app}…", + "app_start_restore": "{app} herstellen…", + "app_unknown": "Onbekende app", + "app_unsupported_remote_type": "Niet ondersteund besturings type voor de app", + "app_upgrade_app_name": "Bezig {app} te upgraden…", + "app_upgrade_failed": "Het is niet gelukt app {app} bij te werken: {error}", + "app_upgrade_script_failed": "Er is een fout opgetreden in het upgradescript van de app", + "app_upgrade_several_apps": "De volgende apps zullen worden geüpgraded: {apps}", + "app_upgrade_some_app_failed": "Sommige apps konden niet worden bijgewerkt", + "app_upgraded": "{app} is bijgewerkt", + "app_yunohost_version_not_supported": "Deze app vereist YunoHost <= {required} maar op dit moment is de geïnstalleerde versie {current}", + "apps_already_up_to_date": "Alle apps zijn al bijgewerkt met de nieuwste versie", + "apps_catalog_failed_to_download": "Het is niet gelukt de {apps_catalog} app-catalogus te downloaden: {error}", + "apps_catalog_obsolete_cache": "De app catalogus cache is leeg of verouderd.", + "apps_catalog_update_success": "De applicatie catalogus is bijgewerkt!", + "apps_catalog_updating": "Bijwerken van applicatie catalogus…", + "ask_admin_fullname": "Volledige naam van beheerder", + "ask_admin_username": "Gebruikersnaam van beheerder", + "ask_dyndns_recovery_password": "DynDNS herstelwachtwoord", + "ask_dyndns_recovery_password_explain": "Gelieve een herstelwachtwoord voor jouw DynDNS domein te kiezen, in het geval je dit later moet resetten.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Gelieve het herstelwachtwoord van dit DynDNS domein in te voeren.", + "ask_dyndns_recovery_password_explain_unavailable": "Dit DynDNS domein is al geregistreerd. Als jij de persoon was die dit oorspronkelijk had geregistreerd, mag je het herstelwachtwoord invoeren om het domein te recupereren.", + "ask_fullname": "Volledige naam (voornaam en naam)", + "ask_main_domain": "Hoofd-domein", + "ask_new_admin_password": "Nieuw administratorwachtwoord", + "ask_new_domain": "Nieuw domein", + "ask_new_path": "Nieuw pad", + "ask_password": "Wachtwoord", + "ask_user_domain": "Domein om te gebruiken voor het emailadres van de gebruiker", + "automatic_task": "Automatische taak", + "backup_abstract_method": "Deze backup methode is nog niet geïmplementeerd", + "backup_actually_backuping": "Maken van een backup van de verzamelde documenten…", + "backup_applying_method_copy": "Kopiëren van alle documenten naar de backup…", + "backup_applying_method_custom": "Oproepen van de gepersonaliseerde backup methode '{method}'…", + "backup_applying_method_tar": "Maken van backup TAR archief…", + "backup_archive_app_not_found": "'{app}' kon niet in het backup archief gevonden worden", + "backup_archive_broken_link": "Het backup archief kon niet geopend worden (Ongeldige verwijzing naar {path})", + "backup_archive_cant_retrieve_info_json": "Kon info van '{archive}' niet ophalen… het info.json document kon niet gevonden worden (of is geen geldige json document).", + "backup_archive_corrupted": "Het lijkt dat de backup '{archive}' gecorrumpeerd is : {error}", + "backup_archive_name_exists": "Er bestaat al een backuparchief met de naam '{name}'.", + "backup_archive_name_unknown": "Onbekend lokaal backup archief namens '{name}' gevonden", + "backup_archive_open_failed": "Kan het backup archief niet openen", + "backup_archive_system_part_not_available": "Het deel '{part}' van het systeem is niet beschikbaar in deze backup", + "backup_archive_writing_error": "Kan de documenten '{source}' (genoemd in het archief '{dest}') niet toevoegen om bewaard te worden in het gecomprimeerd archief '{archive}'", + "backup_ask_for_copying_if_needed": "Wil je tijdelijk de backup uitvoeren met gebruik van {size}MB? (Deze manier wordt gebruikt omdat sommige documenten niet konden worden voorbereid met een meer efficiënte methode.)", + "backup_cant_mount_uncompress_archive": "Kon het ongecomprimeerd archief niet optuigen als schrijfbeveiligd", + "backup_cleaning_failed": "Kan tijdelijke backup map niet leeg maken", + "backup_copying_to_organize_the_archive": "Kopiëren van {size}MB om het archief te organizeren", + "backup_couldnt_bind": "Kan {src} niet binden met {dest}.", + "backup_create_size_estimation": "Het archief zal ongeveer {size} data bevatten.", + "backup_created": "Backup aangemaakt: {name}", + "backup_creation_failed": "Aanmaken van backup mislukt", + "backup_csv_addition_failed": "Kon de bestanden voor de backup niet bewaren in het CSV bestand", + "backup_csv_creation_failed": "Kon het CSV bestand niet maken dat nodig is voor herstel", + "backup_custom_backup_error": "Gepersonaliseerde backup methode kon niet voorbij de 'backup' stap", + "backup_custom_mount_error": "Gepersonaliseerde backup methode kon niet voorbij de 'mount' stap", + "backup_delete_error": "Kon '{path}' niet verwijderen", + "backup_deleted": "Backup werd verwijderd: {name}", + "backup_hook_unknown": "backup hook '{hook}' onbekend", + "backup_method_copy_finished": "Backup kopie voltooid", + "backup_method_custom_finished": "Gepersonaliseerde backup methode '{method}' voltooid", + "backup_method_tar_finished": "TAR backup archief aangemaakt", + "backup_mount_archive_for_restore": "Voorbereiden om archief te herstellen…", + "backup_no_uncompress_archive_dir": "Het pad van het ongecomprimeerd archief bestaat niet", + "backup_output_directory_forbidden": "Kies een ander output pad. Backups kunnen niet aangemaakt worden in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var of /home/yunohost.backup/archives sub-mappen", + "backup_output_directory_not_empty": "Je moet een lege doelmap kiezen", + "backup_output_directory_required": "Je moet een output pad geven voor de backup", + "backup_output_symlink_dir_broken": "Je archief pad '{path}' is een kapotte symlink. Mischien vergat je om deze te re/mounten of steek het opslag medium in waar het naar verwijst.", + "backup_running_hooks": "Uitvoeren van backup scripts…", + "backup_system_part_failed": "Kon het '{part}'-systeemdeel niet bewaren in de backup", + "backup_unable_to_organize_files": "Kon de snelle methode niet gebruiken om de bestanden te organizeren in het archief", + "backup_with_no_backup_script_for_app": "De app '{app}' heeft geen backup script. Negeren.", + "backup_with_no_restore_script_for_app": "{app} heeft geen herstel script, je zal niet automatisch de backup van deze app kunnen herstellen.", + "cannot_open_file": "Niet mogelijk om bestand {file} te openen (reden: {error})", + "cannot_write_file": "Niet gelukt om bestand {file} te schrijven (reden: {error})", + "certmanager_acme_not_configured_for_domain": "De ACME-uitdaging kan momenteel niet worden uitgevoerd voor {domain} omdat de nginx-configuratie het bijbehorende codefragment mist... Zorg ervoor dat je nginx-configuratie up-to-date is door `yunohost tools regen-conf nginx --dry-run --with-diff` te gebruiken.", + "certmanager_attempt_to_renew_nonLE_cert": "Het certificaat voor het domein '{domain}' is niet uitgegeven door Let's Encrypt. Kan het niet automatisch vernieuwen!", + "certmanager_attempt_to_renew_valid_cert": "Het certificaat voor het domein '{domain}' staat niet op het punt te verlopen! (Je kunt --force gebruiken als je weet wat je doet)", + "certmanager_attempt_to_replace_valid_cert": "Je probeert een goed en geldig certificaat voor domein {domain} te overschrijven! (Gebruik --force om dit te omzeilen)", + "certmanager_cannot_read_cert": "Er is iets misgegaan bij het proberen te openen van het huidige certificaat voor domein {domain} (bestand: {file}), reden: {reason}", + "certmanager_cert_install_failed": "Let's Encrypt certificaat installatie mislukt voor {domains}", + "certmanager_cert_install_failed_selfsigned": "Zelfondertekend certificaat installatie mislukt voor {domains}", + "certmanager_cert_install_success": "Let's Encrypt certificaat is nu geïnstalleerd voor het domein '{domain}'", + "corrupted_json": "Corrupte JSON gelezen van {ressource} (reden: {error})", + "corrupted_toml": "Ongeldige TOML werd gelezen van {ressource} (reason: {error})", + "corrupted_yaml": "Ongeldig YAML bestand op {ressource} (reden: {error})", + "domain_cert_gen_failed": "Kan certificaat niet genereren", + "domain_created": "Domein succesvol aangemaakt", + "domain_creation_failed": "Kan domein niet aanmaken {domain}: {error}", + "domain_deleted": "Domein succesvol verwijderd", + "domain_deletion_failed": "Kan domein niet verwijderen {domain}: {error}", + "domain_dyndns_already_subscribed": "U heeft reeds een domein bij DynDNS geregistreerd", + "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", + "download_bad_status_code": "{url} stuurt status code {code}", + "download_ssl_error": "SSL fout gedurende verbinding met {url}", + "download_timeout": "{url} neemt te veel tijd om te antwoorden, we geven het op.", + "download_unknown_error": "Fout tijdens het downloaden van data van {url}: {error}", + "downloading": "Downloaden…", + "dyndns_ip_update_failed": "Kan het IP adres niet updaten bij DynDNS", + "dyndns_ip_updated": "IP adres is aangepast bij DynDNS", + "dyndns_unavailable": "Domein '{domain}' is niet beschikbaar.", + "error_changing_file_permissions": "Fout tijdens het veranderen van machtiging voor {path}: {error}", + "error_removing": "Fout tijdens het verwijderen van {path}: {error}", + "error_writing_file": "Fout tijdens het schrijven van bestand {file}: {error}", + "extracting": "Uitpakken…", + "file_not_exist": "Bestand bestaat niet: '{path}'", + "good_practices_about_admin_password": "Je staat op het punt een nieuw beheerderswachtwoord in te voeren. Het wachtwoord moet minimaal 8 tekens lang zijn—hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens).", + "good_practices_about_user_password": "Je staat op het punt een nieuw gebruikerswachtwoord in te voeren. Het wachtwoord moet minimaal 8 tekens lang zijn—hoewel het een goede gewoonte is om een langer wachtwoord te gebruiken (d.w.z. een wachtwoordzin) en/of een variatie van tekens te gebruiken (hoofdletters, kleine letters, cijfers en speciale tekens).", + "group_already_exist": "Groep {group} bestaat al", + "group_already_exist_on_system": "Groep {group} bestaat al in de systeemgroepen", + "installation_complete": "Installatie voltooid", + "invalid_url": "Kon niet verbinden met {url}... misschien is de dienst uit de lucht, of ben je niet goed verbonden via IPv4 of IPv6.", + "mail_alias_remove_failed": "Kan mail-alias '{mail}' niet verwijderen", + "operation_interrupted": "Werd de bewerking handmatig onderbroken?", + "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_1": "Het wachtwoord moet minimaal 8 tekens lang zijn", + "password_too_simple_2": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters en kleine letters bevatten", + "password_too_simple_3": "Het wachtwoord moet minimaal 8 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten", + "password_too_simple_4": "Het wachtwoord moet minimaal 12 tekens lang zijn en moet cijfers, hoofdletters, kleine letters en speciale tekens bevatten", + "pattern_backup_archive_name": "Moet een geldige bestandsnaam zijn van maximaal 30 tekens; alleen alfanumerieke tekens en -_. zijn toegestaan", + "pattern_domain": "Moet een geldige domeinnaam zijn (mijneigendomein.nl, bijvoorbeeld)", + "pattern_email": "Moet een geldig e-mailadres bevatten, zonder '+' symbool er in (bv. abc@example.org)", + "pattern_email_forward": "Het moet een geldig e-mailadres zijn, '+' symbool is toegestaan (ikzelf@mijndomein.nl bijvoorbeeld, of ikzelf+yunohost@mijndomein.nl)", + "pattern_mailbox_quota": "Mailbox quota moet een waarde bevatten met b/k/M/G/T erachter of 0 om geen quota in te stellen", + "pattern_password": "Wachtwoord moet tenminste 3 karakters lang zijn", + "port_already_closed": "Poort {port} is al gesloten", + "port_already_opened": "Poort {port} is al open", + "restore_hook_unavailable": "De herstel-hook '{part}' is niet beschikbaar op dit systeem", + "service_add_failed": "Kan service '{service}' niet toevoegen", + "service_already_started": "Service '{service}' draait al", + "service_cmd_exec_failed": "Kan '{command}' niet uitvoeren", + "service_disabled": "Service '{service}' wordt niet meer gestart als het systeem opstart.", + "service_remove_failed": "Kan service '{service}' niet verwijderen", + "service_removed": "Service '{service}' werd verwijderd", + "service_stop_failed": "Kan service '{service}' niet stoppen", + "service_unknown": "De service '{service}' bestaat niet", + "unexpected_error": "Er is een onbekende fout opgetreden: {error}", + "unknown_error_reading_file": "Ongekende fout tijdens het lezen van bestand {file} (cause:{error})", + "unknown_group": "Groep '{group}' is onbekend", + "unknown_user": "Gebruiker '{user}' is onbekend", + "unrestore_app": "App '{app}' wordt niet teruggezet", + "updating_apt_cache": "Lijst van beschikbare pakketten wordt bijgewerkt…", + "upgrading_packages": "Pakketten worden geüpdate…", + "upnp_dev_not_found": "Geen UPnP apparaten gevonden", + "upnp_disabled": "UPnP succesvol uitgeschakeld", + "upnp_enabled": "UPnP succesvol ingeschakeld", + "upnp_port_open_failed": "Kan UPnP poorten niet openen", + "user_deleted": "Gebruiker werd verwijderd", + "user_home_creation_failed": "Kan de map '{home}' voor gebruiker niet aanmaken", + "user_unknown": "Gebruikersnaam {user} is onbekend", + "user_update_failed": "Kan gebruiker niet bijwerken {user}: {error}", + "yunohost_configured": "YunoHost configuratie is OK", + "app_upgrade_bad_quality": "Deze applicatie is op dit moment gesignaleerd als stuk op de YunoHost applicatie catalogus. Dit kan een voorlopig probleem zijn terwijl de beheerders proberen om het probleem op te lossen. In tussentijd is het bijwerken van deze app uitgeschakeld." +} diff --git a/locales/nn.json b/locales/nn.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/nn.json @@ -0,0 +1 @@ +{} diff --git a/locales/oc.json b/locales/oc.json new file mode 100644 index 0000000..80afa9b --- /dev/null +++ b/locales/oc.json @@ -0,0 +1,464 @@ +{ + "aborting": "Interrupcion.", + "action_invalid": "Accion « {action} » incorrècta", + "additional_urls_already_added": "URL addicionadal «{url}'» es ja estada aponduda per la permission «{permission}»", + "additional_urls_already_removed": "URL addicionala {url} es ja estada elimida per la permission «{permission}»", + "admin_password": "Senhal d’administracion", + "admins": "Administrators", + "all_users": "Totes los utilizaires de YunoHost", + "already_up_to_date": "I a pas res a far. Tot es ja a jorn.", + "app_action_broke_system": "Aquesta accion sembla aver copat de servicis importants : {services}", + "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}.", + "app_already_installed": "{app} es ja installat", + "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_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_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_no_script": "L’aplicacion {app_name} pren pas en compte lo cambiament d’URL, benlèu que vos cal l’actualizar.", + "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_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.", + "app_id_invalid": "ID d’aplicacion incorrècte", + "app_install_failed": "Installacion impossibla de {app} : {error}", + "app_install_files_invalid": "Installacion impossibla d’aquestes fichièrs", + "app_install_script_failed": "Una error s’es producha en installar lo script de l’aplicacion", + "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta :\n{apps}", + "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_manifest_install_ask_admin": "Causissètz un administrator per aquesta aplicacion", + "app_manifest_install_ask_domain": "Causissètz lo domeni ont volètz installar aquesta aplicacion", + "app_manifest_install_ask_is_public": "Aquesta aplicacion serà visible pels visitaires anonims ?", + "app_manifest_install_ask_password": "Causissètz lo senhal administrator per aquesta aplicacion", + "app_manifest_install_ask_path": "Causissètz lo camin ont volètz installar aquesta aplicacion", + "app_not_correctly_installed": "{app} sembla pas ben installat", + "app_not_installed": "Impossible de trobar l’aplicacion {app} dins la lista de las aplicacions installadas : {all_apps}", + "app_not_properly_removed": "{app} es pas estat corrèctament suprimit", + "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.", + "app_remove_after_failed_install": "Supression de l’aplicacion aprèp fracàs de l’installacion…", + "app_removed": "{app} es estada suprimida", + "app_requirements_checking": "Verificacion dels paquets requesits per {app}…", + "app_restore_failed": "Impossible de restaurar l’aplicacion « {app} »: {error}", + "app_restore_script_failed": "Una error s’es producha a l’interior del script de restauracion de l’aplicacion", + "app_sources_fetch_failed": "Recuperacion dels fichièrs fonts impossibla, l’URL es corrècta ?", + "app_start_backup": "Recuperacion dels fichièrs de salvagardar per {app}…", + "app_start_install": "Installacion de {app}…", + "app_start_remove": "Supression de {app}…", + "app_start_restore": "Restauracion de {app}…", + "app_unknown": "Aplicacion desconeguda", + "app_unsupported_remote_type": "Lo tipe alonhat utilizat per l’aplicacion es pas suportat", + "app_upgrade_app_name": "Actualizacion de {app}…", + "app_upgrade_failed": "Impossible d’actualizar {app} : {error}", + "app_upgrade_script_failed": "Una error s’es producha pendent l’execucion de l’script de mesa a nivèl de l’aplicacion", + "app_upgrade_several_apps": "Las aplicacions seguentas seràn actualizadas : {apps}", + "app_upgrade_some_app_failed": "D’aplicacions se pòdon pas actualizar", + "app_upgraded": "{app} es estada actualizada", + "apps_already_up_to_date": "Totas las aplicacions son ja al jorn", + "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 !", + "apps_catalog_updating": "Actualizacion del catalòg d’aplicacion…", + "ask_fullname": "Nom complèt", + "ask_main_domain": "Domeni màger", + "ask_new_admin_password": "Nòu senhal administrator", + "ask_new_domain": "Nòu domeni", + "ask_new_path": "Nòu camin", + "ask_password": "Senhal", + "ask_user_domain": "Domeni d’utilizar per l’adreça de corrièl de l’utilizaire e lo compte XMPP", + "backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat", + "backup_actually_backuping": "Creacion d’un archiu de seguretat a partir dels fichièrs recuperats…", + "backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda…", + "backup_applying_method_custom": "Crida del metòde de salvagarda personalizat « {method} »…", + "backup_applying_method_tar": "Creacion de l’archiu TAR de la salvagarda…", + "backup_archive_app_not_found": "L’aplicacion « {app} » es pas estada trobada dins l’archiu de la salvagarda", + "backup_archive_broken_link": "Impossible d’accedir a l’archiu de salvagarda (ligam invalid cap a {path})", + "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_corrupted": "Sembla que l’archiu de la salvagarda « {archive} » es corromput : {error}", + "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", + "backup_archive_open_failed": "Impossible de dobrir l’archiu de salvagarda", + "backup_archive_system_part_not_available": "La part « {part} » del sistèma es pas disponibla dins aquesta salvagarda", + "backup_archive_writing_error": "Impossible d’ajustar los fichièrs « {source} » a la salvagarda (nomenats dins l’archiu « {dest} »)dins l’archiu comprimit « {archive} »", + "backup_ask_for_copying_if_needed": "Volètz far una salvagarda en utilizant {size} Mo temporàriament ? (Aqueste biais de far es emplegat perque unes fichièrs an pas pogut èsser preparats amb un metòde mai eficaç.)", + "backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit", + "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_couldnt_bind": "Impossible de ligar {src} amb {dest}.", + "backup_create_size_estimation": "L’archiu contendrà apr’aquí {size} de donadas.", + "backup_created": "Salvagarda acabada: {name}", + "backup_creation_failed": "Creacion impossibla de l’archiu de salvagarda", + "backup_csv_addition_failed": "Impossible d’ajustar de fichièrs a la salvagarda dins lo fichièr CSV", + "backup_csv_creation_failed": "Creacion impossibla del fichièr CSV necessari a las operacions futuras de restauracion", + "backup_custom_backup_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « backup »", + "backup_custom_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « mount »", + "backup_delete_error": "Supression impossibla de « {path} »", + "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_custom_finished": "Lo metòde de salvagarda personalizat « {method} » es acabat", + "backup_method_tar_finished": "L’archiu TAR de la salvagarda es estat creat", + "backup_mount_archive_for_restore": "Preparacion de l’archiu per restauracion…", + "backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas", + "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", + "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_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_running_hooks": "Execucion dels scripts de salvagarda…", + "backup_system_part_failed": "Impossible de salvagardar la part « {part} » del sistèma", + "backup_unable_to_organize_files": "Impossible d’organizar los fichièrs dins l’archiu amb lo metòde rapid", + "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.", + "cannot_open_file": "Impossible de dobrir lo fichièr {file} (rason : {error})", + "cannot_write_file": "Escritura impossibla del fichièr {file} (rason : {error})", + "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.", + "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain} es pas provesit per Let’s Encrypt. Impossible de lo renovar automaticament !", + "certmanager_attempt_to_renew_valid_cert": "Lo certificat pel domeni {domain} es a man d’expirar ! (Podètz utilizar --force se sabètz çò que fasètz)", + "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_cannot_read_cert": "Quicòm a trucat en ensajar de dobrir lo certificat actual pel domeni {domain} (fichièr : {file}), rason : {reason}", + "certmanager_cert_install_success": "Lo certificat Let’s Encrypt es ara installat pel domeni « {domain} »", + "certmanager_cert_install_success_selfsigned": "Lo certificat auto-signat es ara installat pel domeni « {domain} »", + "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni « {domain} »", + "certmanager_cert_signing_failed": "Signatura impossibla del nòu certificat", + "certmanager_certificate_fetching_or_enabling_failed": "Sembla qu’utilizar lo nòu certificat per {domain} fonciona pas…", + "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_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_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", + "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})", + "certmanager_unable_to_parse_self_CA_name": "Analisi impossibla del nom de l’autoritat del certificat auto-signat (fichièr : {file})", + "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_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}] ", + "corrupted_json": "Fichièr Json corromput legit de {ressource} (rason : {error})", + "corrupted_toml": "Fichièr TOML corromput en lectura de {ressource} estant (rason : {error})", + "corrupted_yaml": "Fichièr YAML corromput legit de {ressource} (rason : {error})", + "diagnosis_basesystem_hardware": "L’arquitectura del servidor es {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Lo modèl del servidor es {model}", + "diagnosis_basesystem_host": "Lo servidor fonciona amb Debian {debian_version}", + "diagnosis_basesystem_kernel": "Lo servidor fonciona amb lo nuclèu Linuxl {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Utilizatz de versions inconsistentas dels paquets de YunoHost… probablament a causa d'una actualizacion fracassada o parciala.", + "diagnosis_basesystem_ynh_main_version": "Lo servidor fonciona amb YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} version : {version} ({repo})", + "diagnosis_cache_still_valid": "(Memòria cache totjorn valida pel diagnostic {category}. Se tornarà pas diagnosticar pel moment !)", + "diagnosis_cant_run_because_of_dep": "Execucion impossibla del diagnostic per {category} mentre que i a de problèmas importants ligats amb {dep}.", + "diagnosis_description_basesystem": "Sistèma de basa", + "diagnosis_description_dnsrecords": "Enregistraments DNS", + "diagnosis_description_ip": "Connectivitat Internet", + "diagnosis_description_mail": "Corrièl", + "diagnosis_description_ports": "Exposicion dels pòrts", + "diagnosis_description_regenconf": "Configuracion sistèma", + "diagnosis_description_services": "Verificacion d’estat de servicis", + "diagnosis_description_systemresources": "Resorgas sistèma", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a solament {free} ({free_percent}%). Siatz prudent.", + "diagnosis_diskusage_ok": "Lo lòc d’emmagazinatge {mountpoint} (sul periferic {device}) a encara {free} ({free_percent}%) de liure !", + "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_dns_bad_conf": "Configuracion DNS incorrècta o inexistenta pel domeni {domain} (categoria {category})", + "diagnosis_dns_discrepancy": "La configuracion DNS seguenta sembla pas la configuracion recomandada :
Tipe : {type}
Nom : {name}
Valors actualas : {current}
Valor esperada : {content}", + "diagnosis_dns_good_conf": "Bona configuracion DNS pel domeni {domain} (categoria {category})", + "diagnosis_dns_missing_record": "Segon la configuracion DNS recomandada, vos calriá ajustar un enregistrament DNS.
Tipe: {type}
Nom: {name}
Valor: {content}", + "diagnosis_domain_expiration_not_found": "Impossible de verificar la data d’expiracion d’unes domenis", + "diagnosis_domain_expiration_success": "Vòstres domenis son enregistrats e expiraràn pas lèu.", + "diagnosis_domain_expiration_warning": "D’unes domenis expiraràn lèu !", + "diagnosis_domain_expires_in": "{domain} expiraà d’aquí {days} jorns.", + "diagnosis_domain_not_found_details": "Lo domeni {domain} existís pas a la basa de donadas WHOIS o a expirat !", + "diagnosis_everything_ok": "Tot sembla corrècte per {category} !", + "diagnosis_failed": "Recuperacion impossibla dels resultats del diagnostic per la categoria « {category} » : {error}", + "diagnosis_failed_for_category": "Lo diagnostic a reüssit per la categoria « {category} » : {error}", + "diagnosis_found_errors": "{errors} errors importantas trobadas ligadas a {category} !", + "diagnosis_found_errors_and_warnings": "Avèm trobat {errors} problèma(s) important(s) (e {warnings} avis(es)) ligats a {category} !", + "diagnosis_found_warnings": "Trobat {warnings} element(s) que se poirián melhorar per {category}.", + "diagnosis_http_connection_error": "Error de connexion : connexion impossibla al domeni demandat, benlèu qu’es pas accessible.", + "diagnosis_http_could_not_diagnose": "Impossible de diagnosticar se lo domeni es accessible de l’exterior.", + "diagnosis_http_could_not_diagnose_details": "Error : {error}", + "diagnosis_http_ok": "Lo domeni {domain} accessible de l’exterior.", + "diagnosis_http_unreachable": "Lo domeni {domain} es pas accessible via HTTP de l’exterior.", + "diagnosis_ignored_issues": "(+ {nb_ignored} problèma(es) ignorat(s))", + "diagnosis_ip_broken_dnsresolution": "La resolucion del nom de domeni es copada per una rason… Lo parafuòc bloca las requèstas DNS ?", + "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_connected_ipv4": "Lo servidor es connectat a Internet via IPv4 !", + "diagnosis_ip_connected_ipv6": "Lo servidor es connectat a Internet via IPv6 !", + "diagnosis_ip_dnsresolution_working": "La resolucion del nom de domeni fonciona !", + "diagnosis_ip_global": "IP Global  : {global}", + "diagnosis_ip_local": "IP locala : {local}", + "diagnosis_ip_no_ipv4": "Lo servidor a pas d’adreça IPv4 activa.", + "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_weird_resolvconf": "La resolucion del nom de domeni sembla foncionar, mas sembla qu’utiilizatz un fichièr /etc/resolv.conf personalizat.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Error : {error}", + "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).", + "diagnosis_mail_queue_unavailable_details": "Error : {error}", + "diagnosis_no_cache": "I a pas encara de diagnostic de cache per la categoria « {category} »", + "diagnosis_package_installed_from_sury": "D’unes paquets sistèma devon èsser meses a nivèl", + "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_ports_needed_by": "Es necessari qu’aqueste pòrt siá accessible pel servici {service}", + "diagnosis_ports_ok": "Lo pòrt {port} es accessible de l’exterior.", + "diagnosis_ports_unreachable": "Lo pòrt {port} es pas accessible de l’exterior.", + "diagnosis_ram_low": "Lo sistèma a {available} ({available_percent}%) de memòria RAM disponibla d’un total de {total}). Atencion.", + "diagnosis_ram_ok": "Lo sistèma a encara {available} ({available_percent}%) de memòria RAM disponibla d’un total de {total}).", + "diagnosis_ram_verylow": "Lo sistèma a solament {available} ({available_percent}%) de memòria RAM disponibla ! (d’un total de {total})", + "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_security_vulnerable_to_meltdown": "Semblatz èsser vulnerable a la vulnerabilitat de seguretat critica de Meltdown", + "diagnosis_services_bad_status": "Lo servici {service} es {status} :(", + "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_services_conf_broken": "La configuracion es copada pel servici {service} !", + "diagnosis_services_running": "Lo servici {service} es lançat !", + "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.", + "diagnosis_swap_ok": "Lo sistèma a {total} d’escambi !", + "diagnosis_unknown_categories": "La categorias seguentas son desconegudas : {categories}", + "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: {error}", + "domain_deleted": "Domeni suprimit", + "domain_deletion_failed": "Supression impossibla del domeni {domain}: {error}", + "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_exists": "Lo domeni existís ja", + "domain_hostname_failed": "Fracàs de la creacion d’un nòu nom d’òst. Aquò poirà provocar de problèmas mai tard (mas es pas segur… benlèu que coparà pas res).", + "domain_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", + "domains_available": "Domenis disponibles :", + "done": "Acabat", + "download_bad_status_code": "{url} tòrna lo còdi d’estat {code}", + "download_ssl_error": "Error SSL en se connectant a {url}", + "download_timeout": "{url} a trigat per respondre, avèm quitat d’esperar.", + "download_unknown_error": "Error en telecargar de donadas de {url} : {error}", + "downloading": "Telecargament…", + "dpkg_is_broken": "Podètz pas far aquò pel moment perque dpkg/APT (los gestionaris de paquets del sistèma) sembla èsser mal configurat… Podètz ensajar de solucionar aquò en vos connectar via SSH e en executar « sudo dpkg --configure -a ».", + "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)", + "dyndns_could_not_check_available": "Verificacion impossibla de la disponibilitat de {domain} sus {provider}.", + "dyndns_domain_not_provided": "Lo provesidor DynDNS {provider} pòt pas fornir lo domeni {domain}.", + "dyndns_ip_update_failed": "Impossible d’actualizar l’adreça IP sul domeni DynDNS", + "dyndns_ip_updated": "Vòstra adreça IP actualizada pel domeni DynDNS", + "dyndns_key_not_found": "Clau DNS introbabla pel domeni", + "dyndns_no_domain_registered": "Cap de domeni pas enregistrat amb DynDNS", + "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.", + "dyndns_unavailable": "Lo domeni {domain} es pas disponible.", + "error_changing_file_permissions": "Error en modificar las permissions per {path} : {error}", + "error_removing": "Error en suprimir {path} : {error}", + "error_writing_file": "Error en escriure lo fichièr {file} : {error}", + "extracting": "Extraccion…", + "field_invalid": "Camp incorrècte : « {field} »", + "file_does_not_exist": "Lo camin {path} existís pas.", + "file_not_exist": "Lo fichièr « {path} » existís pas", + "firewall_reload_failed": "Impossible de recargar lo parafuòc. Per mai informacions, consultatz lo jornal.", + "firewall_reloaded": "Parafuòc recargat", + "global_settings_setting_admin_strength": "Fòrça del senhal administrator", + "global_settings_setting_nginx_compatibility_help": "Solucion de compromés entre compatibilitat e seguretat pel servidor web NGINX Afècta los criptografs (e d’autres aspèctes ligats amb la seguretat)", + "global_settings_setting_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_user_strength": "Fòrça del senhal utilizaire", + "good_practices_about_admin_password": "Sètz per definir un nòu senhal per l’administracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far d’utilizar un senhal mai long qu’aquò (ex. una passafrasa) e/o d’utilizar mantun tipe de caractèrs (majuscula, minuscula, nombre e caractèrs especials).", + "good_practices_about_user_password": "Sètz a mand de definir un nòu senhal d’utilizaire. Lo nòu senhal deu conténer almens 8 caractèrs, es de bon far d’utilizar un senhal mai long (es a dire una frasa de senhal) e/o utilizar mantun tipe de caractèrs (majusculas, minusculas, nombres e caractèrs especials).", + "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_cannot_be_deleted": "Lo grop « {group} » pòt pas èsser suprimit manualament.", + "group_created": "Grop « {group} » creat", + "group_creation_failed": "Fracàs de la creacion del grop « {group} » : {error}", + "group_deleted": "Lo grop « {group} » es estat suprimit", + "group_deletion_failed": "Fracàs de la supression del grop « {group} » : {error}", + "group_unknown": "Lo grop « {group} » es desconegut", + "group_update_failed": "Actualizacion impossibla del grop « {group} » : {error}", + "group_updated": "Lo grop « {group} » es estat actualizat", + "group_user_already_in_group": "L’utilizaire {user} es ja dins lo grop « {group} »", + "group_user_not_in_group": "L’utilizaire {user} es pas dins lo grop {group}", + "hook_exec_failed": "Fracàs de l’execucion del script : « {path} »", + "hook_exec_not_terminated": "Lo escript « {path} » a pas acabat corrèctament", + "hook_json_return_error": "Fracàs de la lectura del retorn de l’script {path}. Error : {msg}. Contengut brut : {raw_content}", + "hook_list_by_invalid": "La proprietat de tria de las accions es invalida", + "hook_name_unknown": "Nom de script « {name} » desconegut", + "installation_complete": "Installacion acabada", + "invalid_url": "Fracàs de connexion a {url}... benlèu que lo servici es fora servici, o sètz pas corrèctament connectat a Internet en IPv4/IPv6.", + "log_app_action_run": "Executar l’accion de l’aplicacion « {} »", + "log_app_change_url": "Cambiar l’URL de l’aplicacion « {} »", + "log_app_install": "Installar l’aplicacion « {} »", + "log_app_makedefault": "Far venir « {} » l’aplicacion per defaut", + "log_app_remove": "Levar l’aplicacion « {} »", + "log_app_upgrade": "Actualizar l’aplicacion « {} »", + "log_available_on_yunopaste": "Lo jornal es ara disponible via {url}", + "log_backup_restore_app": "Restaurar « {} » a partir d’una salvagarda", + "log_backup_restore_system": "Restaurar lo sistèma a partir d’una salvagarda", + "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error}", + "log_does_exists": "I a pas cap de jornal d’audit per l’operacion amb lo nom « {log} », utilizatz « yunohost log list » per veire totes los jornals d’operacion disponibles", + "log_domain_add": "Ajustar lo domeni « {} » dins la configuracion sistèma", + "log_domain_main_domain": "Far venir « {} » lo domeni màger", + "log_domain_remove": "Tirar lo domeni « {} » d’a la configuracion sistèma", + "log_dyndns_subscribe": "S’abonar al subdomeni YunoHost « {} »", + "log_dyndns_update": "Actualizar l’adreça IP ligada a vòstre jos-domeni YunoHost « {} »", + "log_help_to_get_failed_log": "L’operacion « {desc} » a pas reüssit ! Per obténer d’ajuda, mercés de partejar lo jornal d’audit complèt d’aquesta operacion en utilizant la comanda « yunohost log share {name} »", + "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name} »", + "log_letsencrypt_cert_install": "Installar un certificat Let's Encrypt sul domeni « {} »", + "log_letsencrypt_cert_renew": "Renovar lo certificat Let's Encrypt de « {} »", + "log_link_to_failed_log": "L’operacion « {desc} » a pas capitat ! Per obténer d’ajuda, mercés de fornir lo jornal complèt de l’operacion", + "log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}", + "log_operation_unit_unclosed_properly": "L’operacion a pas acabat corrèctament", + "log_regen_conf": "Regenerar las configuracions del sistèma « {} »", + "log_remove_on_failed_install": "Tirar « {} » aprèp una installacion pas reüssida", + "log_selfsigned_cert_install": "Installar lo certificat auto-signat sul domeni « {} »", + "log_tools_migrations_migrate_forward": "Executar las migracions", + "log_tools_postinstall": "Realizar la post installacion del servidor YunoHost", + "log_tools_reboot": "Reaviar lo servidor", + "log_tools_shutdown": "Atudar lo servidor", + "log_tools_upgrade": "Actualizacion dels paquets sistèma", + "log_user_create": "Ajustar l’utilizaire « {} »", + "log_user_delete": "Levar l’utilizaire « {} »", + "log_user_group_create": "Crear lo grop « {} »", + "log_user_group_delete": "Suprimir lo grop « {} »", + "log_user_group_update": "Actualizar lo grop « {} »", + "log_user_update": "Actualizar las informacions de l’utilizaire « {} »", + "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail} »", + "mail_domain_unknown": "Lo domeni de corrièl « {domain} » es desconegut.", + "mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail} »", + "mail_unavailable": "Aquesta adreça electronica es reservada e deu èsser automaticament atribuida al tot bèl just primièr utilizaire", + "mailbox_disabled": "La bóstia de las letras es desactivada per l’utilizaire {user}", + "mailbox_used_space_dovecot_down": "Lo servici corrièl Dovecot deu èsser aviat, se volètz conéisser l’espaci ocupat per la messatjariá", + "main_domain_change_failed": "Modificacion impossibla del domeni màger", + "main_domain_changed": "Lo domeni màger es estat modificat", + "migrations_already_ran": "Aquelas migracions s’executèron ja : {ids}", + "migrations_dependencies_not_satisfied": "Executatz aquestas migracions : « {dependencies_id} », abans la migracion {id}.", + "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_list_conflict_pending_done": "Podètz pas utilizar --previous e --done a l’encòp.", + "migrations_loading_migration": "Cargament de la migracion {id}…", + "migrations_migration_has_failed": "La migracion {id} a pas capitat, abandon. Error : {exception}", + "migrations_must_provide_explicit_targets": "Devètz fornir una cibla explicita quand utilizatz using --skip o --force-rerun", + "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.", + "migrations_no_migrations_to_run": "Cap de migracion de lançar", + "migrations_no_such_migration": "I a pas cap de migracion apelada « {id} »", + "migrations_not_pending_cant_skip": "Aquestas migracions son pas en espèra, las podètz pas doncas ignorar : {ids}", + "migrations_running_forward": "Execucion de la migracion {id}…", + "migrations_skip_migration": "Passatge de la migracion {id}…", + "migrations_success_forward": "Migracion {id} corrèctament realizada", + "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 ».", + "nftables_unavailable": "Podètz pas jogar amb nftables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", + "not_enough_disk_space": "Espaci disc insufisent sus « {path} »", + "operation_interrupted": "L’operacion es estada interrompuda manualament ?", + "password_listed": "Aqueste senhal es un dels mai utilizats al monde. Se vos plai utilizatz-ne un mai unic.", + "password_too_simple_1": "Lo senhal deu conténer almens 8 caractèrs", + "password_too_simple_2": "Lo senhal deu conténer almens 8 caractèrs e numbres, majusculas e minusculas", + "password_too_simple_3": "Lo senhal deu conténer almens 8 caractèrs e nombres, majusculas e minusculas e caractèrs especials", + "password_too_simple_4": "Lo senhal deu conténer almens 12 caractèrs, de nombre, majusculas, minisculas e caractèrs specials", + "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", + "pattern_domain": "Deu èsser un nom de domeni valid (ex : mon-domeni.org)", + "pattern_email": "Deu èsser una adreça electronica valida (ex : escais@domeni.org)", + "pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o 0 per desactivar la quòta", + "pattern_password": "Deu conténer almens 3 caractèrs", + "pattern_password_app": "O planhèm, los senhals devon pas conténer los caractèrs seguents : {forbidden_chars}", + "pattern_port_or_range": "Deu èsser un numèro de pòrt valid (ex : 0-65535) o un interval de pòrt (ex : 100:200)", + "pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses", + "permission_already_allowed": "Lo grop « {group} » a ja la permission « {permission} » activada", + "permission_already_disallowed": "Lo grop « {group} » a ja la permission « {permission} » desactivada", + "permission_cannot_remove_main": "La supression d’una permission màger es pas autorizada", + "permission_created": "Permission « {permission} » creada", + "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} »: {error}", + "permission_not_found": "Permission « {permission} » pas trobada", + "permission_update_failed": "Fracàs de l’actualizacion de la permission '{permission}': {error}", + "permission_updated": "La permission « {permission} » es estada actualizada", + "port_already_closed": "Lo pòrt {port} es ja tampat", + "port_already_opened": "Lo pòrt {port} es ja dubèrt", + "regenconf_dry_pending_applying": "Verificacion de la configuracion que seriá estada aplicada a la categoria « {category} »…", + "regenconf_failed": "Regeneracion impossibla de la configuracion per la(s) categoria(s) : {categories}", + "regenconf_file_backed_up": "Lo fichièr de configuracion « {conf} » es estat salvagardat dins « {backup} »", + "regenconf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »", + "regenconf_file_kept_back": "S’espèra que lo fichièr de configuracion « {conf} » siá suprimit per regen-conf (categoria {category} mas es estat mantengut.", + "regenconf_file_manually_modified": "Lo fichièr de configuracion « {conf} » es estat modificat manualament e serà pas actualizat", + "regenconf_file_manually_removed": "Lo fichièr de configuracion « {conf} » es estat suprimit manualament e serà pas creat", + "regenconf_file_remove_failed": "Supression impossibla del fichièr de configuracion « {conf} »", + "regenconf_file_removed": "Lo fichièr de configuracion « {conf} » es estat suprimit", + "regenconf_file_updated": "Lo fichièr de configuracion « {conf} » es estat actualizat", + "regenconf_now_managed_by_yunohost": "Lo fichièr de configuracion « {conf} » es ara gerit per YunoHost (categoria {category}).", + "regenconf_pending_applying": "Aplicacion de la configuracion en espèra per la categoria « {category} »…", + "regenconf_up_to_date": "La configuracion es ja a jorn per la categoria « {category} »", + "regenconf_updated": "La configuracion es estada actualizada per la categoria « {category} »", + "regenconf_would_be_updated": "La configuracion seriá estada actualizada per la categoria « {category} »", + "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app} »", + "restore_already_installed_apps": "Restauracion impossibla de las aplicacions seguentas que son ja installadas : {apps}", + "restore_cleaning_failed": "Impossible de netejar lo repertòri temporari de restauracion", + "restore_complete": "Restauracion acabada", + "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers}", + "restore_extracting": "Extraccions dels fichièrs necessaris dins de l’archiu…", + "restore_failed": "Impossible de restaurar lo sistèma", + "restore_hook_unavailable": "Lo script de restauracion « {part} » es pas disponible sus vòstre sistèma e es pas tanpauc dins l’archiu", + "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space} octets, necessari : {needed_space} octets, marge de seguretat : {margin} octets)", + "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_hooks": "Execucion dels scripts de restauracion…", + "restore_system_part_failed": "Restauracion impossibla de la part « {part} » del sistèma", + "root_password_desynchronized": "Lo senhal de l’administrator es estat cambiat, mas YunoHost a pas pogut l’espandir al senhal root !", + "server_reboot": "Lo servidor es per reaviar", + "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers}", + "server_shutdown": "Lo servidor serà atudat", + "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers}", + "service_add_failed": "Apondon impossible del servici « {service} »", + "service_added": "Lo servici « {service} » es ajustat", + "service_already_started": "Lo servici « {service} » es ja aviat", + "service_already_stopped": "Lo servici « {service} » es ja arrestat", + "service_cmd_exec_failed": "Impossible d’executar la comanda « {command} »", + "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", + "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_mysql": "garda las donadas de las aplicacions (base de donadas SQL)", + "service_description_nftables": "gerís los pòrts de connexion dobèrts e tampats als servicis", + "service_description_nginx": "fornís o permet l’accès a totes los sites web albergats sus vòstre servidor", + "service_description_postfix": "emplegat per enviar e recebre de corrièls", + "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_slapd": "garda los utilizaires, domenis e lors informacions ligadas", + "service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)", + "service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma", + "service_disable_failed": "Impossible de desactivar lo servici « {service} »", + "service_disabled": "Lo servici « {service} » es desactivat.", + "service_enable_failed": "Impossible d’activar lo servici « {service} »", + "service_enabled": "Lo servici « {service} » es activat.", + "service_reload_failed": "Impossible de recargar lo servici « {service} »", + "service_reload_or_restart_failed": "Impossible de recargar o reaviar lo servici « {service} »", + "service_reloaded": "Lo servici « {service} » es estat tornat cargar", + "service_reloaded_or_restarted": "Lo servici « {service} » es estat recargat o reaviat", + "service_remove_failed": "Impossible de levar lo servici « {service} »", + "service_removed": "Lo servici « {service} » es estat levat", + "service_restart_failed": "Impossible de reaviar lo servici « {service} »", + "service_restarted": "Lo servici '{service}' es estat reaviat", + "service_start_failed": "Impossible d’aviar lo servici « {service} »", + "service_started": "Lo servici « {service} » es aviat", + "service_stop_failed": "Impossible d’arrestar lo servici « {service} »", + "service_stopped": "Lo servici « {service} » es estat arrestat", + "service_unknown": "Servici « {service} » desconegut", + "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", + "this_action_broke_dpkg": "Aquesta accion a copat dpkg/apt (los gestionaris de paquets del sistèma)… Podètz ensajar de resòlver aqueste problèma en vos connectant amb SSH e executant « sudo dpkg --configure -a ».", + "unbackup_app": "L’aplicacion « {app} » serà pas salvagardada", + "unexpected_error": "Una error inesperada s’es producha: {error}", + "unknown_error_reading_file": "Error desconeguda en ensajar de legir lo fichièr {file} (rason : {error})", + "unknown_group": "Grop « {group} » desconegut", + "unknown_user": "Utilizaire « {user} » desconegut", + "unlimit": "Cap de quòta", + "unrestore_app": "L’aplicacion « {app} » serà pas restaurada", + "update_apt_cache_failed": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}", + "update_apt_cache_warning": "I a agut d’errors en actualizar la memòria cache d’APT (lo gestionari de paquets de Debian). Aquí avètz las linhas de sources.list que pòdon vos ajudar a identificar las linhas problematicas : \n{sourceslist}", + "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles…", + "upgrading_packages": "Actualizacion dels paquets…", + "upnp_dev_not_found": "Cap de periferic compatible UPnP pas trobat", + "upnp_disabled": "UPnP es desactivat", + "upnp_enabled": "UPnP es activat", + "upnp_port_open_failed": "Impossible de dobrir los pòrts amb UPnP", + "user_already_exists": "L’utilizaire {user} existís ja", + "user_created": "L’utilizaire es creat", + "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}: {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}: {error}", + "user_updated": "L’utilizaire es estat modificat", + "yunohost_already_installed": "YunoHost es ja installat", + "yunohost_configured": "YunoHost es ara configurat", + "yunohost_installing": "Installacion de YunoHost…", + "yunohost_not_installed": "YunoHost es pas corrèctament installat. Mercés d’executar « yunohost tools postinstall »" +} diff --git a/locales/pl.json b/locales/pl.json new file mode 100644 index 0000000..274e4f8 --- /dev/null +++ b/locales/pl.json @@ -0,0 +1,704 @@ +{ + "aborting": "Przerywanie.", + "action_invalid": "Nieprawidłowe działanie '{action}'", + "additional_urls_already_added": "Dodatkowy URL '{url}' już dodany w dodatkowym URL dla uprawnienia '{permission}'", + "additional_urls_already_removed": "Dodatkowy URL '{url}' już usunięty w dodatkowym URL dla uprawnienia '{permission}'", + "admin_password": "Hasło administratora", + "admins": "Administratorzy", + "all_users": "Wszyscy użytkownicy YunoHost", + "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", + "app_action_broke_system": "Wydaje się, że ta akcja przerwała te ważne usługi: {services}", + "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_failed": "Nie udało się uruchomić akcji {action} dla aplikacji {app}", + "app_already_installed": "{app} jest już zainstalowana", + "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_arch_not_supported": "Ta aplikacja może być zainstalowana tylko na architekturach {required}, a twoja architektura serwera to {current}", + "app_argument_choice_invalid": "Wybierz poprawną wartość dla argumentu '{name}': '{value}' nie znajduje się w liście poprawnych opcji ({choices})", + "app_argument_invalid": "Wybierz poprawną wartość dla argumentu '{name}': {error}", + "app_change_url_failed": "Nie udało się zmienić adresu URL aplikacji {app}: {error}", + "app_change_url_identical_domains": "Stara i nowa domena/ścieżka_url są identyczne („{domain}{path}”), nic nie trzeba robić.", + "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_require_full_domain": "Nie można przenieść aplikacji {app} na nowy adres URL, ponieważ wymaga ona pełnej domeny (tj. ze ścieżką = /)", + "app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL", + "app_change_url_success": "Adres URL aplikacji {app} to teraz {domain}{path}", + "app_config__core_name": "Kafelki i uprawnienia", + "app_config_permission_allowed": "Grupy/użytkownicy zezwolono na dostęp", + "app_config_permission_allowed_warn_protected": "NB: to uprawnienie jest 'chronione', dlatego grupy 'odwiedzający' nie można dodać/usunąć z autoryzowanych grup.", + "app_config_permission_description": "Opis", + "app_config_permission_description_help": "To naprawdę przydatne tylko wtedy, gdy używasz trybu portalu 'opisowego'", + "app_config_permission_extraperm_section_name": "Uprawnienie '{perm}'", + "app_config_permission_label": "Etykieta", + "app_config_permission_location": "Odpowiedni do[{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Wykorzystanie własnego logo", + "app_config_permission_logo_help": "Obsługiwane są tylko pliki PNG", + "app_config_permission_show_tile": "Wyświetl kafelek w portalu", + "app_config_unable_to_apply": "Nie udało się zastosować wartości panelu konfiguracji.", + "app_config_unable_to_read": "Nie udało się odczytać wartości panelu konfiguracji.", + "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}”", + "app_extraction_failed": "Nie można wyodrębnić plików instalacyjnych", + "app_failed_to_download_asset": "Nie udało się pobrać zasobu '{source_id}' ({url}) dla {app}: {out}", + "app_full_domain_unavailable": "Przepraszamy, ta aplikacja musi być zainstalowana we własnej domenie, ale inna aplikacja jest już zainstalowana w tej domenie „{domain}”. Zamiast tego możesz użyć subdomeny dedykowanej tej aplikacji.", + "app_id_invalid": "Nieprawidłowy identyfikator aplikacji(ID)", + "app_install_failed": "Nie udało się zainstalować {app}: {error}", + "app_install_files_invalid": "Te pliki nie mogą zostać zainstalowane", + "app_install_script_failed": "Wystąpił błąd w skrypcie instalacyjnym aplikacji", + "app_location_unavailable": "Ten adres URL jest niedostępny lub koliduje z już zainstalowanymi aplikacjami:\n{apps}", + "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_manifest_install_ask_admin": "Wybierz użytkownika administratora dla tej aplikacji", + "app_manifest_install_ask_domain": "Wybierz domenę, w której ta aplikacja ma zostać zainstalowana", + "app_manifest_install_ask_init_admin_permission": "Kto powinien mieć dostęp do funkcji administracyjnych tej aplikacji? (Można to później zmienić)", + "app_manifest_install_ask_init_main_permission": "Kto powinien mieć dostęp do tej aplikacji? (Można to później zmienić)", + "app_manifest_install_ask_is_public": "Czy ta aplikacja powinna być udostępniana anonimowym użytkownikom?", + "app_manifest_install_ask_password": "Wybierz hasło administratora dla tej aplikacji", + "app_manifest_install_ask_path": "Wybierz ścieżkę adresu URL (po domenie), w której ta aplikacja ma zostać zainstalowana", + "app_not_correctly_installed": "Wygląda na to, że aplikacja {app} jest nieprawidłowo 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_not_installed": "Nie można znaleźć aplikacji {app} na liście zainstalowanych aplikacji: {all_apps}", + "app_not_properly_removed": "Aplikacja {app} nie została poprawnie usunięta", + "app_packaging_format_not_supported": "Ta aplikacja nie może zostać zainstalowana, ponieważ jej format opakowania nie jest obsługiwany przez twoją wersję YunoHost. Prawdopodobnie powinieneś rozważyć aktualizację swojego systemu.", + "app_remove_after_failed_install": "Usuwanie aplikacji po niepowodzeniu instalacji…", + "app_removed": "Odinstalowano {app}", + "app_requirements_checking": "Sprawdzam wymagania dla aplikacji {app}…", + "app_resource_failed": "Nie udało się zapewnić, anulować obsługi administracyjnej lub zaktualizować zasobów aplikacji {app}: {error}", + "app_restore_failed": "Nie można przywrócić {app}: {error}", + "app_restore_script_failed": "Wystąpił błąd w skrypcie przywracania aplikacji", + "app_sources_fetch_failed": "Nie można pobrać plików źródłowych, czy adres URL jest poprawny?", + "app_start_backup": "Zbieram pliki do utworzenia kopii zapasowej dla {app}…", + "app_start_install": "Instalowanie {app}…", + "app_start_remove": "Usuwanie {app}…", + "app_start_restore": "Przywracanie {app}…", + "app_unknown": "Nieznana aplikacja", + "app_unsupported_remote_type": "Niewspierany typ zdalny użyty w aplikacji", + "app_upgrade_app_name": "Aktualizuję {app}…", + "app_upgrade_bad_quality": "Ta aplikacja jest obecnie oznaczona jako uszkodzona w katalogu aplikacji YunoHost. Może to być problem tymczasowy, ponieważ administratorzy próbują go rozwiązać. W międzyczasie, aktualizacja tej aplikacji jest wyłączona.", + "app_upgrade_broke_the_system": "Aktualizacja {app} pozornie zadziałała, ale pozostawiła system w stanie uszkodzonym i dlatego jest uważana za awarię.", + "app_upgrade_cli_bad_quality": "Pominięto aktualizacje aplikacji {app} ponieważ jest ona obecnie oznaczona jako uszkodzona w katalogu aplikacji YunoHost.", + "app_upgrade_cli_up_to_date": "{app} jest już aktualna ({current_version})", + "app_upgrade_cli_url_required": "{app} nie znajduje się już w katalogu (czyżby?) i dlatego nie można go zaktualizować automatycznie. Należy użyć `yunohost app upgrade {app}`, aby podać adres URL repozytorium za pomocą opcji `-u`.", + "app_upgrade_cli_will_force_upgrade": "{app} zostanie wymuszona aktualizacja ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} zostanie zaktualizowana z wersji {current_version} do wersji {new_version}", + "app_upgrade_continuing_with_other_apps": "Nie udało się uaktualnić {app}, ale kontynuowano uaktualnianie innych aplikacji (ponieważ użyto opcji `--continue-on-failure`)", + "app_upgrade_fail_requirements": "Dostępna jest nowa wersja tej aplikacji ({new_version}), ale niektóre wymagania nie zostały spełnione:\n{failed_requirements}", + "app_upgrade_failed": "Nie udało się zaktualizować {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "Nie udało się uaktualnić aplikacji '{app}', w wyniku czego system uległ uszkodzeniu.", + "app_upgrade_script_failed": "Wystąpił błąd w skrypcie aktualizacji aplikacji", + "app_upgrade_several_apps": "Następujące aplikacje zostaną uaktualnione: {apps}", + "app_upgrade_some_app_failed": "Niektórych aplikacji nie udało się zaktualizować", + "app_upgrade_specific_channel_msg": "Pamiętaj że obecnie używasz kanału `{channel}` jako źródła aktualizacji. Koniecznie sprawdź trwającą dyskusję [tutaj]({pr_url}).", + "app_upgrade_up_to_date": "Wymuszona aktualizacja aplikacji (do tej samej wersji) może czasami okazać się przydatna w celu ponownego odbudowanie aplikacji i przywrócenia konfiguracji.", + "app_upgrade_upgradable": "Aplikację można uaktualnić z wersji {current_version} do {new_version}", + "app_upgrade_url_required": "Ta aplikacja nie istnieje (już?) w katalogu, musisz więc ręcznie wykonać aktualizację.
Z poziomu wiersza poleceń możesz użyć `yunohost app upgrade ` i podać adres URL repozytorium za pomocą opcji `-u`.", + "app_upgraded": "Zaktualizowano {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", + "apps_catalog_failed_to_download": "Nie można pobrać katalogu aplikacji app catalog: {error}", + "apps_catalog_obsolete_cache": "Pamięć podręczna katalogu aplikacji jest pusta lub przestarzała.", + "apps_catalog_update_success": "Katalog aplikacji został zaktualizowany!", + "apps_catalog_updating": "Aktualizowanie katalogu aplikacji…", + "apps_confirm_partial_upgrade": "Niektórych aplikacji dla których złożono wniosek o aktualizację, nie można zaktualizować. Czy mimo to kontynuować instalację pozostałych aplikacji?", + "apps_no_target_can_be_upgraded": "Nie można uaktualnić żadnych aplikacji", + "apps_upgrade_cancelled": "Aktualizacje kilku innych aplikacji wciąż oczekują na zatwierdzenie, ale ich aktualizacja została anulowana (użyj `--continue-on-failure`, aby kontynuować mimo wszystko): {apps}", + "ask_admin_fullname": "Pełne imię i nazwisko administratora", + "ask_admin_username": "Nazwa użytkownika administratora", + "ask_dyndns_recovery_password": "Hasło odzyskiwania DynDNS", + "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.", + "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ć.", + "ask_fullname": "Pełne imię i nazwisko", + "ask_main_domain": "Domena główna", + "ask_new_admin_password": "Nowe hasło administracyjne", + "ask_new_domain": "Nowa domena", + "ask_new_path": "Nowa ścieżka", + "ask_password": "Hasło", + "ask_user_domain": "Domena używana dla adresu e-mail użytkownika", + "automatic_task": "Zadanie automatyczne", + "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_app_script_failed": "Nie udało się zebrać plików do utworzenia kopii zapasowej dla {app}.", + "backup_applying_method_copy": "Kopiowanie wszystkich plików do kopii zapasowej…", + "backup_applying_method_custom": "Wywołuję niestandardową metodę tworzenia kopii zapasowych '{method}'…", + "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_archive_cant_retrieve_info_json": "Nieudane wczytanie informacji dla archiwum '{archive}'… Plik info.json nie może zostać odzyskany (lub jest niepoprawny).", + "backup_archive_corrupted": "Wygląda na to, że archiwum kopii zapasowej '{archive}' jest uszkodzone: {error}", + "backup_archive_name_exists": "Archiwum kopii zapasowych o nazwie '{name}' już istnieje.", + "backup_archive_name_unknown": "Nieznane, lokalne archiwum kopii zapasowej o nazwie '{name}'", + "backup_archive_open_failed": "Nie można otworzyć archiwum kopii zapasowej", + "backup_archive_system_part_not_available": "Część systemowa '{part}' jest niedostępna w tej kopii zapasowej", + "backup_archive_writing_error": "Nie udało się dodać plików '{source}' (nazwanych w archiwum '{dest}') do utworzenia kopii zapasowej skompresowanego archiwum '{archive}'", + "backup_ask_for_copying_if_needed": "Czy chcesz wykonać kopię zapasową tymczasowo używając {size} MB? (Ta metoda jest stosowana, ponieważ niektóre pliki nie mogły zostać przygotowane przy użyciu bardziej wydajnej metody.)", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "Kopia zapasowa {name} została usunięta, ponieważ została zastąpiona nowszą kopią zapasową {newname}", + "backup_cant_mount_uncompress_archive": "Nie można zamontować nieskompresowanego archiwum jako chronione przed zapisem", + "backup_cleaning_failed": "Nie udało się wyczyścić folderu tymczasowej kopii zapasowej", + "backup_copying_to_organize_the_archive": "Kopiowanie {size} MB w celu zorganizowania archiwum", + "backup_couldnt_bind": "Nie udało się powiązać {src} z {dest}.", + "backup_create_size_estimation": "Archiwum będzie zawierać około {size} danych.", + "backup_created": "Utworzono kopię zapasową: {name}", + "backup_creation_failed": "Nie udało się utworzyć archiwum kopii zapasowej", + "backup_csv_addition_failed": "Nie udało się dodać plików do kopii zapasowej do pliku CSV", + "backup_csv_creation_failed": "Nie udało się utworzyć wymaganego pliku CSV do przywracania", + "backup_custom_backup_error": "Niestandardowa metoda tworzenia kopii zapasowej nie mogła przejść kroku 'backup'", + "backup_custom_mount_error": "Niestandardowa metoda tworzenia kopii zapasowej nie mogła przejść etapu „mount”", + "backup_delete_error": "Nie udało się usunąć '{path}'", + "backup_deleted": "Usunięto kopię zapasową: {name}", + "backup_hook_unknown": "Skrypt kopii zapasowej '{hook}' jest nieznany", + "backup_method_copy_finished": "Zakończono tworzenie kopii zapasowej", + "backup_method_custom_finished": "Tworzenie kopii zapasowej według własnej metody '{method}' zakończone", + "backup_method_tar_finished": "Utworzono archiwum kopii zapasowej TAR", + "backup_mount_archive_for_restore": "Przygotowywanie archiwum do przywrócenia…", + "backup_no_file_collected": "Nie udało się zebrać plików do utworzenia kopii zapasowej", + "backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum", + "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", + "backup_output_directory_not_empty": "Należy wybrać pusty katalog dla danych wyjściowych", + "backup_output_directory_required": "Musisz wybrać katalog dla kopii zapasowej", + "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_running_hooks": "Wykonywanie skryptów kopii zapasowej…", + "backup_system_part_failed": "Nie udało się wykonać kopii zapasowej części systemu ‘{part}’", + "backup_unable_to_organize_files": "Nie można użyć szybkiej metody porządkowania plików w archiwum", + "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.", + "cannot_open_file": "Nie można otworzyć pliku {file} (przyczyna: {error})", + "cannot_write_file": "Nie można zapisać pliku {file} (przyczyna: {error})", + "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_attempt_to_renew_nonLE_cert": "Certyfikat dla domeny '{domain}' nie został wystawiony przez Let's Encrypt. Automatyczne odnowienie jest niemożliwe!", + "certmanager_attempt_to_renew_valid_cert": "Certyfikat dla domeny '{domain}' nie jest bliski wygaśnięciu! (Możesz użyć komendy z dopiskiem --force jeśli wiesz co robisz)", + "certmanager_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ąć)", + "certmanager_cannot_read_cert": "Wystąpił problem podczas próby otwarcia bieżącego certyfikatu dla domeny {domain} (plik: {file}), przyczyna: {reason}", + "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": "Pomyślna instalacja certyfikatu Let's Encrypt dla domeny '{domain}'", + "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}", + "certmanager_cert_renew_success": "Pomyślne odnowienie certyfikatu Let's Encrypt dla domeny '{domain}'", + "certmanager_cert_signing_failed": "Nie udało się zarejestrować nowego certyfikatu", + "certmanager_certificate_fetching_or_enabling_failed": "Próba użycia nowego certyfikatu dla {domain} zakończyła się niepowodzeniem…", + "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ć.)", + "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.)", + "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.)", + "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_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", + "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})", + "certmanager_unable_to_parse_self_CA_name": "Nie można spasować nazwy organu samopodpisywanego (pliku: {file})", + "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_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_forbidden_keyword": "Słowo kluczowe '{keyword}' jest już zarezerwowane. Nie możesz tworzyć ani używać panelu konfiguracji z pytaniem o tym identyfikatorze.", + "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_no_panel": "Nie znaleziono panelu konfiguracji.", + "config_unknown_filter_key": "Klucz filtru '{filter_key}' jest niepoprawny.", + "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}'", + "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}] ", + "confirm_app_insufficient_ram": "Ta aplikacja wymaga więcej pamięci RAM do zainstalowania niż jest obecnie dostępne. 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}'", + "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}]", + "confirm_tos_acknowledgement": "Przeczytałem i rozumiem Warunki korzystania z usług [{answers}]", + "corrupted_json": "Uszkodzony JSON odczytany z {ressource} (reason: {error})", + "corrupted_toml": "Uszkodzony TOML odczytany z {ressource} (reason: {error})", + "corrupted_yaml": "Uszkodzony YAML odczytany z {ressource} (reason: {error})", + "danger": "Zagrożeniæ:", + "diagnosis_apps_allgood": "Wszystkie zainstalowane aplikacje są zgodne z podstawowymi zasadami pakowania", + "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_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_deprecated_practices": "Zainstalowana wersja tej aplikacji nadal korzysta z bardzo starych i przestarzałych praktyk pakowania. Naprawdę powinieneś rozważyć jego aktualizację.", + "diagnosis_apps_issue": "Znaleziono problem z aplikacją {app}", + "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_apps_outdated_packaging_format": "Ta aplikacja korzysta z przestarzałego formatu pakietów i wkrótce przestanie być obsługiwana przez YunoHost. Zdecydowanie powinieneś jej aktualizację.", + "diagnosis_apps_outdated_ynh_requirement": "Zainstalowana wersja tej aplikacji wymaga jedynie yunohost >= 2.x, 3.x lub 4.x, co sugeruje, że nie jest ona zgodna z zalecanymi praktykami pakowania i narzędziami. Naprawdę powinieneś rozważyć jej aktualizację.", + "diagnosis_apps_security_issue_error": "Aplikacja {app} jest obecnie w wersji '{current_version}', która jest podatna na POWAŻNY błąd bezpieczeństwa: {title}. Zaleca się JAK NAJSZYBSZĄ aktualizację do wersji '{fixed_in_version}'. Więcej informacji: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "Aplikacja {app} jest obecnie w wersji '{current_version}', która jest podatna na umiarkowaną lukę w zabezpieczeniach: {title}. Zaleca się jej aktualizację do wersji '{fixed_in_version}'. Więcej informacji: {more_infos_list}", + "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.", + "diagnosis_basesystem_hardware": "Architektura sprzętowa serwera to {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Model serwera to {model}", + "diagnosis_basesystem_host": "Serwer działa pod kontrolą systemu Debian {debian_version}", + "diagnosis_basesystem_kernel": "Serwer działa pod kontrolą jądra Linuksa {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Używasz niespójnych wersji pakietów YunoHost… najprawdopodobniej z powodu nieudanej lub częściowej aktualizacji.", + "diagnosis_basesystem_ynh_main_version": "Serwer działa pod kontrolą oprogramowania YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "Wersja {package}: {version} ({repo})", + "diagnosis_cache_still_valid": "(Pamięć podręczna jest nadal ważna dla diagnozy {category}. Nie będę przeprowadzać ponownej diagnozy teraz!)", + "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_description_apps": "Aplikacje", + "diagnosis_description_basesystem": "Baza systemu", + "diagnosis_description_dnsrecords": "Rekordy DNS", + "diagnosis_description_ip": "Połączenie z internetem", + "diagnosis_description_mail": "Email", + "diagnosis_description_ports": "Ujawnione porty", + "diagnosis_description_regenconf": "Konfiguracja systemu", + "diagnosis_description_services": "Kontrola stanu usług", + "diagnosis_description_systemresources": "Zasoby systemu", + "diagnosis_description_web": "Sieć", + "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_diskusage_ok": "Przestrzeń {mountpoint} (na dysku {device}) nadal ma {free} ({free_percent}%) wolnego miejsca z całej puli {total}!", + "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!", + "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_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ść: {content}", + "diagnosis_dns_good_conf": "Rekordy DNS zostały poprawnie skonfigurowane dla domeny {domain} (category {category})", + "diagnosis_dns_missing_record": "Zgodnie z zalecaną konfiguracją DNS powinieneś dodać rekord DNS z następującymi informacjami.
Typ: {type}
Nazwa: {name}
Wartość: {content}", + "diagnosis_dns_point_to_doc": "Jeśli potrzebujesz pomocy w konfiguracji rekordów DNS, sprawdź dokumentację pod adresem https://doc.yunohost.org/dns_config.", + "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.", + "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_domain_expiration_error": "Niektóre domeny wygasną BARDZO WKRÓTCE!", + "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_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_domain_expires_in": "Domena {domain} wygasa za {days} dni.", + "diagnosis_domain_not_found_details": "Domena {domain} nie istnieje w bazie WHOIS lub wygasła!", + "diagnosis_everything_ok": "Wszystko wygląda dobrze dla {category}!", + "diagnosis_failed": "Nie udało się pobrać wyniku diagnostyki dla kategorii „{category}”: {error}", + "diagnosis_failed_for_category": "Diagnostyka nie powiodła się dla kategorii „{category}”: {error}", + "diagnosis_found_errors": "Znaleziono {errors} istotne problemy związane z {category}", + "diagnosis_found_errors_and_warnings": "Znaleziono {errors} istotnych problemów (i {warnings} ostrzeżeń) związanych z {category}!", + "diagnosis_found_warnings": "Znaleziono {warnings} elementów, które można ulepszyć dla {category}.", + "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://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Wygląda na to że zamiast serwera odebrał inny komputer (być może router internetowy).
1. Najczęstszą przyczyną tego problemu jest to, że porty 80 (i 443) nie są prawidłowo przekierowywane do serwera.
2. W przypadku bardziej złożonych konfiguracji: upewnij się, że żadna zapora sieciowa ani odwrotny serwer proxy nie zakłóca połączenia.", + "diagnosis_http_connection_error": "Błąd połączenia: nie można nawiązać połączenia z żądaną domeną, jest bardzo prawdopodobne, że jest ona nieosiągalna.", + "diagnosis_http_could_not_diagnose": "Nie można zdiagnozować czy domeny są osiągalne z zewnątrz w IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Błąd: {error}", + "diagnosis_http_hairpinning_issue": "Wygląda na to, że sieć lokalna nie ma \"hairpinning\".", + "diagnosis_http_hairpinning_issue_details": "Prawdopodobnie jest to wina twojego routera/skrzynki ISP. W rezultacie osoby spoza twojej sieci lokalnej będą mogły uzyskać dostęp do twojego serwera, ale osoby z sieci lokalnej (jak ty, prawdopodobnie?) nie będą mogły uzyskać dostępu do twojego serwera, korzystając z nazwy domeny lub globalnego adresu IP. Możesz poprawić sytuację, sprawdzając https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "Konfiguracja nginx tej domeny została prawdopodobnie zmodyfikowana ręcznie i uniemożliwia YunoHost zdiagnozowanie, czy jest ona dostępna przez HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Aby naprawić problem, sprawdź różnice z poziomu wiersza poleceń, korzystając z polecenia yunohost tools regen-conf nginx --dry-run --with-diffi jeśli są one zadowalające, zastosuj zmiany za pomocą polecenia yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "Domena {domain} jest dostępna przez HTTP z poziomu sieci zewnętrznej.", + "diagnosis_http_partially_unreachable": "Domena {domain} wydaje się nieosiągalna przez HTTP z zewnątrz sieci lokalnej w IPv{failed}, mimo że działa w IPv{passed}.", + "diagnosis_http_special_use_tld": "Domena {domain} opiera się na specjalnej domenie najwyższego poziomu (TLD), takiej jak .local lub .test i dlatego nie przewiduje się jej udostępniania poza siecią lokalną.", + "diagnosis_http_timeout": "Przekroczono limit czasu podczas próby połączenia z serwerem z zewnątrz. Wygląda na to, że serwer jest niedostępny.
1. Najczęstszą przyczyną tego problemu jest to, że porty 80 (i 443) nie są poprawnie przekierowywane na serwer.
2. Należy również upewnić się, że usługa nginx jest uruchomiona.
3. W przypadku bardziej złożonych konfiguracji: upewnij się, że żadna zapora sieciowa ani odwrotny serwer proxy nie zakłóca działania serwera.", + "diagnosis_http_unreachable": "Domena {domain} wydaje się niedostępna przez HTTP spoza sieci lokalnej.", + "diagnosis_ignore_already_filtered": "(Istnieje już filtr diagnostyczny {category} z tymi kryteriami)", + "diagnosis_ignore_criteria_error": "Kryteria powinny mieć formę klucz=wartość (np. domena=yolo.test)", + "diagnosis_ignore_filter_added": "Dodano filtr diagnostyczny dla {category}", + "diagnosis_ignore_filter_removed": "Usunięto filtr diagnostyczny dla {category}", + "diagnosis_ignore_missing_criteria": "Należy podać co najmniej jedno kryterium będące kategorią diagnozy, którą należy zignorować", + "diagnosis_ignore_no_filter_found": "(Nie ma takiego filtra diagnozy {category} z tymi kryteriami do usunięcia)", + "diagnosis_ignore_no_issue_found": "Nie znaleziono problemów odpowiadających podanym kryteriom.", + "diagnosis_ignored_issues": "(+ {nb_ignored} zignorowano problem(y))", + "diagnosis_ip_broken_dnsresolution": "Wygląda na to, że z jakiegoś powodu nie działa rozpoznawanie nazw domen… Czy zapora sieciowa blokuje żądania DNS?", + "diagnosis_ip_broken_resolvconf": "Wygląda na to że na twoim serwerze nie działa rozpoznawanie nazw domen, ma to prawdopodobnie związek z tym że plik /etc/resolv.conf nie wskazuje do 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "Serwer jest połączony z Internet z użyciem IPv4!", + "diagnosis_ip_connected_ipv6": "Serwer nie jest połączony z internetem z użyciem IPv6!", + "diagnosis_ip_dnsresolution_working": "Rozpoznawanie nazw domen działa!", + "diagnosis_ip_global": "Globalny IP: {global}", + "diagnosis_ip_local": "Lokalny IP: {local}", + "diagnosis_ip_no_ipv4": "Serwer nie ma działającego protokołu IPv4.", + "diagnosis_ip_no_ipv6": "Serwer nie ma działającego połączenia z użyciem IPv6.", + "diagnosis_ip_no_ipv6_tip": "Posiadanie działającego protokołu IPv6 nie jest obowiązkowe dla działania serwera, ale wpływa na lepsze kondycję całego Internetu. Protokół IPv6 powinien być zazwyczaj automatycznie konfigurowany przez system lub dostawcę, jeśli jest dostępny. W przeciwnym razie może być konieczne ręczne skonfigurowanie kilku elementów, zgodnie z opisem w dokumentacji dostępnej tutaj: https://doc.yunohost.org/ipv6. Jeśli nie możesz włączyć IPv6 lub wydaje ci się to zbyt techniczne, możesz bezpiecznie zignorować to ostrzeżenie.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 powinien być zazwyczaj automatycznie konfigurowany przez system lub dostawcę, jeśli jest dostępny. W przeciwnym razie może być konieczne ręczne skonfigurowanie kilku rzeczy, zgodnie z opisem w dokumentacji: https://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "Wygląda na to że serwer w ogóle nie jest połączony z Internetem!?", + "diagnosis_ip_weird_resolvconf": "Rozwiązywanie DNS wydaje się działać, ale wygląda na to że używasz niestandardowego pliku /etc/resolv.conf.", + "diagnosis_ip_weird_resolvconf_details": "Plik /etc/resolv.conf powinien być dowiązaniem symbolicznym do pliku /etc/resolvconf/run/resolv.conf wskazującym na 127.0.0.1 (dnsmasq). Jeśli chcesz ręcznie skonfigurować resolvery DNS, edytuj plik /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blocklist_listed_by": "Twój IP lub domena {item} znajduje się na czarnej liście {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Adresy IP i domeny używane przez ten serwer nie znajdują się na czarnej liście", + "diagnosis_mail_blocklist_reason": "Powód umieszczenia na liście blokowanych to: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Wygląda na to że powód wspomina o 'open resolver'.
Zwykle oznacza to, że twój serwer nie korzysta z lokalnego DNS, lecz z publicznego, otwartego.
Sprawdź plik /etc/resolv.conf – powinien zawierać nameserver 127.0.0.1.
Ponieważ ten plik jest zazwyczaj generowany automatycznie, nie edytuj go ręcznie. Sprawdź ustawienia DHCP, lub VPN jeśli z nich korzystasz, albo jeśli korzystasz z obrazu Debiana utworzonego na przykład przez dostawcę VPS, poszukaj konfiguracji cloudinit.
Serdecznie zapraszamy do skorzystania z kanałów wsparcia YunoHost w celu uzyskania pomocy w tej sprawie.
Dosłowny powód na czarnej liście to: {reason}", + "diagnosis_mail_blocklist_website": "Po ustaleniu przyczyny umieszczenia cię na liście i jej naprawieniu, możesz poprosić o usunięcie swojego adresu IP lub domeny z witryny na {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Usługa inna niż SMTP odpowiedziała na porcie 25 w IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Może być to że zamiast twojego serwera odbiera inny komputer.", + "diagnosis_mail_ehlo_could_not_diagnose": "Nie udało się ustalić czy serwer pocztowy postfix jest dostępny z zewnątrz w IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Błąd: {error}", + "diagnosis_mail_ehlo_ok": "Serwer poczty SMTP jest dostępny z zewnątrz, dzięki czemu możliwe jest odbieranie wiadomości e-mail!", + "diagnosis_mail_ehlo_unreachable": "Serwer poczty SMTP jest niedostępny z zewnątrz przez IPv{ipversion}. Nie będzie mógł odbierać wiadomości e-mail.", + "diagnosis_mail_ehlo_unreachable_details": "Nie można otworzyć połączenia na porcie 25 z serwerem w IPv{ipversion}. Wygląda na to że jest on niedostępny.
1. Najczęstszą przyczyną tego problemu jest to że port 25 nie jest poprawnie przekierowany do twojego serwera.
2. Należy również upewnić się, że usługa postfix jest uruchomiona.
3. W przypadku bardziej złożonych konfiguracji: upewnij się, że żadna zapora sieciowa ani odwrotny serwer proxy nie zakłóca połączenia.", + "diagnosis_mail_ehlo_wrong": "Inny serwer SMTP odpowiada na IPv{ipversion}. Twój serwer prawdopodobnie nie będzie mógł odbierać wiadomości e-mail.", + "diagnosis_mail_ehlo_wrong_details": "EHLO odebrane przez zdalny diagnostyk w IPv{ipversion} różni się od domeny twojego serwera.
Otrzymano EHLO: {wrong_ehlo}
Oczekiwano: {right_ehlo}
Najczęstszą przyczyną tego problemu jest że port 25 nie jest poprawnie przekierowany do twojego serwera. Upewnij się również że żadna zapora sieciowa ani odwrotny serwer proxy nie zakłóca działania usługi.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Odwrotny DNS nie jest poprawnie skonfigurowany dla IPv{ipversion}. Niektóre wiadomości e-mail mogą nie zostać dostarczone lub zostać oznaczone jako spam.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Aktualny odwrotny DNS: {rdns_domain}
Oczekiwana wartość: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "W IPv{ipversion} nie zdefiniowano odwrotnego DNS. Niektóre wiadomości e-mail mogą nie zostać dostarczone lub zostać oznaczone jako spam.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Niektórzy dostawcy internetu nie pozwalają na skonfigurowanie odwrotnego DNS (lub ich funkcja może być uszkodzona…). Jeśli z tego powodu występują problemy, rozważ następujące rozwiązania:
- Niektórzy dostawcy usług internetowych oferują alternatywę w postaci korzystania z przekaźnika serwera poczty, chociaż sugeruje to, że przekaźnik będzie mógł śledzić ruch e-mail.
- Alternatywą, która pozwala zachować prywatność, jest użycie sieci VPN *z dedykowanym publicznym adresem IP*, aby ominąć tego typu ograniczenia. Zobacz https://doc.yunohost.org/vpn_advantage
- Możesz też przejść do innego dostawcy internetu", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Niektórzy dostawcy internetu nie pozwalają na skonfigurowanie odwrotnego DNS (lub ich funkcja może być uszkodzona…). Jeśli odwrotny DNS jest poprawnie skonfigurowany dla IPv4, możesz spróbować wyłączyć używanie IPv6 podczas wysyłania wiadomości e-mail uruchamiając polecenie yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Uwaga: to ostatnie rozwiązanie oznacza, że nie będziesz mógł wysyłać ani odbierać wiadomości e-mail z kilku serwerów obsługujących wyłącznie IPv6.", + "diagnosis_mail_fcrdns_nok_details": "Najpierw spróbuj skonfigurować odwrotny DNS z {ehlo_domain} w interfejsie routera internetowego lub u dostawcy hostingu. (Niektórzy dostawcy hostingu mogą wymagać wysłania zgłoszenia do pomocy technicznej w tej sprawie).", + "diagnosis_mail_fcrdns_ok": "Twój odwrotny DNS jest poprawnie skonfigurowany!", + "diagnosis_mail_outgoing_port_25_blocked": "Serwer poczty SMTP nie może wysyłać e-mail do innych serwerów ponieważ port wychodzący 25 jest zablokowany w IPv{ipversion}.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Najpierw spróbuj odblokować port wychodzący 25 w interfejsie routera internetowego albo u dostawcy hostingu. (Niektórzy dostawcy hostingu mogą wymagać wysłania zgłoszenia do pomocy technicznej w tej sprawie).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Niektórzy dostawcy internetu nie pozwalają odblokować portu wychodzącego 25 ponieważ nie zależy im na neutralności sieci.
- Niektórzy z nich oferują alternatywę w postaci korzystania z przekaźnika serwera pocztowego choć sugeruje że to będzie mógł śledzić ruch e-mail.
- Alternatywą sprzyjającą prywatności jest korzystanie z sieci VPN *z dedykowanym publicznym adresem IP*, aby ominąć tego typu ograniczenia. Zobacz https://doc.yunohost.org/vpn_advantage
- Możesz również rozważyć przejście na dostawcę bardziej dbającego o neutralność sieci", + "diagnosis_mail_outgoing_port_25_ok": "Serwer poczty SMTP może wysyłać e-mail (port wychodzący 25 nie jest blokowany).", + "diagnosis_mail_queue_ok": "{nb_pending} oczekujących e-mail w kolejkach pocztowych", + "diagnosis_mail_queue_too_big": "Zbyt wiele oczekujących e-mail w kolejce pocztowej ({nb_pending} e-mail)", + "diagnosis_mail_queue_unavailable": "Nie można sprawdzić liczby oczekujących e-mail w kolejce", + "diagnosis_mail_queue_unavailable_details": "Błąd: {error}", + "diagnosis_never_ran_yet": "Wygląda że ten serwer został niedawno skonfigurowany i nie ma jeszcze raportu diagnostycznego do wyświetlenia. Powinieneś uruchomienia pełnej diagnostyki, albo z poziomu webadmin, albo używając 'yunohost diagnosis run' z wiersza poleceń.", + "diagnosis_no_cache": "Brak pamięci podręcznej diagnostyki dla kategorii '{category}'", + "diagnosis_package_installed_from_sury": "Niektóre pakiety systemowe powinny zostać obniżone z wersji", + "diagnosis_package_installed_from_sury_details": "Niektóre pakiety zostały przypadkowo zainstalowane z zewnętrznego repozytorium o nazwie Sury. Zespół YunoHost ulepszył strategię obsługi tych pakietów, ale można się spodziewać że niektóre konfiguracje które zainstalowały aplikacje PHP7.3 będąc nadal na Stretch będą miały pewne niespójności. Aby naprawić tę sytuację, spróbuj uruchomić następujące polecenie: {cmd_to_fix}", + "diagnosis_package_security_issue_error": "Pakiet systemowy '{package}' jest obecnie w wersji '{current_version}', która jest podatna na POWAŻNY błąd bezpieczeństwa: {title}. Zaleca się JAK NAJSZYBSZĄ aktualizację do wersji '{fixed_in_version}'. Więcej informacji: {more_infos_list}", + "diagnosis_package_security_issue_warning": "Pakiet systemowy '{package}' jest obecnie w wersji '{current_version}', która jest podatna na umiarkowaną lukę w zabezpieczeniach: {title}. Zaleca się jego aktualizację do wersji '{fixed_in_version}'. Więcej informacji: {more_infos_list}", + "diagnosis_ports_could_not_diagnose": "Nie udało się zdiagnozować czy porty są dostępne z zewnątrz w IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Błąd: {error}", + "diagnosis_ports_forwarding_tip": "Aby rozwiązać ten problem, najprawdopodobniej musisz skonfigurować przekierowanie portów na routerze internetowym zgodnie z opisem w https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Udostępnienie tego portu jest konieczne dla funkcji {category} (usługa {service})", + "diagnosis_ports_ok": "Do portu {port} można dotrzeć z zewnątrz.", + "diagnosis_ports_partially_unreachable": "Port {port} nie jest osiągalny z zewnątrz w IPv{failed}.", + "diagnosis_ports_unreachable": "Do portu {port} nie można dotrzeć z zewnątrz.", + "diagnosis_processes_killed_by_oom_reaper": "Niektóre procesy zostały niedawno zamknięte przez system z powodu braku pamięci. Zazwyczaj jest to objawem braku pamięci w systemie lub nadmiernego zużycia pamięci przez proces. Podsumowanie zamkniętych procesów:\n{kills_summary}", + "diagnosis_ram_low": "System ma {available} ({available_percent}%) dostępnej pamięci RAM (z {total}). Zachowaj ostrożność.", + "diagnosis_ram_ok": "System nadal ma {available} ({available_percent}%) wolnej pamięci RAM z całej puli {total}.", + "diagnosis_ram_verylow": "W systemie jest tylko {available} ({available_percent}%) dostępnej pamięci RAM! (z {total})", + "diagnosis_regenconf_allgood": "Wszystkie pliki konfiguracyjne są zgodne z zalecaną konfiguracją!", + "diagnosis_regenconf_manually_modified": "Wygląda na to, że plik konfiguracyjny {file} został zmodyfikowany ręcznie.", + "diagnosis_regenconf_manually_modified_details": "To prawdopodobnie w porządku jeśli wiesz co robisz! YunoHost automatycznie przestanie aktualizować ten plik… Pamiętaj jednak że aktualizacje YunoHost mogą zawierać ważne zalecane zmiany. Jeśli chcesz, możesz sprawdzić różnice poleceniem yunohost tools regen-conf {category} --dry-run --with-diff i wymusić przywrócenie zalecanej konfiguracji poleceniem yunohost tools regen-conf {category} --force.", + "diagnosis_rfkill_wifi": "Karta Wi-Fi jest wyłączona i jeden ostrzeżenie systemowe może uniemożliwić instalację aplikacji", + "diagnosis_rfkill_wifi_details": "To ostrzeżenie pojawia się w wielu wynikach poleceń, powodując awarie niektórych aplikacji. Zazwyczaj wymagane jest podanie kodu kraju za pomocą polecenia sudo raspi-config. Oto błąd:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "Główny system plików ma łącznie tylko {space}, co jest dość niepokojące! Prawdopodobnie bardzo szybko zabraknie ci miejsca na dysku! Zaleca się, aby główny system plików miał co najmniej 16 GB.", + "diagnosis_rootfstotalspace_warning": "Główny system plików ma łącznie tylko {space}. To może być w porządku, ale bądź ostrożny bo ostatecznie możesz szybko zabraknąć miejsca na dysku… Zaleca się aby główny system plików miał co najmniej 16 GB.", + "diagnosis_security_vulnerable_to_meltdown": "Wygląda na to że jesteś podatny na krytyczną lukę w zabezpieczeniach Meltdown", + "diagnosis_security_vulnerable_to_meltdown_details": "Aby rozwiązać ten problem, należy zaktualizować system i uruchomić go ponownie aby załadować nowe Linux kernel (lub skontaktować się z dostawcą serwera jeśli to nie pomoże). Więcej informacji na stronie https://meltdownattack.com/.", + "diagnosis_services_bad_status": "Usługa {service} ma {status} :(", + "diagnosis_services_bad_status_tip": "Możesz spróbować ponownie uruchomić usługę i jeśli to nie zadziała, sprawdź logi usługi w webadmin (z poziomu wiersza poleceń możesz to zrobić za pomocą yunohost service restart {service} i yunohost service log {service}).", + "diagnosis_services_conf_broken": "Konfiguracja usługi {service} jest uszkodzona!", + "diagnosis_services_running": "Usługa {service} działa!", + "diagnosis_sshd_config_inconsistent": "Wygląda na to że port SSH został ręcznie zmodyfikowany w /etc/ssh/sshd_config. Od wersji YunoHost 4.2 dostępne jest nowe globalne ustawienie 'security.ssh.ssh_port', które pozwala uniknąć ręcznej edycji konfiguracji.", + "diagnosis_sshd_config_inconsistent_details": "Uruchom polecenie yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT aby zdefiniować port SSH, i sprawdź z yunohost tools regen-conf ssh --dry-run --with-diff oraz yunohost tools regen-conf ssh --force aby przywrócić konfigurację do wartości zalecanej przez YunoHost.", + "diagnosis_sshd_config_insecure": "Konfiguracja SSH prawdopodobnie została zmodyfikowana ręcznie, i nie jest bezpieczna ponieważ nie zawiera żadnych 'AllowGroups' ani 'AllowUsers' ograniczających dostęp do autoryzowanych kont.", + "diagnosis_swap_none": "System w ogóle nie posiada pamięci swap. Warto rozważyć dodanie co najmniej {recommended} pamięci swap aby uniknąć sytuacji w których systemowi zabraknie pamięci.", + "diagnosis_swap_notsomuch": "System ma tylko {total} pamięci swap. Powinieneś rozważyć posiadanie co najmniej {recommended} aby uniknąć sytuacji w których systemowi zabraknie pamięci.", + "diagnosis_swap_ok": "System posiada {total} swap!", + "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_unknown_categories": "Następujące kategorie są nieznane: {categories}", + "diagnosis_using_stable_codename": "apt (menedżer pakietów systemu) jest obecnie skonfigurowany do instalowania pakietów o nazwie kodowej 'stable', a nie o nazwie kodowej bieżącej wersji Debiana (bookworm).", + "diagnosis_using_stable_codename_details": "Zazwyczaj jest to spowodowane nieprawidłową konfiguracją u dostawcy hostingu. Jest to niebezpieczne, ponieważ gdy tylko kolejna wersja Debiana stanie się nową wersją 'stable', apt będzie chciał zaktualizować wszystkie pakiety systemowe bez przeprowadzania odpowiedniej procedury migracji. Zaleca się naprawienie tego problemu poprzez edycję kodu źródłowego apt dla repozytorium Debiana od stable na bookworm. Odpowiednim plikiem konfiguracyjnym powinien być /etc/apt/sources.list lub plik w /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (menedżer pakietów systemu) jest obecnie skonfigurowany tak aby instalować wszelkie uaktualnienia 'testowe' na twojej instancji YunoHost.", + "diagnosis_using_yunohost_testing_details": "To prawdopodobnie jest w porządku jeśli wiesz co robisz, ale przed zainstalowaniem aktualizacji YunoHost zapoznaj się z informacjami o wydaniu! Jeśli chcesz wyłączyć aktualizacje 'testing', usuń słowo testing z pliku /etc/apt/sources.list.d/yunohost.list.", + "disk_space_not_sufficient_install": "Na dysku nie ma wystarczająco miejsca aby zainstalować tę aplikację", + "disk_space_not_sufficient_update": "Na dysku nie ma wystarczająco miejsca aby zaktualizować tę aplikację", + "domain_cannot_remove_main": "Nie możesz usunąć '{domain}' ponieważ jest to domena główna. Najpierw musisz ustawić inną domenę jako domenę główną używając 'yunohost domain main-domain -n '. Oto lista innych domen: {other_domains}", + "domain_cannot_remove_main_add_new_one": "Nie możesz usunąć '{domain}' ponieważ jest to domena główna i twoja jedyna domena. Najpierw musisz dodać inną domenę za pomocą 'yunohost domain add ', a następnie ustawić ją jako domenę główną za pomocą 'yunohost domain main-domain -n ' po czym możesz usunąć domenę '{domain}' za pomocą 'yunohost domain remove '{domain}'.", + "domain_cert_gen_failed": "Nie można wygenerować certyfikatu", + "domain_config_acme_eligible": "Uprawniania ACME", + "domain_config_acme_eligible_explain": "Ta domena prawdopodobnie nie jest gotowa na certyfikat Let's Encrypt. Sprawdź konfigurację DNS i dostępność serwera HTTP. Sekcje 'Rekordy DNS' i 'Sieć' na stronie diagnostycznej pomogą ci zrozumieć co jest nieprawidłowo skonfigurowane.", + "domain_config_api_protocol": "API protokołu", + "domain_config_auth_application_key": "Klucz aplikacji", + "domain_config_auth_application_secret": "Klucz tajny aplikacji", + "domain_config_auth_consumer_key": "Klucz konsumenta", + "domain_config_auth_entrypoint": "Punkt wejścia API", + "domain_config_auth_key": "Klucz uwierzytelniający", + "domain_config_auth_secret": "Tajny klucz uwierzytelniania", + "domain_config_auth_token": "Token uwierzytelniający", + "domain_config_cert_install": "Zainstaluj certyfikat Let's Encrypt", + "domain_config_cert_issuer": "Organ certyfikacji", + "domain_config_cert_name": "Certyfikat", + "domain_config_cert_no_checks": "Zignoruj kontrole diagnostyczne", + "domain_config_cert_renew": "Odnów certyfikat Let's Encrypt", + "domain_config_cert_renew_help": "Certyfikat zostanie automatycznie odnowiony w ciągu ostatnich 15 dni ważności. Możesz go odnowić ręcznie, jeśli chcesz. (Niezalecane).", + "domain_config_cert_summary": "Status certyfikatu", + "domain_config_cert_summary_abouttoexpire": "Obecny certyfikat wkrótce wygaśnie. Wkrótce powinien zostać automatycznie odnowiony.", + "domain_config_cert_summary_expired": "KRYTYCZNE: Obecny certyfikat jest nieprawidłowy! HTTPS w ogóle nie będzie działać!", + "domain_config_cert_summary_letsencrypt": "Świetnie! Wykorzystujesz właściwy certyfikaty Let's Encrypt!", + "domain_config_cert_summary_ok": "OK, obecny certyfikat wygląda dobrze!", + "domain_config_cert_summary_selfsigned": "UWAGA: Obecny certyfikat jest podpisany przez samego użytkownika. Przeglądarki będą wyświetlać niepokojące ostrzeżenie nowym użytkownikom!", + "domain_config_cert_validity": "Ważność", + "domain_config_custom_css": "Niestandardowy plik CSS", + "domain_config_custom_css_help": "Jest to przeznaczone dla zaawansowanych administratorów którzy chcą dostosować wygląd portalu", + "domain_config_default_app": "Domyślna aplikacja", + "domain_config_default_app_help": "Użytkownicy zostaną automatycznie przekierowani do tej aplikacji po otwarciu tej domeny. Jeśli nie zostanie określona żadna aplikacja, użytkownicy zostaną przekierowani do formularza logowania do portalu.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Pokaż listę aplikacji publicznych odwiedzającym", + "domain_config_enable_public_apps_page_help": "Po wejściu na portal użytkownicy zobaczą stronę 'aplikacji publicznych' a nie tylko formularz logowania.", + "domain_config_feature_name": "Funkcje", + "domain_config_mail_in": "Odbieranie maili", + "domain_config_mail_out": "Wysyłanie maili", + "domain_config_portal_logo": "Własne logo", + "domain_config_portal_logo_help": "Akceptujemy pliki .svg, .png i .jpeg. Preferujemy monochromatyczny plik .svg z fill: currentColor aby logo dopasowywało się do motywu.", + "domain_config_portal_name": "Personalizacja portalu", + "domain_config_portal_public_intro": "Spersonalizowana prezentacja publiczna", + "domain_config_portal_public_intro_help": "Możesz użyć HTML, podstawowe style zostaną zastosowane do ogólnych elementów.", + "domain_config_portal_theme": "Domyślny motyw kolorów", + "domain_config_portal_theme_help": "Użytkownicy mogą wybrać inną opcję w swoich ustawieniach.", + "domain_config_portal_tile_theme": "Motyw wyświetlania kafelków aplikacji", + "domain_config_portal_title": "Własny tytuł", + "domain_config_portal_user_intro": "Spersonalizowane wprowadzenie dla użytkowników", + "domain_config_portal_user_intro_help": "Możesz użyć HTML, podstawowe style zostaną zastosowane do ogólnych elementów.", + "domain_config_search_engine": "Adres URL wyszukiwarki", + "domain_created": "Utworzono domenę", + "domain_deleted": "Usunięto domenę", + "domain_dns_pushing": "Przesyłanie rekordów DNS…", + "domain_dns_registrar_experimental": "Jak dotąd interfejs API **{registrar}** nie został odpowiednio przetestowany i zweryfikowany przez społeczność YunoHost. Wsparcie jest **bardzo eksperymentalne** – bądź ostrożny!", + "domain_dns_registrar_managed_in_parent_domain": "Ta domena jest subdomeną od {parent_domain_link}. Konfiguracją rejestratora DNS należy zarządzać w panelu konfiguracyjnym domeny {parent_domain}.", + "domain_dns_registrar_not_supported": "YunoHost nie mógł automatycznie wykryć rejestratora obsługującego tę domenę. Należy ręcznie skonfigurować rekordy DNS postępując zgodnie z dokumentacją dostępną pod adresem https://doc.yunohost.org/dns_config.", + "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://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Można również ręcznie skonfigurować rekordy DNS zgodnie z dokumentacją na stronie https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_use_auto": "Użyj funkcji automatycznego DNS", + "domain_dns_registrar_yunohost": "Ta domena to nohost.me / nohost.st / ynh.fr i dlatego jej konfiguracja DNS jest automatycznie obsługiwana przez YunoHost bez konieczności dalszej konfiguracji. (patrz polecenie 'yunohost dyndns update')", + "domain_dyndns_already_subscribed": "Już zasubskrybowałeś do domenę DynDNS", + "domain_exists": "Domena już istnieje", + "domain_hostname_failed": "Nie można ustawić nowej nazwy hosta. Może to później spowodować problem (ale może wszystko będzie w porządku).", + "domain_registrar_is_not_configured": "Rejestrator nie został jeszcze skonfigurowany dla domeny {domain}.", + "domains_available": "Dostępne domeny:", + "done": "Gotowe", + "download_bad_status_code": "{url} zwrócił kod stanu {code}", + "download_ssl_error": "Błąd SSL podczas łączenia z {url}", + "download_timeout": "{url} potrzebował zbyt dużo czasu na odpowiedź, rezygnacja.", + "download_unknown_error": "Błąd podczas pobierania danych z {url}: {error}", + "downloading": "Pobieranie…", + "error_changing_file_permissions": "Błąd podczas zmiany uprawnień dla {path}: {error}", + "error_removing": "Błąd podczas usuwania {path}: {error}", + "error_writing_file": "Błąd podczas zapisywania pliku {file}: {error}", + "extracting": "Rozpakowywanie…", + "file_not_exist": "Plik nie istnieje: „{path}”", + "firewall_reloaded": "Przeładowano zaporę sieciową", + "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_antispam_name": "Filtr antyspamowy", + "global_settings_setting_backup_compress_tar_archives": "Kompresuj kopie zapasowe", + "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.", + "global_settings_setting_backup_name": "Kopia zapasowa", + "global_settings_setting_dns_exposure": "Wersje IP do uwzględnienia w konfiguracji i diagnostyce DNS", + "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_email_name": "Email", + "global_settings_setting_experimental_name": "Eksperymentalne", + "global_settings_setting_misc_name": "Inne", + "global_settings_setting_network_name": "Sieć", + "global_settings_setting_nginx_compatibility": "Kompatybilność z NGINX", + "global_settings_setting_nginx_redirect_to_https": "Wymuszaj HTTPS", + "global_settings_setting_password_name": "Hasła", + "global_settings_setting_passwordless_sudo": "Umożliw administratorom korzystania z 'sudo' bez konieczności ponownego wpisywania hasła", + "global_settings_setting_pop3_enabled": "Włącz POP3", + "global_settings_setting_pop3_enabled_help": "Włącz protokołu POP3 dla serwera poczty. POP3 to starszy protokół umożliwiający dostęp do skrzynek pocztowych z poziomu klientów poczty e-mail. Jest on lżejszy, ale ma mniej funkcji niż IMAP (domyślnie włączony)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_postfix_compatibility": "Kompatybilność Postfix", + "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_security_experimental_enabled_help": "Uruchom eksperymentalne funkcje bezpieczeństwa (nie włączaj, jeśli nie wiesz co robisz!)", + "global_settings_setting_security_name": "Bezpieczeństwo", + "global_settings_setting_smtp_allow_ipv6": "Zezwól na IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Zezwól na wykorzystywanie IPv6 do odbierania i wysyłania maili", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Włącz przekaźnik SMTP", + "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_smtp_relay_host": "Host przekaźnika SMTP", + "global_settings_setting_smtp_relay_password": "Hasło przekaźnika SMTP", + "global_settings_setting_smtp_relay_port": "Port przekaźnika SMTP", + "global_settings_setting_smtp_relay_user": "Nazwa użytkownika przekaźnika SMTP", + "global_settings_setting_ssh_compatibility": "Kompatybilność z SSH", + "global_settings_setting_ssh_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Logowanie hasłem", + "global_settings_setting_ssh_password_authentication_help": "Zezwól na logowanie hasłem przez SSH", + "global_settings_setting_ssh_port": "Port SSH", + "global_settings_setting_user_strength": "Wymagania dotyczące siły hasła użytkownika", + "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", + "global_settings_setting_webadmin_allowlist_enabled_help": "Zezwól tylko kilku adresom IP na dostęp do panelu webadmin.", + "installation_complete": "Instalacja zakończona", + "invalid_password": "Nieprawidłowe hasło", + "invalid_url": "Nie udało się połączyć z {url}… być może strona nie jest dostępna, lub nie jesteś prawidłowo połączony z Internetem po IPv4/IPv6.", + "log_letsencrypt_cert_renew": "Odnów '{}' certyfikat Let's Encrypt", + "log_settings_reset": "Resetuj ustawienia", + "log_settings_set": "Zastosuj ustawienia", + "log_tools_migrations_migrate_forward": "Uruchom migracje", + "log_user_import": "Importuj użytkowników", + "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków", + "pydantic_type_error": "Nieprawidłowy typ.", + "restore_complete": "Przywracanie zakończone", + "root_password_changed": "Hasło root zostało zmienione", + "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", + "service_disabled": "Usługa '{service}' nie będzie już uruchamiana podczas uruchamiania systemu.", + "service_enabled": "Usługa '{service}' będzie teraz automatycznie uruchamiana podczas uruchamiania systemu.", + "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", + "service_remove_failed": "Nie można usunąć usługi '{service}", + "service_removed": "Usunięto usługę '{service}'", + "session_expired": "Sesja wygasła", + "system_upgraded": "Zaktualizowano system", + "unknown_error_reading_file": "Nieznany błąd podczas próby odczytania pliku {file} (przyczyna: {error})", + "unknown_group": "Nieznana grupa '{group}'", + "unknown_user": "Nieznany użytkownik '{user}'", + "unlimit": "Brak limitu", + "upgrading_packages": "Aktualizowanie paczek…", + "user_created": "Utworzono użytkownika", + "user_deleted": "Usunięto użytkownika", + "visitors": "Odwiedzający", + "yunohost_api": "API YunoHost", + "yunohost_installing": "Instalowanie YunoHost…", + "domain_config_search_engine_help": "To opcjonalna funkcja, umożliwiająca wyświetlanie paska wyszukiwania w portalu (na przykład jeśli chcesz używać portalu YunoHost jako strony głównej przeglądarki). Powinien to być adres URL z pustym ciągiem zapytania jak `https://duckduckgo.com/?q=`, gdzie `q=` jest pustym parametrem zapytania duckduckgo", + "domain_config_search_engine_name": "Nazwa wyszukiwarki", + "domain_config_show_other_domains_apps": "Pokaż aplikacje z innych domen", + "domain_creation_failed": "Nie można utworzyć domeny {domain}: {error}", + "domain_deletion_failed": "Nie można usunąć domeny {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "To polecenie pokazuje *zalecaną* konfigurację. Nie konfiguruje ono jednak automatycznie konfiguracji DNS. To Ty jesteś odpowiedzialny za skonfigurowanie strefy DNS u swojego rejestratora zgodnie z tą rekomendacją.", + "domain_dns_conf_special_use_tld": "Domena ta opiera się na specjalnej domenie najwyższego poziomu (TLD) takiej jak .local lub .test i dlatego nie oczekuje się że będzie miała rzeczywiste rekordy DNS.", + "domain_dns_push_already_up_to_date": "Rekordy są już aktualne, nic nie trzeba robić.", + "domain_dns_push_failed": "Aktualizacja rekordów DNS nie powiodła się.", + "domain_dns_push_failed_to_list": "Nie udało się wyświetlić listy bieżących rekordów za pomocą interfejsu API rejestratora: {error}", + "domain_dns_push_managed_in_parent_domain": "Funkcja automatycznej konfiguracji DNS jest zarządzana w domenie nadrzędnej {parent_domain}.", + "domain_dns_push_not_applicable": "Funkcja automatycznej konfiguracji DNS nie ma zastosowania do domeny {domain}. Należy ręcznie skonfigurować rekordy DNS postępując zgodnie z dokumentacją dostępną pod adresem https://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "Częściowa aktualizacja rekordów DNS: zgłoszono pewne ostrzeżenia/błędy.", + "domain_dns_push_record_failed": "Nie udało się {action} zapisać {type}/{name}: {error}", + "domain_dns_push_success": "Zaktualizowano rekordy DNS!", + "domain_remove_confirm_apps_removal": "Usunięcie tej domeny spowoduje usunięcie następujących aplikacji:\n{apps}\n\nCzy na pewno chcesz to zrobić? [{answers}]", + "domain_uninstall_app_first": "Te aplikacje są nadal zainstalowane w twojej domenie:\n{apps}\n\nOdinstaluj je za pomocą \"yunohost app remove the_app_id\" lub przenieś je do innej domeny za pomocą \"yunohost app change-url the_app_id\" przed przystąpieniem do usuwania domeny", + "domain_unknown": "Domena '{domain}' nieznana", + "dpkg_is_broken": "Nie możesz tego zrobić w tej chwili ponieważ dpkg/APT (menedżer pakietów systemowych) wydaje się być uszkodzony… Możesz spróbować rozwiązać ten problem łącząc się przez SSH i uruchamiając `sudo apt install --fix-broken` i/lub `sudo dpkg --configure -a` i/lub `sudo dpkg --audit`.", + "dpkg_lock_not_available": "Nie można teraz uruchomić tego polecenia ponieważ inny program najwyraźniej korzysta z blokady dpkg (menedżera pakietów systemowych)", + "dyndns_could_not_check_available": "Nie można sprawdzić czy domena {domain} jest dostępna u {provider}.", + "dyndns_domain_not_provided": "Dostawca DynDNS {provider} nie może zapewnić domeny {domain}.", + "dyndns_ip_update_failed": "Nie można zaktualizować adresu IP w DynDNS", + "dyndns_ip_updated": "Zaktualizowano twój adres IP w DynDNS", + "dyndns_key_not_found": "Nie znaleziono klucza DNS dla domeny", + "dyndns_no_domain_registered": "Brak domeny zarejestrowanej w DynDNS", + "dyndns_no_recovery_password": "Nie podano hasła odzyskiwania! W przypadku utraty kontroli nad tą domeną, należy skontaktować się z administratorem w zespole YunoHost!", + "dyndns_provider_unreachable": "Nie można nawiązać połączenia z dostawcą DynDNS {provider}: albo twój YunoHost nie jest prawidłowo podłączony do Internetu albo serwer dynette jest wyłączony.", + "dyndns_set_recovery_password_denied": "Nie udało się ustawić hasła odzyskiwania: klucz nieprawidłowy", + "dyndns_set_recovery_password_failed": "Nie udało się ustawić hasła odzyskiwania: {error}", + "dyndns_set_recovery_password_invalid_password": "Nie udało się ustawić hasła odzyskiwania: hasło nie jest wystarczająco silne", + "dyndns_set_recovery_password_success": "Hasło odzyskiwania ustawione!", + "dyndns_set_recovery_password_unknown_domain": "Nie udało się ustawić hasła odzyskiwania: domena nie jest zarejestrowana", + "dyndns_subscribe_failed": "Nie udało się subskrybować domeny DynDNS: {error}", + "dyndns_subscribed": "Domena DynDNS subskrybowana", + "dyndns_too_many_requests": "Usługa dyndns YunoHost otrzymała od ciebie zbyt wiele żądań. Odczekaj około 1 godziny przed ponowną próbą.", + "dyndns_unavailable": "Domena '{domain}' jest niedostępna.", + "dyndns_unsubscribe_already_unsubscribed": "Domena została już zakończona", + "dyndns_unsubscribe_denied": "Nie udało się anulować subskrypcji domeny: nieprawidłowe dane uwierzytelniające", + "dyndns_unsubscribe_failed": "Nie można anulować subskrypcji domeny DynDNS: {error}", + "dyndns_unsubscribed": "Domena DynDNS zakończona", + "field_invalid": "Nieprawidłowe pole '{field}'", + "file_does_not_exist": "Plik {path} nie istnieje.", + "firewall_reload_failed": "Nie udało się przeładować zapory. Więcej informacji w logu.", + "global_settings_reset_success": "Zresetuj ustawienia globalne", + "global_settings_setting_dns_custom_resolvers_enabled": "Użyj niestandardowych resolverów DNS", + "global_settings_setting_dns_custom_resolvers_enabled_help": "Domyślnie YunoHost korzysta z listy zaufanych resolverów zlokalizowanych w Europie. Zaawansowani użytkownicy mogą zamiast tego określić własne resolvery.", + "global_settings_setting_dns_custom_resolvers_list": "Adresy niestandardowych resolverów", + "global_settings_setting_dns_custom_resolvers_list_help": "Lista co najmniej 2 serwerów DNS dla każdego używanego protokołu IP (IPv4/IPv6). Przykład: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "global_settings_setting_enable_blocklists": "Włącz listy blokowania dla ruchu przychodzącego", + "global_settings_setting_enable_blocklists_help": "Blokuje serwery wymienione przez spamcop.net, spamhaus.org i abuseat.org aby zapobiegać spamu. Może to jednak powodować problemy z dostarczaniem wiadomości na niektórych nieszkodliwych serwerach pocztowych, które mogą być wymienione przez te podmioty zewnętrzne. W takim przypadku wiadomości wysyłane z tych serwerów nie zostaną odebrane.", + "global_settings_setting_nginx_compatibility_help": "Kompromis między kompatybilnością a bezpieczeństwem serwera NGINX. Wpływa na szyfry (i inne aspekty związane z bezpieczeństwem)", + "global_settings_setting_nginx_name": "NGINX (serwer web)", + "global_settings_setting_nginx_redirect_to_https_help": "Domyślnie przekierowuj żądania HTTP do HTTPs (NIE WYŁĄCZAJ chyba że naprawdę wiesz, co robisz!)", + "global_settings_setting_portal_allow_edit_email": "Zezwól użytkownikom na edycję ich głównego adresu e-mail", + "global_settings_setting_portal_allow_edit_email_alias": "Zezwalaj użytkownikom na dodawanie, usuwanie i edytowanie aliasów e-mail", + "global_settings_setting_portal_allow_edit_email_alias_help": "Jeśli opcja ta jest wyłączona, należy poprosić administratorów o jej wyłączenie.", + "global_settings_setting_portal_allow_edit_email_forward": "Zezwalaj użytkownikom na dodawanie, usuwanie i edytowanie przekazywania e-mail", + "global_settings_setting_portal_allow_edit_email_forward_help": "Jeśli opcja ta jest wyłączona, należy poprosić administratorów o jej wyłączenie.", + "global_settings_setting_portal_allow_edit_email_help": "Jeśli opcja ta jest wyłączona, należy poprosić administratorów o jej wyłączenie.", + "global_settings_setting_portal_name": "Portal", + "global_settings_setting_postfix_compatibility_help": "Kompromis między kompatybilnością a bezpieczeństwem serwera Postfix. Wpływa na szyfry (i inne aspekty związane z bezpieczeństwem)", + "global_settings_setting_postfix_name": "Postfix (serwer poczty SMTP)", + "global_settings_setting_root_access_explain": "W systemach Linux \"root\" to absolutny administrator. W kontekście YunoHost bezpośrednie logowanie SSH jako \"root\" jest domyślnie wyłączone – z wyjątkiem sieci lokalnej serwera. Członkowie grupy \"admins\" mogą użyć polecenia sudo aby działać jako root z poziomu wiersza poleceń. Posiadanie (solidnego) hasła root może być jednak pomocne w debugowaniu systemu, jeśli z jakiegoś powodu zwykli administratorzy nie mogą się już zalogować.", + "global_settings_setting_root_access_name": "Zmień hasło root", + "global_settings_setting_smtp_backup_mx_domains": "Domeny które będą pełnić funkcję drugorzędnych MX dla", + "global_settings_setting_smtp_backup_mx_domains_help": "Zezwól temu serwerowi na działanie jako zapasowa *dodatkowa* domena MX dla wymienionej domeny. Oznacza to, że jeśli główna domena MX dla danej domeny będzie niedostępna (na przykład z powodu awarii), wiadomości e-mail będą nadal wysyłane na ten serwer, który będzie je przechowywał przez maksymalnie 20 dni i spróbuje przekazać do właściwego miejsca docelowego po ponownym uruchomieniu. Można podać kilka domen, rozdzielając je przecinkami.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Kopia zapasowa SMTP wiadomości e-mail na białej liście MX", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Działając jako dodatkowy MX, należy podać pełną listę dozwolonych adresów e-mail odbiorców (w przeciwnym razie wiadomości zostaną odrzucone i odrzucone). Można podać kilka wpisów, rozdzielonych przecinkami.", + "global_settings_setting_ssh_compatibility_help": "Kompromis między kompatybilnością a bezpieczeństwem serwera SSH. Wpływa na szyfry (i inne aspekty związane z bezpieczeństwem). Więcej informacji można znaleźć na https://infosec.mozilla.org/guidelines/openssh.", + "global_settings_setting_ssh_port_help": "Preferowany jest port niższy niż 1024 aby zapobiec próbom przejęcia uprawnień przez usługi inne niż administrator na komputerze zdalnym. Należy również unikać używania portu już używanego, takiego jak 80 lub 443.", + "global_settings_setting_tls_passthrough_enabled": "Włącz przekazywanie oparte na TLS/SNI", + "global_settings_setting_tls_passthrough_enabled_help": "To zaawansowana funkcja umożliwiająca odwrotne proxy całej domeny do innego komputera *bez* odszyfrowywania ruchu. Przydatna gdy chcesz udostępnić kilka komputerów za tym samym adresem IP, ale jednocześnie pozwolić każdemu z nich obsłużyć zakończenie SSL.", + "global_settings_setting_tls_passthrough_explain": "Ta funkcja jest ZAAWANSOWANA i EKSPERYMENTALNA i spowoduje znaczące zmiany w konfiguracji nginx tego serwera. NIE używaj jej jeśli nie wiesz co robisz! W szczególności pamiętaj że fail2ban nie może zostać wdrożony na serwerze proxy (nftables nie blokuje szkodliwego ruchu, ponieważ wszystkie pakiety IP są wyświetlane jako pochodzące z serwera frontowego). Ponadto, na razie konfiguracja nginx serwera proxy wymaga ręcznej modyfikacji aby akceptowała `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "Lista przekierowań", + "global_settings_setting_tls_passthrough_list_help": "Powinna to być lista DOMAIN;DESTINATION;PORT, na przykład domain.tld;192.168.1.42;443 albo domain.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "Przekazywanie TLS/oparte na SNI", + "global_settings_setting_webadmin_allowlist": "Lista dozwolonych adresów IP Webadmin", + "global_settings_setting_webadmin_allowlist_help": "Adresy IP uprawnione do dostępu do webadmin. Dozwolona jest notacja CIDR.", + "global_settings_setting_webadmin_name": "Webadmin", + "good_practices_about_admin_password": "Zamierzasz teraz zdefiniować nowe hasło administracyjne. Hasło powinno mieć co najmniej 8 znaków – choć dobrą praktyką jest użycie dłuższego hasła (tj. frazy kluczowej) i/lub użycie różnych znaków (wielkich i małych liter, cyfr i znaków specjalnych).", + "good_practices_about_user_password": "Zamierzasz teraz zdefiniować nowe hasło użytkownika. Hasło powinno mieć co najmniej 8 znaków – choć dobrą praktyką jest użycie dłuższego hasła (tj. frazy kluczowej) i/lub kombinacji znaków (wielkie i małe litery, cyfry i znaki specjalne).", + "group_already_exist": "Grupa {group} już istnieje", + "group_already_exist_on_system": "Grupa {group} już istnieje w grupach systemowych", + "group_already_exist_on_system_but_removing_it": "Grupa {group} już istnieje w grupach systemowych, ale YunoHost ją usunie…", + "group_cannot_be_deleted": "Grupy {group} nie można usunąć ręcznie.", + "group_cannot_edit_all_users": "Grupy 'all_users' nie można edytować ręcznie. Jest to specjalna grupa przeznaczona dla wszystkich użytkowników zarejestrowanych w YunoHost.", + "group_cannot_edit_primary_group": "Grupy '{group}' nie można edytować ręcznie. Jest to grupa podstawowa przeznaczona dla tylko jednego użytkownika.", + "group_cannot_edit_visitors": "Grupy 'goście' nie można edytować ręcznie. Jest to specjalna grupa reprezentująca anonimowych gości", + "group_cannot_remove_last_admin": "Użytkownik '{user}' jest ostatnim użytkownikiem w grupie 'admins' i nie zostanie z niej usunięty.", + "group_created": "Utworzono grupę '{group}'", + "group_creation_failed": "Nie udało się utworzyć grupy '{group}': {error}", + "group_deleted": "Grupa '{group}' została usunięta", + "group_deletion_failed": "Nie można usunąć grupy '{group}': {error}", + "group_mailalias_add": "Alias e-mail '{mail}' zostanie dodany do grupy '{group}'", + "group_mailalias_remove": "Alias e-mail '{mail}' zostanie usunięty z grupy '{group}'", + "group_no_change": "Nic nie trzeba zmieniać dla grupy '{group}'", + "group_unknown": "Grupa '{group}' jest nieznana", + "group_update_aliases": "Aktualizowanie aliasów dla grupy '{group}'", + "group_update_failed": "Nie można zaktualizować grupy '{group}': {error}", + "group_updated": "Grupa '{group}' została zaktualizowana", + "group_user_add": "Użytkownik '{user}' zostanie dodany do grupy '{group}'", + "group_user_already_in_group": "Użytkownik {user} jest już w grupie {group}", + "group_user_not_in_group": "Użytkownik {user} nie należy do grupy {group}", + "group_user_remove": "Użytkownik '{user}' zostanie usunięty z grupy '{group}'", + "hook_exec_failed": "Nie można uruchomić skryptu: {path}", + "hook_exec_not_terminated": "Skrypt nie zakończył się prawidłowo: {path}", + "hook_json_return_error": "Nie udało się odczytać skryptu powracającego z {path}. Błąd: {msg}. Surowa zawartość: {raw_content}", + "hook_list_by_invalid": "Tej właściwości nie można używać do listy skrypty", + "hook_name_unknown": "Nazwa akcji '{name}' nieznana", + "invalid_credentials": "Nieprawidłowe hasło lub nazwa użytkownika", + "invalid_number": "Musi to być liczba", + "invalid_regex": "Nieprawidłowe regex: '{regex}'", + "invalid_shell": "Nieprawidłowe shell: {shell}", + "ldap_attribute_already_exists": "Atrybut LDAP '{attribute}' już istnieje i ma wartość '{value}'", + "ldap_server_down": "Nie można nawiązać połączenia z serwerem LDAP", + "ldap_server_is_down_restart_it": "Usługa LDAP jest wyłączona. Spróbuj ją ponownie uruchomić…", + "log_app_action_run": "Uruchom akcję aplikacji '{}'", + "log_app_change_url": "Zmień adres URL aplikacji '{}'", + "log_app_config_set": "Zastosuj konfigurację do aplikacji '{}'", + "log_app_install": "Zainstaluj aplikację '{}'", + "log_app_makedefault": "Ustaw '{}' jako domyślną aplikację", + "log_app_remove": "Usuń aplikację '{}'", + "log_app_upgrade": "Zaktualizuj aplikację '{}'", + "log_available_on_yunopaste": "Ten dziennik jest teraz dostępny pod {url}", + "log_backup_create": "Utwórz archiwum kopii zapasowej", + "log_backup_restore_app": "Przywróć '{}' z archiwum kopii zapasowej", + "log_backup_restore_system": "Przywróć system z archiwum kopii zapasowej", + "log_corrupted_md_file": "Plik metadanych YAML powiązany z dziennikami jest uszkodzony: '{md_file}\nBłąd: {error}'", + "log_diagnosis_run": "Uruchom diagnostykę", + "log_does_exists": "Brak dziennika operacji o nazwie '{log}', użyj 'yunohost log list' aby zobaczyć wszystkie dostępne dzienniki operacji", + "log_domain_add": "Dodaj domenę '{}'", + "log_domain_config_set": "Zaktualizuj konfigurację dla domeny '{}'", + "log_domain_dns_push": "Prześlij rekordy DNS dla domeny '{}'", + "log_domain_main_domain": "Ustaw '{}' jako domenę główną", + "log_domain_remove": "Usuń domenę '{}'", + "log_dyndns_subscribe": "Zarejestruj subdomenę YunoHost '{}'", + "log_dyndns_unsubscribe": "Usuń subdomenę YunoHost '{}'", + "log_dyndns_update": "Zaktualizuj adres IP powiązany z subdomeną YunoHost '{}'", + "log_help_to_get_failed_log": "Nie udało się ukończyć operacji '{desc}'. Udostępnij pełny dziennik tej operacji za pomocą 'yunohost log share {name}' aby uzyskać pomoc", + "log_help_to_get_log": "Aby wyświetlić dziennik operacji '{desc}', użyj 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Zainstaluj certyfikat Let's Encrypt w domenie '{}'", + "log_link_to_failed_log": "Nie udało się ukończyć operacji '{desc}'. Aby uzyskać pomoc, udostępnij dziennik operacji klikając tutaj", + "log_link_to_log": "Pełny dziennik tej operacji: '{desc}'", + "log_operation_unit_unclosed_properly": "Operacja nie zakończyła się prawidłowo", + "log_regen_conf": "Wygeneruj ponownie konfiguracje systemu '{}'", + "log_remove_on_failed_install": "Usuń '{}' po nieudanej instalacji", + "log_resource_snippet": "Dostarczanie/usuwanie/aktualizowanie zasobu", + "log_selfsigned_cert_install": "Zainstaluj certyfikat podpisany samodzielnie w domenie '{}'", + "log_settings_reset_all": "Zresetuj wszystkie ustawienia", + "log_tools_postinstall": "Wykonaj instalację po-instalacyjną serwera YunoHost", + "log_tools_reboot": "Zrestartuj serwer", + "log_tools_shutdown": "Wyłącz serwer", + "log_tools_update": "Pobieranie dostępnych aktualizacji systemu i odświeżanie katalogu aplikacji", + "log_tools_upgrade": "Pakiety aktualizacji systemu", + "log_user_create": "Dodaj użytkownika '{}'", + "log_user_delete": "Usuń użytkownika '{}'", + "log_user_group_create": "Utwórz grupę '{}'", + "log_user_group_delete": "Usuń grupę '{}'", + "log_user_group_update": "Zaktualizuj grupę '{}'", + "log_user_update": "Zaktualizuj informacje dla użytkownika '{}'", + "mail_alias_remove_failed": "Nie można usunąć aliasu e-mail '{mail}'", + "mail_alias_unauthorized": "Nie masz uprawnień do dodawania aliasów powiązanych z domeną '{domain}'", + "mail_already_exists": "Adres e-mail '{mail}' już istnieje", + "mail_domain_unknown": "Nieprawidłowy adres e-mail dla domeny '{domain}'. Proszę użyć domeny administrowanej przez ten serwer.", + "mail_edit_operation_unauthorized": "Nie masz uprawnień do wprowadzenia tej zmiany na swoim koncie.", + "mail_forward_remove_failed": "Nie można usunąć przekierowania e-mail '{mail}'", + "mail_unavailable": "Ten adres e-mail jest zarezerwowany dla grupy administratorów", + "mailbox_disabled": "E-mail wyłączony dla użytkownika {user}", + "mailbox_used_space_dovecot_down": "Aby zobaczyć ilość miejsca na dysku wykorzystanego przez pocztę e-mail, musisz uruchomić usługę Dovecot", + "main_domain_change_failed": "Nie można zmienić domeny głównej", + "main_domain_changed": "Główna domena została zmieniona", + "migration_0027_cleaning_up": "Czyszczenie pamięci podręcznej i pakietów nie jest już przydatne…", + "migration_0027_delayed_api_restart": "Interfejs API YunoHost zostanie automatycznie uruchomiony ponownie za 15 sekund. Może on być niedostępny przez kilka sekund, po czym konieczne będzie ponowne zalogowanie.", + "migration_0027_general_warning": "Na koniec, prosimy pamiętać, że ta migracja to **delikatna operacja**. Zespół YunoHost dołożył wszelkich starań aby ją przejrzeć i przetestować, jednak migracja może nadal uszkodzić części systemu lub jego aplikacje.\n\nW związku z tym zaleca się:\n - **Wykonanie kopii zapasowych** wszelkich krytycznych danych lub aplikacji. Więcej informacji na stronie https://doc.yunohost.org/backup;\n - **Cierpliwość** po uruchomieniu migracji: w zależności od połączenia internetowego i sprzętu, poprawna aktualizacja może potrwać do godziny;\n - **Skontaktuj się ze społecznością** na forum, jeśli potrzebujesz pomocy w rozwiązywaniu problemów.", + "migration_0027_main_upgrade": "Rozpoczęcie głównej aktualizacji…", + "migration_0027_modified_files": "Należy pamiętać że następujące pliki zostały zmodyfikowane ręcznie i mogą zostać nadpisane po aktualizacji: {manually_modified_files}", + "migration_0027_not_bullseye": "Obecna dystrybucja Debiana to nie Bullseye! Jeśli uruchomiłeś już migrację Bullseye -> Bookworm, ten błąd jest symptomem tego że procedura migracji nie powiodła się w 100% (w przeciwnym razie YunoHost oznaczyłby ją jako zakończoną). Zaleca się sprawdzenie co się stało z zespołem wsparcia, który będzie potrzebował **pełnego** logu migracji, który można znaleźć w Narzędziach > Logach w webadmin.", + "migration_0027_not_enough_free_space": "W folderze /var/ jest dość mało wolnego miejsca! Powinieneś mieć co najmniej 1 GB wolnego miejsca aby przeprowadzić tę migrację.", + "migration_0027_patch_yunohost_conflicts": "Stosowanie poprawki w celu obejścia problemu konfliktu…", + "migration_0027_patching_sources_list": "Korekta pliku sources.lists…", + "migration_0027_problematic_apps_warning": "Należy pamiętać że wykryto następujące potencjalnie problematyczne zainstalowane aplikacje. Wygląda na to że nie zostały one zainstalowane z katalogu aplikacji YunoHost lub nie są oznaczone jako 'działające'. W związku z tym nie można zagwarantować, że będą nadal działać po aktualizacji: {problematic_apps}", + "migration_0027_start": "Rozpoczęcie migracji do Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Wystąpił błąd podczas głównej aktualizacji, system prawdopodobnie nadal działa na Debianie Bullseye.", + "migration_0027_system_not_fully_up_to_date": "Twój system nie jest w pełni aktualny. Przed migracją do Bookworm przeprowadź regularną aktualizację.", + "migration_0027_yunohost_upgrade": "Rozpoczęcie aktualizacji YunoHost core…", + "migration_not_enough_space": "Przygotuj wystarczającą ilość miejsca w {path} aby przeprowadzić migrację.", + "migration_postgresql_previous_not_installed": "PostgreSQL nie został zainstalowany w twoim systemie. Nic nie musisz robić.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 jest zainstalowany, ale nie PostgreSQL 15!? Mogło się zdarzyć coś dziwnego w twoim systemie :(…" +} diff --git a/locales/pt.json b/locales/pt.json new file mode 100644 index 0000000..602e049 --- /dev/null +++ b/locales/pt.json @@ -0,0 +1,243 @@ +{ + "aborting": "Abortando.", + "action_invalid": "Ação inválida '{action}'", + "additional_urls_already_added": "A URL adicional '{url}' já está adicionada para a permissão '{permission}'", + "additional_urls_already_removed": "A URL adicional '{url}' já foi removida da permissão '{permission}'", + "admin_password": "Senha de administração", + "admins": "Admins", + "already_up_to_date": "Nada a ser feito. Tudo já está atualizado.", + "app_action_broke_system": "Esta ação parece ter quebrado estes serviços importantes: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Estes serviços devem estar funcionado para executar esta ação: {services}. Tente reiniciá-los para continuar (e possivelmente investigar o porquê de não estarem funcionado).", + "app_already_installed": "{app} já está instalada", + "app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Confira em `app changeurl` se está disponível.", + "app_argument_choice_invalid": "Escolha um valor válido para o argumento '{name}' : '{value}' não está entre as opções disponíveis ({choices})", + "app_argument_invalid": "Escolha um valor válido para o argumento '{name}': {error}", + "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain}{path}'), nada para fazer.", + "app_change_url_no_script": "A aplicação '{app_name}' ainda não permite modificar a URL. Talvez devesse atualizá-la.", + "app_change_url_success": "A URL agora é {domain}{path}", + "app_config_unable_to_apply": "Falha ao aplicar valores do painel de configuração.", + "app_config_unable_to_read": "Falha ao ler valores do painel de configuração.", + "app_extraction_failed": "Não foi possível extrair os arquivos para instalação", + "app_full_domain_unavailable": "Desculpe, esse app deve ser instalado num domínio próprio mas já há outros apps instalados no domínio '{domain}'. Você pode usar um subdomínio dedicado a esse aplicativo.", + "app_id_invalid": "App ID invaĺido", + "app_install_failed": "Não foi possível instalar {app}: {error}", + "app_install_files_invalid": "Esses arquivos não podem ser instalados", + "app_install_script_failed": "Ocorreu um erro dentro do script de instalação do aplicativo", + "app_location_unavailable": "Esta url ou não está disponível ou está em conflito com outra(s) aplicação(ões) já instalada(s):\n{apps}", + "app_make_default_location_already_used": "Não foi passível fazer a aplicação '{app}' ser a padrão no domínio, '{domain}' já está sendo usado por '{other_app}'", + "app_manifest_install_ask_admin": "Escolha um usuário de administrador para essa aplicação", + "app_manifest_install_ask_domain": "Escolha o domínio em que esta aplicação deve ser instalada", + "app_manifest_install_ask_is_public": "Essa aplicação deve ser visível para visitantes anônimos?", + "app_manifest_install_ask_password": "Escolha uma senha de administrador para essa aplicação", + "app_manifest_install_ask_path": "Escolha o caminho da url (depois do domínio) em que essa aplicação deve ser instalada", + "app_not_correctly_installed": "{app} parece não estar corretamente instalada", + "app_not_installed": "Não foi possível encontrar {app} na lista de aplicações instaladas: {all_apps}", + "app_not_properly_removed": "{app} não foi corretamente removido", + "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_remove_after_failed_install": "Removendo a aplicação após a falha da instalação…", + "app_removed": "{app} desinstalada", + "app_requirements_checking": "Verificando os pacotes necessários para {app}…", + "app_restore_failed": "Não foi possível restaurar {app}: {error}", + "app_restore_script_failed": "Ocorreu um erro dentro do script de restauração da aplicação", + "app_sources_fetch_failed": "Não foi possível carregar os arquivos de código fonte, a URL está correta?", + "app_start_backup": "Obtendo os arquivos para fazer o backup de {app}…", + "app_start_install": "Instalando {app}…", + "app_start_remove": "Removendo {app}…", + "app_start_restore": "Restaurando {app}…", + "app_unknown": "Aplicação desconhecida", + "app_unsupported_remote_type": "A aplicação não possui suporte ao tipo remoto utilizado", + "app_upgrade_app_name": "Atualizando {app}…", + "app_upgrade_failed": "Não foi possível atualizar {app}: {error}", + "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_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações", + "app_upgraded": "{app} atualizado", + "apps_already_up_to_date": "Todas as aplicações já estão atualizadas", + "apps_catalog_failed_to_download": "Não foi possível fazer o download do catálogo de aplicações {apps_catalog}: {error}", + "apps_catalog_obsolete_cache": "O cache do catálogo de aplicações está vazio ou obsoleto.", + "apps_catalog_update_success": "O catálogo de aplicações foi atualizado!", + "apps_catalog_updating": "Atualizando o catálogo de aplicações…", + "ask_main_domain": "Domínio principal", + "ask_new_admin_password": "Nova senha de administração", + "ask_new_domain": "Novo domínio", + "ask_new_path": "Novo caminho", + "ask_password": "Senha", + "ask_user_domain": "Domínio para usar para o endereço de email e conta XMPP do usuário", + "backup_abstract_method": "Este método de backup ainda não foi implementado", + "backup_actually_backuping": "Criando cópia de backup dos arquivos obtidos…", + "backup_applying_method_copy": "Copiando todos os arquivos para o backup…", + "backup_applying_method_custom": "Chamando o método personalizado de backup '{method}'…", + "backup_applying_method_tar": "Criando o arquivo TAR de backup…", + "backup_archive_app_not_found": "Não foi possível encontrar {app} no arquivo de backup", + "backup_archive_broken_link": "Não foi possível acessar o arquivo de backup (link quebrado ao {path})", + "backup_archive_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_archive_corrupted": "Parece que o arquivo de backup '{archive}' está corrompido: {error}", + "backup_archive_name_exists": "Já existe um arquivo de backup com esse nome.", + "backup_archive_name_unknown": "Desconhece-se o arquivo local de backup de nome '{name}'", + "backup_archive_open_failed": "Não foi possível abrir o arquivo de backup", + "backup_archive_system_part_not_available": "A seção do sistema '{part}' está indisponível neste backup", + "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_ask_for_copying_if_needed": "Você quer efetuar o backup usando {size}MB temporariamente? (E necessário fazer dessa forma porque alguns arquivos não puderam ser preparados usando um método mais eficiente)", + "backup_cant_mount_uncompress_archive": "Não foi possível montar o arquivo descomprimido como protegido contra escrita", + "backup_cleaning_failed": "Não foi possível limpar o diretório temporário de backup", + "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", + "backup_couldnt_bind": "Não foi possível vincular {src} ao {dest}.", + "backup_create_size_estimation": "O arquivo irá conter cerca de {size} de dados.", + "backup_created": "Backup completo: {name}", + "backup_creation_failed": "Não foi possível criar o arquivo de backup", + "backup_csv_addition_failed": "Não foi possível adicionar os arquivos que estarão no backup ao arquivo CSV", + "backup_csv_creation_failed": "Não foi possível criar o arquivo CSV necessário para a restauração", + "backup_custom_backup_error": "O método personalizado de backup não pôde passar do passo de 'backup'", + "backup_custom_mount_error": "O método personalizado de backup não pôde passar do passo de 'mount'", + "backup_delete_error": "Não foi possível remover '{path}'", + "backup_deleted": "Backup removido: {name}", + "backup_hook_unknown": "O gancho de backup '{hook}' é desconhecido", + "backup_method_copy_finished": "Cópia de backup finalizada", + "backup_method_custom_finished": "Método de backup personalizado '{method}' finalizado", + "backup_method_tar_finished": "Arquivo de backup TAR criado", + "backup_mount_archive_for_restore": "Preparando o arquivo para restauração…", + "backup_no_uncompress_archive_dir": "Não existe tal diretório de arquivo descomprimido", + "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", + "backup_output_directory_not_empty": "Você deve escolher um diretório de saída que esteja vazio", + "backup_output_directory_required": "Você deve especificar um diretório de saída para o backup", + "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_running_hooks": "Executando os hooks de backup…", + "backup_system_part_failed": "Não foi possível fazer o backup da parte do sistema '{part}'", + "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_with_no_backup_script_for_app": "A aplicação '{app}' não tem um script de backup. Ignorando.", + "backup_with_no_restore_script_for_app": "A aplicação {app} não tem um script de restauração, você não será capaz de automaticamente restaurar o backup dessa aplicação.", + "cannot_open_file": "Não foi possível abrir o arquivo {file} (reason: {error})", + "cannot_write_file": "Não foi possível abrir o arquivo {file} (reason: {error})", + "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_attempt_to_replace_valid_cert": "Você está tentando sobrescrever um certificado bom e válido para o domínio {domain}! (Use --force para prosseguir mesmo assim)", + "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_cert_renew_success": "Certificado Let's Encrypt renovado para o domínio '{domain}'", + "certmanager_cert_signing_failed": "Não foi possível assinar o novo certificado", + "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_domain_http_not_working": "O domínio {domain} não parece estar acessível por HTTP. Por favor cheque a categoria 'Web' no diagnóstico para mais informações. (Se você sabe o que está fazendo, use '--no-checks' para desativar estas checagens.)", + "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.)", + "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})", + "certmanager_unable_to_parse_self_CA_name": "Não foi possível processar nome da autoridade de auto-assinatura (arquivo: {file})", + "config_apply_failed": "Aplicar as novas configuração falhou: {error}", + "config_cant_set_value_on_section": "Você não pode setar um único valor na seção de configuração inteira.", + "config_forbidden_keyword": "A palavra chave '{keyword}' é reservada, você não pode criar ou usar um painel de configuração com uma pergunta com esse id.", + "config_no_panel": "Painel de configuração não encontrado.", + "config_unknown_filter_key": "A chave de filtro '{filter_key}' está incorreta.", + "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_warning": "Aviso: Pode ser que essa aplicação funcione, mas ela não está bem integrada ao YunoHost. Algumas funcionalidades como single sign-on e backup/restauração podem não estar disponíveis. Instalar mesmo assim? [{answers}] ", + "corrupted_json": "JSON corrompido lido do {ressource} (motivo: {error})", + "corrupted_toml": "TOML corrompido lido em {ressource} (motivo: {error})", + "corrupted_yaml": "YAML corrompido lido do {ressource} (motivo: {error})", + "danger": "Perigo:", + "diagnosis_apps_allgood": "Todos os apps instalados respeitam práticas básicas de empacotamento", + "diagnosis_apps_bad_quality": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.", + "diagnosis_apps_broken": "Esta aplicação está atualmente marcada como quebrada no catálogo de apps do YunoHost. Isto pode ser um problema temporário enquanto os mantenedores consertam o problema. Enquanto isso, atualizar este app está desabilitado.", + "diagnosis_apps_deprecated_practices": "A versão instalada deste app usa práticas de empacotamento extremamente velhas que não são mais usadas. Você deve considerar seriamente atualizá-lo.", + "diagnosis_apps_issue": "Um problema foi encontrado para o app {app}", + "diagnosis_apps_not_in_app_catalog": "Esta aplicação não está no catálogo de aplicações do YunoHost. Se estava no passado e foi removida, você deve considerar desinstalar este app já que ele não mais receberá atualizações e pode comprometer a integridade e segurança do seu sistema.", + "diagnosis_apps_outdated_ynh_requirement": "A versão instalada deste app requer tão somente yunohost >= 2.x, o que tende a indicar que o app não está atualizado com as práticas de empacotamento recomendadas. Você deve considerar seriamente atualizá-lo.", + "diagnosis_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.", + "diagnosis_basesystem_hardware": "A arquitetura hardware do servidor é {virt} {arch}", + "diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}", + "diagnosis_basesystem_host": "O Servidor está rodando Debian {debian_version}", + "diagnosis_basesystem_kernel": "O servidor está rodando Linux kernel {kernel_version}", + "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_main_version": "O servidor está rodando YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "Versão {package}: {version} ({repo})", + "diagnosis_cache_still_valid": "(O cache para a categoria de diagnóstico {category} ainda é valido. Não será diagnosticada novamente ainda)", + "diagnosis_cant_run_because_of_dep": "Impossível fazer diagnóstico para {category} enquanto ainda existem problemas importantes relacionados a {dep}.", + "diagnosis_description_apps": "Aplicações", + "diagnosis_description_basesystem": "Sistema base", + "diagnosis_description_dnsrecords": "Registros DNS", + "diagnosis_description_ip": "Conectividade internet", + "diagnosis_description_mail": "Email", + "diagnosis_description_ports": "Exposição de portas", + "diagnosis_description_regenconf": "Configurações do sistema", + "diagnosis_description_services": "Cheque de status dos serviços", + "diagnosis_description_systemresources": "Recursos do sistema", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Unidade de armazenamento {mountpoint} (no dispositivo {device}_) tem somente {free} ({free_percent}%) de espaço restante (de {total}). Tenha cuidado.", + "domain_cert_gen_failed": "Não foi possível gerar o certificado", + "domain_created": "Domínio criado com êxito", + "domain_creation_failed": "Não foi possível criar o domínio {domain}: {error}", + "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_exists": "O domínio já existe", + "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.", + "download_bad_status_code": "{url} retornou o código de status {code}", + "download_ssl_error": "Erro de SSL ao conectar-se a {url}", + "download_timeout": "{url} demorou muito para responder, desistiu.", + "download_unknown_error": "Erro quando baixando os dados de {url} : {error}", + "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_unavailable": "O domínio '{domain}' não está disponível.", + "error_changing_file_permissions": "Erro ao alterar as permissões para {path}: {error}", + "error_removing": "Erro ao remover {path}: {error}", + "error_writing_file": "Erro ao gravar arquivo {file}: {error}", + "extracting": "Extração em curso…", + "field_invalid": "Campo inválido '{field}'", + "file_not_exist": "O ficheiro não existe: '{path}'", + "firewall_reloaded": "Firewall recarregada com êxito", + "installation_complete": "Instalação concluída", + "invalid_url": "URL inválida {url} (Esse site existe ?)", + "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}'", + "main_domain_change_failed": "Incapaz alterar o domínio raiz", + "main_domain_changed": "Domínio raiz alterado com êxito", + "nftables_unavailable": "Não pode alterar aqui a nftables. Ou o seu kernel não o suporta ou está num espaço reservado", + "password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres", + "pattern_domain": "Deve ser um nome de domínio válido (p.e. meu-dominio.org)", + "pattern_email": "Deve ser um endereço de correio válido (p.e. alguem@dominio.org)", + "pattern_password": "Deve ter no mínimo 3 caracteres", + "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 '{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_remove_failed": "Incapaz de remover 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}'", + "service_started": "O serviço '{service}' foi iniciado com êxito", + "service_stop_failed": "Incapaz parar o serviço '{service}'", + "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: {error}", + "unknown_error_reading_file": "Erro desconhecido ao tentar ler o arquivo {file} (motivo: {error})", + "unknown_group": "Grupo '{group}' desconhecido", + "unknown_user": "Nome de utilizador '{user}' desconhecido", + "updating_apt_cache": "A atualizar a lista de pacotes disponíveis…", + "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}: {error}", + "user_deleted": "Utilizador eliminado com êxito", + "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'" +} diff --git a/locales/pt_BR.json b/locales/pt_BR.json new file mode 100644 index 0000000..369e922 --- /dev/null +++ b/locales/pt_BR.json @@ -0,0 +1,918 @@ +{ + "aborting": "Abortando.", + "action_invalid": "Ação inválida '{action}'", + "additional_urls_already_added": "URL adicional '{url}' já adicionado ao URL adicional para permissão '{permission}'", + "additional_urls_already_removed": "URL adicional '{url}' já removida no URL adicional para a permissão '{permission}'", + "admin_password": "Senha de administração", + "admins": "Administradores", + "all_users": "Todos os usuários do YunoHost", + "already_up_to_date": "Nada a fazer. Tudo já está atualizado.", + "app_action_broke_system": "Essa ação parece ter interrompido estes serviços importantes: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Esses serviços necessários devem estar em execução para executar esta ação: {services}. Tente reiniciá-los para continuar (e possivelmente investigar por que eles estão inativos).", + "app_action_failed": "Falha ao executar a ação {action} para o aplicativo {app}", + "app_already_installed": "{app} já está instalado", + "app_already_installed_cant_change_url": "Este aplicativo já está instalado. O URL não pode ser alterado apenas por esta função. Verifique em 'app changeurl' se estiver disponível.", + "app_arch_not_supported": "Este aplicativo só pode ser instalado em arquiteturas {required}, mas a arquitetura do seu servidor é {current}", + "app_argument_choice_invalid": "Escolha um valor válido para o argumento '{name}': '{value}' não está entre as opções disponíveis ({choices})", + "app_argument_invalid": "Escolha um valor válido para o argumento '{name}': {error}", + "app_change_url_failed": "Não foi possível alterar o URL de {app}: {error}", + "app_change_url_identical_domains": "O antigo e o novo domínio/caminho_do_url são idênticos ('{domain}{path}'), nada a fazer.", + "app_change_url_no_script": "O aplicativo '{app_name}' ainda não dá suporte à modificação de URL. Talvez você deva atualizá-lo.", + "app_change_url_require_full_domain": "{app} não pode ser movido para este novo URL porque requer um domínio completo (ou seja, com caminho = /)", + "app_change_url_script_failed": "Ocorreu um erro dentro do script de alteração de URL", + "app_change_url_success": "URL de {app} agora é {domain}{path}", + "app_config__core_name": "Blocos e permissões", + "app_config_permission_allowed": "Grupos/usuários com permissão de acesso", + "app_config_permission_allowed_warn_protected": "Nota: esta permissão é 'protegida' e, portanto, o grupo 'visitantes' não pode ser realmente adicionado/removido dos grupos autorizados.", + "app_config_permission_description": "Descrição", + "app_config_permission_description_help": "Isso só é realmente útil se você estiver usando o modo de portal 'descritivo'", + "app_config_permission_extraperm_section_name": "Permissão '{perm}'", + "app_config_permission_label": "Rótulo", + "app_config_permission_location": "Corresponde a [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Logotipo personalizado para usar", + "app_config_permission_logo_help": "Apenas PNG são suportados", + "app_config_permission_show_tile": "Exibir bloco no portal", + "app_config_unable_to_apply": "Falha ao aplicar os valores do painel de configuração.", + "app_config_unable_to_read": "Falha ao ler os valores do painel de configuração.", + "app_corrupt_source": "O YunoHost conseguiu baixar o recurso '{source_id}' ({url}) para {app}, mas o recurso não corresponde ao checksum esperado. Isso pode significar que ocorreu alguma falha temporária de rede em seu servidor OU que o recurso foi alterado de alguma forma pelo mantenedor upstream (ou por um agente malicioso?) e os responsáveis pelo pacote YunoHost precisam investigar e talvez atualizar o manifesto do aplicativo para levar em consideração essa alteração.\nChecksum sha256 esperado: {expected_sha256}\nChecksum sha256 baixado: {computed_sha256}\nTamanho do arquivo baixado: {size}", + "app_extraction_failed": "Não foi possível extrair os arquivos de instalação", + "app_failed_to_download_asset": "Falha ao baixar o ativo '{source_id}' ({url}) para {app}: {out}", + "app_full_domain_unavailable": "Desculpe, este aplicativo deve ser instalado em um domínio próprio, mas outros aplicativos já estão instalados no domínio '{domain}'. Você pode usar um subdomínio dedicado a este aplicativo.", + "app_id_invalid": "ID de aplicativo inválido", + "app_install_failed": "Não foi possível instalar {app}: {error}", + "app_install_files_invalid": "Esses arquivos não podem ser instalados", + "app_install_script_failed": "Ocorreu um erro dentro do script de instalação do aplicativo", + "app_location_unavailable": "Este URL não está disponível ou entra em conflito com os aplicativos já instalados:\n{apps}", + "app_make_default_location_already_used": "Não foi possível tornar '{app}' o aplicativo padrão no domínio, '{domain}' já está em uso por '{other_app}'", + "app_manifest_install_ask_admin": "Escolha um usuário administrador para este aplicativo", + "app_manifest_install_ask_domain": "Escolha o domínio onde este aplicativo deve ser instalado", + "app_manifest_install_ask_init_admin_permission": "Quem deve ter acesso aos recursos de administração deste aplicativo? (Isso pode ser alterado posteriormente)", + "app_manifest_install_ask_init_main_permission": "Quem deve ter acesso a este aplicativo? (Isso pode ser alterado posteriormente)", + "app_manifest_install_ask_is_public": "Este aplicativo deve ser exposto a visitantes anônimos?", + "app_manifest_install_ask_password": "Escolha uma senha de administração para este aplicativo", + "app_manifest_install_ask_path": "Escolha o caminho da URL (após o domínio) onde este aplicativo deve ser instalado", + "app_not_correctly_installed": "{app} parece estar instalado incorretamente", + "app_not_enough_disk": "Este aplicativo requer {required} de espaço livre.", + "app_not_enough_ram": "Este aplicativo requer {required} de RAM para instalar/atualizar, mas apenas {current} está disponível no momento.", + "app_not_installed": "Não foi possível encontrar {app} na lista de aplicativos instalados: {all_apps}", + "app_not_properly_removed": "{app} não foi removido corretamente", + "app_packaging_format_not_supported": "Este aplicativo não pode ser instalado porque seu formato de empacotamento não é compatível com sua versão do YunoHost. Você provavelmente deve considerar atualizar seu sistema.", + "app_remove_after_failed_install": "Removendo o aplicativo após falha na instalação…", + "app_removed": "{app} desinstalado", + "app_requirements_checking": "Verificando os requisitos para {app}…", + "app_resource_failed": "Falha no provisionamento, desprovisionamento ou atualização de recursos para {app}: {error}", + "app_restore_failed": "Não foi possível restaurar {app}: {error}", + "app_restore_script_failed": "Ocorreu um erro dentro do script de restauração do aplicativo", + "app_sources_fetch_failed": "Não foi possível obter os arquivos de origem, o URL está correto?", + "app_start_backup": "Coletando arquivos para backup de {app}…", + "app_start_install": "Instalando {app}…", + "app_start_remove": "Removendo {app}…", + "app_start_restore": "Restaurando {app}…", + "app_unknown": "Aplicativo desconhecido", + "app_unsupported_remote_type": "Tipo de URL remoto sem suporte usado pelo aplicativo", + "app_upgrade_app_name": "Agora atualizando {app}…", + "app_upgrade_bad_quality": "Este aplicativo está atualmente sinalizado como com problemas no catálogo de aplicativos da YunoHost. Isso pode ser um problema temporário enquanto os responsáveis pela manutenção tentam corrigi-lo. Enquanto isso, a atualização deste aplicativo está desativada.", + "app_upgrade_broke_the_system": "A atualização de {app} aparentemente funcionou, mas deixou o sistema em um estado quebrado e, portanto, é considerado uma falha.", + "app_upgrade_cli_bad_quality": "Ignorando atualizações para {app} porque ele está atualmente sinalizado como quebrado no catálogo de aplicativos da YunoHost.", + "app_upgrade_cli_up_to_date": "{app} já está atualizado ({current_version})", + "app_upgrade_cli_url_required": "{app} não está no catálogo (mais?) e, portanto, não pode ser atualizado automaticamente. Você deve usar 'yunohost app upgrade {app}' para fornecer a URL do repositório usando a opção '-u'.", + "app_upgrade_cli_will_force_upgrade": "{app} será forçado a atualizar ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} será atualizado de {current_version} para {new_version}", + "app_upgrade_continuing_with_other_apps": "A atualização do aplicativo {app} falhou, mas a atualização dos outros aplicativos continuará mesmo assim (porque a opção `--continue-on-failure` foi usada)", + "app_upgrade_fail_requirements": "Uma nova versão está disponível para este aplicativo ({new_version}), mas alguns requisitos não são atendidos:\n{failed_requirements}", + "app_upgrade_failed": "Falha ao atualizar {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "Falha ao atualizar o aplicativo '{app}' e deixou o sistema em um estado quebrado.", + "app_upgrade_script_failed": "Ocorreu um erro dentro do script de atualização do aplicativo", + "app_upgrade_several_apps": "Os seguintes aplicativos serão atualizados: {apps}", + "app_upgrade_some_app_failed": "Alguns aplicativos não puderam ser atualizados", + "app_upgrade_specific_channel_msg": "Observe que você está usando '{channel}' como fonte para atualizações. Certifique-se de verificar a discussão em andamento [aqui]({pr_url}).", + "app_upgrade_up_to_date": "Às vezes, a atualização forçada do aplicativo (para a mesma versão) pode ser útil para recompilar o aplicativo e as configurações.", + "app_upgrade_upgradable": "O aplicativo pode ser atualizado da versão {current_version} para {new_version}", + "app_upgrade_url_required": "Este aplicativo não existe (mais?) no catálogo, portanto, você deve cuidar das atualizações manualmente.
Na linha de comando, você pode usar `yunohost app upgrade ` e fornecer a URL do repositório usando a opção `-u`.", + "app_upgraded": "{app} atualizado", + "app_yunohost_version_not_supported": "Este aplicativo requer YunoHost >= {required}, mas a versão instalada atual é {current}.", + "apps_already_up_to_date": "Todos os aplicativos já estão atualizados", + "apps_catalog_failed_to_download": "Não foi possível baixar o catálogo de aplicativos {apps_catalog}: {error}", + "apps_catalog_obsolete_cache": "O cache do catálogo de aplicativos está vazio ou obsoleto.", + "apps_catalog_update_success": "O catálogo de aplicativos foi atualizado!", + "apps_catalog_updating": "Atualizando o catálogo de aplicativos…", + "apps_confirm_partial_upgrade": "Alguns aplicativos para os quais uma atualização foi solicitada não podem ser atualizados. Prosseguir com os outros de qualquer maneira?", + "apps_no_target_can_be_upgraded": "Nenhum aplicativo pode ser atualizado", + "apps_upgrade_cancelled": "As atualizações ainda estavam pendentes para vários outros aplicativos, mas sua atualização foi cancelada (use '--continue-on-failure' para continuar de qualquer maneira): {apps}", + "ask_admin_fullname": "Nome completo do administrador", + "ask_admin_username": "Nome de usuário do administrador", + "ask_dyndns_recovery_password": "Senha de recuperação de DynDNS", + "ask_dyndns_recovery_password_explain": "Escolha uma senha de recuperação para o seu domínio DynDNS, caso precise redefini-la mais tarde.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Digite a senha de recuperação para este domínio DynDNS.", + "ask_dyndns_recovery_password_explain_unavailable": "Este domínio DynDNS já está registrado. Se você for a pessoa que registrou originalmente este domínio, poderá inserir a senha de recuperação para recuperar esse domínio.", + "ask_fullname": "Nome completo", + "ask_main_domain": "Domínio principal", + "ask_new_admin_password": "Nova senha de administração", + "ask_new_domain": "Novo domínio", + "ask_new_path": "Novo caminho", + "ask_password": "Senha", + "ask_user_domain": "Domínio a ser usado para o endereço de e-mail do usuário", + "automatic_task": "Tarefa automática", + "backup_abstract_method": "Este método de backup ainda não foi implementado", + "backup_actually_backuping": "Criando um arquivo de backup a partir dos arquivos coletados…", + "backup_app_script_failed": "Falha ao coletar arquivos para backup de {app}.", + "backup_applying_method_copy": "Copiando todos os arquivos para backup…", + "backup_applying_method_custom": "Chamar o método de backup personalizado '{method}'…", + "backup_applying_method_tar": "Criando o arquivo TAR de backup…", + "backup_archive_app_not_found": "Não foi possível encontrar o {app} no arquivo de backup", + "backup_archive_broken_link": "Não foi possível acessar o arquivo de backup (link quebrado para {path})", + "backup_archive_cant_retrieve_info_json": "Não foi possível carregar informações para o arquivo '{archive}' ... O arquivo info.json não pode ser recuperado (ou não é um JSON válido).", + "backup_archive_corrupted": "Parece que o arquivo de backup '{archive}' está corrompido: {error}", + "backup_archive_name_exists": "Já existe um arquivo de backup com o nome '{name}'.", + "backup_archive_name_unknown": "Arquivo de backup local desconhecido chamado '{name}'", + "backup_archive_open_failed": "Não foi possível abrir o arquivo de backup", + "backup_archive_system_part_not_available": "Parte do sistema '{part}' indisponível neste backup", + "backup_archive_writing_error": "Não foi possível adicionar os arquivos '{source}' (nomeados no arquivo '{dest}') para backup no arquivo compactado '{archive}'", + "backup_ask_for_copying_if_needed": "Deseja realizar o backup usando {size}MB temporariamente? (Essa forma é usada, pois alguns arquivos não puderam ser preparados usando um método mais eficiente.)", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "O backup {name} foi excluído porque foi substituído por um backup mais recente {newname}", + "backup_cant_mount_uncompress_archive": "Não foi possível montar o arquivo sem compactação como protegido contra gravação", + "backup_cleaning_failed": "Não foi possível limpar a pasta de backup temporária", + "backup_copying_to_organize_the_archive": "Copiando {size}MB para organizar o arquivo", + "backup_couldnt_bind": "Não foi possível vincular {src} a {dest}.", + "backup_create_size_estimation": "O arquivo conterá aproximadamente {size} de dados.", + "backup_created": "Backup criado: {name}", + "backup_creation_failed": "Não foi possível criar o arquivo de backup", + "backup_csv_addition_failed": "Não foi possível adicionar arquivos para backup no arquivo CSV", + "backup_csv_creation_failed": "Não foi possível criar o arquivo CSV necessário para a restauração", + "backup_custom_backup_error": "O método de backup personalizado não pôde passar da etapa de 'backup'", + "backup_custom_mount_error": "O método de backup personalizado não pôde passar da etapa de 'montagem'", + "backup_delete_error": "Não foi possível excluir '{path}'", + "backup_deleted": "Backup excluído: {name}", + "backup_hook_unknown": "O hook de backup '{hook}' é desconhecido", + "backup_method_copy_finished": "Cópia de backup finalizada", + "backup_method_custom_finished": "Método de backup personalizado '{method}' concluído", + "backup_method_tar_finished": "Arquivo de backup TAR criado", + "backup_mount_archive_for_restore": "Preparando arquivo para restauração…", + "backup_no_file_collected": "Falha ao coletar arquivos para backup", + "backup_no_uncompress_archive_dir": "Não existe esse diretório de arquivo sem compactação", + "backup_output_directory_forbidden": "Escolha um diretório de saída diferente. Os backups não podem ser criados nas subpastas /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", + "backup_output_directory_not_empty": "Você deve escolher um diretório de saída vazio", + "backup_output_directory_required": "Você deve fornecer um diretório de saída para o backup", + "backup_output_symlink_dir_broken": "Seu diretório de arquivo '{path}' é um link simbólico quebrado. Talvez você tenha esquecido de remontar ou conectar o meio de armazenamento para o qual ele aponta.", + "backup_running_hooks": "Executando hooks de backup…", + "backup_system_part_failed": "Não foi possível fazer backup da parte do sistema '{part}'", + "backup_unable_to_organize_files": "Não foi possível usar o método rápido para organizar arquivos no arquivo", + "backup_with_no_backup_script_for_app": "O aplicativo '{app}' não tem script de backup. Ignorando.", + "backup_with_no_restore_script_for_app": "O aplicativo {app} não possui um script de restauração; portanto, você não poderá restaurar automaticamente o backup deste aplicativo.", + "cannot_open_file": "Não foi possível abrir o arquivo {file} (motivo: {error})", + "cannot_write_file": "Não foi possível gravar o arquivo {file} (motivo: {error})", + "certmanager_acme_not_configured_for_domain": "O desafio ACME não pode ser executado para {domain} neste momento porque sua configuração nginx não possui o trecho de código correspondente… Certifique-se de que sua configuração nginx esteja atualizada usando `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 pela 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 está prestes a expirar! (Você pode usar --force se souber o que está fazendo)", + "certmanager_attempt_to_replace_valid_cert": "Você está tentando substituir um certificado bom e válido para o domínio {domain}! (Use --force para ignorar)", + "certmanager_cannot_read_cert": "Ocorreu um erro ao tentar abrir o certificado atual para o domínio {domain} (arquivo: {file}), motivo: {reason}", + "certmanager_cert_install_failed": "Falha na instalação do certificado Let's Encrypt para {domains}", + "certmanager_cert_install_failed_selfsigned": "Falha na instalação do certificado autoassinado para {domains}", + "certmanager_cert_install_success": "Certificado Let's Encrypt agora está instalado para o domínio '{domain}'", + "certmanager_cert_install_success_selfsigned": "Certificado autoassinado agora está instalado para o domínio '{domain}'", + "certmanager_cert_renew_failed": "Falha na renovação do certificado Let's Encrypt para {domains}", + "certmanager_cert_renew_success": "Vamos criptografar o certificado renovado para o domínio '{domain}'", + "certmanager_cert_signing_failed": "Não foi possível assinar o novo certificado", + "certmanager_certificate_fetching_or_enabling_failed": "Tentar usar o novo certificado para {domain} não funcionou …", + "certmanager_domain_cert_not_selfsigned": "O certificado para o domínio {domain} não é autoassinado. Tem certeza de que deseja substituí-lo? (Use '--force' para fazer isso.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Os registros DNS para o domínio '{domain}' são diferentes do IP deste servidor. Verifique a categoria 'Registros DNS' (básicos) no diagnóstico para obter mais informações. Se você modificou recentemente seu registro A, aguarde a propagação (existem verificadores de propagação de DNS online). (Se você souber o que está fazendo, use '--no-checks' para desativar essas verificações.)", + "certmanager_domain_http_not_working": "O domínio {domain} parece não estar acessível via HTTP. Verifique a categoria 'Web' no diagnóstico para obter mais informações. (Se você souber o que está fazendo, use '--no-checks' para desativar essas verificações.)", + "certmanager_domain_not_diagnosed_yet": "Ainda não há resultados de diagnóstico para o domínio {domain}. Execute novamente um diagnóstico para as categorias 'Registros DNS' e 'Web' na seção de diagnóstico para verificar se o domínio está pronto para o Let's Encrypt. (Ou, se você souber o que está fazendo, use '--no-checks' para desativar essas verificações.)", + "certmanager_hit_rate_limit": "Muitos certificados já foram emitidos para esse conjunto exato 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 do 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 autoassinatura (arquivo: {file})", + "certmanager_unable_to_parse_self_CA_name": "Não foi possível analisar o nome da autoridade de autoassinatura (arquivo: {file})", + "config_action_disabled": "Não foi possível executar a ação '{action}' porque ela está desativada. Certifique-se de atender às restrições. Ajuda: {help}", + "config_action_failed": "Falha ao executar a ação '{action}': {error}", + "config_apply_failed": "Falha na aplicação da nova configuração: {error}", + "config_cant_set_value_on_section": "Você não pode definir um único valor em uma seção de configuração inteira.", + "config_forbidden_keyword": "A palavra-chave '{keyword}' é reservada, você não pode criar ou usar um painel de configuração com uma pergunta com este ID.", + "config_forbidden_readonly_type": "O tipo '{type}' não pode ser definido como somente leitura, use outro tipo para renderizar esse valor (ID de argumento relevante: '{id}').", + "config_no_panel": "Nenhum painel de configuração encontrado.", + "config_unknown_filter_key": "A chave de filtro '{filter_key}' está incorreta.", + "confirm_app_install_danger": "ATENÇÃO! Este aplicativo ainda está em fase experimental (ou até mesmo não funciona de fato)! É altamente recomendável que você NÃO o instale, a menos que saiba o que está fazendo. NÃO haverá suporte caso este aplicativo não funcione ou danifique seu sistema... Se, mesmo assim, você estiver disposto a correr esse risco, digite '{answers}'", + "confirm_app_install_thirdparty": "ATENÇÃO! Este aplicativo não faz parte do catálogo de aplicativos do YunoHost. A instalação de aplicativos de terceiros pode comprometer a integridade e a segurança do seu sistema. É altamente recomendável que você NÃO o instale, a menos que saiba o que está fazendo. NÃO haverá suporte caso este aplicativo apresente problemas ou danifique seu sistema. Se, mesmo assim, você estiver disposto a correr esse risco, digite '{answers}'", + "confirm_app_install_warning": "Aviso: Este aplicativo pode funcionar, mas não está bem integrado ao YunoHost. Alguns recursos, como login único e backup/restauração, podem não estar disponíveis. Instalar mesmo assim? [{answers}] ", + "confirm_app_insufficient_ram": "Este aplicativo requer mais RAM para ser instalado do que a disponível atualmente. Mesmo que o aplicativo pudesse ser executado, seu processo de instalação/atualização exige uma grande quantidade de RAM, o que pode causar o travamento e a falha catastrófica do seu servidor. Se você ainda assim estiver disposto a correr esse risco, digite '{answers}'", + "confirm_notifications_read": "AVISO: Você deve verificar as notificações do aplicativo acima antes de continuar, pode haver coisas importantes a saber. [{answers}]", + "confirm_tos_acknowledgement": "Eu li e compreendi os Termos de Serviço [{answers}]", + "corrupted_json": "JSON corrompido lido de {ressource}: {error}", + "corrupted_toml": "TOML corrompido lido de {ressource}: {error}", + "corrupted_yaml": "YAML corrompido lido de {ressource}: {error}", + "danger": "Atenção:", + "diagnosis_apps_allgood": "Todos os aplicativos instalados respeitam as práticas básicas de empacotamento", + "diagnosis_apps_bad_quality": "Este aplicativo está atualmente sinalizado como com problemas no catálogo de aplicativos da YunoHost. Isso pode ser um problema temporário enquanto os responsáveis pela manutenção tentam corrigi-lo. Enquanto isso, a atualização deste aplicativo está desativada.", + "diagnosis_apps_broken": "Este aplicativo está atualmente sinalizado como com problemas no catálogo de aplicativos da YunoHost. Isso pode ser um problema temporário enquanto os responsáveis pela manutenção tentam corrigi-lo. Enquanto isso, a atualização deste aplicativo está desativada.", + "diagnosis_apps_deprecated_practices": "A versão instalada deste aplicativo ainda usa algumas práticas de empacotamento muito antigas e obsoletas. Você realmente deve considerar atualizá-lo.", + "diagnosis_apps_issue": "Foi encontrado um problema para o aplicativo {app}", + "diagnosis_apps_not_in_app_catalog": "Este aplicativo não está no catálogo de aplicativos da YunoHost. Se ele já esteve disponível e foi removido, considere desinstalá-lo, pois não receberá atualizações e poderá comprometer a integridade e a segurança do seu sistema.", + "diagnosis_apps_outdated_packaging_format": "Este aplicativo usa um formato de empacotamento obsoleto e em breve não será suportado pelo YunoHost. Você realmente deve considerar atualizá-lo.", + "diagnosis_apps_outdated_ynh_requirement": "A versão instalada deste aplicativo requer apenas o yunohost >= 2.x, 3.x ou 4.x, o que geralmente indica que ele não está atualizado com as práticas de empacotamento e auxiliares recomendadas. Você deveria considerar seriamente atualizá-lo.", + "diagnosis_apps_security_issue_error": "O aplicativo {app} está atualmente na versão '{current_version}', que é vulnerável a uma falha de segurança GRAVE: {title}. Recomenda-se atualizá-lo O MAIS RÁPIDO POSSÍVEL para a versão '{fixed_in_version}'. Mais informações: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "O aplicativo {app} está atualmente na versão '{current_version}', que é vulnerável a um problema de segurança moderado: {title}. Recomenda-se atualizá-lo para a versão '{fixed_in_version}'. Mais informações: {more_infos_list}", + "diagnosis_backports_in_sources_list": "Parece que o apt (gerenciador de pacotes) está configurado para usar o repositório backports. A menos que você realmente saiba o que está fazendo, desaconselhamos fortemente a instalação de pacotes do backports, pois isso provavelmente causará instabilidades ou conflitos no seu sistema.", + "diagnosis_basesystem_hardware": "A arquitetura de hardware do servidor é {virt} {arch}", + "diagnosis_basesystem_hardware_model": "O modelo do servidor é {model}", + "diagnosis_basesystem_host": "O servidor está executando o Debian {debian_version}", + "diagnosis_basesystem_kernel": "O servidor está executando o kernel Linux {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Você está executando versões inconsistentes dos pacotes YunoHost... provavelmente por causa de uma atualização parcial ou com falha.", + "diagnosis_basesystem_ynh_main_version": "O servidor está executando o YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "{package} versão: {version} ({repo})", + "diagnosis_cache_still_valid": "(Cache ainda válido para o diagnóstico {category}. Não vou diagnosticá-lo novamente ainda!)", + "diagnosis_cant_run_because_of_dep": "Não foi possível executar o diagnóstico para a categoria {category} enquanto houver problemas importantes relacionados a {dep}.", + "diagnosis_description_apps": "Aplicativos", + "diagnosis_description_basesystem": "Sistema base", + "diagnosis_description_dnsrecords": "Registros DNS", + "diagnosis_description_ip": "Conectividade com a Internet", + "diagnosis_description_mail": "E-mail", + "diagnosis_description_ports": "Exposição de portas", + "diagnosis_description_regenconf": "Configurações do sistema", + "diagnosis_description_services": "Verificação do status dos serviços", + "diagnosis_description_systemresources": "Recursos do sistema", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "O armazenamento {mountpoint} (no dispositivo {device}) tem apenas {free} ({free_percent}%) de espaço livre (de {total}). Tenha cuidado.", + "diagnosis_diskusage_ok": "O armazenamento {mountpoint} (no dispositivo {device}) ainda tem {free} ({free_percent}%) de espaço livre (de {total})!", + "diagnosis_diskusage_verylow": "O armazenamento {mountpoint} (no dispositivo {device}) tem apenas {free} ({free_percent}%) de espaço livre (de {total}). Você realmente deve considerar limpar algum espaço!", + "diagnosis_display_tip": "Para ver os problemas encontrados, você pode ir para a seção Diagnóstico do webadmin ou executar 'yunohost diagnosis show --issues --human-readable' na linha de comando.", + "diagnosis_dns_bad_conf": "Alguns registros DNS estão ausentes ou incorretos para o domínio {domain} (categoria {category})", + "diagnosis_dns_discrepancy": "O seguinte registro DNS parece não seguir a configuração recomendada:
Tipo: {type}
Nome: {name}
Valor atual: {current}
Valor esperado: {content}", + "diagnosis_dns_good_conf": "Os registros DNS estão configurados corretamente para o domínio {domain} (categoria {category})", + "diagnosis_dns_missing_record": "De acordo com a configuração de DNS recomendada, você deve adicionar um registro DNS com as seguintes informações.
Tipo: {type}
Nome: {name}
Valor: {content}", + "diagnosis_dns_point_to_doc": "Verifique a documentação em https://doc.yunohost.org/dns_config se precisar de ajuda para configurar os registros DNS.", + "diagnosis_dns_specialusedomain": "O domínio {domain} é baseado em um domínio de nível superior (TLD) de uso especial, como .local ou .test e, portanto, não se espera que tenha registros DNS reais.", + "diagnosis_dns_try_dyndns_update_force": "A configuração de DNS deste domínio deve ser gerenciada automaticamente pelo YunoHost. Se não for esse o caso, você pode tentar forçar uma atualização usando yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Alguns domínios expirarão MUITO EM BREVE!", + "diagnosis_domain_expiration_not_found": "Não foi possível verificar a data de expiração de alguns domínios", + "diagnosis_domain_expiration_not_found_details": "As informações WHOIS para o domínio {domain} não parecem conter as informações sobre a data de expiração?", + "diagnosis_domain_expiration_success": "Seus domínios estão registrados e não vão expirar tão cedo.", + "diagnosis_domain_expiration_warning": "Alguns domínios expirarão em breve!", + "diagnosis_domain_expires_in": "{domain} expira em {days} dias.", + "diagnosis_domain_not_found_details": "O domínio {domain} não existe no banco de dados WHOIS ou expirou!", + "diagnosis_everything_ok": "Tudo parece OK para {category}!", + "diagnosis_failed": "Falha ao buscar o resultado do diagnóstico para a categoria '{category}': {error}", + "diagnosis_failed_for_category": "Falha no diagnóstico para a categoria '{category}': {error}", + "diagnosis_found_errors": "Encontrado {errors} problema(s) significativo(s) relacionado(s) a {category}!", + "diagnosis_found_errors_and_warnings": "Encontrado {errors} problema(s) significativo(s) (e {warnings} aviso(s)) relacionado a {category}!", + "diagnosis_found_warnings": "Encontrado {warnings} item(ns) que poderia(m) ser melhorado(s) para {category}.", + "diagnosis_high_number_auth_failures": "Houve um número suspeitamente alto de falhas de autenticação recentemente. Talvez seja interessante verificar se o fail2ban está em execução e configurado corretamente, ou usar uma porta personalizada para SSH, conforme explicado em https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Parece que outra máquina (talvez seu roteador de internet) respondeu em vez do seu servidor.
1. A causa mais comum para esse problema é que as portas 80 (e 443) não estão corretamente encaminhadas para o seu servidor.
2. Em configurações mais complexas: certifique-se de que nenhum firewall ou proxy reverso esteja interferindo.", + "diagnosis_http_connection_error": "Erro de conexão: não foi possível se conectar ao domínio solicitado; é muito provável que esteja inacessível.", + "diagnosis_http_could_not_diagnose": "Não foi possível diagnosticar se os domínios podem ser acessados externamente no endereço IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Erro: {error}", + "diagnosis_http_hairpinning_issue": "Sua rede local não parece ter o NAT hairpinning ativado.", + "diagnosis_http_hairpinning_issue_details": "Isso provavelmente ocorre devido ao seu roteador/dispositivo de internet. Como resultado, pessoas de fora da sua rede local poderão acessar seu servidor normalmente, mas pessoas de dentro da rede local (como você, provavelmente?) não poderão acessar o servidor usando o nome de domínio ou o IP global. Você pode tentar melhorar a situação consultando https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "A configuração nginx desse domínio parece ter sido modificada manualmente e impede que o YunoHost diagnostique se ele pode ser acessado em HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Para corrigir a situação, inspecione a diferença na linha de comando usando yunohost tools regen-conf nginx --dry-run --with-diff e, se estiver tudo certo, aplique as alterações com yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "O domínio {domain} pode ser acessado por meio de HTTP de fora da rede local.", + "diagnosis_http_partially_unreachable": "O domínio {domain} parece inacessível por meio de HTTP de fora da rede local em IPv{failed}, embora funcione em IPv{passed}.", + "diagnosis_http_special_use_tld": "O domínio {domain} é baseado em um domínio de nível superior (TLD) de uso especial, como .local ou .test e, portanto, não se espera que seja exposto fora da rede local.", + "diagnosis_http_timeout": "Tempo limite expirado ao tentar contatar seu servidor externamente. Parece estar inacessível.
1. A causa mais comum para esse problema é que as portas 80 (e 443) não estão corretamente encaminhadas para o seu servidor.
2. Você também deve verificar se o serviço nginx está em execução.
3. Em configurações mais complexas: verifique se nenhum firewall ou proxy reverso está interferindo.", + "diagnosis_http_unreachable": "O domínio {domain} parece inacessível por meio de HTTP de fora da rede local.", + "diagnosis_ignore_already_filtered": "(Já existe um filtro de diagnóstico {category} com esses critérios)", + "diagnosis_ignore_criteria_error": "Os critérios devem estar no formato chave=valor (por exemplo, domain=yolo.test)", + "diagnosis_ignore_filter_added": "Adicionado um filtro de diagnóstico {category}", + "diagnosis_ignore_filter_removed": "Removido um filtro de diagnóstico {category}", + "diagnosis_ignore_missing_criteria": "Você deve fornecer pelo menos um critério sendo a categoria de diagnóstico a ser ignorada", + "diagnosis_ignore_no_filter_found": "(Não existe esse filtro de diagnóstico {category} com esses critérios para remover)", + "diagnosis_ignore_no_issue_found": "Nenhum problema foi encontrado que correspondesse aos critérios fornecidos.", + "diagnosis_ignored_issues": "(+ {nb_ignored} problema(s) ignorado(s))", + "diagnosis_ip_broken_dnsresolution": "A resolução de nomes de domínio parece estar quebrada por algum motivo... Um firewall está bloqueando solicitações de DNS?", + "diagnosis_ip_broken_resolvconf": "A resolução de nomes de domínio parece estar quebrada em seu servidor, o que parece relacionado ao /etc/resolv.conf não apontar para 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "O servidor está conectado à Internet através do IPv4!", + "diagnosis_ip_connected_ipv6": "O servidor está conectado à Internet através do IPv6!", + "diagnosis_ip_dnsresolution_working": "A resolução de nomes de domínio está funcionando!", + "diagnosis_ip_global": "IP Global: {global}", + "diagnosis_ip_local": "IP Local: {local}", + "diagnosis_ip_no_ipv4": "O servidor não tem IPv4 funcionando.", + "diagnosis_ip_no_ipv6": "O servidor não tem IPv6 funcionando.", + "diagnosis_ip_no_ipv6_tip": "Ter um IPv6 funcionando não é obrigatório para o funcionamento do seu servidor, mas é melhor para a saúde da internet como um todo. O IPv6 geralmente deve ser configurado automaticamente pelo sistema ou pelo seu provedor, se estiver disponível. Caso contrário, você pode precisar configurar alguns itens manualmente, conforme explicado na documentação aqui: https://doc.yunohost.org/ipv6. Se você não puder habilitar o IPv6 ou se isso parecer muito técnico para você, pode ignorar este aviso com segurança.", + "diagnosis_ip_no_ipv6_tip_important": "O IPv6 geralmente deve ser configurado automaticamente pelo sistema ou pelo seu provedor, se estiver disponível. Caso contrário, você pode precisar configurar alguns itens manualmente, conforme explicado na documentação aqui: https://doc.yunohost.org/ipv6.", + "diagnosis_ip_not_connected_at_all": "O servidor parece não estar conectado à Internet!?", + "diagnosis_ip_weird_resolvconf": "A resolução DNS parece estar funcionando, mas parece que você está usando um /etc/resolv.conf personalizado.", + "diagnosis_ip_weird_resolvconf_details": "O arquivo /etc/resolv.conf deve ser um link simbólico para /etc/resolvconf/run/resolv.conf apontando para 127.0.0.1 (dnsmasq). Se você deseja configurar manualmente os resolvedores DNS, edite /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blocklist_listed_by": "Seu IP ou domínio {item} está na lista de bloqueios em {blocklist_name}", + "diagnosis_mail_blocklist_ok": "Os IPs e domínios usados por este servidor não parecem estar na lista de bloqueios", + "diagnosis_mail_blocklist_reason": "O motivo da lista de bloqueio é: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Parece que o motivo menciona 'open resolver'.
Isso geralmente significa que seu servidor não está usando seu DNS local, mas sim um DNS público e aberto.
Verifique o conteúdo de /etc/resolv.conf. Ele deve conter nameserver 127.0.0.1.
Como esse arquivo geralmente é gerado automaticamente, não o edite manualmente. Verifique suas configurações de DHCP ou de VPN, se estiver usando uma, ou, se você usou uma imagem Debian criada por um provedor de VPS, por exemplo, procure por uma configuração cloudinit.
Você pode entrar em contato com o suporte da YunoHost para obter ajuda com esse problema.
O motivo exato da lista negra é: {reason}", + "diagnosis_mail_blocklist_website": "Depois de identificar por que você está listado e corrigi-lo, sinta-se à vontade para pedir que seu IP ou domínio seja removido em {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Um serviço não-SMTP respondeu na porta 25 no endereço IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Pode ser devido a uma outra máquina respondendo em vez do seu servidor.", + "diagnosis_mail_ehlo_could_not_diagnose": "Não foi possível diagnosticar se o servidor de e-mail postfix pode ser acessado externamente no endereço IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Erro: {error}", + "diagnosis_mail_ehlo_ok": "O servidor de e-mail SMTP pode ser acessado externamente e, portanto, pode receber e-mails!", + "diagnosis_mail_ehlo_unreachable": "O servidor de e-mail SMTP não pode ser acessado externamente no endereço IPv{ipversion}. Ele não poderá receber e-mails.", + "diagnosis_mail_ehlo_unreachable_details": "Não foi possível abrir uma conexão na porta 25 com o seu servidor no endereço IPv{ipversion}. Parece estar inacessível.
1. A causa mais comum para esse problema é que a porta 25 não está corretamente encaminhada para o seu servidor.
2. Você também deve verificar se o serviço Postfix está em execução.
3. Em configurações mais complexas: verifique se nenhum firewall ou proxy reverso está interferindo.", + "diagnosis_mail_ehlo_wrong": "Um servidor de e-mail SMTP diferente respondeu no endereço IPv{ipversion}. Seu servidor provavelmente não poderá receber e-mails.", + "diagnosis_mail_ehlo_wrong_details": "O EHLO recebido pelo diagnosticador remoto no endereço IPv{ipversion} é diferente do domínio do seu servidor.
EHLO recebido: {wrong_ehlo}
Esperado: {right_ehlo}
A causa mais comum para esse problema é que a porta 25 não está corretamente encaminhada para o seu servidor. Como alternativa, verifique se nenhum firewall ou proxy reverso está interferindo.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "O DNS reverso não está configurado corretamente para o endereço IPv{ipversion}. Alguns e-mails podem não ser entregues ou ser sinalizados como SPAM.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS reverso atual: {rdns_domain}
Valor esperado: {ehlo_domain}", + "diagnosis_mail_fcrdns_dns_missing": "Nenhum DNS reverso é definido para o endereço IPv{ipversion}. Alguns e-mails podem não ser entregues ou ser sinalizados como SPAM.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Alguns provedores não permitem configurar o DNS reverso (ou o recurso pode estar com defeito...). Se você estiver enfrentando problemas por causa disso, considere as seguintes soluções:
- Alguns provedores de internet oferecem a alternativa de usar um servidor de retransmissão de e-mail, embora isso implique que o servidor de retransmissão poderá monitorar seu tráfego de e-mail.
- Uma alternativa que respeita a privacidade é usar uma VPN *com um IP público dedicado* para contornar esse tipo de limitação. Veja https://doc.yunohost.org/vpn_advantage
- Ou é possível mudar para um provedor diferente", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Alguns provedores não permitem configurar o DNS reverso (ou o recurso pode estar com defeito...). Se o seu DNS reverso estiver configurado corretamente para IPv4, você pode tentar desativar o uso de IPv6 ao enviar e-mails executando o comando yunohost settings set email.smtp.smtp_allow_ipv6 -v off. Observação: essa última solução significa que você não poderá enviar ou receber e-mails dos poucos servidores que suportam apenas IPv6.", + "diagnosis_mail_fcrdns_nok_details": "Primeiramente, você deve tentar configurar o DNS reverso com {ehlo_domain} na interface do seu roteador de internet ou na interface do seu provedor de hospedagem. (Alguns provedores de hospedagem podem exigir que você abra um chamado de suporte para isso).", + "diagnosis_mail_fcrdns_ok": "Seu DNS reverso está configurado corretamente!", + "diagnosis_mail_outgoing_port_25_blocked": "O servidor de e-mail SMTP não pode enviar e-mails para outros servidores porque a porta de saída 25 está bloqueada para o endereço IPv{ipversion}.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Você deve primeiro tentar desbloquear a porta de saída 25 na interface do roteador de Internet ou na interface do provedor de hospedagem. (Alguns provedores de hospedagem podem exigir que você envie um tíquete de suporte para isso).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alguns provedores não permitem desbloquear a porta 25 de saída porque não se importam com a Neutralidade da Rede.
- Alguns deles oferecem a alternativa de usar um servidor de retransmissão de e-mail, embora isso implique que o servidor de retransmissão poderá espionar seu tráfego de e-mail.
- Uma alternativa que respeita a privacidade é usar uma VPN *com um IP público dedicado* para contornar esses tipos de restrições. Veja https://doc.yunohost.org/vpn_advantage
- Você também pode considerar mudar para um provedor mais alinhado com a Neutralidade da Rede", + "diagnosis_mail_outgoing_port_25_ok": "O servidor de e-mail SMTP é capaz de enviar e-mails (a porta de saída 25 não está bloqueada).", + "diagnosis_mail_queue_ok": "{nb_pending} e-mail(s) pendente(s) nas filas de e-mail", + "diagnosis_mail_queue_too_big": "Muitos e-mails pendentes na fila de e-mails ({nb_pending} e-mails)", + "diagnosis_mail_queue_unavailable": "Não foi possível consultar o número de e-mails pendentes na fila", + "diagnosis_mail_queue_unavailable_details": "Erro: {error}", + "diagnosis_never_ran_yet": "Parece que este servidor foi configurado recentemente e ainda não há nenhum relatório de diagnóstico para exibir. Você deve começar executando um diagnóstico completo, seja pelo painel de administração web ou usando o comando 'yunohost diagnosis run' na linha de comando.", + "diagnosis_no_cache": "Ainda não há cache de diagnóstico para a categoria '{category}'", + "diagnosis_package_installed_from_sury": "Alguns pacotes do sistema devem ser revertidos para versões anteriores", + "diagnosis_package_installed_from_sury_details": "Alguns pacotes foram instalados inadvertidamente a partir de um repositório de terceiros chamado Sury. A equipe da YunoHost aprimorou a estratégia de gerenciamento desses pacotes, mas é esperado que algumas configurações que instalaram aplicativos PHP 7.3 enquanto ainda estavam no Debian Stretch apresentem algumas inconsistências. Para corrigir essa situação, tente executar o seguinte comando: {cmd_to_fix}", + "diagnosis_package_security_issue_error": "O pacote de sistema '{package}' está atualmente na versão '{current_version}', que é vulnerável a uma falha de segurança GRAVE: {title}. Recomenda-se atualizar O MAIS RÁPIDO POSSÍVEL para a versão '{fixed_in_version}'. Mais informações: {more_infos_list}", + "diagnosis_package_security_issue_warning": "O pacote de sistema '{package}' está atualmente na versão '{current_version}', que é vulnerável a um problema de segurança moderado: {title}. Recomenda-se atualizá-lo para a versão '{fixed_in_version}'. Mais informações: {more_infos_list}", + "diagnosis_ports_could_not_diagnose": "Não foi possível diagnosticar se as portas podem ser acessadas externamente no endereço IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Erro: {error}", + "diagnosis_ports_forwarding_tip": "Para resolver esse problema, você provavelmente precisará configurar o encaminhamento de portas no seu roteador de internet, conforme descrito em https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "A exposição desta porta é necessária para recursos da categoria {category} (serviço {service})", + "diagnosis_ports_ok": "A porta {port} pode ser acessada externamente.", + "diagnosis_ports_partially_unreachable": "A porta {port} não pode ser acessada externamente no endereço IPv{failed}.", + "diagnosis_ports_unreachable": "A porta {port} não pode ser acessada externamente.", + "diagnosis_processes_killed_by_oom_reaper": "Alguns processos foram recentemente encerrados pelo sistema por falta de memória. Isso geralmente indica falta de memória no sistema ou um processo consumindo muita memória. Resumo dos processos encerrados:\n{kills_summary}", + "diagnosis_ram_low": "O sistema tem {available} ({available_percent}%) de RAM disponível (de {total}). Tenha cuidado.", + "diagnosis_ram_ok": "O sistema ainda tem {available} ({available_percent}%) de RAM disponível de {total}.", + "diagnosis_ram_verylow": "O sistema tem apenas {available} ({available_percent}%) de RAM disponível! (de {total})", + "diagnosis_regenconf_allgood": "Todos os arquivos de configuração estão de acordo com a configuração recomendada!", + "diagnosis_regenconf_manually_modified": "O arquivo de configuração {file} parece ter sido modificado manualmente.", + "diagnosis_regenconf_manually_modified_details": "Isso provavelmente não será um problema se você souber o que está fazendo! O YunoHost deixará de atualizar este arquivo automaticamente… Mas tenha cuidado, pois as atualizações do YunoHost podem conter alterações importantes recomendadas. Se desejar, você pode inspecionar as diferenças com o comando yunohost tools regen-conf {category} --dry-run --with-diff e forçar a redefinição para a configuração recomendada com o comando yunohost tools regen-conf {category} --force", + "diagnosis_rfkill_wifi": "A placa Wi-Fi está desativada e um aviso do sistema pode impedir a instalação de aplicativos", + "diagnosis_rfkill_wifi_details": "Este aviso aparece em muitas saídas de comandos, causando problemas em alguns aplicativos. Geralmente, é necessário especificar o código do seu país com o comando sudo raspi-config. Aqui está o erro:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "O sistema de arquivos raiz tem apenas um total de {space}, o que é bastante preocupante! Você provavelmente ficará sem espaço em disco muito rapidamente! Recomenda-se ter pelo menos 16 GB para o sistema de arquivos raiz.", + "diagnosis_rootfstotalspace_warning": "O sistema de arquivos raiz possui apenas {space} no total. Isso pode ser suficiente, mas tenha cuidado, pois você pode ficar sem espaço em disco rapidamente... Recomenda-se ter pelo menos 16 GB para o sistema de arquivos raiz.", + "diagnosis_security_vulnerable_to_meltdown": "Você parece vulnerável à vulnerabilidade de segurança crítica do Meltdown", + "diagnosis_security_vulnerable_to_meltdown_details": "Para corrigir isso, você deve atualizar seu sistema e reiniciar para carregar o novo kernel do Linux (ou entrar em contato com o provedor do servidor se isso não funcionar). Consulte https://meltdownattack.com/ para obter mais informações.", + "diagnosis_services_bad_status": "Serviço {service} está {status} :(", + "diagnosis_services_bad_status_tip": "Você pode tentar reiniciar o serviço e, se não funcionar, verifique os logs do serviço no webadmin (na linha de comando, você pode fazer isso com yunohost service restart {service} e yunohost service log {service}).", + "diagnosis_services_conf_broken": "A configuração está corrompida para o serviço {service}!", + "diagnosis_services_running": "O serviço {service} está em execução!", + "diagnosis_sshd_config_inconsistent": "Parece que a porta SSH foi modificada manualmente em /etc/ssh/sshd_config. A partir do YunoHost 4.2, uma nova configuração global, 'security.ssh.ssh_port', está disponível para evitar a edição manual da configuração.", + "diagnosis_sshd_config_inconsistent_details": "Execute o comando yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT para definir a porta SSH e marque as opções yunohost tools regen-conf ssh --dry-run --with-diff e yunohost tools regen-conf ssh --force para redefinir sua configuração de acordo com as recomendações do YunoHost.", + "diagnosis_sshd_config_insecure": "A configuração SSH parece ter sido modificada manualmente e é insegura porque não contém nenhuma diretiva 'AllowGroups' ou 'AllowUsers' para limitar o acesso a usuários autorizados.", + "diagnosis_swap_none": "O sistema não possui nenhuma área de swap. Você deve considerar adicionar pelo menos {recommended} de swap para evitar situações em que o sistema fique sem memória.", + "diagnosis_swap_notsomuch": "O sistema tem apenas {total} de swap. Você deve considerar ter pelo menos {recommended} para evitar situações em que o sistema fica sem memória.", + "diagnosis_swap_ok": "O sistema tem {total} de swap!", + "diagnosis_swap_tip": "Tenha cuidado e esteja ciente de que, se o servidor estiver hospedando swap em um cartão SD ou armazenamento SSD, isso pode reduzir drasticamente a expectativa de vida do dispositivo.", + "diagnosis_unknown_categories": "As seguintes categorias são desconhecidas: {categories}", + "diagnosis_using_stable_codename": "O apt (o gerenciador de pacotes do sistema) está atualmente configurado para instalar pacotes a partir do codinome 'stable', ao invés do codinome da versão atual do Debian (bookworm).", + "diagnosis_using_stable_codename_details": "Isso geralmente é causado por uma configuração incorreta do seu provedor de hospedagem. Isso é perigoso, pois assim que a próxima versão do Debian se tornar a nova versão 'stable', o apt tentará atualizar todos os pacotes do sistema sem passar por um procedimento de migração adequado. Recomenda-se corrigir isso editando o código-fonte do apt para o repositório base do Debian e substituindo a palavra-chave stable por bookworm. O arquivo de configuração correspondente deve ser /etc/apt/sources.list ou um arquivo em /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "O apt (o gerenciador de pacotes do sistema) está atualmente configurado para instalar qualquer atualização de 'testing' para o núcleo do YunoHost.", + "diagnosis_using_yunohost_testing_details": "Isso provavelmente não será um problema se você souber o que está fazendo, mas preste atenção às notas de lançamento antes de instalar as atualizações do YunoHost! Se você quiser desativar as atualizações de 'testing', remova a palavra-chave testing do arquivo /etc/apt/sources.list.d/yunohost.list.", + "disk_space_not_sufficient_install": "Não há espaço em disco suficiente para instalar este aplicativo", + "disk_space_not_sufficient_update": "Não há espaço em disco suficiente para atualizar este aplicativo", + "domain_cannot_remove_main": "Você não pode remover '{domain}', pois é o domínio principal. Primeiro, você precisa definir outro domínio como principal usando o comando 'yunohost domain main-domain -n '. Aqui está a lista de domínios candidatos: {other_domains}", + "domain_cannot_remove_main_add_new_one": "Você não pode remover '{domain}', pois é o domínio principal e o único domínio que você possui. Primeiro, você precisa adicionar outro domínio usando 'yunohost domain add ', depois defini-lo como domínio principal usando 'yunohost domain main-domain -n ' e, por fim, poderá remover o domínio '{domain}' usando 'yunohost domain remove {domain}'.", + "domain_cert_gen_failed": "Não foi possível gerar o certificado", + "domain_config_acme_eligible": "Elegibilidade ACME", + "domain_config_acme_eligible_explain": "Este domínio parece não estar pronto para um certificado Let's Encrypt. Verifique sua configuração de DNS e a acessibilidade do servidor HTTP. As seções 'Registros DNS' e 'Web' na página de diagnóstico podem ajudar você a entender o que está configurado incorretamente.", + "domain_config_api_protocol": "Protocolo da API", + "domain_config_auth_application_key": "Chave de aplicação", + "domain_config_auth_application_secret": "Chave secreta do aplicativo", + "domain_config_auth_consumer_key": "Chave do consumidor", + "domain_config_auth_entrypoint": "Ponto de entrada da API", + "domain_config_auth_key": "Chave de autenticação", + "domain_config_auth_secret": "Autenticação secreta", + "domain_config_auth_token": "Token de autenticação", + "domain_config_cert_install": "Instalar o certificado Let's Encrypt", + "domain_config_cert_issuer": "Autoridade de certificação", + "domain_config_cert_name": "Certificado", + "domain_config_cert_no_checks": "Ignorar verificações de diagnóstico", + "domain_config_cert_renew": "Renovar o certificado Let's Encrypt", + "domain_config_cert_renew_help": "O certificado será renovado automaticamente durante os últimos 15 dias de validade. Você pode renová-lo manualmente, se desejar. (Não recomendado).", + "domain_config_cert_summary": "Status do certificado", + "domain_config_cert_summary_abouttoexpire": "O certificado atual está prestes a expirar. Em breve, deve ser renovado automaticamente.", + "domain_config_cert_summary_expired": "CRÍTICO: O certificado atual não é válido! HTTPS não funcionará de jeito nenhum!", + "domain_config_cert_summary_letsencrypt": "Ótimo! Você está usando um certificado válido do Let's Encrypt!", + "domain_config_cert_summary_ok": "Ok, o certificado atual parece bom!", + "domain_config_cert_summary_selfsigned": "AVISO: O certificado atual é autoassinado. Os navegadores exibirão um aviso assustador para novos visitantes!", + "domain_config_cert_validity": "Validade", + "domain_config_custom_css": "Folha de estilo CSS personalizada", + "domain_config_custom_css_help": "Isso é para administradores avançados que desejam personalizar a aparência do portal", + "domain_config_default_app": "Aplicativo padrão", + "domain_config_default_app_help": "As pessoas serão redirecionadas automaticamente para este aplicativo ao abrir este domínio. Se nenhum aplicativo for especificado, as pessoas serão redirecionadas para o formulário de login do portal.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Mostrar a lista de aplicativos públicos para os visitantes", + "domain_config_enable_public_apps_page_help": "Os visitantes verão uma página de 'aplicativos públicos' ao chegar ao portal, em vez de apenas o formulário de login.", + "domain_config_feature_name": "Recursos", + "domain_config_mail_in": "Receber e-mails", + "domain_config_mail_out": "Enviar e-mails", + "domain_config_portal_logo": "Logotipo personalizado", + "domain_config_portal_logo_help": "Aceita .svg, .png e .jpeg. Prefira um .svg monocromático com fill: currentColor para que o logotipo se adapte aos temas.", + "domain_config_portal_name": "Personalização do portal", + "domain_config_portal_public_intro": "Apresentação pública personalizada", + "domain_config_portal_public_intro_help": "Você pode usar HTML, estilos básicos serão aplicados a elementos genéricos.", + "domain_config_portal_theme": "Tema de cores padrão", + "domain_config_portal_theme_help": "Os usuários podem escolher outro em suas configurações.", + "domain_config_portal_tile_theme": "Tema de exibição do blocos de aplicativos", + "domain_config_portal_title": "Título personalizado", + "domain_config_portal_user_intro": "Apresentação personalizada do usuário", + "domain_config_portal_user_intro_help": "Você pode usar HTML, estilos básicos serão aplicados a elementos genéricos.", + "domain_config_search_engine": "URL do mecanismo de pesquisa", + "domain_config_search_engine_help": "Esta é uma funcionalidade opcional que permite exibir uma barra de pesquisa no portal (por exemplo, se você quiser usar o portal YunoHost como página inicial do seu navegador). A URL deve conter uma string de consulta vazia, como `https://duckduckgo.com/?q=`, onde `q=` é o parâmetro de consulta vazio do DuckDuckGo", + "domain_config_search_engine_name": "Nome do mecanismo de pesquisa", + "domain_config_show_other_domains_apps": "Mostrar aplicativos de outros domínios", + "domain_created": "Domínio criado", + "domain_creation_failed": "Não foi possível criar o domínio {domain}: {error}", + "domain_deleted": "Domínio excluído", + "domain_deletion_failed": "Não foi possível excluir o domínio {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "Este comando mostra a configuração *recomendada*. Ele não configura o DNS para você. É sua responsabilidade configurar sua zona DNS no seu registrar de acordo com esta recomendação.", + "domain_dns_conf_special_use_tld": "Esse domínio é baseado em um domínio de nível superior (TLD) de uso especial, como .local ou .test e, portanto, não se espera que tenha registros DNS reais.", + "domain_dns_push_already_up_to_date": "Registros já atualizados, nada a fazer.", + "domain_dns_push_failed": "A atualização dos registros DNS falhou miseravelmente.", + "domain_dns_push_failed_to_list": "Falha ao listar os registros atuais usando a API do registrar: {error}", + "domain_dns_push_managed_in_parent_domain": "O recurso de configuração automática de DNS é gerenciado no domínio pai {parent_domain}.", + "domain_dns_push_not_applicable": "O recurso de configuração automática de DNS não é aplicável ao domínio {domain}. Você deve configurar manualmente seus registros DNS seguindo a documentação em https://doc.yunohost.org/dns_config.", + "domain_dns_push_partial_failure": "Registros DNS parcialmente atualizados: alguns avisos/erros foram relatados.", + "domain_dns_push_record_failed": "Falha ao {action} registro {type}/{name} : {error}", + "domain_dns_push_success": "Registros DNS atualizados!", + "domain_dns_pushing": "Submetendo registros DNS…", + "domain_dns_registrar_experimental": "Até agora, a interface com a API do **{registrar}** não foi devidamente testada e revisada pela comunidade YunoHost. O suporte é **muito experimental** - tenha cuidado!", + "domain_dns_registrar_managed_in_parent_domain": "Este domínio é um subdomínio de {parent_domain_link}. A configuração do registrar DNS deve ser gerenciada no painel de configuração do {parent_domain}.", + "domain_dns_registrar_not_supported": "O YunoHost não pôde detectar automaticamente o registrar que lida com esse domínio. Você deve configurar manualmente seus registros DNS seguindo a documentação em https://doc.yunohost.org/dns_config.", + "domain_dns_registrar_supported": "A YunoHost detectou automaticamente que este domínio é gerenciado pelo registrar **{registrar}**. Se desejar, a YunoHost configurará automaticamente esta zona DNS, desde que você forneça as credenciais de API apropriadas. Você pode encontrar a documentação sobre como obter suas credenciais de API nesta página: https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Você também pode configurar seus registros DNS manualmente seguindo a documentação em https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_use_auto": "Usar o recurso de DNS automático", + "domain_dns_registrar_yunohost": "Este domínio é um nohost.me / nohost.st / ynh.fr e sua configuração DNS é, portanto, tratada automaticamente pelo YunoHost sem qualquer configuração adicional. (veja o comando 'yunohost dyndns update')", + "domain_dyndns_already_subscribed": "Você já se inscreveu em um domínio DynDNS", + "domain_exists": "O domínio já existe", + "domain_hostname_failed": "Não foi possível definir o novo nome do host. Isso pode causar um problema mais tarde (ou pode não causar problemas).", + "domain_registrar_is_not_configured": "O registrar ainda não está configurado para o domínio {domain}.", + "domain_remove_confirm_apps_removal": "A remoção desse domínio removerá esses aplicativos:\n{apps}\n\nTem certeza de que quer fazer isso? [{answers}]", + "domain_uninstall_app_first": "Esses aplicativos ainda estão instalados no seu domínio:\n{apps}\n\nDesinstale-os usando 'yunohost app remove the_app_id' ou mova-os para outro domínio usando 'yunohost app change-url the_app_id' antes de prosseguir com a remoção do domínio", + "domain_unknown": "Domínio '{domain}' desconhecido", + "domains_available": "Domínios disponíveis:", + "done": "Concluir", + "download_bad_status_code": "{url} retornou o código de status {code}", + "download_ssl_error": "Erro de SSL ao se conectar a {url}", + "download_timeout": "{url} demorou muito para responder, abortado.", + "download_unknown_error": "Erro ao baixar dados de {url}: {error}", + "downloading": "Baixando…", + "dpkg_is_broken": "Você não pode fazer isso agora porque o dpkg/APT (os gerenciadores de pacotes do sistema) parece estar com problemas... Você pode tentar resolver esse problema conectando-se via SSH e executando `sudo apt install --fix-broken` e/ou `sudo dpkg --configure -a` e/ou `sudo dpkg --audit`.", + "dpkg_lock_not_available": "Este comando não pode ser executado agora porque outro programa parece estar usando o bloqueio do dpkg (o gerenciador de pacotes do sistema)", + "dyndns_could_not_check_available": "Não foi possível verificar se {domain} está disponível em {provider}.", + "dyndns_domain_not_provided": "O provedor DynDNS {provider} não pode fornecer o domínio {domain}.", + "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP para DynDNS", + "dyndns_ip_updated": "Atualizou seu IP no DynDNS", + "dyndns_key_not_found": "Chave DNS não encontrada para o domínio", + "dyndns_no_domain_registered": "Nenhum domínio registrado no DynDNS", + "dyndns_no_recovery_password": "Nenhuma senha de recuperação especificada! Caso você perca o controle deste domínio, você precisará entrar em contato com um administrador da equipe YunoHost!", + "dyndns_provider_unreachable": "Não foi possível acessar o provedor DynDNS {provider}: seu YunoHost não está conectado corretamente à Internet ou o servidor dynette está inativo.", + "dyndns_set_recovery_password_denied": "Falha ao definir a senha de recuperação: chave inválida", + "dyndns_set_recovery_password_failed": "Falha ao definir a senha de recuperação: {error}", + "dyndns_set_recovery_password_invalid_password": "Falha ao definir a senha de recuperação: a senha não é forte o suficiente", + "dyndns_set_recovery_password_success": "Senha de recuperação definida!", + "dyndns_set_recovery_password_unknown_domain": "Falha ao definir a senha de recuperação: domínio não registrado", + "dyndns_subscribe_failed": "Não foi possível assinar o domínio DynDNS: {error}", + "dyndns_subscribed": "Domínio DynDNS inscrito", + "dyndns_too_many_requests": "O serviço DynDNS da YunoHost recebeu muitas solicitações suas, espere cerca de 1 hora antes de tentar novamente.", + "dyndns_unavailable": "O domínio '{domain}' não está disponível.", + "dyndns_unsubscribe_already_unsubscribed": "A inscrição do domínio já foi cancelada", + "dyndns_unsubscribe_denied": "Falha ao cancelar a assinatura do domínio: credenciais inválidas", + "dyndns_unsubscribe_failed": "Não foi possível cancelar a inscrição do domínio DynDNS: {error}", + "dyndns_unsubscribed": "Domínio DynDNS com inscrição cancelada", + "error_changing_file_permissions": "Erro ao alterar permissões para {path}: {error}", + "error_removing": "Erro ao remover {path}: {error}", + "error_writing_file": "Erro ao gravar o arquivo {file}: {error}", + "extracting": "Extraindo…", + "field_invalid": "Campo inválido '{field}'", + "file_does_not_exist": "O arquivo {path} não existe.", + "file_not_exist": "O arquivo não existe: '{path}'", + "firewall_reload_failed": "Não foi possível recarregar o firewall. Mais informações no log.", + "firewall_reloaded": "Firewall recarregado", + "global_settings_reset_success": "Redefinir configurações globais", + "global_settings_setting_admin_strength": "Requisitos de força da senha do administrador", + "global_settings_setting_admin_strength_help": "Esses requisitos só são aplicados ao criar ou alterar a senha", + "global_settings_setting_antispam_name": "Antispam", + "global_settings_setting_backup_compress_tar_archives": "Compactar backups", + "global_settings_setting_backup_compress_tar_archives_help": "Ao criar novos backups, compacte os arquivos (.tar.gz) em vez de usar arquivos sem compactação (.tar). Observação: habilitar essa opção significa criar arquivos de backup mais leves, mas o procedimento de backup inicial será significativamente mais longo e exigirá mais processamento da CPU.", + "global_settings_setting_backup_name": "Backup", + "global_settings_setting_dns_custom_resolvers_enabled": "Usar resolvedores DNS personalizados", + "global_settings_setting_dns_custom_resolvers_enabled_help": "Por padrão, o YunoHost usa uma lista de servidores confiáveis localizados na Europa. Usuários avançados podem optar por especificar servidores personalizados.", + "global_settings_setting_dns_custom_resolvers_list": "Endereços de resolvedores personalizados", + "global_settings_setting_dns_custom_resolvers_list_help": "Uma lista de pelo menos 2 servidores DNS por protocolo IP em uso (IPv4/IPv6). Exemplo: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "global_settings_setting_dns_exposure": "Versões de IP a serem consideradas para configuração e diagnóstico de DNS", + "global_settings_setting_dns_exposure_help": "Nota: Isso afeta apenas as verificações de configuração e diagnóstico de DNS recomendadas. Isso não afeta as configurações do sistema.", + "global_settings_setting_email_name": "E-mail", + "global_settings_setting_enable_blocklists": "Habilitar listas de bloqueio para tráfego de entrada", + "global_settings_setting_enable_blocklists_help": "Bloqueia servidores listados por spamcop.net, spamhaus.org e abuseat.org para prevenir SPAM. No entanto, isso pode causar problemas de entrega para alguns servidores de e-mail inofensivos que podem estar listados por esses terceiros, caso em que os e-mails enviados desses servidores não serão recebidos.", + "global_settings_setting_experimental_name": "Experimental", + "global_settings_setting_misc_name": "Outro", + "global_settings_setting_network_name": "Rede", + "global_settings_setting_nginx_compatibility": "Compatibilidade NGINX", + "global_settings_setting_nginx_compatibility_help": "Compatibilidade versus compensação de segurança para o servidor web NGINX. Afeta as cifras (e outros aspectos relacionados à segurança)", + "global_settings_setting_nginx_name": "NGINX (servidor web)", + "global_settings_setting_nginx_redirect_to_https": "Forçar HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Redirecione solicitações HTTP para HTTPs por padrão (NÃO DESLIGUE a menos que você realmente saiba o que está fazendo!)", + "global_settings_setting_password_name": "Senhas", + "global_settings_setting_passwordless_sudo": "Permitir que os administradores usem 'sudo' sem digitar novamente suas senhas", + "global_settings_setting_pop3_enabled": "Ativar POP3", + "global_settings_setting_pop3_enabled_help": "Habilite o protocolo POP3 para o servidor de e-mail. O POP3 é um protocolo mais antigo para acessar caixas de correio de clientes de e-mail e é mais leve, mas tem menos recursos que o IMAP (ativado por padrão)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Permitir que os usuários editem seu endereço de e-mail principal", + "global_settings_setting_portal_allow_edit_email_alias": "Permitir que os usuários adicionem, removam e editem aliases de e-mail", + "global_settings_setting_portal_allow_edit_email_alias_help": "Se desativado, eles precisam pedir aos administradores que façam isso por eles.", + "global_settings_setting_portal_allow_edit_email_forward": "Permitir que os usuários adicionem, removam e editem o encaminhamento de e-mails", + "global_settings_setting_portal_allow_edit_email_forward_help": "Se desativado, eles precisam pedir aos administradores que façam isso por eles.", + "global_settings_setting_portal_allow_edit_email_help": "Se desativado, eles precisam pedir aos administradores que façam isso por eles.", + "global_settings_setting_portal_name": "Portal de usuário", + "global_settings_setting_postfix_compatibility": "Compatibilidade do Postfix", + "global_settings_setting_postfix_compatibility_help": "Compatibilidade versus compensação de segurança para o servidor Postfix. Afeta as cifras (e outros aspectos relacionados à segurança)", + "global_settings_setting_postfix_name": "Postfix (servidor de e-mail SMTP)", + "global_settings_setting_root_access_explain": "Em sistemas Linux, o usuário 'root' é o administrador absoluto. No contexto do YunoHost, o login SSH direto como 'root' está desabilitado por padrão, exceto na rede local do servidor. Membros do grupo 'admins' podem usar o comando sudo para agir como root a partir da linha de comando. No entanto, pode ser útil ter uma senha de root (forte) para depurar o sistema caso, por algum motivo, os administradores comuns não consigam mais fazer login.", + "global_settings_setting_root_access_name": "Alterar senha de root", + "global_settings_setting_root_password": "Nova senha de root", + "global_settings_setting_root_password_confirm": "Nova senha de root (confirmar)", + "global_settings_setting_security_experimental_enabled": "Recursos de segurança experimentais", + "global_settings_setting_security_experimental_enabled_help": "Habilite recursos de segurança experimentais (não habilite isso se você não souber o que está fazendo!)", + "global_settings_setting_security_name": "Segurança", + "global_settings_setting_smtp_allow_ipv6": "Permitir IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Permitir o uso do IPv6 para receber e enviar e-mails", + "global_settings_setting_smtp_backup_mx_domains": "Domínios para atuar como MX secundário", + "global_settings_setting_smtp_backup_mx_domains_help": "Permita que este servidor atue como um domínio MX *secundário* de backup para o domínio listado. Isso significa que, se o MX principal do domínio estiver inacessível (por exemplo, devido a uma interrupção), os e-mails ainda serão enviados para este servidor, que os manterá por um período máximo de 20 dias e tentará encaminhá-los para o destino real assim que o serviço for restabelecido. Vários domínios podem ser fornecidos, separados por vírgulas.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Lista de permissões de e-mails MX de backup SMTP", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Ao atuar como um servidor MX secundário, é necessário fornecer a lista completa de endereços de e-mail de destinatários autorizados (caso contrário, as mensagens serão recusadas e descartadas). É possível fornecer várias entradas, separadas por vírgulas.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Ativar retransmissão SMTP", + "global_settings_setting_smtp_relay_enabled_help": "Habilite o servidor SMTP para enviar e-mails (SMTP relay) em vez desta instância do Yunohost. Isso é útil se você estiver em uma das seguintes situações: sua porta 25 está bloqueada pelo seu provedor de internet ou VPS, você tem um IP residencial listado no DUHL, você não consegue configurar o DNS reverso ou este servidor não está diretamente exposto à internet e você deseja usar outro servidor para enviar e-mails.", + "global_settings_setting_smtp_relay_host": "Host de retransmissão SMTP", + "global_settings_setting_smtp_relay_password": "Senha de retransmissão SMTP", + "global_settings_setting_smtp_relay_port": "Porta de retransmissão SMTP", + "global_settings_setting_smtp_relay_user": "Usuário de retransmissão SMTP", + "global_settings_setting_ssh_compatibility": "Compatibilidade SSH", + "global_settings_setting_ssh_compatibility_help": "Compatibilidade versus compensação de segurança para o servidor SSH. Afeta as cifras (e outros aspectos relacionados à segurança). Consulte https://infosec.mozilla.org/guidelines/openssh para obter mais informações.", + "global_settings_setting_ssh_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Autenticação por senha", + "global_settings_setting_ssh_password_authentication_help": "Permitir autenticação por senha para SSH", + "global_settings_setting_ssh_port": "Porta SSH", + "global_settings_setting_ssh_port_help": "Uma porta inferior a 1024 é preferida para evitar tentativas de usurpação da porta por serviços não-administradores na máquina remota. Você também deve evitar usar uma porta já em uso, como 80 ou 443.", + "global_settings_setting_tls_passthrough_enabled": "Habilitar encaminhamento baseado em TLS-passthrough / SNI", + "global_settings_setting_tls_passthrough_enabled_help": "Esta é uma funcionalidade avançada para redirecionar um domínio inteiro para outra máquina *sem* descriptografar o tráfego. Útil quando você deseja expor várias máquinas por trás do mesmo IP, mas ainda permitir que cada máquina lide com a terminação SSL.", + "global_settings_setting_tls_passthrough_explain": "Este recurso é AVANÇADO e EXPERIMENTAL e provocará grandes alterações na configuração do nginx deste servidor. Por favor, NÃO o utilize se não souber o que está fazendo! Em particular, você deve estar ciente de que o fail2ban não pode ser implementado no servidor proxy (o nftables não pode bloquear tráfego malicioso, pois todos os pacotes IP aparecem como provenientes do servidor principal). Além disso, por enquanto, a configuração do nginx do servidor proxy precisa ser ajustada manualmente para aceitar o `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "Lista de encaminhamento", + "global_settings_setting_tls_passthrough_list_help": "Deve ser uma lista de DOMÍNIO;DESTINO;PORTA, como domain.tld;192.168.1.42;443 ou domain.tld;servidor.local;8123", + "global_settings_setting_tls_passthrough_name": "Encaminhamento baseado em TLS/SNI", + "global_settings_setting_user_strength": "Requisitos de força da senha do usuário", + "global_settings_setting_user_strength_help": "Esses requisitos só são aplicados ao criar ou alterar a senha", + "global_settings_setting_webadmin_allowlist": "Lista de permissões de IP do Webadmin", + "global_settings_setting_webadmin_allowlist_enabled": "Ativar lista de permissões de IP do Webadmin", + "global_settings_setting_webadmin_allowlist_enabled_help": "Permita que apenas alguns IPs acessem o webadmin.", + "global_settings_setting_webadmin_allowlist_help": "Endereços IP permitidos para acessar o webadmin. A notação CIDR é permitida.", + "global_settings_setting_webadmin_name": "Webadmin", + "good_practices_about_admin_password": "Você está prestes a definir uma nova senha de administrador. A senha deve ter pelo menos 8 caracteres, embora seja uma boa prática usar uma senha mais longa (ou seja, uma frase secreta) e/ou usar uma variação de caracteres (maiúsculas, minúsculas, dígitos e caracteres especiais).", + "good_practices_about_user_password": "Você está prestes a definir uma nova senha de usuário. A senha deve ter pelo menos 8 caracteres, embora seja uma boa prática usar uma senha mais longa (por exemplo, uma frase secreta) e/ou uma variação de caracteres (maiúsculas, minúsculas, dígitos e caracteres especiais).", + "group_already_exist": "Grupo {group} já existe", + "group_already_exist_on_system": "O grupo {group} já existe nos grupos do sistema", + "group_already_exist_on_system_but_removing_it": "O grupo {group} já existe nos grupos do sistema, mas o YunoHost irá removê-lo…", + "group_cannot_be_deleted": "O grupo {group} não pode ser excluído manualmente.", + "group_cannot_edit_all_users": "O grupo 'all_users' não pode ser editado manualmente. É um grupo especial destinado a conter todos os usuários registrados no YunoHost", + "group_cannot_edit_primary_group": "O grupo '{group}' não pode ser editado manualmente. É o grupo principal destinado a conter apenas um usuário específico.", + "group_cannot_edit_visitors": "O grupo 'visitors' não pode ser editado manualmente. É um grupo especial que representa visitantes anônimos", + "group_cannot_remove_last_admin": "O usuário '{user}' é o último usuário no grupo 'admins' e não será removido dele.", + "group_created": "Grupo '{group}' criado", + "group_creation_failed": "Não foi possível criar o grupo '{group}': {error}", + "group_deleted": "Grupo '{group}' excluído", + "group_deletion_failed": "Não foi possível excluir o grupo '{group}': {error}", + "group_mailalias_add": "O alias de e-mail '{mail}' será adicionado ao grupo '{group}'", + "group_mailalias_remove": "O alias de e-mail '{mail}' será removido do grupo '{group}'", + "group_no_change": "Nada a mudar para o grupo '{group}'", + "group_unknown": "O grupo '{group}' é desconhecido", + "group_update_aliases": "Atualizando aliases para o grupo '{group}'", + "group_update_failed": "Não foi possível atualizar o grupo '{group}': {error}", + "group_updated": "Grupo '{group}' atualizado", + "group_user_add": "O usuário '{user}' será adicionado ao grupo '{group}'", + "group_user_already_in_group": "O usuário {user} já está no grupo {group}", + "group_user_not_in_group": "O usuário {user} não está no grupo {group}", + "group_user_remove": "O usuário '{user}' será removido do grupo '{group}'", + "hook_exec_failed": "Não foi possível executar o script: {path}", + "hook_exec_not_terminated": "O script não foi concluído corretamente: {path}", + "hook_json_return_error": "Não foi possível ler o retorno do hook {path}. Erro: {msg}. Conteúdo bruto: {raw_content}", + "hook_list_by_invalid": "Esta propriedade não pode ser usada para listar hooks", + "hook_name_unknown": "Nome do hook desconhecido '{name}'", + "installation_complete": "Instalação concluída", + "invalid_credentials": "Senha ou nome de usuário inválido", + "invalid_number": "Deve ser um número", + "invalid_password": "Senha inválida", + "invalid_regex": "Expressão regular inválida:'{regex}'", + "invalid_shell": "Shell inválido: {shell}", + "invalid_url": "Falha ao conectar a {url}... talvez o serviço esteja inativo ou você não esteja conectado corretamente à Internet em IPv4/IPv6.", + "ldap_attribute_already_exists": "O atributo LDAP '{attribute}' já existe com o valor '{value}'", + "ldap_server_down": "Não foi possível acessar o servidor LDAP", + "ldap_server_is_down_restart_it": "O serviço LDAP está inativo, tentando reiniciá-lo…", + "log_app_action_run": "Executar ação do aplicativo '{}'", + "log_app_change_url": "Alterar a URL do aplicativo '{}'", + "log_app_config_set": "Aplicar configuração ao aplicativo '{}'", + "log_app_install": "Instalar o aplicativo '{}'", + "log_app_makedefault": "Tornar '{}' o aplicativo padrão", + "log_app_remove": "Remover o aplicativo '{}'", + "log_app_upgrade": "Atualizar o aplicativo '{}'", + "log_available_on_yunopaste": "Este log agora está disponível via {url}", + "log_backup_create": "Criar um arquivo de backup", + "log_backup_restore_app": "Restaurar '{}' de um arquivo de backup", + "log_backup_restore_system": "Restaurar o sistema a partir de um arquivo de backup", + "log_corrupted_md_file": "O arquivo de metadados YAML associado aos logs está corrompido: '{md_file}\nErro: {error}'", + "log_diagnosis_run": "Executar diagnóstico", + "log_does_exists": "Não há log de operação com o nome '{log}', use 'yunohost log list' para ver todos os logs de operação disponíveis", + "log_domain_add": "Adicionar domínio '{}'", + "log_domain_config_set": "Atualizar a configuração do domínio '{}'", + "log_domain_dns_push": "Submeter registros DNS para o domínio '{}'", + "log_domain_main_domain": "Tornar '{}' o domínio principal", + "log_domain_remove": "Remover o domínio '{}'", + "log_dyndns_subscribe": "Registrar o subdomínio YunoHost '{}'", + "log_dyndns_unsubscribe": "Cancelar o registro do subdomínio YunoHost '{}'", + "log_dyndns_update": "Atualizar o IP associado ao seu subdomínio YunoHost '{}'", + "log_help_to_get_failed_log": "A operação '{desc}' não pôde ser concluída. Compartilhe o log completo desta operação usando o comando 'yunohost log share {name}' para obter ajuda", + "log_help_to_get_log": "Para visualizar o log da operação '{desc}', use o comando 'yunohost log show {name}'", + "log_letsencrypt_cert_install": "Instalar um certificado Let's Encrypt no domínio '{}'", + "log_letsencrypt_cert_renew": "Renovar o certificado Let's Encrypt de '{}'", + "log_link_to_failed_log": "Não foi possível concluir a operação '{desc}'. Forneça o log completo desta operação clicando aqui para obter ajuda", + "log_link_to_log": "Log completo desta operação: '{desc}'", + "log_operation_unit_unclosed_properly": "A unidade de operação não foi fechada corretamente", + "log_regen_conf": "Regenerar configurações do sistema de '{}'", + "log_remove_on_failed_install": "Remover '{}' após uma instalação com falha", + "log_resource_snippet": "Provisionamento/desprovisionamento/atualização de um recurso", + "log_selfsigned_cert_install": "Instalar certificado autoassinado no domínio '{}'", + "log_settings_reset": "Redefinir configuração", + "log_settings_reset_all": "Redefinir todas as configurações", + "log_settings_set": "Aplicar configurações", + "log_tools_migrations_migrate_forward": "Executar migrações", + "log_tools_postinstall": "Pós-instalação de seu servidor YunoHost", + "log_tools_reboot": "Reiniciar seu servidor", + "log_tools_shutdown": "Desligar seu servidor", + "log_tools_update": "Buscando atualizações do sistema disponíveis e atualizando o catálogo de aplicativos", + "log_tools_upgrade": "Atualizar pacotes do sistema", + "log_user_create": "Adicionar usuário '{}'", + "log_user_delete": "Excluir usuário '{}'", + "log_user_group_create": "Criar grupo '{}'", + "log_user_group_delete": "Excluir o grupo '{}'", + "log_user_group_update": "Atualizar o grupo '{}'", + "log_user_import": "Importar usuários", + "log_user_update": "Atualizar informações para o usuário '{}'", + "mail_alias_remove_failed": "Não foi possível remover o alias de e-mail '{mail}'", + "mail_alias_unauthorized": "Você não está autorizado a adicionar aliases relacionados ao domínio '{domain}'", + "mail_already_exists": "O endereço de e-mail '{mail}' já existe", + "mail_domain_unknown": "Endereço de e-mail inválido para o domínio '{domain}'. Por favor, use um domínio administrado por este servidor.", + "mail_edit_operation_unauthorized": "Você não está autorizado a fazer essa alteração em sua conta.", + "mail_forward_remove_failed": "Não foi possível remover o encaminhamento de e-mail '{mail}'", + "mail_unavailable": "Este endereço de e-mail é reservado para o grupo de administradores", + "mailbox_disabled": "E-mail desativado para o usuário {user}", + "mailbox_used_space_dovecot_down": "O serviço de caixa de correio Dovecot precisa estar ativo se você quiser obter o espaço usado da caixa de correio", + "main_domain_change_failed": "Não foi possível alterar o domínio principal", + "main_domain_changed": "O domínio principal foi alterado", + "migration_0027_cleaning_up": "Limpar o cache e os pacotes que não são mais necessários…", + "migration_0027_delayed_api_restart": "A API do YunoHost será reiniciada automaticamente em 15 segundos. Ele pode ficar indisponível por alguns segundos e, em seguida, você terá que fazer login novamente.", + "migration_0027_general_warning": "Por fim, observe que esta migração é uma **operação delicada**. A equipe da YunoHost fez o possível para revisá-la e testá-la, mas a migração ainda pode causar problemas em partes do sistema ou de seus aplicativos.\n\nPortanto, recomendamos:\n- **Fazer backups** de todos os dados ou aplicativos críticos. Mais informações em https://doc.yunohost.org/backup;\n- **Ter paciência** após iniciar a migração: dependendo da sua conexão com a internet e do seu hardware, a atualização completa pode levar até uma hora;\n- **Entrar em contato com a comunidade** no fórum caso precise de ajuda para solucionar problemas.", + "migration_0027_main_upgrade": "Iniciando a atualização principal…", + "migration_0027_modified_files": "Observe que os seguintes arquivos foram modificados manualmente e podem ser substituídos após a atualização: {manually_modified_files}", + "migration_0027_not_bullseye": "A distribuição Debian atual não é o Bullseye! Se você já executou a migração do Bullseye para o Bookworm, esse erro indica que o procedimento de migração não foi 100% bem-sucedido (caso contrário, o YunoHost a teria marcado como concluído). Recomenda-se investigar o ocorrido com a equipe de suporte, que precisará do log **completo** da migração, o qual pode ser encontrado em Ferramentas > Logs no painel de administração web.", + "migration_0027_not_enough_free_space": "O espaço livre é muito baixo em /var/! Você deve ter pelo menos 1 GB livre para executar essa migração.", + "migration_0027_patch_yunohost_conflicts": "Aplicando patch para solucionar o problema de conflito…", + "migration_0027_patching_sources_list": "Corrigindo o arquivo sources.lists…", + "migration_0027_problematic_apps_warning": "Observe que os seguintes aplicativos instalados, possivelmente problemáticos, foram detectados. Parece que eles não foram instalados a partir do catálogo de aplicativos do YunoHost ou não estão sinalizados como \"funcionando\". Consequentemente, não é possível garantir que eles continuarão funcionando após a atualização: {problematic_apps}", + "migration_0027_start": "Iniciando a migração para o Debian Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Algo deu errado durante a atualização principal, o sistema parece ainda estar no Debian Bullseye.", + "migration_0027_system_not_fully_up_to_date": "Seu sistema não está totalmente atualizado. Por favor, execute uma atualização regular antes de executar a migração para o Bookworm.", + "migration_0027_yunohost_upgrade": "Iniciando a atualização do núcleo do YunoHost…", + "migration_not_enough_space": "Disponibilize espaço suficiente em {path} para executar a migração.", + "migration_postgresql_previous_not_installed": "O PostgreSQL não foi instalado em seu sistema. Nada a fazer.", + "migration_postgresql_target_not_installed": "O PostgreSQL 13 está instalado, mas não o PostgreSQL 15!? Algo estranho pode ter acontecido no seu sistema :(…", + "migration_python_venv_rebuild_broken_app": "Ignorando {app} porque virtualenv não pode ser facilmente reconstruído para este aplicativo. Em vez disso, você deve corrigir a situação forçando a atualização deste aplicativo usando `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_disclaimer_base": "Após a atualização para o Debian Bookworm, alguns aplicativos Python precisam ser parcialmente reconstruídos para serem convertidos para a nova versão do Python fornecida no Debian (em termos técnicos: o que é chamado de 'virtualenv' precisa ser recriado). Enquanto isso, esses aplicativos Python podem não funcionar. O YunoHost pode tentar reconstruir o virtualenv para alguns deles, conforme detalhado abaixo. Para outros aplicativos, ou se a tentativa de reconstrução falhar, você precisará forçar manualmente uma atualização para esses aplicativos.", + "migration_python_venv_rebuild_disclaimer_ignored": "Virtualenvs não podem ser recriados automaticamente para esses aplicativos. Você precisa forçar uma atualização para eles, o que pode ser feito a partir da linha de comando com: 'yunohost app upgrade --force APP': {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "A reconstrução do virtualenv será tentada para os seguintes aplicativos (Nota: a operação pode levar algum tempo!): {rebuild_apps}", + "migration_python_venv_rebuild_failed": "Falha ao reconstruir o virtualenv Python para {app}. O aplicativo pode não funcionar enquanto este problema não for resolvido. Você deve corrigir a situação forçando a atualização deste aplicativo usando `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_in_progress": "Agora tentando reconstruir o virtualenv Python para `{app}`", + "migration_0031_terms_of_services": "Essa migração é puramente uma mensagem informativa sobre o fato de que o projeto YunoHost agora publica Termos de Serviço relacionados aos serviços técnicos e comunitários.", + "migration_0036_cleaning_up": "Limpar o cache e os pacotes não são mais necessários…", + "migration_0036_delayed_api_restart": "A API do YunoHost será reiniciada automaticamente em 15 segundos. Ele pode ficar indisponível por alguns segundos e, em seguida, você terá que fazer login novamente.", + "migration_0036_general_warning": "Por fim, observe que esta migração é uma **operação delicada**. A equipe da YunoHost fez o possível para revisá-la e testá-la, mas a migração ainda pode causar problemas em partes do sistema ou de seus aplicativos.\n\nPortanto, recomendamos:\n- **Fazer backups** de todos os dados ou aplicativos críticos. Mais informações em https://doc.yunohost.org/backup;\n- **Ter paciência** após iniciar a migração: dependendo da sua conexão com a internet e do seu hardware, a atualização completa pode levar até uma hora;\n- **Entrar em contato com a comunidade** no fórum caso precise de ajuda para solucionar problemas.", + "migration_0036_main_upgrade": "Iniciando a atualização principal…", + "migration_0036_modified_files": "Observe que os seguintes arquivos foram modificados manualmente e podem ser substituídos após a atualização:", + "migration_0036_not_bullseye": "A distribuição Debian atual não é o Bookworm! Se você já executou a migração do Bookworm para o Trixie, esse erro indica que o procedimento de migração não foi 100% bem-sucedido (caso contrário, o YunoHost a teria marcado como concluído). Recomenda-se investigar o ocorrido com a equipe de suporte, que precisará do log **completo** da migração, o qual pode ser encontrado em Ferramentas > Logs no painel de administração web.", + "migration_0036_not_enough_free_space": "O espaço livre é muito baixo em /var/! Você deve ter pelo menos 1 GB livre para executar essa migração.", + "migration_0036_patch_yunohost_dpkg": "Aplicando patch no banco de dados dpkg para contornar problemas de conflito…", + "migration_0036_patching_sources_list": "Corrigindo o arquivo sources.lists…", + "migration_0036_problematic_apps_warning": "Observe que os seguintes aplicativos instalados, que podem apresentar problemas, foram detectados. Parece que eles não foram instalados a partir do catálogo de aplicativos do YunoHost ou não estão sinalizados como \"funcionando\". Consequentemente, não é possível garantir que eles continuarão funcionando após a atualização:", + "migration_0036_start": "Iniciando a migração para o Debian Trixie…", + "migration_0036_still_on_bookworm_after_main_upgrade": "Algo deu errado durante a atualização principal, o sistema parece ainda estar no Debian Bookworm.", + "migration_0036_system_not_fully_up_to_date": "Seu sistema não está totalmente atualizado. Por favor, faça uma atualização regular antes de executar a migração para o Debian Trixie.", + "migration_0036_yunohost_upgrade": "Iniciando a atualização do núcleo do YunoHost…", + "migration_description_0027_migrate_to_bookworm": "Atualize o sistema para Debian Bookworm e YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Exclua as permissões XMPP antigas, o Metronome agora é um aplicativo", + "migration_description_0029_postgresql_13_to_15": "Migrar bancos de dados do PostgreSQL 13 para o 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Reparar o aplicativo Python após a migração do Bookworm", + "migration_description_0031_terms_of_services": "Termos de serviço", + "migration_description_0032_firewall_config": "Migração de arquivo de configuração de firewall interno", + "migration_description_0033_rework_permission_infos": "Retrabalhar a maneira como as permissões do aplicativo são armazenadas", + "migration_description_0034_fix_missing_admins_aliases": "Corrigir aliases de e-mail ausentes para o grupo de administradores", + "migration_description_0035_fix_apps_nodejs_version": "Corrigir versões do nodejs nas configurações do systemd do aplicativo", + "migration_description_0036_migrate_to_trixie": "Atualizar o sistema para Debian Trixie e YunoHost 13", + "migration_ldap_backup_before_migration": "Criar um backup do banco de dados LDAP e das configurações de aplicativos antes da migração real.", + "migration_ldap_can_not_backup_before_migration": "O backup do sistema não pôde ser concluído antes que a migração falhasse. Erro: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Não foi possível migrar... tentando reverter o sistema.", + "migration_ldap_rollback_success": "Sistema revertido.", + "migrations_already_ran": "Essas migrações já foram feitas: {ids}", + "migrations_dependencies_not_satisfied": "Execute estas migrações: '{dependencies_id}', antes da migração {id}.", + "migrations_exclusive_options": "'--auto', '--skip' e '--force-rerun' são opções mutuamente exclusivas.", + "migrations_failed_to_load_migration": "Não foi possível carregar a migração {id}: {error}", + "migrations_list_conflict_pending_done": "Você não pode usar '--previous' e '--done' ao mesmo tempo.", + "migrations_loading_migration": "Carregando migração {id}…", + "migrations_migration_has_failed": "A migração {id} não foi concluída, abortando. Erro: {exception}", + "migrations_must_provide_explicit_targets": "Você deve fornecer destinos explícitos ao usar '--skip' ou '--force-rerun'", + "migrations_need_to_accept_disclaimer": "Para executar a migração {id}, você deve aceitar o seguinte aviso:\n---\n{disclaimer}\n---\nSe você aceitar executar a migração, execute o comando novamente com a opção '--accept-disclaimer'.", + "migrations_no_migrations_to_run": "Nenhuma migração para executar", + "migrations_no_such_migration": "Não há migração chamada '{id}'", + "migrations_not_pending_cant_skip": "Essas migrações não estão pendentes, portanto, não podem ser ignoradas: {ids}", + "migrations_pending_cant_rerun": "Essas migrações ainda estão pendentes, portanto, não podem ser executadas novamente: {ids}", + "migrations_running_forward": "Executando a migração {id}…", + "migrations_skip_migration": "Ignorando a migração {id}…", + "migrations_success_forward": "Migração {id} concluída", + "migrations_to_be_ran_manually": "A migração {id} deve ser executada manualmente. Vá para Ferramentas → Migrações na página webadmin ou execute `yunohost tools migrations run`.", + "nftables_unavailable": "Você não pode jogar com nftables aqui. Você está em um contêiner ou seu kernel não o suporta", + "noninteractive_task": "Tarefa não interativa", + "not_enough_disk_space": "Não há espaço livre suficiente em '{path}'", + "operation_interrupted": "A operação foi interrompida manualmente?", + "other_available_options": "… e {n} outras opções disponíveis não mostradas", + "password_confirmation_not_the_same": "A senha e sua confirmação não correspondem", + "password_listed": "Essa senha está entre as senhas mais usadas no mundo. Por favor, escolha algo mais exclusivo.", + "password_too_long": "Por favor, escolha uma senha com menos de 127 caracteres", + "password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres", + "password_too_simple_2": "A senha precisa ter pelo menos 8 caracteres e conter um dígito, caracteres maiúsculos e minúsculos", + "password_too_simple_3": "A senha precisa ter pelo menos 8 caracteres e conter um dígito, caracteres maiúsculos, minúsculos e especiais", + "password_too_simple_4": "A senha precisa ter pelo menos 12 caracteres e conter um dígito, caracteres maiúsculos, minúsculos e especiais", + "pattern_backup_archive_name": "O nome do arquivo deve ser válido, com no máximo 30 caracteres, alfanuméricos e apenas os caracteres -_.", + "pattern_domain": "Deve ser um nome de domínio válido (por exemplo, meu-dominio.org)", + "pattern_email": "Deve ser um endereço de e-mail válido, sem o símbolo '+' (por exemplo, alguem@example.com)", + "pattern_email_forward": "Deve ser um endereço de e-mail válido, símbolo '+' aceito (por exemplo, alguem+tag@example.com)", + "pattern_fullname": "Deve ser um nome completo válido (pelo menos 3 caracteres)", + "pattern_mailbox_quota": "Deve ser um tamanho com o sufixo b/k/M/G/T ou 0 para não ter uma cota", + "pattern_password": "Deve ter pelo menos 3 caracteres", + "pattern_password_app": "Desculpe, as senhas não podem conter os seguintes caracteres: {forbidden_chars}", + "pattern_port_or_range": "Deve ser um número de porta válido (ou seja, 0-65535) ou intervalo de portas (por exemplo, 100:200)", + "pattern_username": "Deve conter apenas caracteres alfanuméricos minúsculos e sublinhados", + "permission_already_allowed": "O grupo '{group}' já tem a permissão '{permission}' habilitada", + "permission_already_disallowed": "O grupo '{group}' já tem a permissão '{permission}' desabilitada", + "permission_cannot_remove_main": "A remoção de uma permissão principal não é permitida", + "permission_cant_add_to_all_users": "A permissão {permission} não pode ser adicionada a todos os usuários.", + "permission_created": "Permissão '{permission}' criada", + "permission_creation_failed": "Não foi possível criar a permissão '{permission}': {error}", + "permission_currently_allowed_for_all_users": "Atualmente, essa permissão é concedida a todos os usuários, além de outros grupos. Você provavelmente deseja remover a permissão 'all_users' ou remover os outros grupos aos quais ela é concedida atualmente.", + "permission_deleted": "Permissão '{permission}' excluída", + "permission_deletion_failed": "Não foi possível excluir a permissão '{permission}': {error}", + "permission_not_found": "Permissão '{permission}' não encontrada", + "permission_protected": "A permissão {permission} está protegida. Você não pode adicionar ou remover o grupo de visitantes de/para esta permissão.", + "permission_require_account": "A permissão {permission} só faz sentido para usuários que têm uma conta e, portanto, não pode ser habilitada para visitantes.", + "permission_update_failed": "Não foi possível atualizar a permissão '{permission}': {error}", + "permission_updated": "Permissão '{permission}' atualizada", + "port_already_closed": "A porta {port} já está fechada", + "port_already_opened": "A porta {port} já está aberta", + "postinstall_low_rootfsspace": "O sistema de arquivos raiz tem um espaço total inferior a 10 GB, o que é bastante preocupante! É provável que você fique sem espaço em disco muito rapidamente! Recomenda-se ter pelo menos 16 GB para o sistema de arquivos raiz. Se você quiser instalar o YunoHost apesar deste aviso, execute novamente o pós-instalador com a opção --force-diskspace", + "pydantic_type_error": "Tipo inválido.", + "pydantic_type_error_none_not_allowed": "O valor é obrigatório.", + "pydantic_type_error_str": "Tipo inválido, cadeia de caracteres esperada.", + "pydantic_value_error_color": "Não é uma cor válida; o valor deve ser uma cor nomeada ou hexadecimal.", + "pydantic_value_error_const": "Valor inesperado; escolha entre {permitted}", + "pydantic_value_error_date": "Formato de data inválido", + "pydantic_value_error_email": "O valor não é um endereço de e-mail válido", + "pydantic_value_error_number_not_ge": "O valor deve ser maior ou igual a {limit_value}.", + "pydantic_value_error_number_not_le": "O valor deve ser menor ou igual a {limit_value}.", + "pydantic_value_error_str_regex": "Cadeia de caracteres inválida; O valor não respeita o padrão '{pattern}'", + "pydantic_value_error_time": "Formato de hora inválido", + "pydantic_value_error_url_extra": "URL inválido, caracteres extras encontrados após URL válido: '{extra}'", + "pydantic_value_error_url_host": "Host de URL inválido", + "pydantic_value_error_url_port": "Porta de URL inválida, a porta não pode exceder 65535", + "pydantic_value_error_url_scheme": "Esquema de URL inválido ou ausente", + "regenconf_dry_pending_applying": "Verificando a configuração pendente que teria sido aplicada para a categoria '{category}'…", + "regenconf_failed": "Não foi possível gerar novamente a configuração da(s) categoria(s): {categories}", + "regenconf_file_backed_up": "Arquivo de configuração '{conf}' foi copiado para '{backup}'", + "regenconf_file_copy_failed": "Não foi possível copiar o novo arquivo de configuração '{new}' para '{conf}'", + "regenconf_file_kept_back": "Espera-se que o arquivo de configuração '{conf}' seja excluído pelo regen-conf (categoria {category}), mas foi mantido.", + "regenconf_file_manually_modified": "O arquivo de configuração '{conf}' foi modificado manualmente e não será atualizado", + "regenconf_file_manually_removed": "O arquivo de configuração '{conf}' foi removido manualmente e não será criado", + "regenconf_file_remove_failed": "Não foi possível remover o arquivo de configuração '{conf}'", + "regenconf_file_removed": "Arquivo de configuração '{conf}' removido", + "regenconf_file_updated": "Arquivo de configuração '{conf}' atualizado", + "regenconf_need_to_explicitly_specify_ssh": "A configuração do SSH foi modificada manualmente, mas você precisa especificar explicitamente a categoria 'ssh' com --force para realmente aplicar as alterações.", + "regenconf_now_managed_by_yunohost": "O arquivo de configuração '{conf}' agora é gerenciado pelo YunoHost (categoria {category}).", + "regenconf_pending_applying": "Aplicando configuração pendente para a categoria '{category}'…", + "regenconf_up_to_date": "A configuração já está atualizada para a categoria '{category}'", + "regenconf_updated": "Configuração atualizada para '{category}'", + "regenconf_would_be_updated": "A configuração teria sido atualizada para a categoria '{category}'", + "regex_incompatible_with_tile": "/!\\ Empacotadores! A permissão '{permission}' tem show_tile definida como 'true' e, portanto, você não pode definir uma URL de expressão regular como o URL principal", + "regex_with_only_domain": "Você não pode usar uma expressão regular para domínio, apenas para caminho", + "registrar_infos": "Informações do registrar", + "restore_already_installed_app": "Um aplicativo com o ID '{app}' já está instalado", + "restore_already_installed_apps": "Os seguintes aplicativos não podem ser restaurados porque já estão instalados: {apps}", + "restore_backup_too_old": "Este arquivo de backup não pode ser restaurado porque vem de uma versão muito antiga do YunoHost.", + "restore_cleaning_failed": "Não foi possível limpar o diretório de restauração temporária", + "restore_complete": "Restauração concluída", + "restore_confirm_yunohost_installed": "Você realmente deseja restaurar um sistema já instalado? [{answers}]", + "restore_extracting": "Extraindo os arquivos necessários do arquivo…", + "restore_failed": "Não foi possível restaurar o sistema", + "restore_hook_unavailable": "O script de restauração para '{part}' não está disponível em seu sistema e também não está no arquivo", + "restore_may_be_not_enough_disk_space": "Seu sistema parece não ter espaço suficiente (livre: {free_space} B, espaço necessário: {needed_space} B, margem de segurança: {margin} B)", + "restore_not_enough_disk_space": "Não há espaço suficiente (espaço: {free_space} B, espaço necessário: {needed_space} B, margem de segurança: {margin} B)", + "restore_nothings_done": "Nada foi restaurado", + "restore_removing_tmp_dir_failed": "Não foi possível remover um diretório temporário antigo", + "restore_running_app_script": "Restaurando o aplicativo '{app}'…", + "restore_running_hooks": "Executando hooks de restauração…", + "restore_system_part_failed": "Não foi possível restaurar a parte do sistema '{part}'", + "root_password_changed": "A senha do root foi alterada", + "root_password_desynchronized": "A senha de administrador foi alterada, mas o YunoHost não pôde propagar isso para a senha de root!", + "server_reboot": "O servidor será reinicializado", + "server_reboot_confirm": "O servidor será reiniciado imediatamente, tem certeza? [{answers}]", + "server_shutdown": "O servidor será desligado", + "server_shutdown_confirm": "O servidor será desligado imediatamente, tem certeza? [{answers}]", + "service_add_failed": "Não foi possível adicionar o serviço '{service}'", + "service_added": "O serviço '{service}' foi adicionado", + "service_already_started": "O serviço '{service}' já está em execução", + "service_already_stopped": "O serviço '{service}' já foi interrompido", + "service_cmd_exec_failed": "Não foi possível executar o comando '{command}'", + "service_description_dnsmasq": "Lida com a resolução de nomes de domínio (DNS)", + "service_description_dovecot": "Permite que clientes de e-mail acessem/busquem e-mails (via IMAP e POP3)", + "service_description_fail2ban": "Protege contra ataques de força bruta e outros tipos de ataques da Internet", + "service_description_mysql": "Armazena dados de aplicativos (banco de dados SQL)", + "service_description_nftables": "Gerencia portas de conexão abertas e fechadas para serviços", + "service_description_nginx": "Serve ou fornece acesso a todos os sites hospedados em seu servidor", + "service_description_opendkim": "Assina e-mails enviados usando DKIM de forma que eles tenham menos probabilidade de serem sinalizados como SPAM", + "service_description_postfix": "Usado para enviar e receber e-mails", + "service_description_postgresql": "Armazena dados de aplicativos (banco de dados SQL)", + "service_description_redis-server": "Um banco de dados especializado usado para acesso rápido a dados, fila de tarefas e comunicação entre programas", + "service_description_slapd": "Armazena usuários, domínios e informações relacionadas", + "service_description_ssh": "Permite que você se conecte remotamente ao seu servidor por meio de um terminal (protocolo SSH)", + "service_description_yunohost-api": "Gerencia as interações entre a interface web do YunoHost e o sistema", + "service_description_yunohost-portal-api": "Gerencia as interações entre as diferentes interfaces web do portal e o sistema", + "service_description_yunomdns": "Permite que você acesse seu servidor usando 'yunohost.local' em sua rede local", + "service_disable_failed": "Não foi possível fazer com que o serviço '{service}' não iniciasse na inicialização.", + "service_disabled": "O serviço '{service}' não será mais iniciado quando o sistema for inicializado.", + "service_enable_failed": "Não foi possível fazer com que o serviço '{service}' inicie automaticamente na inicialização.", + "service_enabled": "O serviço '{service}' agora será iniciado automaticamente durante as inicializações do sistema.", + "service_not_reloading_because_conf_broken": "Não foi possível recarregar/reiniciar o serviço '{name}' porque sua configuração está corrompida: {errors}", + "service_reload_failed": "Não foi possível recarregar o serviço '{service}'", + "service_reload_or_restart_failed": "Não foi possível recarregar ou reiniciar o serviço '{service}'", + "service_reloaded": "Serviço '{service}' recarregado", + "service_reloaded_or_restarted": "O serviço '{service}' foi recarregado ou reiniciado", + "service_remove_failed": "Não foi possível remover o serviço '{service}'", + "service_removed": "Serviço '{service}' removido", + "service_restart_failed": "Não foi possível reiniciar o serviço '{service}'", + "service_restarted": "Serviço '{service}' reiniciado", + "service_start_failed": "Não foi possível iniciar o serviço '{service}'", + "service_started": "Serviço '{service}' iniciado", + "service_stop_failed": "Não foi possível interromper o serviço '{service}'", + "service_stopped": "Serviço '{service}' interrompido", + "service_unknown": "Serviço '{service}' desconhecido", + "session_expired": "Sessão expirada", + "show_tile_cant_be_enabled_for_regex": "Você não pode habilitar 'show_tile' agora, porque o URL para a permissão '{permission}' é uma expressão regular", + "show_tile_cant_be_enabled_for_url_not_defined": "Você não pode habilitar 'show_tile' agora, porque você deve primeiro definir uma URL para a permissão '{permission}'", + "ssowat_conf_generated": "Configurações de SSO e portal regeneradas", + "system_upgraded": "Sistema atualizado", + "system_username_exists": "O nome de usuário já existe na lista de usuários do sistema", + "this_action_broke_dpkg": "Esta ação quebrou o dpkg/APT (os gerenciadores de pacotes do sistema)... Você pode tentar resolver esse problema conectando-se através do SSH e executando `sudo apt install --fix-broken` e/ou `sudo dpkg --configure -a`.", + "tools_upgrade": "Atualizando pacotes do sistema", + "tools_upgrade_failed": "Não foi possível atualizar os pacotes: {packages_list}", + "tos_dyndns_acknowledgement": "Você optou por registrar um domínio DynDNS, um serviço oferecido pelo projeto YunoHost. Considerando que os nomes de domínio são um aspecto importante dos serviços digitais a longo prazo, lembramos que você deve ler atentamente os Termos de Serviço correspondentes, em especial a seção referente aos domínios gratuitos: .", + "tos_postinstall_acknowledgement": "O projeto YunoHost é uma equipe de voluntários que se uniram para criar um sistema operacional gratuito para servidores, chamado YunoHost. O software YunoHost é publicado sob a licença AGPLv3 (). Em relação a este software, o projeto administra e disponibiliza diversos serviços técnicos e comunitários para várias finalidades. Ao utilizar esses serviços, você concorda em estar sujeito aos seguintes Termos de Serviço: .", + "unable_authenticate": "Falha ao autenticar a sessão", + "unbackup_app": "{app} não será salvo", + "unexpected_error": "Algo inesperado deu errado: {error}", + "unknown_error_reading_file": "Erro desconhecido ao tentar ler o arquivo {file}: {error}", + "unknown_group": "Grupo de sistema desconhecido '{group}'", + "unknown_main_domain_path": "Domínio ou caminho desconhecido para '{app}'. Você precisa especificar um domínio e um caminho para poder especificar uma URL para permissão.", + "unknown_user": "Usuário de sistema desconhecido '{user}'", + "unlimit": "Sem cota", + "unrestore_app": "{app} não será restaurado", + "update_apt_cache_failed": "Não é possível atualizar o cache do APT (gerenciador de pacotes do Debian). Aqui está um despejo das linhas de sources.list, que pode ajudar a identificar linhas problemáticas: \n{sourceslist}", + "update_apt_cache_warning": "Algo deu errado ao atualizar o cache do APT (gerenciador de pacotes do Debian). Aqui está um despejo das linhas de sources.list, que pode ajudar a identificar linhas problemáticas: \n{sourceslist}", + "updating_apt_cache": "Buscando atualizações disponíveis para pacotes do sistema…", + "upgrading_packages": "Atualizando pacotes…", + "upnp_dev_not_found": "Nenhum dispositivo UPnP encontrado", + "upnp_disabled": "UPnP desativado", + "upnp_enabled": "UPnP ativado", + "upnp_port_open_failed": "Não foi possível abrir a porta via UPnP", + "user_already_exists": "O usuário '{user}' já existe", + "user_cannot_delete_last_admin": "O usuário '{user}' é o último usuário no grupo 'admins' e não será excluído.", + "user_created": "Usuário criado", + "user_creation_failed": "Não foi possível criar o usuário {user}: {error}", + "user_deleted": "Usuário excluído", + "user_deletion_failed": "Não foi possível excluir o usuário {user}: {error}", + "user_home_creation_failed": "Não foi possível criar a pasta pessoal '{home}' para o usuário", + "user_import_bad_file": "Seu arquivo CSV não está formatado corretamente, ele será ignorado para evitar uma possível perda de dados", + "user_import_bad_line": "Linha {line} incorreta: {details}", + "user_import_cannot_edit_or_delete_admins": "Não foi possível editar ou excluir '{user}' por meio da importação porque o usuário é administrador", + "user_import_failed": "A operação de importação de usuários falhou completamente", + "user_import_missing_columns": "As seguintes colunas estão ausentes: {columns}", + "user_import_nothing_to_do": "Nenhum usuário precisa ser importado", + "user_import_partial_failed": "A operação de importação de usuários falhou parcialmente", + "user_import_success": "Usuários importados com sucesso", + "user_unknown": "Usuário desconhecido: {user}", + "user_update_failed": "Não foi possível atualizar o usuário {user}: {error}", + "user_updated": "Informações do usuário alteradas", + "visitors": "Visitantes", + "yunohost_already_installed": "YunoHost já está instalado", + "yunohost_api": "API do YunoHost", + "yunohost_configured": "YunoHost agora está configurado", + "yunohost_installing": "Instalando o YunoHost…", + "yunohost_not_installed": "O YunoHost não está instalado corretamente. Por favor, execute 'yunohost tools postinstall'", + "yunohost_postinstall_end_tip": "A pós-instalação foi concluída! Para finalizar a configuração, considere o seguinte:\n- Diagnosticar possíveis problemas através da seção 'Diagnóstico' do painel de administração web (ou executando 'yunohost diagnosis run' na linha de comando);\n- Ler as seções 'Finalizando sua configuração' e 'Conhecendo o YunoHost' na documentação do administrador: https://doc.yunohost.org/admin." +} diff --git a/locales/ru.json b/locales/ru.json new file mode 100644 index 0000000..580ca2a --- /dev/null +++ b/locales/ru.json @@ -0,0 +1,926 @@ +{ + "aborting": "Прерывание.", + "action_invalid": "Неверное действие '{action}'", + "additional_urls_already_added": "Этот URL «{url}» уже добавлен в дополнительный URL для разрешения «{permission}»", + "additional_urls_already_removed": "Этот URL «{url}» уже удален из дополнительных URL для разрешения «{permission}»", + "admin_password": "Пароль администратора", + "admins": "Администраторы", + "all_users": "Все пользователи YunoHost", + "already_up_to_date": "Ничего делать не требуется. Всё уже обновлено.", + "app_action_broke_system": "Это действие, по-видимому, нарушило эти важные службы: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Для выполнения этого действия должны быть запущены следующие службы: {services}. Попробуйте перезапустить их, чтобы продолжить (и, возможно, выяснить, почему они не работают).", + "app_action_failed": "Не удалось выполнить действие {action} для приложения {app}", + "app_already_installed": "{app} уже установлено", + "app_already_installed_cant_change_url": "Это приложение уже установлено. URL не может быть изменен только с помощью этой функции. Изучите `app changeurl`, если это доступно.", + "app_arch_not_supported": "Это приложение может быть установлено только на архитектуры {required}, но архитектура вашего сервер - {current}", + "app_argument_choice_invalid": "Выберите корректное значение аргумента '{name}'; '{value}' не входит в число возможных вариантов: '{choices}'", + "app_argument_invalid": "Недопустимое значение аргумента '{name}': {error}", + "app_change_url_failed": "Невозможно изменить URL для {app}: {error}", + "app_change_url_identical_domains": "Старый и новый domain/url_path идентичны ('{domain}{path}'), ничего делать не надо.", + "app_change_url_no_script": "Приложение '{app_name}' не поддерживает изменение URL. Возможно, вам нужно обновить приложение.", + "app_change_url_require_full_domain": "{app} не может быть перемещено на данный URL, потому что оно требует весь домен (т.е., путь - /)", + "app_change_url_script_failed": "Произошла ошибка внутри скрипта смены URL", + "app_change_url_success": "Успешно изменён URL {app} на {domain}{path}", + "app_config__core_name": "Плитки и разрешения", + "app_config_permission_allowed": "Группы/пользователи, которым разрешен доступ", + "app_config_permission_allowed_warn_protected": "Примечание: это разрешение является «защищенным», поэтому группу «посетители» нельзя добавить/удалить из авторизованных групп.", + "app_config_permission_description": "Описание", + "app_config_permission_description_help": "Это действительно полезно только в том случае, если вы используете «описательный» режим портала", + "app_config_permission_extraperm_section_name": "Разрешение '{perm}'", + "app_config_permission_label": "Ярлык", + "app_config_permission_location": "Соответствует [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Использовать собственный логотип", + "app_config_permission_logo_help": "Поддерживаются только файлы формата PNG", + "app_config_permission_show_tile": "Отображение плиток в портале", + "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": "Не удалось скачать материал «{source_id}» ({url}) для {app}: {out}", + "app_full_domain_unavailable": "Извините, это приложение должно быть установлено в собственном домене, но другие приложения уже установлены в домене '{domain}'. Вместо этого вы можете использовать отдельный поддомен для этого приложения.", + "app_id_invalid": "Неправильный ID приложения", + "app_install_failed": "Невозможно установить {app}: {error}", + "app_install_files_invalid": "Эти файлы не могут быть установлены", + "app_install_script_failed": "Произошла ошибка в скрипте установки приложения", + "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_manifest_install_ask_password": "Выберите пароль администратора для этого приложения", + "app_manifest_install_ask_path": "Выберите URL путь (часть после домена), по которому должно быть установлено это приложение", + "app_not_correctly_installed": "{app} , кажется, установлены неправильно", + "app_not_enough_disk": "Это приложение требует {required} свободного места.", + "app_not_enough_ram": "Это приложение требует {required} ОЗУ для установки/обновления, но сейчас доступно только {current}.", + "app_not_installed": "{app} не найдено в списке установленных приложений: {all_apps}", + "app_not_properly_removed": "{app} удалены неправильно", + "app_packaging_format_not_supported": "Это приложение не может быть установлено, поскольку его формат не поддерживается вашей версией YunoHost. Возможно, вам следует обновить систему.", + "app_remove_after_failed_install": "Удаление приложения после ошибки установки…", + "app_removed": "{app} удалено", + "app_requirements_checking": "Проверка зависимостей для {app}…", + "app_resource_failed": "Установка, удаление или обновление ресурсов {app} провалилась: {error}", + "app_restore_failed": "Не удалось восстановить {app}: {error}", + "app_restore_script_failed": "Произошла ошибка внутри сценария восстановления приложения", + "app_sources_fetch_failed": "Невозможно получить исходные файлы, проверьте правильность URL?", + "app_start_backup": "Сбор файлов для резервного копирования {app}…", + "app_start_install": "Устанавливается {app}…", + "app_start_remove": "Удаление {app}…", + "app_start_restore": "Восстановление {app}…", + "app_unknown": "Неизвестное приложение", + "app_unsupported_remote_type": "Неподдерживаемый удаленный тип, используемый для приложения", + "app_upgrade_app_name": "Обновление {app}…", + "app_upgrade_bad_quality": "Это приложение в настоящее время помечено как неработающее в каталоге приложений YunoHost. Это может быть временная проблема, пока разработчики пытаются её исправить. В то же время обновление этого приложения отключено.", + "app_upgrade_broke_the_system": "Обновление {app} вроде бы сработало, но оставило систему в нерабочем состоянии, поэтому считается неудачным.", + "app_upgrade_cli_bad_quality": "Пропуск обновлений для {app}, поскольку в настоящее время оно помечено как неработающее в каталоге приложений YunoHost.", + "app_upgrade_cli_up_to_date": "{app} уже обновленио ({current_version})", + "app_upgrade_cli_url_required": "{app} удалено из каталога и поэтому не может быть обновлено автоматически. Вам следует использовать `yunohost app upgrade {app}`, чтобы указать URL репозитория с помощью опции `-u`.", + "app_upgrade_cli_will_force_upgrade": "{app} будет принудительно обновлено до ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} будет обновлено с версии {current_version} на {new_version}", + "app_upgrade_continuing_with_other_apps": "Не удалось обновить {app}, но обновление других приложений всё равно продолжается (потому что был использован параметр `--continue-on-failure`)", + "app_upgrade_fail_requirements": "Для этого приложения доступна новая версия ({new_version}), но некоторые не выполнены:\n{failed_requirements}", + "app_upgrade_failed": "Невозможно обновить {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "Не удалось обновить приложение '{app}', и оно оставило систему в нерабочем состоянии.", + "app_upgrade_script_failed": "Внутри скрипта обновления приложения произошла ошибка", + "app_upgrade_several_apps": "Будут обновлены следующие приложения: {apps}", + "app_upgrade_some_app_failed": "Некоторые приложения не удалось обновить", + "app_upgrade_specific_channel_msg": "Обратите внимание, что в настоящее время вы используете `{channel}` в качестве источника для обновлений. Обязательно ознакомьтесь с текущим обсуждением [здесь]({pr_url}).", + "app_upgrade_up_to_date": "Принудительное обновление приложения (до той же версии) иногда может быть полезно для пересборки приложения и конфигураций.", + "app_upgrade_upgradable": "Приложение можно обновить с версии {current_version} до {new_version}", + "app_upgrade_url_required": "Это приложение отсутствует в каталоге (или уже удалено?), поэтому вам придётся самостоятельно следить за его обновлениями.
Из командной строки можно использовать команду `yunohost app upgrade ` и указать URL репозитория с помощью опции `-u`.", + "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_catalog_obsolete_cache": "Кэш каталога приложений пуст или устарел.", + "apps_catalog_update_success": "Каталог приложений был обновлён!", + "apps_catalog_updating": "Обновление каталога приложений…", + "apps_confirm_partial_upgrade": "Некоторые приложения, для которых было запрошено обновление, не могут быть обновлены. Продолжить с остальными?", + "apps_no_target_can_be_upgraded": "Ни одно приложение не может быть обновлено", + "apps_upgrade_cancelled": "Обновления для нескольких других приложений всё ещё находились в процессе выполнения, но их обновление было отменено (используйте параметр `--continue-on-failure`, чтобы продолжить выполнение в любом случае): {apps}", + "ask_admin_fullname": "Полное имя администратора", + "ask_admin_username": "Имя пользователя администратора", + "ask_dyndns_recovery_password": "Пароль восстановления DynDNS", + "ask_dyndns_recovery_password_explain": "Пожалуйста, выберите пароль восстановления для Вашего домена DynDNS, для случая, если Вам понадобится сбросить его позже.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Пожалуйста, выберите пароль восстановления для Вашего домена DynDNS.", + "ask_dyndns_recovery_password_explain_unavailable": "Этот домен DynDNS уже зарегистрирован. Если Вы — личность, которая изначально зарегистрировала этот домен, Вы можете ввести пароль, чтобы заново занять этот домен.", + "ask_fullname": "Полное имя", + "ask_main_domain": "Основной домен", + "ask_new_admin_password": "Новый пароль администратора", + "ask_new_domain": "Новый домен", + "ask_new_path": "Новый путь", + "ask_password": "Пароль", + "ask_user_domain": "Домен, используемый в качестве адреса электронной почты пользователя", + "automatic_task": "Автоматическая задача", + "backup_abstract_method": "Этот метод резервного копирования еще не реализован", + "backup_actually_backuping": "Создание резервного архива из собранных файлов…", + "backup_app_script_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_corrupted": "Похоже, что архив резервной копии '{archive}' поврежден : {error}", + "backup_archive_name_exists": "Резервная копия с именем «{name}» уже существует.", + "backup_archive_name_unknown": "Неизвестный локальный архив резервного копирования с именем '{name}'", + "backup_archive_open_failed": "Не удалось открыть архив резервной копии", + "backup_archive_system_part_not_available": "Системная часть '{part}' недоступна в этой резервной копии", + "backup_archive_writing_error": "Не удалось добавить файлы '{source}' (названные в архиве '{dest}') для резервного копирования в архив '{archive}'", + "backup_ask_for_copying_if_needed": "Хотите ли вы временно выполнить резервное копирование с использованием {size}MB? (Этот способ используется, поскольку некоторые файлы не могут быть подготовлены более эффективным методом.)", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "Резервная копия {name} была удалена, поскольку она заменена более новой резервной копией {newname}", + "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'", + "backup_custom_mount_error": "Пользовательский метод резервного копирования не смог пройти этап 'mount'", + "backup_delete_error": "Не удалось удалить '{path}'", + "backup_deleted": "Резервная копия удалена: {name}", + "backup_hook_unknown": "Хук резервного копирования «{hook}» неизвестен", + "backup_method_copy_finished": "Создание копии бэкапа завершено", + "backup_method_custom_finished": "Пользовательский метод резервного копирования '{method}' завершен", + "backup_method_tar_finished": "Создан резервный TAR-архив", + "backup_mount_archive_for_restore": "Подготовка архива для восстановления…", + "backup_no_file_collected": "Не удалось собрать файлы для резервного копирования", + "backup_no_uncompress_archive_dir": "Такой несжатой директории не существует", + "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_output_symlink_dir_broken": "Директория «{path}» Вашего архива — сломанная символьная ссылка. Может быть, Вы забыли смонтировать или подключить устройство, на которое она ссылается.", + "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} не имеет сценария восстановления, вы не сможете автоматически восстановить это приложение из резервной копии.", + "cannot_open_file": "Не могу открыть файл {file} (причина: {error})", + "cannot_write_file": "Не могу записать файл {file} (причина: {error})", + "certmanager_acme_not_configured_for_domain": "Задача ACME не может быть запущена для {domain} прямо сейчас, потому что в его конфигурации nginx отсутствует соответствующий фрагмент кода… Пожалуйста, убедитесь, что Ваша конфигурация nginx обновлена, используя «yunohost tools regen-conf nginx --dry-run --with-diff».", + "certmanager_attempt_to_renew_nonLE_cert": "Сертификат для домена '{domain}' не выпущен Let's Encrypt. Невозможно продлить его автоматически!", + "certmanager_attempt_to_renew_valid_cert": "Срок действия сертификата для домена '{domain}' НЕ истекает! (Вы можете использовать --force, если знаете, что делаете)", + "certmanager_attempt_to_replace_valid_cert": "Вы пытаетесь перезаписать хороший и действительный сертификат для домена {domain}! (Используйте --force для обхода)", + "certmanager_cannot_read_cert": "При попытке открыть текущий сертификат для домена {domain}что-то пошло не так (файл: {file}), причина: {reason}", + "certmanager_cert_install_failed": "Не удалась установка сертификата Let's Encrypt для {domains}", + "certmanager_cert_install_failed_selfsigned": "Установка само-подписанного сертификата для {domains} не удалась", + "certmanager_cert_install_success": "Сертификат Let's Encrypt для домена '{domain}' установлен", + "certmanager_cert_install_success_selfsigned": "Самоподписанный сертификат для домена '{domain}' установлен", + "certmanager_cert_renew_failed": "Не удалось продлить сертификат Let's Encrypt для {domains}", + "certmanager_cert_renew_success": "Обновлен сертификат Let's Encrypt для домена '{domain}'", + "certmanager_cert_signing_failed": "Не удалось подписать новый сертификат", + "certmanager_certificate_fetching_or_enabling_failed": "Попытка использовать новый сертификат для {domain} не сработала…", + "certmanager_domain_cert_not_selfsigned": "Сертификат для домена {domain} не самоподписанный. Вы уверены, что хотите заменить его? (Для этого используйте '--force'.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "Записи DNS для домена '{domain}' отличаются от IP-адреса этого сервера. Пожалуйста, проверьте категорию \"Записи DNS\" (основные) в диагностике для получения дополнительной информации. Если вы недавно изменили свою запись A, пожалуйста, дождитесь ее распространения (в Интернете доступны некоторые средства проверки распространения DNS). (Если вы знаете, что делаете, используйте \"--no-checks\", чтобы отключить эти проверки)", + "certmanager_domain_http_not_working": "Похоже, домен {domain} не доступен через HTTP. Пожалуйста, проверьте категорию 'Домены' в диагностике для получения дополнительной информации. (Если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", + "certmanager_domain_not_diagnosed_yet": "Для домена {domain} еще нет результатов диагностики. Пожалуйста, перезапустите диагностику для категорий 'DNS-записи' и 'Домены', чтобы проверить, готов ли домен к Let's Encrypt. (Или, если вы знаете, что делаете, используйте '--no-checks', чтобы отключить эти проверки.)", + "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})", + "certmanager_unable_to_parse_self_CA_name": "Не удалось проанализировать имя самоподписанного центра сертификации (файл: {file})", + "config_action_disabled": "Не удалось выполнить действие '{action}', поскольку оно отключено. Убедитесь, что выполнены все ограничения. Справка: {help}", + "config_action_failed": "Не удалось выполнить действие '{action}': {error}", + "config_apply_failed": "Не удалось применить новую конфигурацию: {error}", + "config_cant_set_value_on_section": "Вы не можете установить одно значение на весь раздел конфигурации.", + "config_forbidden_keyword": "Ключевое слово '{keyword}' зарезервировано, вы не можете создать или использовать панель конфигурации с вопросом с таким id.", + "config_forbidden_readonly_type": "Тип '{type}' не может быть установлен как только для чтения, используйте другой тип для отображения этого значения (соответствующий идентификатор аргумента: '{id}').", + "config_no_panel": "Панель конфигурации не найдена.", + "config_unknown_filter_key": "Ключ фильтра '{filter_key}' неверен.", + "confirm_app_install_danger": "ОПАСНО! Это приложение все еще является экспериментальным (если не сказать, что оно явно не работает)! Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку… Если вы все равно готовы рискнуть, введите '{answers}'", + "confirm_app_install_thirdparty": "ВАЖНО! Это приложение не входит в каталог приложений YunoHost. Установка сторонних приложений может нарушить целостность и безопасность вашей системы. Вам НЕ следует устанавливать его, если вы НЕ знаете, что делаете. Если это приложение не будет работать или сломает вашу систему, мы НЕ будем оказывать техническую поддержку… Если вы все равно готовы рискнуть, введите '{answers}'", + "confirm_app_install_warning": "Предупреждение: Это приложение может работать, но пока еще недостаточно интегрировано в YunoHost. Некоторые функции, такие как единая регистрация и резервное копирование/восстановление, могут быть недоступны. Все равно установить? [{answers}] ", + "confirm_app_insufficient_ram": "Для установки этого приложения требуется больше оперативной памяти, чем доступно в настоящее время. Даже если это приложение сможет работать, процесс его установки/обновления требует большого количества оперативной памяти, поэтому ваш сервер может зависнуть и выйти из строя. Если вы всё же готовы пойти на этот риск, введите '{answers}'", + "confirm_notifications_read": "ВНИМАНИЕ: прежде чем продолжить, проверьте уведомления приложения выше, там может быть важная информация. [{answers}]", + "confirm_tos_acknowledgement": "Я прочитал(а) и понял(а) Условия предоставления услуг [{answers}]", + "corrupted_json": "Повреждённый json получен от {ressource} (причина: {error})", + "corrupted_toml": "Поврежденный TOML, прочитанный из {ressource} (причина: {error})", + "corrupted_yaml": "Повреждённой YAML получен от {ressource} (причина: {error})", + "danger": "Опасно:", + "diagnosis_apps_allgood": "Все установленные приложения соблюдают основные правила упаковки", + "diagnosis_apps_bad_quality": "В настоящее время это приложение отмечено как неработающее в каталоге приложений YunoHost. Это может быть временной проблемой, пока сопровождающий пытается исправить проблему. Пока что обновление этого приложения отключено.", + "diagnosis_apps_broken": "В настоящее время это приложение отмечено как неработающее в каталоге приложений YunoHost. Это может быть временной проблемой, пока сопровождающие пытаются исправить проблему. Пока что обновления для этого приложения отключены.", + "diagnosis_apps_deprecated_practices": "Установленная версия этого приложения все еще использует некоторые устаревшие пакеты. Вам стоит подумать об обновлении.", + "diagnosis_apps_issue": "Обнаружена проблема для приложения {app}", + "diagnosis_apps_not_in_app_catalog": "Этого приложения нет в каталоге приложений YunoHost. Если оно было в прошлом и было удалено, вам следует рассмотреть возможность удаления этого приложения, поскольку оно не будет обновляться и может нарушить целостность и безопасность вашей системы.", + "diagnosis_apps_outdated_packaging_format": "Это приложение использует устаревший формат пакетов и в скором времени перестанет поддерживаться YunoHost. Вам следует серьезно подумать об его обновлении.", + "diagnosis_apps_outdated_ynh_requirement": "Установленная версия этого приложения требует только yunohost >= 2.x, 3.x или 4.x, что указывает на то, что оно не соответствует рекомендуемым практикам упаковки и помощникам. Вам следует рассмотреть возможность его обновления.", + "diagnosis_apps_security_issue_error": "Приложение {app} версии '{current_version}' (установленная у вас) имеет СЕРЬЕЗНЫЕ проблемы с безопасностью: {title}. Рекомендуется КАК МОЖНО СКОРЕЕ обновить его до версии '{fixed_in_version}'. Подробнее: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "Приложение {app} в версии '{current_version}' (установленная у вас) имеет проблемы с безопасностью: {title}. Рекомендуется обновить его до версии '{fixed_in_version}'. Подробнее: {more_infos_list}", + "diagnosis_backports_in_sources_list": "Похоже, что apt (менеджер пакетов) настроен на использование репозитория backports. Если вы действительно не знаете, что делаете, мы настоятельно не рекомендуем устанавливать пакеты из backports, поскольку это может привести к нестабильности или конфликтам в вашей системе.", + "diagnosis_basesystem_hardware": "Аппаратная архитектура сервера – {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Модель сервера {model}", + "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_cant_run_because_of_dep": "Невозможно выполнить диагностику для {category}, пока есть важные проблемы, связанные с {dep}.", + "diagnosis_description_apps": "Приложения", + "diagnosis_description_basesystem": "Основная система", + "diagnosis_description_dnsrecords": "DNS-записи", + "diagnosis_description_ip": "Интернет-соединение", + "diagnosis_description_mail": "Электронная почта", + "diagnosis_description_ports": "Открытые порты", + "diagnosis_description_regenconf": "Конфигурации системы", + "diagnosis_description_services": "Проверка статусов сервисов", + "diagnosis_description_systemresources": "Системные ресурсы", + "diagnosis_description_web": "Доступность доменов", + "diagnosis_diskusage_low": "В хранилище {mountpoint} (на устройстве {device}) осталось {free} ({free_percent}%) места (из {total}). Будьте осторожны.", + "diagnosis_diskusage_ok": "Хранилище {mountpoint} (на устройстве {device}) имеет еще {free} ({free_percent}%) свободного места (всего {total})!", + "diagnosis_diskusage_verylow": "В хранилище {mountpoint} (на устройстве {device}) осталось лишь {free} ({free_percent}%) свободного пространства (из {total}). Вам действительно следует подумать об освобождении места!", + "diagnosis_display_tip": "Чтобы увидеть найденные проблемы, вы можете перейти в раздел Диагностика в веб-интерфейсе или выполнить команду 'yunohost diagnosis show --issues --human-readable' из командной строки.", + "diagnosis_dns_bad_conf": "Некоторые записи DNS для домена {domain} (категория {category}) отсутствуют или неверны", + "diagnosis_dns_discrepancy": "Следующая запись DNS, по-видимому, не соответствует рекомендуемой конфигурации:
Тип: {type}
Имя: {name}
Текущее значение: {current}
Ожидаемое значение: {content}", + "diagnosis_dns_good_conf": "DNS-записи правильно настроены для домена {domain} (категория {category})", + "diagnosis_dns_missing_record": "В соответствии с рекомендуемой конфигурацией DNS, вам необходимо добавить запись DNS со следующей информацией.
Тип: {type}
Имя: {name}
Значение: {content}", + "diagnosis_dns_point_to_doc": "Если вам нужна помощь по настройке DNS-записей, обратитесь к документации на сайте https://doc.yunohost.org/dns_config.", + "diagnosis_dns_specialusedomain": "Домен {domain} создан на основе домена верхнего уровня специального назначения (TLD), такого как .local или .test, и поэтому не должен иметь фактических записей DNS.", + "diagnosis_dns_try_dyndns_update_force": "Конфигурация DNS этого домена должна автоматически управляться YunoHost. Если это не так, вы можете попробовать принудительно обновить конфигурацию с помощью команды yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Срок действия некоторых доменов истекает ОЧЕНЬ СКОРО!", + "diagnosis_domain_expiration_not_found": "Невозможно проверить срок действия для некоторых доменов", + "diagnosis_domain_expiration_not_found_details": "Информация WHOIS для домена {domain} не содержит сведений о дате истечения срока действия?", + "diagnosis_domain_expiration_success": "Ваши домены зарегистрированы и не потеряют актуальность в ближайшее время.", + "diagnosis_domain_expiration_warning": "Срок действия некоторых доменов скоро истечёт!", + "diagnosis_domain_expires_in": "Срок действия домена {domain} заканчивается через {days} дней.", + "diagnosis_domain_not_found_details": "Домен {domain} не существует в базе данных WHOIS или срок его действия истёк!", + "diagnosis_everything_ok": "Все выглядит отлично для {category}!", + "diagnosis_failed": "Не удалось получить результат диагностики для категории '{category}': {error}", + "diagnosis_failed_for_category": "Не удалось провести диагностику для категории '{category}': {error}", + "diagnosis_found_errors": "Есть {errors} существенная проблема(ы), связанная с {category}!", + "diagnosis_found_errors_and_warnings": "Найдены {errors} значительных проблем (и {warnings} предупреждений), связанных с {category}!", + "diagnosis_found_warnings": "Найдено {warnings} элементов, которые можно улучшить для {category}.", + "diagnosis_high_number_auth_failures": "В последнее время наблюдается подозрительно высокое количество сбоев аутентификации. Рекомендуется убедиться, что fail2ban работает и правильно настроен, или использовать пользовательский порт для SSH, как описано в https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Похоже, что вместо вашего сервера ответил другой компьютер (возможно, ваш интернет-маршрутизатор).
1. Наиболее частая причина этой проблемы заключается в том, что порты 80 (и 443) неправильно перенаправлены на ваш сервер.
2. В более сложных настройках: убедитесь, что брандмауэр или обратный прокси не создают помех.", + "diagnosis_http_connection_error": "Ошибка подключения: не удалось подключиться к запрошенному домену, скорее всего, он недоступен.", + "diagnosis_http_could_not_diagnose": "Не удалось определить, доступны ли домены извне в IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Ошибка: {error}", + "diagnosis_http_hairpinning_issue": "В вашей локальной сети, по-видимому, не включена функция hairpinning.", + "diagnosis_http_hairpinning_issue_details": "Вероятно, это связано с вашим интернет-провайдером/маршрутизатором. В результате, люди, находящиеся за пределами вашей локальной сети, смогут получить доступ к вашему серверу, как и ожидалось, но люди, находящиеся внутри локальной сети (например, вы, вероятно?), не смогут этого сделать при использовании доменного имени или глобального IP-адреса. Вы можете улучшить ситуацию, ознакомившись с информацией на сайте https://doc.yunohost.org/dns_local_network", + "diagnosis_http_nginx_conf_not_up_to_date": "Конфигурация nginx для этого домена, по-видимому, была изменена вручную, что не позволяет YunoHost определить, доступен ли он по HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Чтобы исправить ситуацию, проверьте разницу из командной строки с помощью yunohost tools regen-conf nginx --dry-run --with-diff и, если вас всё устраивает, примените изменения с помощью yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "Домен {domain} доступен через HTTP из-за пределов локальной сети.", + "diagnosis_http_partially_unreachable": "Домен {domain} недоступен через HTTP из-за пределов локальной сети в IPv{failed}, хотя он работает в IPv{passed}.", + "diagnosis_http_special_use_tld": "Домен {domain} создан на основе домена верхнего уровня специального назначения (TLD), такого как .local или .test, и поэтому не должен быть доступен за пределами локальной сети.", + "diagnosis_http_timeout": "Истекло время ожидания при попытке подключиться к вашему серверу извне. Похоже, он недоступен.
1. Наиболее частая причина этой проблемы заключается в том, что порты 80 (и 443) неправильно перенаправлены на ваш сервер.
2. Вы также должны убедиться, что служба nginx работает.
3. В более сложных настройках: убедитесь, что брандмауэр или обратный прокси не создают помех.", + "diagnosis_http_unreachable": "Домен {domain} недоступен через HTTP из-за пределов локальной сети.", + "diagnosis_ignore_already_filtered": "(Уже существует фильтр диагностики {category} с этими критериями)", + "diagnosis_ignore_criteria_error": "Критерии должны иметь формат ключ=значение (например, domain=yolo.test)", + "diagnosis_ignore_filter_added": "Добавлен фильтр диагностики {category}", + "diagnosis_ignore_filter_removed": "Удалён фильтр диагностики {category}", + "diagnosis_ignore_missing_criteria": "Вы должны указать как минимум один критерий, который будет категорией диагностики для игнорирования", + "diagnosis_ignore_no_filter_found": "(Нет такого фильтра диагностики {category} с этими критериями для удаления)", + "diagnosis_ignore_no_issue_found": "Не найдено ни одной проблемы, соответствующей заданным критериям.", + "diagnosis_ignored_issues": "(+ {nb_ignored} проигнорированных проблем)", + "diagnosis_ip_broken_dnsresolution": "По какой-то причине разрешение доменных имён не работает... Может быть, брандмауэр блокирует DNS-запросы?", + "diagnosis_ip_broken_resolvconf": "Похоже, на вашем сервере не работает разрешение доменных имён, что, вероятно, связано с тем, что файл /etc/resolv.conf не указывает на 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "Сервер подключён к Интернету через IPv4!", + "diagnosis_ip_connected_ipv6": "Сервер подключён к Интернету через IPv6!", + "diagnosis_ip_dnsresolution_working": "Разрешение доменных имён работает!", + "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://doc.yunohost.org/ipv6. Если вы не можете включить IPv6 или это кажется вам слишком сложным, вы можете безопасно игнорировать это предупреждение.", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 обычно должен настраиваться автоматически системой или вашим провайдером, если он доступен. В противном случае вам может потребоваться настроить некоторые параметры вручную, как описано в документации по адресу: https://doc.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 должен быть символьной ссылкой на /etc/resolvconf/run/resolv.conf, указывающей на 127.0.0.1 (dnsmasq). Если вы хотите настроить разрешения DNS вручную, отредактируйте файл /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blocklist_listed_by": "Ваш IP-адрес или домен {item} занесены в чёрный список {blocklist_name}", + "diagnosis_mail_blocklist_ok": "IP-адреса и домены, используемые этим сервером, не фигурируют в списках заблокированных адресов (чёрных списках)", + "diagnosis_mail_blocklist_reason": "Причина блокировки: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Похоже, причина связана с 'open resolver'.
Обычно это означает, что ваш сервер использует не локальный DNS, а общедоступный, открытый.
Проверьте содержимое файла /etc/resolv.conf, в нём должно быть значение nameserver 127.0.0.1.
Поскольку этот файл обычно создается автоматически, не редактируйте его вручную. Проверьте настройки DHCP или настройки VPN, если вы их используете, или, если вы использовали образ Debian, созданный, например, поставщиком VPS, найдите конфигурацию cloudinit.
Вы можете обратиться за помощью по этому вопросу в каналы поддержки YunoHost.
Дословная причина, указанная в чёрном списке: {reason}", + "diagnosis_mail_blocklist_website": "После выяснения причины включения в список и её устранения, вы можете запросить удаление вашего IP-адреса или домена на {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Служба, не относящаяся к SMTP, ответила на порту 25 на IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Это может быть связано с тем, что вместо вашего сервера отвечает другой компьютер.", + "diagnosis_mail_ehlo_could_not_diagnose": "Не удалось определить, доступен ли почтовый сервер postfix извне в IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Ошибка: {error}", + "diagnosis_mail_ehlo_ok": "Почтовый сервер SMTP доступен извне и поэтому может принимать электронные письма!", + "diagnosis_mail_ehlo_unreachable": "Почтовый сервер SMTP недоступен извне по IPv{ipversion}. Он не сможет принимать электронные письма.", + "diagnosis_mail_ehlo_unreachable_details": "Не удалось установить соединение по порту 25 с вашим сервером в IPv{ipversion}. Похоже, он недоступен.
1. Наиболее частая причина этой проблемы заключается в том, что порт 25 неправильно перенаправлен на ваш сервер.
2. Вы также должны убедиться, что служба postfix работает.
3. В более сложных настройках: убедитесь, что брандмауэр или обратный прокси не мешают работе.", + "diagnosis_mail_ehlo_wrong": "Другой почтовый сервер SMTP отвечает на IPv{ipversion}. Ваш сервер, вероятно, не сможет принимать электронные письма.", + "diagnosis_mail_ehlo_wrong_details": "EHLO, полученный удаленным диагностическим устройством в IPv{ipversion}, отличается от домена вашего сервера.
Полученный 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_4": "Некоторые провайдеры не позволяют настраивать обратный DNS (или их функция может быть неисправна…). Если у вас возникли проблемы из-за этого, рассмотрите следующие решения:
- Некоторые интернет-провайдеры предлагают альтернативу в виде использования почтового сервера-ретранслятора, это также означает, что ретранслятор сможет отслеживать ваш почтовый трафик.
- Альтернативой, обеспечивающей конфиденциальность, является использование VPN *с выделенным публичным IP-адресом* для обхода такого рода ограничений. См. https://doc.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_details": "Сначала следует попробовать настроить обратный DNS с помощью {ehlo_domain} в интерфейсе интернет-маршрутизатора или интерфейсе хостинг-провайдера. (Некоторые хостинг-провайдеры могут потребовать от вас отправить им заявку в службу поддержки).", + "diagnosis_mail_fcrdns_ok": "Ваш обратный DNS настроен правильно!", + "diagnosis_mail_outgoing_port_25_blocked": "Почтовый сервер SMTP не может отправлять электронные письма на другие серверы, поскольку исходящий порт 25 заблокирован в IPv{ipversion}.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Сначала попробуйте разблокировать исходящий порт 25 в интерфейсе интернет-маршрутизатора или интерфейсе хостинг-провайдера. (Некоторые хостинг-провайдеры могут потребовать от вас отправить им заявку в службу поддержки для этого).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Некоторые провайдеры не позволяют разблокировать исходящий порт 25, потому что не заботятся о сетевом нейтралитете.
- Некоторые из них предлагают альтернативу в виде использования почтового сервера-ретранслятора, хотя это означает, что ретранслятор сможет отслеживать ваш почтовый трафик.
- Альтернативой, обеспечивающей конфиденциальность, является использование VPN *с выделенным публичным IP-адресом* для обхода подобных ограничений. См. https://doc.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_never_ran_yet": "Похоже, что этот сервер был настроен недавно и пока не содержит отчёта о диагностике. Вам следует начать с полной диагностики, либо через веб-панель администратора, либо с помощью команды 'yunohost diagnosis run' из командной строки.", + "diagnosis_no_cache": "Пока нет кэша диагностики для категории '{category}'", + "diagnosis_package_installed_from_sury": "Некоторые системные пакеты должны быть понижены до более ранней версии", + "diagnosis_package_installed_from_sury_details": "Некоторые пакеты были случайно установлены из стороннего репозитория под названием Sury. Команда YunoHost улучшила стратегию обработки этих пакетов, но ожидается, что в некоторых настройках, в которых были установлены приложения PHP7.3, пока ещё на Stretch, остались некоторые несоответствия. Чтобы исправить эту ситуацию, попробуйте выполнить следующую команду: {cmd_to_fix}", + "diagnosis_package_security_issue_error": "Системный пакет '{package}' версии '{current_version}' (сейчас установленная) имеет СЕРЬЕЗНЫЕ проблемы с безопасностью: {title}. Рекомендуется КАК МОЖНО СКОРЕЕ обновить его до версии '{fixed_in_version}'. Подробнее: {more_infos_list}", + "diagnosis_package_security_issue_warning": "Системный пакет '{package}' версии '{current_version}' (сейчас установленная) имеет проблемы с безопасностью: {title}. Рекомендуется обновить его до версии '{fixed_in_version}'. Подробнее: {more_infos_list}", + "diagnosis_ports_could_not_diagnose": "Не удалось определить, доступны ли порты извне в IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Ошибка: {error}", + "diagnosis_ports_forwarding_tip": "Чтобы решить эту проблему, вам, скорее всего, необходимо настроить переадресацию портов на вашем маршрутизаторе, как описано в https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Открытие этого порта необходимо для функций {category} (служба {service})", + "diagnosis_ports_ok": "Порт {port} доступен извне.", + "diagnosis_ports_partially_unreachable": "Порт {port} недоступен извне в IPv{failed}.", + "diagnosis_ports_unreachable": "Порт {port} недоступен извне.", + "diagnosis_processes_killed_by_oom_reaper": "Некоторые процессы были недавно завершены системой из-за нехватки памяти. Обычно это является признаком нехватки памяти в системе или процессом, потребляющим слишком много памяти. Сводка завершенных процессов:\n{kills_summary}", + "diagnosis_ram_low": "В системе {available} ({available_percent}%) доступной оперативной памяти (из {total}). Будьте осторожны.", + "diagnosis_ram_ok": "В системе доступно {available} ({available_percent}%) оперативной памяти из {total}.", + "diagnosis_ram_verylow": "В системе доступно лишь {available} ({available_percent}%) оперативной памяти! (Из {total})", + "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_rfkill_wifi": "Wi-Fi-карта отключена и системное предупреждение может препятствовать установке приложений", + "diagnosis_rfkill_wifi_details": "Это предупреждение появляется во многих выводах команд, что приводит к сбоям в работе некоторых приложений. Обычно требуется указать код вашей страны с помощью команды sudo raspi-config. Вот ошибка:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "Корневая файловая система имеет всего {space}, что вызывает серьезную обеспокоенность! Скорее всего, вы очень быстро исчерпаете дисковое пространство! Рекомендуется иметь не менее 16 ГБ для корневой файловой системы.", + "diagnosis_rootfstotalspace_warning": "Корневая файловая система имеет всего {space} места. Это может быть нормально, но будьте осторожны, потому что в конечном итоге вы можете быстро исчерпать дисковое пространство... Рекомендуется иметь не менее 16 ГБ для корневой файловой системы.", + "diagnosis_security_vulnerable_to_meltdown": "Вы подвержены критической уязвимости Meltdown", + "diagnosis_security_vulnerable_to_meltdown_details": "Чтобы исправить эту проблему, вам необходимо обновить систему и перезагрузить компьютер, чтобы загрузить новое ядро Linux (или обратиться к поставщику серверных услуг, если это не помогло). Дополнительную информацию см. на сайте https://meltdownattack.com/.", + "diagnosis_services_bad_status": "Служба {service} имеет статус {status} :(", + "diagnosis_services_bad_status_tip": "Вы можете попробовать перезапустить службу, и если это не поможет, посмотрите журналы службы в веб-панели администратора (из командной строки это можно сделать с помощью yunohost service restart {service} and yunohost service log {service}).", + "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_sshd_config_inconsistent_details": "Пожалуйста, выполните yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT, чтобы определить порт SSH, и проверьте yunohost tools regen-conf ssh --dry-run --with-diff и yunohost tools regen-conf ssh --force, чтобы сбросить ваш conf в соответствии с рекомендациями YunoHost.", + "diagnosis_sshd_config_insecure": "Похоже, что конфигурация SSH была изменена вручную, и она небезопасна, поскольку не содержит директив 'AllowGroups' или 'AllowUsers' для ограничения доступа авторизованных пользователей.", + "diagnosis_swap_none": "Система вообще не имеет свопа. Вы должны рассмотреть возможность добавления по крайней мере {recommended} объема подкачки, чтобы избежать ситуаций, когда в системе заканчивается память.", + "diagnosis_swap_notsomuch": "В системе имеется только {total} своп. Вам следует иметь не менее {recommended}, чтобы избежать ситуаций, когда в системе заканчивается память.", + "diagnosis_swap_ok": "Система имеет {total} свопа!", + "diagnosis_swap_tip": "Будьте осторожны и помните, что если сервер размещает подкачку на SD-карте или SSD-накопителе, это может значительно сократить срок службы устройства.", + "diagnosis_unknown_categories": "Следующие категории неизвестны: {categories}", + "diagnosis_using_stable_codename": "apt (системный менеджер пакетов) в настоящее время настроен на установку пакетов с кодовым названием «stable» вместо кодового названия текущей версии Debian (bookworm).", + "diagnosis_using_stable_codename_details": "Обычно это вызвано неправильной настройкой со стороны вашего хостинг-провайдера. Это опасно, потому что как только следующая версия Debian станет новой «стабильной», apt захочет обновить все системные пакеты без прохождения надлежащей процедуры миграции. Рекомендуется исправить это, отредактировав источник apt для базового репозитория Debian и заменив ключевое слово stable на bookworm. Соответствующий файл конфигурации должен быть /etc/apt/sources.list или файл в /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (системный менеджер пакетов) в настоящее время настроен на установку любых «тестовых» обновлений для ядра YunoHost.", + "diagnosis_using_yunohost_testing_details": "Это, возможно, нормально, если вы знаете, что делаете, но обратите внимание на примечания к выпуску перед установкой обновлений YunoHost! Если вы хотите отключить «тестовые» обновления, вам следует удалить ключевое слово testing из файла /etc/apt/sources.list.d/yunohost.list.", + "disk_space_not_sufficient_install": "Недостаточно места на диске для установки этого приложения", + "disk_space_not_sufficient_update": "Недостаточно места на диске для обновления этого приложения", + "domain_cannot_remove_main": "Вы не можете удалить '{domain}', поскольку это основной домен. Сначала необходимо установить другой домен в качестве основного с помощью команды 'yunohost domain main-domain -n '; вот список доменов, которые можно использовать: {other_domains}", + "domain_cannot_remove_main_add_new_one": "Вы не можете удалить '{domain}', поскольку это основной домен и ваш единственный домен. Сначала необходимо добавить другой домен с помощью команды 'yunohost domain add ', затем установить его в качестве основного домена с помощью команды 'yunohost domain main-domain -n ' , после чего вы сможете удалить домен '{domain}' с помощью команды 'yunohost domain remove {domain}'.", + "domain_cert_gen_failed": "Не удалось сгенерировать сертификат", + "domain_config_acme_eligible": "Право на участие в ACME", + "domain_config_acme_eligible_explain": "Этот домен, похоже, не готов для получения сертификата Let's Encrypt. Проверьте настройки DNS и доступность HTTP-сервера. Разделы «Записи DNS» и «Веб» на странице диагностики помогут вам понять, что именно настроено неверно.", + "domain_config_api_protocol": "Протокол API", + "domain_config_auth_application_key": "Ключ приложения", + "domain_config_auth_application_secret": "Секретный ключ приложения", + "domain_config_auth_consumer_key": "Потребительский ключ", + "domain_config_auth_entrypoint": "Точка входа API", + "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_name": "Сертификат", + "domain_config_cert_no_checks": "Игнорировать диагностические проверки", + "domain_config_cert_renew": "Обновить сертификат Let's Encrypt", + "domain_config_cert_renew_help": "Сертификат будет автоматически продлён в течение последних 15 дней срока его действия. Вы можете продлить его вручную, если хотите. (Не рекомендуется).", + "domain_config_cert_summary": "Статус сертификата", + "domain_config_cert_summary_abouttoexpire": "Срок действия текущего сертификата истекает. В ближайшее время он должен быть автоматически продлён.", + "domain_config_cert_summary_expired": "КРИТИЧЕСКОЕ: Текущий сертификат недействителен! HTTPS не будет работать вообще!", + "domain_config_cert_summary_letsencrypt": "Отлично! Вы используете действительный сертификат Let's Encrypt!", + "domain_config_cert_summary_ok": "Ладно, текущий сертификат выглядит нормально!", + "domain_config_cert_summary_selfsigned": "ПРЕДУПРЕЖДЕНИЕ: Текущий сертификат является самоподписанным. Браузеры будут отображать пугающее предупреждение для новых посетителей!", + "domain_config_cert_validity": "Достоверность", + "domain_config_custom_css": "Пользовательская таблица стилей CSS", + "domain_config_custom_css_help": "Это предназначено для опытных администраторов, желающих настроить внешний вид портала", + "domain_config_default_app": "Приложение по умолчанию", + "domain_config_default_app_help": "При открытии этого домена пользователи будут автоматически перенаправлены в это приложение. Если приложение не указано, пользователи будут перенаправлены на форму входа в портал.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Показать посетителям список общедоступных приложений", + "domain_config_enable_public_apps_page_help": "Посетители увидят страницу «общедоступных приложений» при переходе на портал, а не только форму входа в систему.", + "domain_config_feature_name": "Особенности", + "domain_config_mail_in": "Входящие письма", + "domain_config_mail_out": "Исходящие письма", + "domain_config_portal_logo": "Пользовательский логотип", + "domain_config_portal_logo_help": "Принимаются форматы .svg, .png и .jpeg. Предпочтительнее монохромный формат .svg с fill: currentColor, чтобы логотип адаптировался к темам.", + "domain_config_portal_name": "Настройка портала", + "domain_config_portal_public_intro": "Пользовательское публичное вступление", + "domain_config_portal_public_intro_help": "Вы можете использовать HTML, базовые стили будут применены к общим элементам.", + "domain_config_portal_theme": "Цветовая схема по умолчанию", + "domain_config_portal_theme_help": "Пользователи могут выбрать другой вариант в настройках.", + "domain_config_portal_tile_theme": "Тема отображения плиток приложений", + "domain_config_portal_title": "Пользовательский заголовок", + "domain_config_portal_user_intro": "Приветствие", + "domain_config_portal_user_intro_help": "Вы можете использовать HTML, базовые стили будут применены к общим элементам.", + "domain_config_search_engine": "URL поисковой системы", + "domain_config_search_engine_help": "Это дополнительная функция, позволяющая отображать панель поиска в портале (например, если вы хотите использовать портал YunoHost в качестве домашней страницы браузера). Это должен быть URL-адрес с пустой строкой запроса, например `https://duckduckgo.com/?q=`, где `q=` — пустой параметр запроса duckduckgo", + "domain_config_search_engine_name": "Название поискового движка", + "domain_config_show_other_domains_apps": "Показать приложения другого домена", + "domain_created": "Домен создан", + "domain_creation_failed": "Невозможно создать домен {domain}: {error}", + "domain_deleted": "Домен удален", + "domain_deletion_failed": "Невозможно удалить домен {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "Эта страница показывает вам *рекомендуемую* конфигурацию. Она не создаёт для вас конфигурацию DNS. Вы должны сами конфигурировать DNS у вашего регистратора в соответствии с этой рекомендацией.", + "domain_dns_conf_special_use_tld": "Этот домен основан на домене верхнего уровня специального назначения (TLD), таком как .local или .test, и поэтому не предполагает наличия реальных записей DNS.", + "domain_dns_push_already_up_to_date": "Записи уже обновлены, ничего делать не нужно.", + "domain_dns_push_failed": "Обновление записей DNS потерпело неудачу.", + "domain_dns_push_failed_to_list": "Не удалось перечислить текущие записи с помощью API регистратора: {error}", + "domain_dns_push_managed_in_parent_domain": "Функция автоматической настройки DNS управляется в родительском домене {parent_domain}.", + "domain_dns_push_not_applicable": "Функция автоматической настройки DNS не применима к домену {domain}. Вам необходимо вручную настроить записи DNS, следуя инструкциям, приведенным в документации по адресу https://doc.yunohost.org/dns_config.", + "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": "На данный момент интерфейс с API **{registrar}** не был должным образом протестирован и проверен сообществом YunoHost. Поддержка находится на **очень экспериментальной стадии** — будьте осторожны!", + "domain_dns_registrar_managed_in_parent_domain": "Этот домен является субдоменом {parent_domain_link}. Настройки DNS-регистратора должны управляться в панели настроек {parent_domain}.", + "domain_dns_registrar_not_supported": "YunoHost не смог автоматически определить регистратора, обслуживающего этот домен. Вам необходимо вручную настроить записи DNS, следуя инструкциям в документации по адресу https://doc.yunohost.org/dns_config.", + "domain_dns_registrar_supported": "YunoHost автоматически определил, что этот домен обслуживается регистратором **{registrar}**. При желании YunoHost автоматически настроит эту зону DNS, если вы предоставите ему соответствующие учётные данные API. Документацию о том, как получить учетные данные API, можно найти на этой странице: https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Вы также можете вручную настроить свои записи DNS, следуя инструкциям в документации по адресу https://doc.yunohost.org/dns_config )", + "domain_dns_registrar_use_auto": "Использовать функцию автоматического DNS", + "domain_dns_registrar_yunohost": "Этот домен соответствует шаблону nohost.me / nohost.st / ynh.fr, поэтому его DNS-конфигурация автоматически обрабатывается YunoHost без дополнительной настройки. (см. команду 'yunohost dyndns update')", + "domain_dyndns_already_subscribed": "Вы уже подписались на домен DynDNS", + "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 the_app_id' или переместите на другой домен с помощью команды 'yunohost app change-url the_app_id'", + "domain_unknown": "Домен '{domain}' неизвестен", + "domains_available": "Доступные домены:", + "done": "Готово", + "download_bad_status_code": "{url} вернул код состояния {code}", + "download_ssl_error": "Ошибка SSL при соединении с {url}", + "download_timeout": "Превышено время ожидания ответа от {url}.", + "download_unknown_error": "Ошибка при загрузке данных с {url} : {error}", + "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_domain_not_provided": "Провайдер DynDNS {provider} не может предоставить домен {domain}.", + "dyndns_ip_update_failed": "Не удалось обновить IP-адрес в DynDNS", + "dyndns_ip_updated": "Обновлён ваш IP-адрес на DynDNS", + "dyndns_key_not_found": "Для домена не найден ключ DNS", + "dyndns_no_domain_registered": "Нет домена, зарегистрированного с DynDNS", + "dyndns_no_recovery_password": "Пароль для восстановления не указан! В случае потери контроля над этим доменом вам необходимо будет связаться с администратором из команды YunoHost!", + "dyndns_provider_unreachable": "Не удается установить связь с провайдером DynDNS {provider}: либо ваш YunoHost не подключен к Интернету, либо сервер dynette не работает.", + "dyndns_set_recovery_password_denied": "Не удалось установить пароль восстановления: неверный ключ", + "dyndns_set_recovery_password_failed": "Не удалось установить пароль для восстановления: {error}", + "dyndns_set_recovery_password_invalid_password": "Не удалось установить пароль для восстановления: пароль недостаточно надёжный", + "dyndns_set_recovery_password_success": "Пароль для восстановления установлен!", + "dyndns_set_recovery_password_unknown_domain": "Не удалось установить пароль для восстановления: домен не зарегистрирован", + "dyndns_subscribe_failed": "Не удалось подписаться на домен DynDNS: {error}", + "dyndns_subscribed": "Домен DynDNS подписан", + "dyndns_too_many_requests": "Служба dyndns YunoHost получила от вас слишком много запросов, подождите около 1 часа, прежде чем повторить попытку.", + "dyndns_unavailable": "Домен '{domain}' недоступен.", + "dyndns_unsubscribe_already_unsubscribed": "Домен уже отписан", + "dyndns_unsubscribe_denied": "Не удалось отписать домен: неверные учётные данные", + "dyndns_unsubscribe_failed": "Не удалось отписаться от домена DynDNS: {error}", + "dyndns_unsubscribed": "Подписка на домен DynDNS отменена", + "error_changing_file_permissions": "Ошибка при изменении разрешений для {path}: {error}", + "error_removing": "Ошибка при удалении {path}: {error}", + "error_writing_file": "Ошибка при записи файла {file}: {error}", + "extracting": "Извлечение…", + "field_invalid": "Недопустимое поле '{field}'", + "file_does_not_exist": "Файл {path} не существует.", + "file_not_exist": "Файл не существует: '{path}'", + "firewall_reload_failed": "Не удалось перезагрузить брандмауэр. Дополнительная информация в журнале.", + "firewall_reloaded": "Брандмауэр перезагружен", + "global_settings_reset_success": "Сбросить глобальные настройки", + "global_settings_setting_admin_strength": "Требования к надёжности пароля администратора", + "global_settings_setting_admin_strength_help": "Эти требования применяются только при инициализации или изменении пароля", + "global_settings_setting_antispam_name": "Антиспам", + "global_settings_setting_backup_compress_tar_archives": "Сжатие резервных копий", + "global_settings_setting_backup_compress_tar_archives_help": "При создании новых резервных копий следует использовать сжатые архивы (.tar.gz) вместо несжатых (.tar). Примечание: включение этой опции позволяет создавать более лёгкие архивы резервных копий, но первоначальная процедура резервного копирования будет значительно дольше и более нагруженной для процессора.", + "global_settings_setting_backup_name": "Резервное копирование", + "global_settings_setting_dns_custom_resolvers_enabled": "Использ. пользовательские DNS-резолверы", + "global_settings_setting_dns_custom_resolvers_enabled_help": "По умолчанию YunoHost использует список надёжных резолверов, расположенных в Европе. Опытные пользователи могут вместо этого указать собственные резолверы.", + "global_settings_setting_dns_custom_resolvers_list": "Адреса пользовательских резолверов", + "global_settings_setting_dns_custom_resolvers_list_help": "Список из не менее 2 DNS-резолверов для каждого используемого IP-протокола (IPv4/IPv6). Пример: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "global_settings_setting_dns_exposure": "Версии IP, которые следует учитывать при настройке и диагностике DNS", + "global_settings_setting_dns_exposure_help": "Примечание: это влияет только на рекомендуемую конфигурацию DNS и диагностические проверки. Это не влияет на конфигурации системы.", + "global_settings_setting_email_name": "Эл. почта", + "global_settings_setting_enable_blocklists": "Включить списки блокировки для входящего трафика", + "global_settings_setting_enable_blocklists_help": "Блокирует серверы, указанные на сайтах spamcop.net, spamhaus.org и abuseat.org, для предотвращения спама. Однако это может вызвать проблемы с доставкой для некоторых безвредных почтовых серверов, которые могут быть указаны этими третьими сторонами, и в этом случае почта, отправленная с этих серверов, не будет получена.", + "global_settings_setting_experimental_name": "Экспериментально", + "global_settings_setting_misc_name": "Другое", + "global_settings_setting_network_name": "Сеть", + "global_settings_setting_nginx_compatibility": "Совместимость NGINX", + "global_settings_setting_nginx_compatibility_help": "Компромисс между совместимостью и безопасностью для веб-сервера NGINX. Влияет на шифры (и другие аспекты, связанные с безопасностью)", + "global_settings_setting_nginx_name": "NGINX (веб-сервер)", + "global_settings_setting_nginx_redirect_to_https": "Принудительно использовать HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "По умолчанию перенаправлять HTTP-запросы на HTTPs (НЕ ВЫКЛЮЧАЙТЕ, если не знаете, что делаете!)", + "global_settings_setting_password_name": "Пароли", + "global_settings_setting_passwordless_sudo": "Разрешить администраторам использовать «sudo» без повторного ввода паролей", + "global_settings_setting_pop3_enabled": "Включить POP3", + "global_settings_setting_pop3_enabled_help": "Включить протокол POP3 для почтового сервера. POP3 - это более старый протокол для доступа к почтовым ящикам от почтовых клиентов, он более лёгкий, но имеет меньше функций, чем IMAP (включен по умолчанию)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Разрешить пользователям редактировать свой основной адрес электронной почты", + "global_settings_setting_portal_allow_edit_email_alias": "Разрешить пользователям добавлять, удалять и редактировать псевдонимы почты", + "global_settings_setting_portal_allow_edit_email_alias_help": "Если отключено, им нужно попросить администраторов сделать это за них.", + "global_settings_setting_portal_allow_edit_email_forward": "Разрешать пользователям добавлять, удалять, редактировать переадресацию почты", + "global_settings_setting_portal_allow_edit_email_forward_help": "Если отключено, им нужно попросить администраторов сделать это за них.", + "global_settings_setting_portal_allow_edit_email_help": "Если отключено, им нужно попросить администраторов сделать это за них.", + "global_settings_setting_portal_name": "Портал", + "global_settings_setting_postfix_compatibility": "Совместимость Postfix", + "global_settings_setting_postfix_compatibility_help": "Компромисс между совместимостью и безопасностью для сервера Postfix. Влияет на шифры (и другие аспекты, связанные с безопасностью)", + "global_settings_setting_postfix_name": "Postfix (почтовый сервер SMTP)", + "global_settings_setting_root_access_explain": "В системах Linux \"root\" является абсолютным администратором. В контексте YunoHost прямой вход по SSH \"root\" по умолчанию отключён - за исключением случаев, когда пользователь находится в локальной сети сервера. Члены группы \"администраторы\" могут использовать команду sudo, чтобы действовать от имени пользователя root из командной строки. Однако может оказаться полезным иметь (надёжный) пароль root для отладки системы, если по какой-либо причине обычные администраторы больше не могут войти в систему.", + "global_settings_setting_root_access_name": "Изменить пароль root", + "global_settings_setting_root_password": "Новый пароль root", + "global_settings_setting_root_password_confirm": "Новый пароль root (подтверждение)", + "global_settings_setting_security_experimental_enabled": "Экспериментальные средства безопасности", + "global_settings_setting_security_experimental_enabled_help": "Включить экспериментальные функции безопасности (не включайте их, если вы не знаете, что делаете!)", + "global_settings_setting_security_name": "Безопасность", + "global_settings_setting_smtp_allow_ipv6": "Разрешить IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Разрешить использование IPv6 для получения и отправки почты", + "global_settings_setting_smtp_backup_mx_domains": "Домены, которые будут выступать в качестве вторичных MX для", + "global_settings_setting_smtp_backup_mx_domains_help": "Разрешить этому серверу выступать в качестве резервного *дополнительного* домена MX для указанного в списке домена. Это означает, что если основной MX для домена недоступен (например, из-за сбоя в работе), письма всё равно будут отправляться на этот сервер, который будет хранить их в течение максимум 20 дней и попытается передать их реальному адресату, как только он восстановит работоспособность. Можно указать несколько доменов, разделённых запятыми.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Белый список электронных писем SMTP для резервного копирования MX", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "При использовании в качестве дополнительного отправителя электронной почты необходимо предоставить исчерпывающий список разрешенных адресов электронной почты получателя (в противном случае письма будут отклонены). Можно указать несколько записей, разделенных запятыми.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Включить SMTP-ретрансляцию", + "global_settings_setting_smtp_relay_enabled_help": "Включить SMTP-ретранслятор для отправки почты вместо этого экземпляра yunohost. Полезно, если вы находитесь в одной из таких ситуаций: ваш 25-й порт заблокирован вашим интернет-провайдером или VPS-провайдером, у вас есть домашний IP-адрес, указанный в DUHL, вы не можете настроить обратный 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_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Аутентификация по паролю", + "global_settings_setting_ssh_password_authentication_help": "Разрешить аутентификацию по паролю для SSH", + "global_settings_setting_ssh_port": "SSH порт", + "global_settings_setting_ssh_port_help": "Предпочтителен порт ниже 1024, чтобы предотвратить попытки захвата удалённого компьютера службами, не являющимися администраторами. Вам также следует избегать использования уже используемых портов, таких как 80 или 443.", + "global_settings_setting_tls_passthrough_enabled": "Включить переадресацию на основе TLS-passthrough / SNI", + "global_settings_setting_tls_passthrough_enabled_help": "Это расширенная функция для обратного прокси всего домена на другую машину без расшифровки трафика. Полезно, если вы хотите предоставить доступ к нескольким машинам с одним и тем же IP-адресом, но при этом разрешить каждой машине обрабатывать завершение SSL.", + "global_settings_setting_tls_passthrough_explain": "Эта функция является РАСШИРЕННОЙ и ЭКСПЕРИМЕНТАЛЬНОЙ и приведёт к серьезным изменениям в конфигурации сервера nginx. Пожалуйста, не используйте её, если вы не знаете, что делаете! В частности, вы должны знать, что fail2ban не может быть реализован на прокси-сервере (nftables не может блокировать вредоносный трафик, поскольку все IP-пакеты отображаются как исходящие от переднего сервера). Кроме того, на данный момент конфигурацию nginx проксируемого сервера необходимо настроить вручную, чтобы он принимал `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "Список переадресации", + "global_settings_setting_tls_passthrough_list_help": "Должен быть список ДОМЕНОВ;НАЗНАЧЕНИЯ;ПОРТОВ, таких как domain.tld;192.168.1.42;443 или domain.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "Переадресация на основе TLS-passthrough / SNI", + "global_settings_setting_user_strength": "Требования к надёжности пароля пользователя", + "global_settings_setting_user_strength_help": "Эти требования применяются только при инициализации или изменении пароля", + "global_settings_setting_webadmin_allowlist": "Список разрешенных IP-адресов веб-панели администратора", + "global_settings_setting_webadmin_allowlist_enabled": "Включить список разрешённых IP-адресов веб-панели", + "global_settings_setting_webadmin_allowlist_enabled_help": "Разрешите доступ к веб-интерфейсу администратора только некоторым IP-адресам.", + "global_settings_setting_webadmin_allowlist_help": "Для доступа к веб-панели администратора разрешены IP-адреса. Допускается использование CIDR-кода.", + "global_settings_setting_webadmin_name": "Веб-панель администратора (Webadmin)", + "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_be_deleted": "Группа {group} не может быть удалена вручную.", + "group_cannot_edit_all_users": "Группа 'all_users' не может быть отредактирована вручную. Это специальная группа, предназначенная для всех пользователей, зарегистрированных в YunoHost", + "group_cannot_edit_primary_group": "Группа '{group}' не может быть отредактирована вручную. Это основная группа, предназначенная для содержания только одного конкретного пользователя.", + "group_cannot_edit_visitors": "Группу \"посетители\" нельзя редактировать вручную. Это специальная группа, представляющая анонимных посетителей", + "group_cannot_remove_last_admin": "Пользователь '{user}' является последним пользователем в группе 'admins' и не будет удалён из неё.", + "group_created": "Группа '{group}' создана", + "group_creation_failed": "Не удалось создать группу '{group}': {error}", + "group_deleted": "Группа '{group}' удалена", + "group_deletion_failed": "Не удалось удалить группу '{group}': {error}", + "group_mailalias_add": "Псевдоним электронной почты '{mail}' будет добавлен в группу '{group}'", + "group_mailalias_remove": "Псевдоним электронной почты '{mail}' будет удалён из группы '{group}'", + "group_no_change": "Ничего не нужно менять для группы '{group}'", + "group_unknown": "Группа '{group}' неизвестна", + "group_update_aliases": "Обновление псевдонимов для группы '{group}'", + "group_update_failed": "Не удалось обновить группу '{group}': {error}", + "group_updated": "Группа '{group}' обновлена", + "group_user_add": "Пользователь '{user}' будет добавлен в группу '{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}", + "hook_json_return_error": "Не удалось прочитать return из hook {path}. Ошибка: {msg}. Исходное содержимое: {raw_content}", + "hook_list_by_invalid": "Это свойство нельзя использовать для составления списка перехватчиков (hooks)", + "hook_name_unknown": "Неизвестное название перехватчика '{name}'", + "installation_complete": "Установка завершена", + "invalid_credentials": "Неправильные пароль или имя пользователя", + "invalid_number": "Должна быть цифра", + "invalid_password": "Неправильный пароль", + "invalid_regex": "Неверный regex:'{regex}'", + "invalid_shell": "Недействительная оболочка: {shell}", + "invalid_url": "Не удалось подключиться к {url}... возможно этот сервис недоступен или вы не подключены к Интернету через IPv4/IPv6.", + "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_install": "Установите приложение '{}'", + "log_app_makedefault": "Сделайте '{}' приложением по умолчанию", + "log_app_remove": "Удалите приложение '{}'", + "log_app_upgrade": "Обновите приложение '{}'", + "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_diagnosis_run": "Запустить диагностику", + "log_does_exists": "Нет логов с именем '{log}', используйте 'yunohost log list' для просмотра всех доступных логов", + "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_unsubscribe": "Отменить регистрацию поддомена YunoHost '{}'", + "log_dyndns_update": "Обновить IP, связанный с вашим поддоменом YunoHost '{}'", + "log_help_to_get_failed_log": "Не удалось завершить операцию '{desc}'. Пожалуйста, предоставьте полный отчет об этой операции, используя команду 'yunohost log share {name}', чтобы получить справку", + "log_help_to_get_log": "Чтобы просмотреть журнал выполнения операции '{desc}', используйте команду 'yunohost log show {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_regen_conf": "Восстановить системные конфигурации '{}'", + "log_remove_on_failed_install": "Удалить '{}' после неудачной установки", + "log_resource_snippet": "Подготовка/отмена подготовки/обновление ресурса", + "log_selfsigned_cert_install": "Установить самоподписанный сертификат для домена '{}'", + "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_update": "Получение доступных обновлений системы и обновление каталога приложений", + "log_tools_upgrade": "Обновление системных пакетов", + "log_user_create": "Добавление пользователя '{}'", + "log_user_delete": "Удаление пользователя '{}'", + "log_user_group_create": "Создать группу '{}'", + "log_user_group_delete": "Удалить группу '{}'", + "log_user_group_update": "Обновить группу '{}'", + "log_user_import": "Импорт пользователей", + "log_user_update": "Обновить информацию для пользователя '{}'", + "mail_alias_remove_failed": "Не удалось удалить псевдоним электронной почты '{mail}'", + "mail_alias_unauthorized": "Вы не имеете права добавлять псевдонимы, относящиеся к домену '{domain}'", + "mail_already_exists": "Почтовый адрес '{mail}' уже существует", + "mail_domain_unknown": "Неверный адрес электронной почты для домена '{domain}'. Пожалуйста, используйте домен, администрируемый этим сервером.", + "mail_edit_operation_unauthorized": "Вы не можете вносить эти изменения в свою учётную запись.", + "mail_forward_remove_failed": "Не удалось удалить переадресацию электронной почты '{mail}'", + "mail_unavailable": "Этот адрес электронной почты зарезервирован для группы администраторов", + "mailbox_disabled": "Электронная почта отключена для пользователя {user}", + "mailbox_used_space_dovecot_down": "Если вы хотите получить доступ к используемому месту в почтовом ящике, должна быть запущена служба почтовых ящиков Dovecot", + "main_domain_change_failed": "Не удаётся изменить основной домен", + "main_domain_changed": "Основной домен был изменён", + "migration_0027_cleaning_up": "Очистка кэша и пакетов больше не нужна…", + "migration_0027_delayed_api_restart": "API YunoHost будет автоматически перезапущен через 15 секунд. Он может быть недоступен в течение нескольких секунд, после чего вам придётся снова войти в систему.", + "migration_0027_general_warning": "В заключение, пожалуйста, обратите внимание, что эта миграция весьма ** деликатная**. Команда YunoHost сделала всё возможное, чтобы проанализировать и протестировать её, но миграция всё равно может привести к сбоям в работе системы или её приложений.\n\nПоэтому рекомендуется:\n - **Выполнить резервное копирование** любых важных данных или приложений. Дополнительная информация на приведена сайте https://doc.yunohost.org/backup;\n - **После запуска миграции наберитесь терпения**: в зависимости от вашего подключения к Интернету и аппаратного обеспечения на корректное обновление может уйти до часа.;\n - **Свяжитесь с сообществом** на форуме, если вам нужна помощь в устранении неполадок.", + "migration_0027_main_upgrade": "Запуск основного обновления…", + "migration_0027_modified_files": "Пожалуйста, обратите внимание, что было обнаружено ручное изменение следующих файлов и они могут быть перезаписаны после обновления: {manually_modified_files}", + "migration_0027_not_bullseye": "Текущий дистрибутив Debian не Bullseye! Если вы уже запустили миграцию Bullseye -> Bookworm, то эта ошибка свидетельствует о том, что процедура миграции не была успешной на 100% (в противном случае YunoHost отметил бы её как завершенную). Рекомендуется выяснить, что произошло, у службы поддержки, которой потребуется полный журнал миграции, который можно найти в меню Сервис > Журналы в веб-панели администратора webadmin.", + "migration_0027_not_enough_free_space": "В /var/ довольно мало свободного места! Для запуска этой миграции у вас должно быть не менее 1 ГБ свободного места.", + "migration_0027_patch_yunohost_conflicts": "Применение исправления для устранения конфликтной ситуации…", + "migration_0027_patching_sources_list": "Исправление файла sources.lists…", + "migration_0027_problematic_apps_warning": "Пожалуйста, обратите внимание, что были обнаружены следующие, возможно, проблемные установленные приложения. Похоже, что они не были установлены из каталога приложений YunoHost или не помечены как \"рабочие\". Следовательно, нельзя гарантировать, что они по-прежнему будут работать после обновления: {problematic_apps}", + "migration_0027_start": "Начинаем миграцию на версию Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Что-то пошло не так во время основного обновления, система, похоже, все ещё работает на Debian Bullseye.", + "migration_0027_system_not_fully_up_to_date": "Ваша система не полностью обновлена. Пожалуйста, выполните обычное обновление перед запуском перехода на Bookworm.", + "migration_0027_yunohost_upgrade": "Запуск обновления ядра YunoHost…", + "migration_not_enough_space": "Выделите достаточное пространство в {path} для запуска миграции.", + "migration_postgresql_previous_not_installed": "В вашей системе не была установлена PostgreSQL. Ничего не нужно делать.", + "migration_postgresql_target_not_installed": "Установлена PostgreSQL 13, но не PostgreSQL 15!? Возможно, в вашей системе произошло что-то странное:(…", + "migration_python_venv_rebuild_broken_app": "Пропускаем {app}, потому что virtualenv не может быть легко восстановлен для этого приложения. Вместо этого вам следует исправить ситуацию, принудительно обновив это приложение с помощью `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_disclaimer_base": "После обновления до Debian Bookworm некоторые приложения на Python необходимо частично перестроить, чтобы перейти на новую версию Python, поставляемую в Debian (с технической точки зрения: необходимо заново создать то, что называется \"virtualenv\"). В то же время эти приложения на Python могут не работать. YunoHost может попытаться перестроить virtualenv для некоторых из них, как описано ниже. Для других приложений, или если попытка перестройки завершится неудачей, вам потребуется вручную принудительно обновить эти приложения.", + "migration_python_venv_rebuild_disclaimer_ignored": "Virtualenvs не могут быть автоматически перестроены для этих приложений. Вам нужно принудительно обновить их, что можно сделать из командной строки с помощью: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "Будет предпринята попытка перестроить virtualenv для следующих приложений (примечание: это может занять некоторое время!): {rebuild_apps}", + "migration_python_venv_rebuild_failed": "Не удалось перестроить Python virtualenv для {app}. Приложение может не работать до тех пор, пока эта проблема не будет устранена. Вам следует исправить ситуацию, принудительно обновив это приложение с помощью команды `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_in_progress": "Сейчас пытаюсь перестроить Python virtualenv для `{app}`", + "migration_0031_terms_of_services": "Эта миграция является чисто информационным сообщением о том, что проект YunoHost теперь публикует Условия использования, связанные с техническими и общественными услугами.", + "migration_0036_cleaning_up": "Очистка кэша и пакетов больше не нужна…", + "migration_0036_delayed_api_restart": "API YunoHost будет автоматически перезапущен через 15 секунд. Он может быть недоступен в течение нескольких секунд, после чего вам придётся снова войти в систему.", + "migration_0036_general_warning": "В заключение, пожалуйста, обратите внимание, что эта миграция весьма ** деликатная**. Команда YunoHost сделала всё возможное, чтобы проанализировать и протестировать её, но миграция всё равно может привести к сбоям в работе системы или её приложений.\n\nПоэтому рекомендуется:\n - **Выполнить резервное копирование** любых важных данных или приложений. Дополнительная информация на сайте https://doc.yunohost.org/backup;\n - **После запуска миграции наберитесь терпения**: в зависимости от вашего подключения к Интернету и аппаратного обеспечения на корректное обновление может уйти до часа.;\n - **Свяжитесь с сообществом** на форуме, если вам нужна помощь в устранении неполадок.", + "migration_0036_main_upgrade": "Запуск основного обновления…", + "migration_0036_modified_files": "Пожалуйста, обратите внимание, что было обнаружено ручное изменение следующих файлов и они могут быть перезаписаны после обновления:", + "migration_0036_not_bullseye": "Текущий дистрибутив Debian не Bookworm! Если вы уже запустили миграцию Bookworm -> Trixie, то эта ошибка свидетельствует о том, что процедура миграции не была успешной на 100% (в противном случае YunoHost отметил бы её как завершенную). Рекомендуется выяснить, что произошло, у службы поддержки, которой потребуется полный журнал миграции, который можно найти в меню Сервис > Журналы в веб-панели администратора webadmin.", + "migration_0036_not_enough_free_space": "В /var/ довольно мало свободного места! Для запуска этой миграции у вас должно быть не менее 1 ГБ свободного места.", + "migration_0036_patch_yunohost_dpkg": "Применение исправления к базе данных dpkg для устранения конфликтных ситуаций…", + "migration_0036_patching_sources_list": "Исправление файла sources.lists…", + "migration_0036_problematic_apps_warning": "Пожалуйста, обратите внимание, что были обнаружены следующие приложения, которые могут вызывать проблемы при установке. Похоже, что они были установлены не из каталога приложений YunoHost или не помечены как \"работающие\". Следовательно, нельзя гарантировать, что они продолжат работать после обновления:", + "migration_0036_start": "Начинаем переход на Trixie…", + "migration_0036_still_on_bookworm_after_main_upgrade": "Что-то пошло не так во время основного обновления, система, похоже, всё ещё работает на Debian Bookworm.", + "migration_0036_system_not_fully_up_to_date": "Ваша система не полностью обновлена. Пожалуйста, выполните обычное обновление перед запуском перехода на Trixie.", + "migration_0036_yunohost_upgrade": "Запуск обновления ядра YunoHost…", + "migration_description_0027_migrate_to_bookworm": "Обновите систему до Debian Bookworm и YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Удалить старые разрешения XMPP, и Metronome теперь станет приложением", + "migration_description_0029_postgresql_13_to_15": "Перенос баз данных с PostgreSQL 13 на 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Восстановление приложения на Python после миграции bookworm", + "migration_description_0031_terms_of_services": "Условия использования", + "migration_description_0032_firewall_config": "Перенос конфигурационного файла внутреннего брандмауэра", + "migration_description_0033_rework_permission_infos": "Изменение способа хранения разрешений для приложений", + "migration_description_0034_fix_missing_admins_aliases": "Исправлены отсутствующие почтовые псевдонимы для группы администраторов", + "migration_description_0035_fix_apps_nodejs_version": "Исправлены версии nodejs в конфигурациях приложения systemd", + "migration_description_0036_migrate_to_trixie": "Обновить систему до Debian Trixie и YunoHost 13", + "migration_ldap_backup_before_migration": "Создание резервной копии базы данных LDAP и настроек приложений перед выполнением миграции.", + "migration_ldap_can_not_backup_before_migration": "Не удалось выполнить резервное копирование системы до сбоя миграции. Ошибка: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Не удалось выполнить перенос… пытаюсь выполнить откат системы.", + "migration_ldap_rollback_success": "Система откатилась назад.", + "migrations_already_ran": "Эти миграции уже выполнены: {ids}", + "migrations_dependencies_not_satisfied": "Запустите эти миграции: '{dependencies_id}', перед миграцией {id}.", + "migrations_exclusive_options": "'--auto', '--skip', and '--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_no_migrations_to_run": "Нет миграций для запуска", + "migrations_no_such_migration": "Не существует миграция под названием '{id}'", + "migrations_not_pending_cant_skip": "Эти миграции не ожидаются, поэтому не могут быть пропущены: {ids}", + "migrations_pending_cant_rerun": "Эти миграции ещё не завершены, поэтому не могут быть запущены снова: {ids}", + "migrations_running_forward": "Запуск миграции {id}…", + "migrations_skip_migration": "Пропуск миграции {id}…", + "migrations_success_forward": "Миграция {id} завершена", + "migrations_to_be_ran_manually": "Миграция {id} должна быть запущена вручную. Пожалуйста, перейдите в раздел Инструменты → Миграции на вэб-странице администратора или выполните команду `yunohost tools migrations run`.", + "nftables_unavailable": "Вы не можете играть с nftables здесь. Либо Вы находитесь в контейнере, либо ваше ядро это не поддерживает", + "noninteractive_task": "Неинтерактивная задача", + "not_enough_disk_space": "Недостаточно свободного места в '{path}'", + "operation_interrupted": "Действие было прервано вручную?", + "other_available_options": "… и {n} других не показанных доступных опций", + "password_confirmation_not_the_same": "Пароль и его подтверждение не совпадают", + "password_listed": "Этот пароль является одним из наиболее часто используемых паролей в мире. Пожалуйста, выберите что-то более уникальное.", + "password_too_long": "Пожалуйста, выберите пароль длиной не более 127 символов", + "password_too_simple_1": "Пароль должен быть не менее 8 символов", + "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_fullname": "Должно быть действительное полное имя (не менее 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_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}", + "permission_updated": "Разрешение '{permission}' обновлено", + "port_already_closed": "Порт {port} уже закрыт", + "port_already_opened": "Порт {port} уже открыт", + "postinstall_low_rootfsspace": "Общий размер корневой файловой системы составляет менее 10 ГБ, что вызывает беспокойство! Скорее всего, свободное место очень быстро закончится! Рекомендуется иметь не менее 16 ГБ для корневой файловой системы. Если вы хотите установить YunoHost, несмотря на это предупреждение, повторно запустите пост-установку с параметром --force-diskspace", + "pydantic_type_error": "Некорректный тип.", + "pydantic_type_error_none_not_allowed": "Требуется значение.", + "pydantic_type_error_str": "Недопустимый тип, ожидается строка.", + "pydantic_value_error_color": "Недопустимый цвет, значение должно быть именованным или шестнадцатеричным.", + "pydantic_value_error_const": "Неожиданное значение; выберите между {permitted}", + "pydantic_value_error_date": "Недопустимый формат даты", + "pydantic_value_error_email": "Значение не является действительным адресом электронной почты", + "pydantic_value_error_number_not_ge": "Значение должно быть больше или равно {limit_value}.", + "pydantic_value_error_number_not_le": "Значение должно быть меньше или равно {limit_value}.", + "pydantic_value_error_str_regex": "Недопустимая строка; значение не соответствует шаблону '{pattern}'", + "pydantic_value_error_time": "Недопустимый формат времени", + "pydantic_value_error_url_extra": "Недопустимый URL-адрес, после допустимого URL-адреса найдены лишние символы: '{extra}'", + "pydantic_value_error_url_host": "Недопустимый URL-адрес хоста", + "pydantic_value_error_url_port": "Неверный URL-адрес порта, значение порта не может превышать 65535", + "pydantic_value_error_url_scheme": "Недопустимая или отсутствующая схема URL-адресов", + "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 была изменена вручную, но Вам нужно явно указать категорию 'ssh' с --force, чтобы применить изменения.", + "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_confirm_yunohost_installed": "Вы действительно хотите восстановить уже установленную систему? [{answers}]", + "restore_extracting": "Извлечение необходимых файлов из архива…", + "restore_failed": "Не удалось восстановить систему", + "restore_hook_unavailable": "Сценарий восстановления для '{part}' недоступен в вашей системе и также отсутствует в архиве", + "restore_may_be_not_enough_disk_space": "Похоже, в вашей системе недостаточно места (свободно: {free_space} Б, необходимое пространство: {needed_space} Б, запас прочности: {margin} Б)", + "restore_not_enough_disk_space": "Недостаточно места (свободно: {free_space} Б, необходимо: {needed_space} Б, запас прочности: {margin} Б)", + "restore_nothings_done": "Ничего не было восстановлено", + "restore_removing_tmp_dir_failed": "Не удалось удалить старый временный каталог", + "restore_running_app_script": "Восстановление приложения '{app}'…", + "restore_running_hooks": "Запуск перехватчиков (hooks) восстановления…", + "restore_system_part_failed": "Не удалось восстановить системный компонент'{part}'", + "root_password_changed": "пароль root был изменен", + "root_password_desynchronized": "Пароль администратора был изменен, но YunoHost не смог распространить его на пароль root!", + "server_reboot": "Сервер перезагрузится", + "server_reboot_confirm": "Сервер немедленно перезагрузится, вы уверены? [{answers}]", + "server_shutdown": "Сервер будет выключен", + "server_shutdown_confirm": "Сервер немедленно выключится, вы уверены? [{answers}]", + "service_add_failed": "Не удалось добавить службу '{service}'", + "service_added": "Служба '{service}' добавлена", + "service_already_started": "Служба '{service}' уже запущена", + "service_already_stopped": "Служба '{service}' уже остановлена", + "service_cmd_exec_failed": "Не удалось выполнить команду '{command}'", + "service_description_dnsmasq": "Управляет разрешением доменных имён (DNS)", + "service_description_dovecot": "Позволяет почтовым клиентам получать доступ к электронной почте (через IMAP и POP3)", + "service_description_fail2ban": "Защищает от атакой подбором и других видов атак из Интернета", + "service_description_mysql": "Хранит данные приложения (база данных SQL)", + "service_description_nftables": "Управляет открытием и закрытием портов подключения к службам", + "service_description_nginx": "Обслуживает или предоставляет доступ ко всем веб-сайтам, размещенным на вашем сервере", + "service_description_opendkim": "Подписывает исходящие электронные письма с помощью DKIM таким образом, чтобы они с меньшей вероятностью были помечены как спам", + "service_description_postfix": "Используется для отправки и получения электронных писем", + "service_description_postgresql": "Хранит данные приложения (база данных SQL)", + "service_description_redis-server": "Специализированная база данных, используемая для быстрого доступа к данным, постановки задач в очередь и обмена данными между программами", + "service_description_slapd": "Хранит пользователей, домены и связанную с ними информацию", + "service_description_ssh": "Позволяет удалённо подключаться к вашему серверу через терминал (протокол SSH)", + "service_description_yunohost-api": "Управляет взаимодействием между веб-интерфейсом YunoHost и системой", + "service_description_yunohost-portal-api": "Управляет взаимодействиями между различными веб-интерфейсами портала и системой", + "service_description_yunomdns": "Позволяет вам связаться с вашим сервером, используя \"yunohost.local\" в вашей локальной сети", + "service_disable_failed": "Не удалось сделать так, чтобы служба '{service}' не запускалась при загрузке.", + "service_disabled": "Служба '{service}' больше не будет запускаться при загрузке системы.", + "service_enable_failed": "Не удалось заставить службу '{service}' автоматически запускаться при загрузке.", + "service_enabled": "Служба '{service}' теперь будет автоматически запускаться во время загрузки системы.", + "service_not_reloading_because_conf_broken": "Не удается перезагрузить/перезапустить службу '{name}', поскольку ее конфигурация нарушена: {errors}", + "service_reload_failed": "Не удалось перезагрузить службу '{service}'", + "service_reload_or_restart_failed": "Не удалось перезагрузить или перезапустить службу '{service}'", + "service_reloaded": "Служба '{service}' перезагружена", + "service_reloaded_or_restarted": "Служба '{service}' была перезагружена или перезапущена", + "service_remove_failed": "Не удалось удалить службу '{service}'", + "service_removed": "Служба '{service}' удалена", + "service_restart_failed": "Не удалось перезапустить службу '{service}'", + "service_restarted": "Служба '{service}' перезапущена", + "service_start_failed": "Не удалось запустить службу '{service}'", + "service_started": "Служба '{service}' запущена", + "service_stop_failed": "Не удалось остановить службу '{service}'", + "service_stopped": "Служба '{service}' остановлена", + "service_unknown": "Неизвестная служба '{service}'", + "session_expired": "Сеанс истёк", + "show_tile_cant_be_enabled_for_regex": "Вы не можете включить 'show_tile' прямо сейчас, потому что URL-адрес для разрешения '{permission}' является регулярным выражением", + "show_tile_cant_be_enabled_for_url_not_defined": "Вы не можете включить 'show_tile' прямо сейчас, потому что сначала вы должны определить URL-адрес для разрешения '{permission}'", + "ssowat_conf_generated": "Восстановлены конфигурации единого входа и портала", + "system_upgraded": "Система обновлена", + "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}", + "tos_dyndns_acknowledgement": "Вы решили зарегистрировать домен DynDNS, который является услугой, предоставляемой проектом YunoHost. Учитывая, что доменные имена являются важным аспектом долгосрочных цифровых услуг, мы напоминаем вам внимательно ознакомиться с соответствующими Условиями предоставления услуг, в частности с разделом, касающимся этих бесплатных доменных имён: .", + "tos_postinstall_acknowledgement": "Проект YunoHost - это команда волонтеров, которые объединились для создания бесплатной операционной системы для серверов под названием YunoHost. Программное обеспечение YunoHost опубликовано под лицензией AGPLv3 (). В связи с этим программным обеспечением проект администрирует и предоставляет доступ к нескольким техническим и общественным услугам для различных целей. Используя эти услуги, вы соглашаетесь соблюдать следующие Условия предоставления услуг: .", + "unable_authenticate": "Не удалось выполнить аутентификацию сеанса", + "unbackup_app": "{app} не будет сохранено", + "unexpected_error": "Что-то неожиданно пошло не так: {error}", + "unknown_error_reading_file": "Неизвестная ошибка при попытке прочитать файл {file} (причина: {error})", + "unknown_group": "Неизвестная '{group}' группа", + "unknown_main_domain_path": "Неизвестный домен или путь для '{app}'. Вам необходимо указать домен и путь, чтобы иметь возможность указать URL-адрес для получения разрешения.", + "unknown_user": "Неизвестный '{user}' пользователь", + "unlimit": "Нет квоты", + "unrestore_app": "{app} не будет восстановлено", + "update_apt_cache_failed": "Не удалось обновить кэш APT (менеджера пакетов Debian). Вот дамп исходных текстов.список строк, которые могут помочь определить проблемные строки: \n{sourceslist}", + "update_apt_cache_warning": "Что-то пошло не так при обновлении кэша APT (менеджера пакетов Debian). Вот дамп исходных текстов.список строк, которые могут помочь определить проблемные строки: \n{sourceslist}", + "updating_apt_cache": "Получение доступных обновлений для системных пакетов…", + "upgrading_packages": "Обновление пакетов…", + "upnp_dev_not_found": "Устройство UPnP не найдено", + "upnp_disabled": "UPnP отключен", + "upnp_enabled": "UPnP включен", + "upnp_port_open_failed": "Не удалось открыть порт через UPnP", + "user_already_exists": "Пользователь '{user}' уже существует", + "user_cannot_delete_last_admin": "Пользователь '{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_cannot_edit_or_delete_admins": "Невозможно изменить или удалить учётную запись '{user}' с помощью импорта, поскольку пользователь является администратором", + "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_api": "YunoHost API", + "yunohost_configured": "Теперь YunoHost настроен", + "yunohost_installing": "Установка YunoHost…", + "yunohost_not_installed": "YunoHost установлен некорректно. Пожалуйста, выполните 'yunohost tools postinstall'", + "yunohost_postinstall_end_tip": "Постустановка завершена! Чтобы завершить настройку, пожалуйста, рассмотрите возможность:\n - поискать потенциальные проблемы с помощью раздела \"Диагностика\" веб-панели администратора (или выполнить 'yunohost diagnosis run' в командной строке).;\n - ознакомиться с разделами \"Завершение настройки\" и \"Знакомство с YunoHost\" в документации администратора: https://doc.yunohost.org/admin.", + "migration_0036_apt_lists_file_still_exists": "Всё ещё существует устаревший файл '{file}', хотя этого быть не должно. Он будет переименован в '{file}.legacy_bookworm'.", + "app_db_prompt_no_app_database": "Похоже, в манифесте этого приложения не указана база данных", + "app_db_prompt_type_not_supported": "Команда не поддерживает этот тип базы данных: {type}", + "migration_0037_upgrade_dkim_keys_disclaimer": "В ходе этой миграции устаревшие 1024-битные ключи DKIM будут заменены на 2048-битные для улучшения доставляемости почты. Это касается следующих доменов, и некоторым из них может потребоваться обновить ключи DKIM в вашей DNS-зоне сразу после миграции: {domains}\nВАЖНО: Во избежание потенциального попадания в чёрный список и проблем с доставкой, эту миграцию необходимо запускать в то время, когда ваш сервер не отправляет почту. В целях безопасности вы можете остановить службу Postfix перед миграцией и перезапустить её через 1 час после обновления ключей DKIM в ваших DNS-зонах.", + "migration_0037_upgrade_dkim_keys_pending_mails": "В вашей очереди находятся {pending_mails} писем. Во избежание потенциального попадания в чёрный список и проблем с доставкой, эту миграцию необходимо запускать в то время, когда ваш сервер не отправляет электронные письма. Вы можете временно остановить Postfix и использовать команду postsuper -d ALL, чтобы освободить очередь писем. Чтобы посмотреть, какие письма находятся в очереди, используйте команду postqueue -p", + "migration_0037_upgrade_dkim_keys_failed": "Не удалось сгенерировать новый 2048-битный ключ для {domains}.", + "migration_0037_upgrade_dkim_keys_manual_action": "Для завершения процесса миграции необходимо обновить открытые ключи DKIM в следующих зонах DNS: {domains}\nЗапустите диагностику или воспользуйтесь вкладкой DNS в разделе Домены веб-панели администратора или командой yunohost domain dns suggest DOMAIN. Если вы решили остановить Postfix, не забудьте перезапустить его через 1 час после редактирования последней зоны DNS.", + "migration_description_0037_upgrade_dkim_keys": "Обновите ключи DKIM для повышения доставляемости почты" +} diff --git a/locales/sk.json b/locales/sk.json new file mode 100644 index 0000000..36adc61 --- /dev/null +++ b/locales/sk.json @@ -0,0 +1,279 @@ +{ + "aborting": "Zrušené.", + "action_invalid": "Nesprávna akcia '{action}'", + "additional_urls_already_added": "Dodatočná URL adresa '{url}' už bola pridaná pre oprávnenie '{permission}'", + "additional_urls_already_removed": "Dodatočná URL adresa '{url}' už bola odstránená pre oprávnenie '{permission}'", + "admin_password": "Heslo pre správu", + "admins": "Správcovia", + "all_users": "Všetci používatelia YunoHost", + "already_up_to_date": "Nič netreba robiť. Všetko je už aktuálne.", + "app_action_broke_system": "Vyzerá, že táto akcia spôsobila nefunkčnosť nasledovných dôležitých služieb: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Pre vykonanie tejto akcie by mali byť spustené nasledovné služby: {services}. Skúste ich reštartovať, prípadne zistite, prečo nebežia.", + "app_action_failed": "Nepodarilo sa spustiť akciu {action} v aplikácii {app}", + "app_already_installed": "{app} je už nainštalovaný/á", + "app_already_installed_cant_change_url": "Táto aplikácia je už nainštalovaná. Adresa URL nemôže byť touto akciou zmenená. Skontrolujte `app changeurl`, ak je dostupné.", + "app_arch_not_supported": "Túto aplikáciu možno nainštalovať iba na architektúrach {required}, ale Váš server beží na architektúre {current}", + "app_argument_choice_invalid": "Vyberte platnú hodnotu pre argument '{name}': '{value}' nie je medzi dostupnými možnosťami ({choices})", + "app_argument_invalid": "Vyberte platnú hodnotu pre argument '{name}': {error}", + "app_change_url_failed": "Nepodarilo sa zmeniť URL adresu aplikácie {app}: {error}", + "app_change_url_identical_domains": "Stará a nová doména/url_cesta sú identické ('{domain}{path}'), nebudú vykonané žiadne zmeny.", + "app_change_url_no_script": "Aplikácia '{app_name}' ešte nepodporuje modifikáciu URL adresy. Skúste ju aktualizovať.", + "app_change_url_script_failed": "Vo skripte na zmenu URL adresy sa vyskytla chyba", + "app_change_url_success": "URL adresa {app} je teraz {domain}{path}", + "app_config_unable_to_apply": "Nepodarilo sa použiť hodnoty z panela s nastaveniami.", + "app_config_unable_to_read": "Nepodarilo sa prečítať hodnoty z panela s nastaveniami.", + "app_extraction_failed": "Chyba pri rozbaľovaní inštalačných súborov", + "app_full_domain_unavailable": "Ľutujeme, túto aplikáciu musíte nainštalovať na samostatnej doméne, ale na doméne '{domain}' sú už nainštalované iné aplikácie. Ako alternatívu môžete použiť poddoménu určenú iba pre túto aplikáciu.", + "app_id_invalid": "Neplatné ID aplikácie", + "app_install_failed": "Nedá sa nainštalovať {app}: {error}", + "app_install_files_invalid": "Tieto súbory sa nedajú nainštalovať", + "app_install_script_failed": "Objavila sa chyba vo vnútri inštalačného skriptu aplikácie", + "app_location_unavailable": "Táto adresa URL je buď nedostupná alebo koliduje s už nainštalovanou aplikáciou(ami):\n{apps}", + "app_make_default_location_already_used": "Nepodarilo sa nastaviť '{app}' ako predvolenú aplikáciu na doméne, doménu '{domain}' už využíva aplikácia '{other_app}'", + "app_manifest_install_ask_admin": "Vyberte používateľa, ktorý bude spravovať túto aplikáciu", + "app_manifest_install_ask_domain": "Vyberte doménu, kam bude táto aplikácia nainštalovaná", + "app_manifest_install_ask_init_admin_permission": "Kto má mať prístup k nastaveniam určených správcovi tejto aplikácie? (Nastavenie môžete neskôr zmeniť)", + "app_manifest_install_ask_init_main_permission": "Kto má mať prístup k tejto aplikácii? (Nastavenie môžete neskôr zmeniť)", + "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), kam bude táto aplikácia nainštalovaná", + "app_not_correctly_installed": "Zdá sa, že {app} nie je správne nainštalovaná", + "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}.", + "app_not_installed": "{app} sa nepodarilo nájsť v zozname nainštalovaných aplikácií: {all_apps}", + "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.", + "app_remove_after_failed_install": "Aplikácia sa po chybe počas inštalácie odstraňuje…", + "app_removed": "{app} bola odinštalovaná", + "app_requirements_checking": "Kontrolujem požiadavky aplikácie {app}…", + "app_restore_failed": "Nepodarilo sa obnoviť {app}: {error}", + "app_restore_script_failed": "Chyba nastala vo vnútri skriptu na obnovu aplikácie", + "app_sources_fetch_failed": "Nepodarilo sa získať zdrojové súbory, je adresa URL správna?", + "app_start_backup": "Zbieram súbory, ktoré budú zálohovať pre {app}…", + "app_start_install": "Inštalujem {app}…", + "app_start_remove": "Odstraňujem {app}…", + "app_start_restore": "Obnovujem {app}…", + "app_unknown": "Neznáma aplikácia", + "app_unsupported_remote_type": "Nepodporovaný vzdialený typ použitý pre aplikáciu", + "app_upgrade_app_name": "Teraz aktualizujem {app}…", + "app_upgrade_failed": "Nemôžem aktualizovať {app}: {error}", + "app_upgrade_script_failed": "Chyba nastala vo vnútri skriptu na aktualizáciu aplikácie", + "app_upgrade_several_apps": "Nasledovné aplikácie budú aktualizované: {apps}", + "app_upgrade_some_app_failed": "Niektoré aplikácie sa nepodarilo aktualizovať", + "app_upgraded": "{app} bola aktualizovaná", + "app_yunohost_version_not_supported": "Táto aplikácia vyžaduje YunoHost >= {required}, ale aktuálne nainštalovaná verzia je {current}", + "apps_already_up_to_date": "Všetky aplikácie sú aktuálne", + "apps_catalog_failed_to_download": "Nepodarilo sa stiahnuť repozitár aplikáciI {apps_catalog}: {error}", + "apps_catalog_obsolete_cache": "Medzipamäť repozitára aplikácií je prázdna alebo zastaralá.", + "apps_catalog_update_success": "Repozitár s aplikáciami bol aktualizovaný!", + "apps_catalog_updating": "Aktualizujem repozitár aplikácií…", + "ask_admin_fullname": "Celé meno správcu", + "ask_admin_username": "Používateľské meno správcu", + "ask_fullname": "Celé meno", + "ask_main_domain": "Hlavná doména", + "ask_new_admin_password": "Nové heslo pre správu", + "ask_new_domain": "Nová doména", + "ask_new_path": "Nová cesta", + "ask_password": "Heslo", + "ask_user_domain": "Doména, ktorá bude použitá pre e-mailové adresy používateľov a ich XMPP účet", + "backup_abstract_method": "Táto metóda zálohovania ešte nebola implementovaná", + "backup_actually_backuping": "Vytváram archív so zálohou vyzbieraných súborov…", + "backup_applying_method_copy": "Kopírujem všetky súbory do zálohy…", + "backup_applying_method_custom": "Volám vlastnú metódu zálohovania '{method}'…", + "backup_applying_method_tar": "Vytváram TAR archív so zálohou…", + "backup_archive_app_not_found": "Nepodarilo sa nájsť {app} v archíve so zálohou", + "backup_archive_broken_link": "Nepodarilo sa získať prístup k archívu so zálohou (neplatný odkaz na {path})", + "backup_archive_cant_retrieve_info_json": "Nepodarilo sa načítať informácie o archíve '{archive}'… Nie je možné získať info.json (alebo to nie je platný súbor json).", + "backup_archive_corrupted": "Zdá sa, že archív so zálohou '{archive}' je poškodený: {error}", + "backup_archive_name_exists": "Archív so zálohou s takýmto názvom už existuje.", + "backup_archive_name_unknown": "Neznámy archív s miestnou zálohou s názvom '{name}'", + "backup_archive_open_failed": "Nepodarilo sa otvoriť archív so zálohou", + "backup_archive_system_part_not_available": "Systémová časť '{part}' nie je prítomná v tejto zálohe", + "backup_archive_writing_error": "Nepodarilo sa pridať súbory '{source}' (vymenované v archíve '{dest}') do zoznamu na zálohovanie do skomprimovaného archívu '{archive}'", + "backup_ask_for_copying_if_needed": "Chcete dočasne vytvoriť zálohu využitím {size} MB? (Využije sa tento spôsob, pretože niektoré súbory nie je možné pripraviť pomocou účinnejšej metódy.)", + "backup_cant_mount_uncompress_archive": "Dekomprimovaný archív sa nepodarilo pripojiť bez ochrany pred zápisom", + "backup_cleaning_failed": "Nepodarilo sa vyčistiť dočasný priečinok pre zálohovanie", + "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á: {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á: {name}", + "backup_hook_unknown": "Obsluha zálohy '{hook}' je neznáma", + "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", + "backup_mount_archive_for_restore": "Pripravujem archív na obnovu…", + "backup_no_uncompress_archive_dir": "Taký dekomprimovaný adresár v archíve neexistuje", + "backup_output_directory_forbidden": "Vyberte si iný adresár pre výstup. Zálohy nie je možné vytvoriť v /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var alebo v podadresároch /home/yunohost.backup/archives", + "backup_output_directory_not_empty": "Pre výstup by ste si mali vybrať prázdny adresár", + "backup_output_directory_required": "Musíte vybrať výstupný adresár pre zálohu", + "backup_output_symlink_dir_broken": "Váš adresár pre archívy '{path}' je neplatným symbolickým odkazom. Možno ste zabudli (opätovne) pripojiť alebo vložiť úložné zariadenie, na ktoré odkazuje.", + "backup_running_hooks": "Spúšťam obslužné skripty záloh…", + "backup_system_part_failed": "Nepodarilo sa pripojiť systémovú časť '{part}'", + "backup_unable_to_organize_files": "Nie je možné použiť rýchlu metódu na organizáciu súborov v archíve", + "backup_with_no_backup_script_for_app": "Aplikácia '{app}' nemá žiaden skript na zálohovanie. Ignorujem.", + "backup_with_no_restore_script_for_app": "Aplikácia {app} nemá žiaden skript na obnovu, nebudete môcť automaticky obnoviť zálohu tejto aplikácie.", + "cannot_open_file": "Nedá sa otvoriť súbor {file} (príčina: {error})", + "cannot_write_file": "Nedá sa zapísať do súboru {file} (príčina: {error})", + "certmanager_acme_not_configured_for_domain": "Výzvu ACME nie je možné momentálne spustiť pre {domain}, pretože jej konfigurácia nginx neobsahuje príslušný kus kódu… Prosím, zabezpečte, aby bola Vaša konfigurácia nginx aktuálna tak, že spustíte `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonLE_cert": "Certifikát pre doménu '{domain}' nevydal Let's Encrypt. Nebude možné ho automaticky obnoviť!", + "certmanager_attempt_to_renew_valid_cert": "Certifikát pre doménu '{domain}' zatiaľ neexpiruje! (Môžete použiť --force, ak viete, čo robíte)", + "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_failed": "Inštalácia Let's Encrypt certifikátu pre {domains} skončila s chybou", + "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í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_certificate_fetching_or_enabling_failed": "Pokus o použitie nového certifikátu pre {domain} skončil s chybou…", + "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ť.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "DNS záznamy pre doménu '{domain}' sa líšia od IP adresy tohto servera. Pre získanie viac informácií skontrolujte, prosím, kategóriu 'DNS záznamy' (základné) v režime diagnostiky. Ak ste nedávno upravovali Váš A záznam, počkajte nejaký čas, kým sa vypropaguje (niektoré služby kontroly DNS propagovania sú dostupné online). (Ak viete, čo robíte, použite '--no-checks' na vypnutie týchto kontrol.)", + "certmanager_domain_http_not_working": "Zdá sa, že doména {domain} nie je dostupná prostredníctvom HTTP. Pre zistenie viac informácií skontrolujte, prosím, kategóriu 'Web' v režime diagnostiky. (Ak viete, čo robíte, použite '--no-checks' na vypnutie týchto kontrol.)", + "certmanager_domain_not_diagnosed_yet": "Pre doménu {domain} zatiaľ neexistujú výsledky diagnostiky. Prosím, opätovne spustite diagnostiku pre kategórie 'DNS záznamy' a 'Web' a skontrolujte, či je doména pripravená na Let's Encrypt. (Alebo ak viete, čo robíte, použite '--no-checks' na vypnutie týchto kontrol.)", + "certmanager_hit_rate_limit": "V poslednom čase bolo pre sadu domén {domain} vydaných príliš mnoho certifikátov. Skúste to, prosím, neskôr. Viac podrobností nájdete na https://letsencrypt.org/docs/rate-limits/", + "certmanager_no_cert_file": "Nepodarilo sa prečítať súbor s certifikátom pre doménu {domain} (súbor: {file})", + "certmanager_self_ca_conf_file_not_found": "Nepodarilo sa nájsť súbor s konfiguráciou pre autoritu na podpisovanie certifikátov (súbor: {file})", + "certmanager_unable_to_parse_self_CA_name": "Nepodarilo sa prečítať názov autority na podpisovanie certifikátov (súbor: {file})", + "config_action_failed": "Nepodarilo sa spustiť operáciu '{action}': {error}", + "config_apply_failed": "Pri nasadzovaní novej konfigurácie došlo k chybe: {error}", + "config_cant_set_value_on_section": "Nemôžete použiť jednoduchú hodnotu na celú časť konfigurácie.", + "config_forbidden_keyword": "Kľúčové slovo '{keyword}' je vyhradené, nemôžete vytvoriť alebo použiť konfiguračný panel s otázkou s týmto identifikátorom.", + "config_no_panel": "Nenašiel sa žiaden konfiguračný panel.", + "config_unknown_filter_key": "Kľúč filtra '{filter_key}' je nesprávny.", + "confirm_app_install_danger": "NEBEZPEČENSTVO! Táto aplikácia je experimentálna (ak vôbec funguje)! Pravdepodobne by ste ju NEMALI inštalovať, pokiaľ si nie ste istý, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", + "confirm_app_install_thirdparty": "NEBEZPEČENSTVO! Táto aplikácia nie je súčasťou katalógu aplikácií YunoHost. Inštalovaním aplikácií tretích strán môžete ohroziť integritu a bezpečnosť Vášho systému. Pravdepodobne by ste NEMALI pokračovať v inštalácií, pokiaľ neviete, čo robíte. NEPOSKYTNEME VÁM ŽIADNU POMOC, ak táto aplikácia nebude fungovať alebo rozbije Váš systém… Ak sa rozhodnete i napriek tomu podstúpiť toto riziko, zadajte '{answers}'", + "confirm_app_install_warning": "Upozornenie: Táto aplikácia môže fungovať, ale nie je dobre integrovaná s YunoHost. Niektoré funkcie ako spoločné prihlásenie (SSO) alebo zálohovanie/obnova nemusia byť dostupné. Nainštalovať aj napriek tomu? [{answers}] ", + "corrupted_json": "Nepodarilo sa načítať JSON {ressource} (príčina: {error})", + "corrupted_toml": "Nepodarilo sa načítať TOML z {ressource} (príčina: {error})", + "corrupted_yaml": "Nepodarilo sa načítať YAML z {ressource} (príčina: {error})", + "danger": "Nebezpečenstvo:", + "diagnosis_apps_allgood": "Všetky nainštalované aplikácie sa riadia základnými zásadami balíčkovania", + "diagnosis_apps_bad_quality": "Táto aplikácia je v katalógu aplikácií YunoHost momentálne označená ako rozbitá. Toto môže byť dočasný problém do momentu, kedy jej správcovia danú chybu neopravia. Kým sa tak stane sú aktualizácie tejto aplikácie vypnuté.", + "diagnosis_apps_broken": "Táto aplikácia je v katalógu aplikácií YunoHost momentálne označená ako rozbitá. Toto môže byť dočasný problém do momentu, kedy jej správcovia danú chybu neopravia. Kým sa tak stane sú aktualizácie tejto aplikácie vypnuté.", + "diagnosis_apps_deprecated_practices": "Táto verzia nainštalovanej aplikácie používa niektoré prehistorické a zastaralé zásady balíčkovania. Naozaj by ste mali zvážiť jej aktualizovanie.", + "diagnosis_apps_issue": "V aplikácií {app} sa našla chyba", + "diagnosis_apps_not_in_app_catalog": "Táto aplikácia sa nenachádza v katalógu aplikácií YunoHost. Ak sa tam v minulosti nachádzala a bola odstránená, mali by ste zvážiť jej odinštalovanie, pretože nebude dostávať žiadne aktualizácie a môže ohroziť integritu a bezpečnosť Vášho systému.", + "diagnosis_apps_outdated_ynh_requirement": "Táto verzia nainštalovanej aplikácie vyžaduje yunohost iba vo verzii 2.x alebo 3.x, čo naznačuje, že neobsahuje aktuálne odporúčané zásady balíčkovania a pomocné skripty. Naozaj by ste mali zvážiť jej aktualizáciu.", + "diagnosis_backports_in_sources_list": "Vyzerá, že apt (správca balíkov) je nastavený na používanie repozitára backports. Inštalovaním balíkov z backports môžete spôsobiť nestabilitu systému a vznik konfliktov, preto - ak naozaj neviete, čo robíte - Vás chceme pred ich používaním dôrazne vystríhať.", + "diagnosis_basesystem_hardware": "Hardvérová architektúra servera je {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Model servera je {model}", + "diagnosis_basesystem_host": "Server beží na Debiane {debian_version}", + "diagnosis_basesystem_kernel": "Server beží na Linuxovom jadre {kernel_version}", + "diagnosis_basesystem_ynh_inconsistent_versions": "Používate nekonzistentné verzie balíkov YunoHost… s najväčšou pravdepodobnosťou kvôli nedokončenej/chybnej aktualizácii.", + "diagnosis_basesystem_ynh_main_version": "Na serveri beží YunoHost {main_version} ({repo})", + "diagnosis_basesystem_ynh_single_version": "verzia {package}: {version} ({repo})", + "diagnosis_cache_still_valid": "(Diagnostické údaje pre {category} sú stále platné. Nespúšťajte diagnostiku znovu!)", + "diagnosis_cant_run_because_of_dep": "Nie je možné spustiť diagnostiku pre {category}, kým existujú významné chyby súvisiace s {dep}.", + "diagnosis_description_apps": "Aplikácie", + "diagnosis_description_basesystem": "Základný systém", + "diagnosis_description_dnsrecords": "DNS záznamy", + "diagnosis_description_ip": "Internetové pripojenie", + "diagnosis_description_mail": "E-mail", + "diagnosis_description_ports": "Otvorenie portov", + "diagnosis_description_regenconf": "Nastavenia systému", + "diagnosis_description_services": "Kontrola stavu služieb", + "diagnosis_description_systemresources": "Systémové prostriedky", + "diagnosis_description_web": "Web", + "diagnosis_diskusage_low": "Na úložisku {mountpoint} (na zariadení {device}) ostáva iba {free} ({free_percent} %) voľného miesta (z celkovej veľkosti {total}). Dávajte pozor.", + "diagnosis_diskusage_ok": "Na úložisku {mountpoint} (na zariadení {device}) ostáva {free} ({free_percent} %) voľného miesta (z celkovej veľkosti {total})!", + "diagnosis_diskusage_verylow": "Na úložisku {mountpoint} (na zariadení {device}) ostáva iba {free} ({free_percent} %) voľného miesta (z celkovej veľkosti {total}). Dobre zvážte vyčistenie úložiska!", + "diagnosis_display_tip": "Pre zobrazenie nájdených problémov prejdite do časti Diagnostiky vo webovej administrácií alebo spustite 'yunohost diagnosis show --issues --human-readable' z rozhrania príkazového riadka.", + "diagnosis_dns_bad_conf": "Niektoré DNS záznamy chýbajú alebo nie sú platné pre doménu {domain} (kategória {category})", + "diagnosis_dns_discrepancy": "Nasledujúci DNS záznam nezodpovedá odporúčanej konfigurácii:
Typ:{type}
Názov:{name}
Aktuálna hodnota: {current}
Očakávaná hodnota: {content}", + "diagnosis_dns_good_conf": "DNS záznamy sú správne nastavené pre doménu {domain} (kategória {category})", + "diagnosis_dns_missing_record": "Podľa odporúčaného nastavenia DNS by ste mali pridať DNS záznam s nasledujúcimi informáciami.
Typ: {type}
Názov: {name}
Hodnota: {content}", + "diagnosis_dns_point_to_doc": "Prosím, pozrite si dokumentáciu na https://doc.yunohost.org/dns_config, ak potrebujete pomôcť s nastavením DNS záznamov.", + "diagnosis_dns_specialusedomain": "Doména {domain} je založená na top-level doméne (TLD) pre zvláštne použitie ako napríklad .local alebo .test a preto sa neočakáva, že bude obsahovať vlastné DNS záznamy.", + "diagnosis_dns_try_dyndns_update_force": "Nastavenie DNS tejto domény by mala byť automaticky spravované YunoHost-om. Ak tomu tak nie je, môžete skúsiť vynútiť jej aktualizáciu pomocou príkazu yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Platnosť niektorých domén expiruje VEĽMI SKORO!", + "diagnosis_domain_expiration_not_found": "Pri niektorých doménach nebolo možné skontrolovať dátum ich vypršania", + "diagnosis_domain_expiration_not_found_details": "WHOIS informácie pre doménu {domain} neobsahujú informáciu o dátume jej vypršania?", + "diagnosis_domain_expiration_success": "Vaše domény sú zaregistrované a tak skoro nevyprší ich platnosť.", + "diagnosis_domain_expiration_warning": "Niektoré z domén čoskoro vypršia!", + "diagnosis_domain_expires_in": "{domain} vyprší o {days} dní.", + "diagnosis_domain_not_found_details": "Doména {domain} neexistuje v databáze WHOIS alebo vypršala jej platnosť!", + "diagnosis_everything_ok": "V kategórii {category} vyzerá byť všetko v poriadku!", + "diagnosis_failed": "Nepodarilo sa získať výsledok diagnostiky pre kategóriu '{category}': {error}", + "diagnosis_failed_for_category": "Diagnostika pre kategóriu '{category}' skončila s chybou: {error}", + "diagnosis_found_errors": "Bolo nájdených {errors} závažných chýb týkajúcich sa {category}!", + "diagnosis_found_errors_and_warnings": "Bolo nájdených {errors} závažných chýb (a {warnings} varovaní) týkajúcich sa {category}!", + "diagnosis_found_warnings": "V kategórii {category} bolo nájdených {warnings} položiek, ktoré je možné opraviť.", + "diagnosis_high_number_auth_failures": "V poslednom čase bol zistený neobvykle vysoký počet neúspešných prihlásení. Uistite sa, či je služba fail2ban spustená a správne nastavená alebo použite vlastný port pre SSH ako je popísané na https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Zdá sa, že miesto vášho servera na vašu požiadavku zareagoval iný počítač (možno váš router).
1. Najčastejšou príčinou tohto problému zvykne byť nesprávne nastavenie presmerovania portu 80 (a 443) na váš server.
2. Pri komplexnejších inštaláciach: ubezpečte sa, že problém nie je spôsobený bránou firewall alebo reverznou proxy.", + "diagnosis_http_connection_error": "Chyba pripojenia: nepodarilo sa pripojiť k požadovanej doméne, podľa všetkého je nedostupná.", + "diagnosis_http_could_not_diagnose": "Nepodarilo sa zistiť, či sú domény dostupné zvonka pomocou IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Chyba: {error}", + "diagnosis_http_hairpinning_issue": "Zdá sa, že Vaša miestna sieť nemá zapnutý NAT hairpinning.", + "diagnosis_http_hairpinning_issue_details": "Toto pravdepodobne spôsobuje zariadenia od vášho poskytovateľa internetu / router. Vo výsledku pre používateľov mimo vašej miestnej siete (zvonku) funguje pripojenie na server normálne, to však neplatí pre používateľov v rámci miestnej siete (ako ste možno aj vy?) pri použití doménového mena alebo globálnej IP adresy. Túto situáciu sa vám možno podarí vyriešiť po prečítaní ", + "diagnosis_http_nginx_conf_not_up_to_date": "Nginx konfigurácia tejto domény sa zdá byť upravená ručne a znemožňuje YunoHost-u zistiť, či je dostupná na HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Pre opravu tohto problému preskúmajte rozdiely medzi konfiguráciami v termináli príkazom yunohost tools regen-conf nginx --dry-run --with-diff a ak so zmenami súhlasíte, aplikujte ich príkazom yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "Doména {domain} je dostupná prostredníctvom HTTP mimo miestnej siete.", + "diagnosis_http_partially_unreachable": "Doména {domain} sa zdá byť nedostupná prostredníctvom HTTP mimo miestnej siete pri použití IPv{failed}, hoci funguje pri IPv{passed}.", + "diagnosis_http_special_use_tld": "Doména {domain} je založená na top-level doméne (TLD) pre zvláštne určenie ako je .local alebo .test a preto sa neočakáva, aby bola dostupná mimo miestnej siete.", + "diagnosis_http_timeout": "Pri pokuse o kontaktovanie servera zvonku vypršal časový limit. Vyzerá byť nedostupný.
1. Najčastejšou príčinou tohto problému zvykne byť nesprávne nastavenie presmerovania portu 80 (a 443) na váš server.
2. Mali by ste skontrolovať, či je služba nginx spustená.
3. Pri komplexnejších inštaláciach: ubezpečte sa, že problém nie je spôsobený bránou firewall alebo reverznou proxy.", + "diagnosis_http_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_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 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}", + "diagnosis_ip_no_ipv4": "Na serveri nefunguje spojenie cez protokol IPv4.", + "diagnosis_ip_no_ipv6": "Na serveri nefunguje spojenie cez protokol IPv6.", + "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://doc.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_not_connected_at_all": "Zdá sa, že tento server nie je vôbec pripojený k internetu!?", + "diagnosis_ip_weird_resolvconf": "Zdá sa, že preklad názvov domén funguje, ale podľa všetkého používate vlastný súbor /etc/resolv.conf.", + "diagnosis_mail_queue_unavailable_details": "Chyba: {error}", + "diagnosis_ram_low": "Systém má {available} ({available_percent} %) dostupnej pamäte RAM (z celkovej pamäte {total}). Buďte opatrný.", + "diagnosis_ram_ok": "Systém má ešte {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_sshd_config_inconsistent": "Zdá sa, že port SSH bol manuálne upravený v /etc/ssh/sshd_config. Od YunoHost 4.2 je dostupné nové globálne nastavenie 'security.ssh.port', aby ste nemuseli konfiguráciu editovať ručne.", + "domain_config_cert_install": "Nainštalovať certifikát Let's Encrypt", + "domain_config_cert_no_checks": "Ignorovať kontroly diagnostiky", + "domain_config_cert_renew_help": "Certifikát bude automaticky obnovený po 15 dňoch platnosti. Ak chcete, môžete ho obnoviť aj ručne. (Neodporúča sa).", + "domain_config_cert_summary": "Stav certifikátu", + "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!", + "domain_config_default_app": "Predvolená aplikácia", + "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.", + "domain_config_mail_in": "Prichádzajúce e-maily", + "domain_config_mail_out": "Odchádzajúce e-maily", + "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}.", + "domains_available": "Dostupné domény:", + "download_bad_status_code": "{url} vrátil stavový kód {code}", + "download_ssl_error": "SSL chyba počas spojenia s {url}", + "download_timeout": "{url} príliš dlho neodpovedá, vzdávam to.", + "download_unknown_error": "Chyba pri sťahovaní dát z {url}: {error}", + "dyndns_could_not_check_available": "Nepodarilo sa zistiť, či je {domain} dostupná na {provider}.", + "dyndns_unavailable": "Doména '{domain}' nie je dostupná.", + "error_changing_file_permissions": "Chyba pri nastavovaní oprávnení pre {path}: {error}", + "error_removing": "Chyba pri odstraňovaní {path}: {error}", + "error_writing_file": "Chyba pri zápise do súboru {file}: {error}", + "file_not_exist": "Súbor neexistuje: '{path}'", + "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ť!)", + "invalid_url": "Nepodarilo sa pripojiť k {url}… možno je služba vypnutá alebo nemáte fungujúce pripojenie k internetu prostredníctvom IPv4/IPv6.", + "log_app_makedefault": "Nastaviť '{}' ako predvolenú aplikáciu", + "log_available_on_yunopaste": "Tento záznam je teraz dostupný na {url}", + "log_help_to_get_failed_log": "Akciu '{desc}' sa nepodarilo dokončiť. Ak potrebujete pomoc, zdieľajte, prosím, úplný záznam tejto operácie pomocou príkazu 'yunohost log share {name}'", + "log_letsencrypt_cert_install": "Inštalovať certifikát Let's Encrypt na doménu '{}'", + "log_letsencrypt_cert_renew": "Obnoviť '{}' certifikát Let's Encrypt", + "log_link_to_failed_log": "Akciu '{desc}' sa nepodarilo dokončiť. Ak potrebujete pomoc, poskytnite, prosím, úplný záznam tejto operácie kliknutím sem", + "main_domain_changed": "Hlavná doména bola zmenená", + "operation_interrupted": "Bola akcia manuálne prerušená?", + "password_too_simple_1": "Heslo sa musí skladať z aspoň 8 znakov", + "registrar_infos": "Informácie o registrátorovi", + "root_password_desynchronized": "Heslo pre správu bolo zmenené, ale YunoHost nedokázal túto zmenu premietnuť do hesla používateľa root!", + "unknown_error_reading_file": "Vyskytla sa neznáma chyba pri čítaní súboru {file} (príčina: {error})", + "unknown_group": "Neznáma skupina '{group}'", + "unknown_user": "Neznámy používateľ '{user}'", + "updating_apt_cache": "Získavam dostupné aktualizácie systémových balíčkov…", + "user_updated": "Informácie o používateľovi boli zmenené" +} diff --git a/locales/sl.json b/locales/sl.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/sl.json @@ -0,0 +1 @@ +{} diff --git a/locales/sv.json b/locales/sv.json new file mode 100644 index 0000000..487d728 --- /dev/null +++ b/locales/sv.json @@ -0,0 +1,25 @@ +{ + "aborting": "Avbryter.", + "action_invalid": "Ej tillåten åtgärd '{action}'", + "admin_password": "Administratörslösenord", + "already_up_to_date": "Ingenting att göra. Allt är redan uppdaterat.", + "app_action_broke_system": "Åtgärden verkar ha fått följande viktiga tjänster att haverera: {services}", + "cannot_open_file": "Kunde inte öppna filen {file} (orsak: {error})", + "cannot_write_file": "Kunde inte skriva till filen {file} (orsak: {error})", + "corrupted_json": "Skadad json läst från {ressource} (anledning: {error})", + "corrupted_toml": "Korrupt toml läst från {ressource} (anledning: {error})", + "corrupted_yaml": "Skadad yaml läst från {ressource} (anledning: {error})", + "download_bad_status_code": "{url} svarade med statuskod {code}", + "download_ssl_error": "Ett SSL-fel påträffades vid anslutning till {url}", + "download_timeout": "Gav upp eftersom {url} tog för lång tid på sig att svara.", + "download_unknown_error": "Fel vid nedladdning av data från {url}: {error}", + "error_changing_file_permissions": "Fel vid ändring av behörigheter för {path}: {error}", + "error_removing": "Fel vid borttagning av {path}: {error}", + "error_writing_file": "Fel vid skrivning av fil {file}: {error}", + "file_not_exist": "Filen finns inte: '{path}'", + "invalid_url": "Ogiltig url {url} (finns den här webbplatsen?)", + "password_too_simple_1": "Lösenordet måste bestå av minst åtta tecken", + "unknown_error_reading_file": "Okänt fel vid försök att läsa filen {file} (anledning: {error})", + "unknown_group": "Okänd grupp '{group}'", + "unknown_user": "Okänd användare '{user}'" +} diff --git a/locales/ta.json b/locales/ta.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/ta.json @@ -0,0 +1 @@ +{} diff --git a/locales/te.json b/locales/te.json new file mode 100644 index 0000000..fda22d8 --- /dev/null +++ b/locales/te.json @@ -0,0 +1,41 @@ +{ + "aborting": "రద్దు చేస్తోంది.", + "action_invalid": "చెల్లని చర్య '{action}'", + "additional_urls_already_added": "'{permission}' అనుమతి కొరకు అదనపు URLలో అదనంగా URL '{url}' ఇప్పటికే జోడించబడింది", + "additional_urls_already_removed": "'{permission}' అనుమతి కొరకు అదనపు URLలో అదనంగా URL '{url}' ఇప్పటికే జోడించబడింది", + "admin_password": "అడ్మినిస్ట్రేషన్ పాస్వర్డ్", + "already_up_to_date": "చేయడానికి ఏమీ లేదు. ప్రతిదీ ఎప్పటికప్పుడు తాజాగా ఉంది.", + "app_action_broke_system": "ఈ చర్య ఈ ముఖ్యమైన సేవలను విచ్ఛిన్నం చేసినట్లుగా కనిపిస్తోంది: {services}", + "app_action_cannot_be_ran_because_required_services_down": "ఈ చర్యను అమలు చేయడానికి ఈ అవసరమైన సేవలు అమలు చేయబడాలి: {services}. కొనసాగడం కొరకు వాటిని పునఃప్రారంభించడానికి ప్రయత్నించండి (మరియు అవి ఎందుకు పనిచేయడం లేదో పరిశోధించవచ్చు).", + "app_already_installed": "{app} ఇప్పటికే ఇన్స్టాల్ చేయబడింది", + "app_argument_choice_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: '{value}' అనేది లభ్యం అవుతున్న ఎంపికల్లో ({choices}) లేదు", + "app_argument_invalid": "ఆర్గ్యుమెంట్ '{name}' కొరకు చెల్లుబాటు అయ్యే వైల్యూ ఎంచుకోండి: {error}", + "app_change_url_success": "{app} URL ఇప్పుడు {domain}{path}", + "app_config_unable_to_apply": "config ప్యానెల్ values దరఖాస్తు చేయడంలో విఫలమయ్యాము.", + "app_config_unable_to_read": "కాన్ఫిగరేషన్ ప్యానెల్ విలువలను చదవడంలో విఫలమైంది.", + "app_extraction_failed": "ఇన్‌స్టాలేషన్ ఫైల్‌లను సంగ్రహించడం సాధ్యపడలేదు", + "app_id_invalid": "చెల్లని యాప్ ID", + "app_install_failed": "{app}ని ఇన్‌స్టాల్ చేయడం సాధ్యపడలేదు: {error}", + "app_install_files_invalid": "ఈ ఫైల్‌లను ఇన్‌స్టాల్ చేయడం సాధ్యం కాదు", + "app_install_script_failed": "యాప్ ఇన్‌స్టాలేషన్ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_manifest_install_ask_admin": "ఈ యాప్ కోసం నిర్వాహక వినియోగదారుని ఎంచుకోండి", + "app_manifest_install_ask_domain": "ఈ యాప్‌ను ఇన్‌స్టాల్ చేయాల్సిన డొమైన్‌ను ఎంచుకోండి", + "app_manifest_install_ask_is_public": "అనామక సందర్శకులకు ఈ యాప్ బహిర్గతం కావాలా?", + "app_manifest_install_ask_password": "ఈ యాప్‌కు అడ్మినిస్ట్రేషన్ పాస్‌వర్డ్‌ను ఎంచుకోండి", + "app_not_correctly_installed": "{app} తప్పుగా ఇన్‌స్టాల్ చేయబడినట్లుగా ఉంది", + "app_not_installed": "ఇన్‌స్టాల్ చేసిన యాప్‌ల జాబితాలో {app}ని కనుగొనడం సాధ్యపడలేదు: {all_apps}", + "app_not_properly_removed": "{app} సరిగ్గా తీసివేయబడలేదు", + "app_remove_after_failed_install": "ఇన్‌స్టాలేషన్ విఫలమైనందున యాప్‌ని తీసివేస్తోంది…", + "app_removed": "{app} అన్‌ఇన్‌స్టాల్ చేయబడింది", + "app_requirements_checking": "{app} కోసం అవసరమైన ప్యాకేజీలను తనిఖీ చేస్తోంది…", + "app_restore_failed": "{app}: {error}ని పునరుద్ధరించడం సాధ్యపడలేదు", + "app_restore_script_failed": "యాప్ పునరుద్ధరణ స్క్రిప్ట్‌లో లోపం సంభవించింది", + "app_sources_fetch_failed": "మూలాధార ఫైల్‌లను పొందడం సాధ్యపడలేదు, URL సరైనదేనా?", + "app_start_backup": "{app} కోసం బ్యాకప్ చేయాల్సిన ఫైల్‌లను సేకరిస్తోంది…", + "app_start_install": "{app}ని ఇన్‌స్టాల్ చేస్తోంది…", + "app_start_remove": "{app}ని తీసివేస్తోంది…", + "app_start_restore": "{app}ని పునరుద్ధరిస్తోంది…", + "app_unknown": "తెలియని యాప్", + "app_upgrade_app_name": "ఇప్పుడు {app}ని అప్‌గ్రేడ్ చేస్తోంది…", + "app_upgrade_failed": "అప్‌గ్రేడ్ చేయడం సాధ్యపడలేదు {app}: {error}" +} diff --git a/locales/tr.json b/locales/tr.json new file mode 100644 index 0000000..caa1f46 --- /dev/null +++ b/locales/tr.json @@ -0,0 +1,64 @@ +{ + "aborting": "İptal ediliyor.", + "action_invalid": "Geçersiz işlem '{action}'", + "additional_urls_already_added": "Ek URL '{url}' zaten '{permission}' izni için ek URL'ye eklendi", + "additional_urls_already_removed": "Zaten ek URL '{url}', '{permission}' izni için ek URL'de kaldırıldı", + "admin_password": "Yönetici parolası", + "admins": "Yöneticiler", + "all_users": "Tüm YunoHost kullanıcıları", + "already_up_to_date": "Yapılacak yeni bir şey yok. Her şey zaten güncel.", + "app_action_broke_system": "Bu işlem bazı hizmetleri bozmuş olabilir: {services}", + "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_action_failed": "{app} uygulaması için {action} eylemini çalıştırma başarısız", + "app_already_installed": "{app} zaten kurulu", + "app_already_installed_cant_change_url": "Bu uygulama zaten kurulu. URL yalnızca bu işlev kullanarak değiştirilemez. Eğer varsa `app changeurl`'i kontrol edin.", + "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_argument_invalid": "'{name}': {error} için geçerli bir değer giriniz", + "app_change_url_failed": "{app}: {error} için url değiştirilemedi", + "app_change_url_identical_domains": "('{domain}{path}') Eski ve yeni alan adının veya URL adresler aynı.Şu anda yapacak bir şey bulunmuyor.", + "app_change_url_no_script": "{app_name} uygulaması henüz URL değişikliğini desteklemiyor. Paket yükseltmeniz gerekebilir.", + "app_change_url_require_full_domain": "{app} bu yeni URL'ye taşınamaz. Çünkü ana etki alanı gerekli (Yani path = / olmalı )", + "app_change_url_script_failed": "URL değiştirme betiğinde bir hata oluştu", + "app_change_url_success": "{app} URL artık {domain}{path}", + "app_config_unable_to_apply": "Yapılandırma paneli değerleri uygulanamadı.", + "app_config_unable_to_read": "Yapılandırma paneli değerleri okunamadı.", + "app_corrupt_source": "YunoHost, {app} için '{source_id}' ({url}) adresinden indirebildi, ancak varlık olması gereken yapılandırmalarla eşleşmiyor. Bu, sunucunuzda geçici bir ağ arızası meydana geldiği veya varlığın bir şekilde yayın yapılan veri sağlacıyısı (veya kötü niyetli bir kişi?) tarafından değiştirildiği ve YunoHost yapımcılarının araştırması ve belki de bu değişikliği dikkate almak için uygulama bildirimini güncellemesi gerektiği anlamına gelebilir.\n Beklenen sha256 sağlama toplamı: {expected_sha256}\n İndirilen sha256 sağlama toplamı: {computed_sha256}\n İndirilen dosya boyutu: {size}", + "app_extraction_failed": "Kurulum dosyaları çıkarılamadı", + "app_failed_to_download_asset": "{app} uygulaması için {source_id}{url} adresinden indirme işlemi sağlanamadı: {out}", + "app_full_domain_unavailable": "Maalesef ki, bu uygulama kendisine ait bir alan adına inmesi gereklidir, ancak diğer uygulamalar zaten {domain} alan adına indirilmiştir. Bu uygulama için ayrılmış bir alt-alan adına kurmanız gerekir.", + "app_id_invalid": "Geçersiz uygulama kimliği", + "app_install_failed": "{app} indirilmedi: {error}", + "app_install_files_invalid": "Bu dosyalar indirilemez", + "app_install_script_failed": "Uygulama indirme script'inde (betikde) bir hata oluştu", + "app_location_unavailable": "Bu link ya kullanılamıyor ya da indirilmiş olan uygulama(lar) ile çakışıyor :{apps}", + "app_make_default_location_already_used": "'{app}' uygulaması varsayılan yapılamıyor; '{domain}' zaten '{other_app}' tarafından kullanılıyor.", + "app_manifest_install_ask_admin": "Bu uygulamayı kullanmak için bir yönetici seç", + "app_manifest_install_ask_domain": "Bu uygulamanın yüklenmesi gereken alan adını seçin", + "app_manifest_install_ask_init_admin_permission": "Kime bu uygulamanın yönetici özelliklerine erişim hakkı verilmeli?( Bu daha sonra değiştirilebilir)", + "app_manifest_install_ask_init_main_permission": "Bu uygulamaya kimin erişimi olmalı?(Bu sonradan değiştirilebilir)", + "app_manifest_install_ask_is_public": "Bu uygulama anonim ziyaretçilere açık olmalı mı?", + "app_manifest_install_ask_password": "Bu uygulama için yönetici şifresi seçin", + "app_manifest_install_ask_path": "Bu uygulamanın yüklenmesi gereken URL yolunu (alan adından sonra) seçin", + "app_not_correctly_installed": "{app} yanlış kurulmuş gibi görünüyor", + "app_not_enough_disk": "Bu uygulama {required} boş alan gerektirir.", + "cannot_open_file": "{file} dosyası açılamadı (nedeni: {error})", + "cannot_write_file": "{file} dosyası yazılamadı (nedeni: {error})", + "corrupted_json": "{ressource} adresinden okunan bozuk json (neden: {error})", + "corrupted_toml": "{ressource} kaynağından okunan bozuk TOML(nedeni: {error})", + "corrupted_yaml": "{ressource} kaynağından bozuk YAML okunuyor (neden: {error})", + "download_bad_status_code": "{url} döndürülen durum kodu {code}", + "download_ssl_error": "{url} ağına bağlanırken SSL hatası", + "download_timeout": "{url} yanıtlaması çok uzun sürdü, pes etti.", + "download_unknown_error": "{url} adresinden veri indirilirken hata oluştu: {error}", + "error_changing_file_permissions": "{path} için izinler değiştirilirken hata oluştu: {error}", + "error_removing": "{path} kaldırılırken hata oluştu: {error}", + "error_writing_file": "{file} dosyası yazılırken hata oluştu: {error}", + "file_not_exist": "Dosya mevcut değil: '{path}'", + "good_practices_about_user_password": "Şimdi yeni bir kullanıcı şifresi tanımlamak üzeresiniz. Parola en az 8 karakter uzunluğunda olmalıdır - ancak daha uzun bir parola (yani bir parola) ve/veya çeşitli karakterler (büyük harf, küçük harf, rakamlar ve özel karakterler) daha iyidir.", + "invalid_url": "{url} adresine bağlanılamadı... hizmet geçici olarak kapalı olabilir ya da IPv4/IPv6 üzerinden internete düzgün bir şekilde bağlı değilsiniz.", + "password_too_simple_1": "Şifre en az 8 karakter uzunluğunda olmalı", + "unknown_error_reading_file": "{file} dosyasını okumaya çalışırken bilinmeyen hata (nedeni: {error})", + "unknown_group": "Bilinmeyen '{group}' grubu", + "unknown_user": "Bilinmeyen '{user}' kullanıcı" +} diff --git a/locales/uk.json b/locales/uk.json new file mode 100644 index 0000000..fc8ee44 --- /dev/null +++ b/locales/uk.json @@ -0,0 +1,926 @@ +{ + "aborting": "Переривання.", + "action_invalid": "Неприпустима дія '{action}'", + "additional_urls_already_added": "Додаткову URL-адресу '{url}' вже додано для дозволу '{permission}'", + "additional_urls_already_removed": "Додаткову URL-адресу '{url}' вже видалено для дозволу '{permission}'", + "admin_password": "Пароль адмініструванні", + "admins": "Адміністратори", + "all_users": "Усі користувачі Yunohost", + "already_up_to_date": "Нічого не потрібно робити. Все вже актуально.", + "app_action_broke_system": "Ця дія, схоже, порушила роботу наступних важливих служб: {services}", + "app_action_cannot_be_ran_because_required_services_down": "Для виконання цієї дії повинні бути запущені наступні необхідні служби: {services}. Спробуйте перезапустити їх, щоб продовжити (і, можливо, з'ясувати, чому вони не працюють).", + "app_action_failed": "Не вдалося запустити дію {action} для застосунку {app}", + "app_already_installed": "{app} уже встановлено", + "app_already_installed_cant_change_url": "Цей застосунок уже встановлено. URL-адреса не може бути змінена тільки цією функцією. Перевірте в `app changeurl`, якщо вона доступна.", + "app_arch_not_supported": "Цей застосунок можна встановити лише на архітектурах {required}, але архітектура вашого сервера {current}", + "app_argument_choice_invalid": "Виберіть дійсне значення для аргументу '{name}': '{value}' не є серед доступних варіантів ({choices})", + "app_argument_invalid": "Виберіть правильне значення для аргументу '{name}': {error}", + "app_change_url_failed": "Не вдалося змінити url для {app}: {error}", + "app_change_url_identical_domains": "Старий і новий domain/url_path збігаються ('{domain}{path}'), нічого робити не треба.", + "app_change_url_no_script": "Застосунок '{app_name}' поки не підтримує зміну URL-адрес. Можливо, вам слід оновити його.", + "app_change_url_require_full_domain": "{app} не може бути переміщено на цю нову URL-адресу, оскільки для цього потрібен повний домен (тобто зі шляхом = /)", + "app_change_url_script_failed": "Виникла помилка всередині скрипта зміни URL-адреси", + "app_change_url_success": "URL-адреса {app} тепер {domain}{path}", + "app_config__core_name": "Плитки та дозволи", + "app_config_permission_allowed": "Групи/користувачі, яким дозволено доступ", + "app_config_permission_allowed_warn_protected": "Примітка: цей дозвіл є «захищеним», тому групу «відвідувачі» неможливо фактично додати/видалити з авторизованих груп.", + "app_config_permission_description": "Опис", + "app_config_permission_description_help": "Це справді корисно лише тоді, коли ви використовуєте «описовий» режим порталу", + "app_config_permission_extraperm_section_name": "Дозвіл '{perm}'", + "app_config_permission_label": "Мітка", + "app_config_permission_location": "Відповідає [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "Користувацький логотип для використання", + "app_config_permission_logo_help": "Підтримується лише PNG", + "app_config_permission_show_tile": "Відображення плитки в порталі", + "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": "Не вдалося завантажити ресурс '{source_id}' ({url}) для {app}: {out}", + "app_full_domain_unavailable": "Вибачте, цей застосунок повинен бути встановлений на власному домені, але інші застосунки вже встановлені на домені '{domain}'. Замість цього ви можете використовувати піддомен, призначений для цього застосунку.", + "app_id_invalid": "Неприпустимий ID застосунку", + "app_install_failed": "Неможливо встановити {app}: {error}", + "app_install_files_invalid": "Ці файли не можуть бути встановлені", + "app_install_script_failed": "Сталася помилка в скрипті встановлення застосунку", + "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_manifest_install_ask_password": "Виберіть пароль адмініструванні для цього застосунку", + "app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок", + "app_not_correctly_installed": "{app}, схоже, неправильно встановлено", + "app_not_enough_disk": "Цей застосунок вимагає {required} вільного місця.", + "app_not_enough_ram": "Для встановлення/оновлення цього застосунку потрібно {required} оперативної пам'яті, але наразі доступно лише {current}.", + "app_not_installed": "Не вдалося знайти {app} в списку встановлених застосунків: {all_apps}", + "app_not_properly_removed": "{app} не було видалено належним чином", + "app_packaging_format_not_supported": "Цей застосунок не може бути встановлено, тому що формат його упакування не підтримується вашою версією YunoHost. Можливо, вам слід оновити систему.", + "app_remove_after_failed_install": "Вилучення додатку після збою встановлення…", + "app_removed": "{app} видалено", + "app_requirements_checking": "Перевіряння необхідних пакунків для {app}…", + "app_resource_failed": "Не вдалося надати, позбавити або оновити ресурси для {app}: {error}", + "app_restore_failed": "Не вдалося відновити {app}: {error}", + "app_restore_script_failed": "Сталася помилка всередині скрипта відновлення застосунку", + "app_sources_fetch_failed": "Не вдалося отримати джерельні файли, URL-адреса правильна?", + "app_start_backup": "Збирання файлів для резервного копіювання {app}…", + "app_start_install": "Встановлення {app}…", + "app_start_remove": "Вилучення {app}…", + "app_start_restore": "Відновлення {app}…", + "app_unknown": "Невідомий застосунок", + "app_unsupported_remote_type": "Для застосунку використовується непідтримуваний віддалений тип", + "app_upgrade_app_name": "Зараз оновлюємо {app}…", + "app_upgrade_bad_quality": "Цей додаток наразі позначено як несправний у каталозі додатків YunoHost. Це може бути тимчасова проблема, поки розробники намагаються її виправити. Тим часом оновлення цього додатка вимкнено.", + "app_upgrade_broke_the_system": "Оновлення {app}, здавалося б, спрацювало, але залишило систему в несправному стані, тому вважається невдалим.", + "app_upgrade_cli_bad_quality": "Пропускаємо оновлення для {app}, оскільки він наразі позначений як несправний у каталозі додатків YunoHost.", + "app_upgrade_cli_up_to_date": "{app} вже оновлено до ({current_version})", + "app_upgrade_cli_url_required": "{app} більше не є в каталозі (чи не так?) і тому не може бути оновлений автоматично. Вам слід використовувати `yunohost app upgrade {app}`, щоб надати URL-адресу репозиторію за допомогою опції `-u`.", + "app_upgrade_cli_will_force_upgrade": "{app} буде примусово оновлено до ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} буде оновлено з {current_version} до {new_version}", + "app_upgrade_continuing_with_other_apps": "Не вдалося оновити {app}, але оновлення інших додатків все одно продовжується (оскільки було використано `--continue-on-failure`)", + "app_upgrade_fail_requirements": "Для цього додатка доступна нова версія ({new_version}), але деякі вимоги не виконано:\n{failed_requirements}", + "app_upgrade_failed": "Не вдалося оновити {app}: {error}", + "app_upgrade_failed_and_broke_the_system": "Не вдалося оновити додаток '{app}', і система перебувала у несправному стані.", + "app_upgrade_script_failed": "Сталася помилка в скрипті оновлення застосунку", + "app_upgrade_several_apps": "Наступні застосунки буде оновлено: {apps}", + "app_upgrade_some_app_failed": "Деякі застосунки не можуть бути оновлені", + "app_upgrade_specific_channel_msg": "Зверніть увагу, що ви зараз використовуєте `{channel}` як джерело для оновлень. Обов'язково перегляньте поточне обговорення [тут]({pr_url}).", + "app_upgrade_up_to_date": "Примусове оновлення додатку (до тієї ж версії) іноді може бути корисним для перебудови додатку та конфігурацій.", + "app_upgrade_upgradable": "Додаток може бути оновлений з версії {current_version} до {new_version}", + "app_upgrade_url_required": "Цей додаток відсутній у каталозі (чи його там вже немає?), тому вам доведеться самостійно дбати про його оновлення.
У командному рядку можна скористатися командою `yunohost app upgrade ` та вказати URL-адресу репозиторію за допомогою опції `-u`.", + "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_catalog_obsolete_cache": "Кеш каталогу застосунків порожній або застарів.", + "apps_catalog_update_success": "Каталог застосунків був оновлений!", + "apps_catalog_updating": "Оновлення каталогу додатків…", + "apps_confirm_partial_upgrade": "Деякі додатки, для яких було запрошено оновлення, неможливо оновити. Продовжити з іншими?", + "apps_no_target_can_be_upgraded": "Немає додатків для оновлення", + "apps_upgrade_cancelled": "Деякі інши додатки все ще очікували на оновлення, але їх було скасовано (використовуйте `--continue-on-failure`, щоб продовжити все одно): {apps}", + "ask_admin_fullname": "Повне ім'я адміністратора", + "ask_admin_username": "Ім'я користувача адміністратора", + "ask_dyndns_recovery_password": "Пароль відновлення DynDNS", + "ask_dyndns_recovery_password_explain": "Будь ласка, виберіть пароль для відновлення доступу до вашого домену DynDNS на випадок, якщо вам знадобиться його скинути пізніше.", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "Введіть пароль відновлення для цього домену DynDNS.", + "ask_dyndns_recovery_password_explain_unavailable": "Цей домен DynDNS вже зареєстрований. Якщо ви особисто зареєстрували цей домен, можете ввести пароль для відновлення домену.", + "ask_fullname": "Повне ім'я", + "ask_main_domain": "Основний домен", + "ask_new_admin_password": "Новий пароль адмініструванні", + "ask_new_domain": "Новий домен", + "ask_new_path": "Новий шлях", + "ask_password": "Пароль", + "ask_user_domain": "Домен для адреси ел. пошти користувача", + "automatic_task": "Автоматичне завдання", + "backup_abstract_method": "Цей спосіб резервного копіювання ще не реалізований", + "backup_actually_backuping": "Створення резервного архіву зі зібраних файлів…", + "backup_app_script_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_corrupted": "Схоже, що архів резервної копії '{archive}' пошкоджений: {error}", + "backup_archive_name_exists": "Архів резервної копії з назвою '{name}' вже існує.", + "backup_archive_name_unknown": "Невідомий локальний архів резервного копіювання з назвою '{name}'", + "backup_archive_open_failed": "Не вдалося відкрити архів резервної копії", + "backup_archive_system_part_not_available": "Системна частина '{part}' недоступна в цій резервній копії", + "backup_archive_writing_error": "Не вдалося додати файли '{source}' (названі в архіві '{dest}') для резервного копіювання в стислий архів '{archive}'", + "backup_ask_for_copying_if_needed": "Ви бажаєте тимчасово виконати резервне копіювання з використанням {size} МБ? (Цей спосіб використовується, оскільки деякі файли не можуть бути підготовлені дієвіше.)", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "Резервну копію {name} видалено, оскільки її замінено новішою резервною копією {newname}", + "backup_cant_mount_uncompress_archive": "Не вдалося змонтувати нестислий архів як захищений від запису", + "backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання", + "backup_copying_to_organize_the_archive": "Копіювання {size} МБ для організації архіву", + "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_hook_unknown": "Гачок (hook) резервного копіювання '{hook}' невідомий", + "backup_method_copy_finished": "Резервне копіювання завершено", + "backup_method_custom_finished": "Користувацький спосіб резервного копіювання '{method}' завершено", + "backup_method_tar_finished": "Створено архів резервного копіювання TAR", + "backup_mount_archive_for_restore": "Підготовлення архіву для відновлення…", + "backup_no_file_collected": "Не вдалося зібрати файли для резервного копіювання", + "backup_no_uncompress_archive_dir": "Немає такого каталогу нестислого архіву", + "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_output_symlink_dir_broken": "Ваш архівний каталог '{path}' є неробочим символічним посиланням. Можливо, ви забули перемонтувати або підключити носій, на який вона вказує.", + "backup_running_hooks": "Запуск гачків (hook) резервного копіювання…", + "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} не має скрипта відновлення, ви не зможете автоматично відновити резервну копію цього застосунку.", + "cannot_open_file": "Не можу відкрити файл {file} (причина: {error})", + "cannot_write_file": "Не можу записати файл {file} (причина: {error})", + "certmanager_acme_not_configured_for_domain": "Завдання ACME не може бути запущене для {domain} прямо зараз, тому що в його nginx-конфігурації відсутній відповідний фрагмент коду… Будь ласка, переконайтеся, що конфігурація nginx оновлена за допомогою `yunohost tools regen-conf nginx --dry-run --with-diff`.", + "certmanager_attempt_to_renew_nonLE_cert": "Сертифікат для домену '{domain}' не випущено Let's Encrypt. Неможливо продовжити його автоматично!", + "certmanager_attempt_to_renew_valid_cert": "Строк дії сертифіката для домена '{domain}' не закінчується! (Ви можете використовувати --force, якщо знаєте, що робите)", + "certmanager_attempt_to_replace_valid_cert": "Ви намагаєтеся перезаписати хороший дійсний сертифікат для домену {domain}! (Використовуйте --force для обходу)", + "certmanager_cannot_read_cert": "Щось не так сталося при спробі відкрити поточний сертифікат для домена {domain} (файл: {file}), причина: {reason}", + "certmanager_cert_install_failed": "Не вдалося встановити сертифікат Let's Encrypt для {domains}", + "certmanager_cert_install_failed_selfsigned": "Не вдалося встановити самопідписаний сертифікат для {domains}", + "certmanager_cert_install_success": "Сертифікат Let's Encrypt тепер встановлений для домена '{domain}'", + "certmanager_cert_install_success_selfsigned": "Самопідписаний сертифікат тепер встановлений для домену '{domain}'", + "certmanager_cert_renew_failed": "Помилка оновлення сертифіката Let's Encrypt для {domains}", + "certmanager_cert_renew_success": "Сертифікат Let's Encrypt оновлений для домену '{domain}'", + "certmanager_cert_signing_failed": "Не вдалося підписати новий сертифікат", + "certmanager_certificate_fetching_or_enabling_failed": "Спроба використовувати новий сертифікат для {domain} не спрацювала…", + "certmanager_domain_cert_not_selfsigned": "Сертифікат для домену {domain} не є самопідписаним. Ви впевнені, що хочете замінити його? (Для цього використовуйте '--force'.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домену '{domain}' відрізняються від IP цього сервера. Будь ласка, перевірте категорію 'DNS-записи' (основні) в діагностиці для отримання додаткових даних. Якщо ви недавно змінили запис A, будь ласка, зачекайте, поки він пошириться (деякі програми перевірки поширення DNS доступні в Інтернеті). (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки.)", + "certmanager_domain_http_not_working": "Домен {domain}, схоже, не доступний через HTTP. Будь ласка, перевірте категорію 'Мережа' в діагностиці для отримання додаткових даних. (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки.)", + "certmanager_domain_not_diagnosed_yet": "Поки немає результатів діагностики для домену {domain}. Будь ласка, повторно проведіть діагностику для категорій 'DNS-записи' і 'Мережа' в розділі діагностики, щоб перевірити, чи готовий домен до Let's Encrypt. (Або, якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки.)", + "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})", + "certmanager_unable_to_parse_self_CA_name": "Не вдалося розібрати назву самопідписного центру (файл: {file})", + "config_action_disabled": "Не вдалося запустити дію '{action}', оскільки вона вимкнена, переконайтеся, що виконані її обмеження. довідка: {help}", + "config_action_failed": "Не вдалося запустити дію '{action}': {error}", + "config_apply_failed": "Не вдалося застосувати нову конфігурацію: {error}", + "config_cant_set_value_on_section": "Ви не можете встановити одне значення на весь розділ конфігурації.", + "config_forbidden_keyword": "Ключове слово '{keyword}' зарезервовано, ви не можете створити або використовувати панель конфігурації з запитом із таким ID.", + "config_forbidden_readonly_type": "Тип '{type}' не може бути встановлений як readonly, використовуйте інший тип для показу цього значення (відповідний arg id: '{id}').", + "config_no_panel": "Панель конфігурації не знайдено.", + "config_unknown_filter_key": "Ключ фільтра '{filter_key}' недійсний.", + "confirm_app_install_danger": "НЕБЕЗПЕЧНО! Відомо, що цей додаток все ще експериментальний (якщо не сказати, що він явно не працює)! Вам не слід встановлювати його, якщо ви не знаєте, що робите. Ніякої підтримки не буде надано, якщо цей додаток не буде працювати або зламає вашу систему… Якщо ви все одно готові ризикнути, введіть '{answers}'", + "confirm_app_install_thirdparty": "НЕБЕЗПЕЧНО! Цей додаток не входить у каталог додатків YunoHost. Встановлення сторонніх додатків може порушити цілісність і безпеку вашої системи. Вам не слід встановлювати його, якщо ви не знаєте, що робите. НІЯКОЇ ПІДТРИМКИ НЕ БУДЕ, якщо цей застосунок не буде працювати або зламає вашу систему… Якщо ви все одно готові піти на такий ризик, введіть '{answers}'", + "confirm_app_install_warning": "Попередження: Цей застосунок може працювати, але він не дуже добре інтегрований в YunoHost. Деякі функції, такі як єдина реєстрація та резервне копіювання/відновлення, можуть бути недоступні. Все одно встановити? [{answers}]. ", + "confirm_app_insufficient_ram": "Для встановлення цього додатка потрібно більше оперативної пам'яті, ніж є в наявності. Навіть якби цей додаток можна було б запустити, процес його встановлення/оновлення вимагає великої кількості оперативної пам'яті, тому ваш сервер може зависнути і вийти з ладу. Якщо ви все одно готові піти на цей ризик, введіть '{answers}'", + "confirm_notifications_read": "ПОПЕРЕДЖЕННЯ: Перш ніж продовжити, перевірте сповіщення застосунку вище, там можуть бути важливі повідомлення. [{answers}]", + "confirm_tos_acknowledgement": "Я прочитав(-ла) та зрозумів(-ла) Умови надання послуг [{answers}]", + "corrupted_json": "Пошкоджений JSON, зчитаний з {ressource} (причина: {error})", + "corrupted_toml": "Пошкоджений TOML, зчитаний з {ressource} (причина: {error})", + "corrupted_yaml": "Пошкоджений YAML, зчитаний з {ressource} (причина: {error})", + "danger": "Небезпека:", + "diagnosis_apps_allgood": "Усі встановлені застосунки дотримуються основних способів упакування", + "diagnosis_apps_bad_quality": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", + "diagnosis_apps_broken": "Цей застосунок наразі позначено як зламаний у каталозі застосунків YunoHost. Це може бути тимчасовою проблемою, поки організатори намагаються вирішити цю проблему. Тим часом оновлення цього застосунку вимкнено.", + "diagnosis_apps_deprecated_practices": "Установлена версія цього застосунку все ще використовує деякі надто застарілі практики упакування. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_issue": "Виявлено проблему із застосунком {app}", + "diagnosis_apps_not_in_app_catalog": "Цей застосунок не міститься у каталозі застосунків YunoHost. Якщо він був у минулому і був видалений, вам слід подумати про видалення цього застосунку, оскільки він не отримає оновлення, і це може поставити під загрозу цілісність та безпеку вашої системи.", + "diagnosis_apps_outdated_packaging_format": "Цей додаток використовує застарілий формат пакування та незабаром не підтримуватиметься YunoHost. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_outdated_ynh_requirement": "Встановлена версія цього додатку вимагає лише Yunohost >= 2.x чи 3.х, що, як правило, вказує на те, що воно не відповідає сучасним рекомендаційним практикам упакування та порадникам. Вам дійсно варто подумати про його оновлення.", + "diagnosis_apps_security_issue_error": "Додаток {app} наразі має версію '{current_version}', яка має СЕРЙОЗНІ проблеми безпеки: {title}. Рекомендується оновити його ЯК МОЖЛИВО ШВИДШЕ до версії '{fixed_in_version}'. Додаткова інформація: {more_infos_list}", + "diagnosis_apps_security_issue_warning": "Додаток {app} наразі має версію '{current_version}', яка має помірну вразливість до проблеми безпеки: {title}. Рекомендується оновити його до '{fixed_in_version}'. Додаткова інформація: {more_infos_list}", + "diagnosis_backports_in_sources_list": "Схоже, що apt (менеджер пакетів) налаштований на використання репозиторія backports. Якщо ви не знаєте, що робите, ми наполегливо не радимо встановлювати пакети з backports, тому що це може привести до нестабільності або конфліктів у вашій системі.", + "diagnosis_basesystem_hardware": "Архітектура апаратного забезпечення сервера - {virt} {arch}", + "diagnosis_basesystem_hardware_model": "Модель сервера - {model}", + "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_cant_run_because_of_dep": "Неможливо запустити діагностику для {category}, поки є важливі проблеми, пов'язані з {dep}.", + "diagnosis_description_apps": "Застосунки", + "diagnosis_description_basesystem": "Основна система", + "diagnosis_description_dnsrecords": "DNS-записи", + "diagnosis_description_ip": "Інтернет-з'єднання", + "diagnosis_description_mail": "Е-пошта", + "diagnosis_description_ports": "Виявлення портів", + "diagnosis_description_regenconf": "Конфігурації системи", + "diagnosis_description_services": "Перевірка стану служб", + "diagnosis_description_systemresources": "Системні ресурси", + "diagnosis_description_web": "Мережа", + "diagnosis_diskusage_low": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Будьте уважні.", + "diagnosis_diskusage_ok": "У сховищі {mountpoint} (на пристрої {device}) залишилося {free} ({free_percent}%) вільного місця (з {total})!", + "diagnosis_diskusage_verylow": "Сховище {mountpoint} (на пристрої {device}) має тільки {free} ({free_percent}%) вільного місця (з {total}). Вам дійсно варто подумати про очищення простору!", + "diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадмініструванні або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.", + "diagnosis_dns_bad_conf": "Деякі DNS-записи відсутні або неправильні для домену {domain} (категорія {category})", + "diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації:
Тип: {type}
Назва: {name}
Поточне значення: {current}
Очікуване значення: {content}", + "diagnosis_dns_good_conf": "DNS-записи правильно налаштовані для домену {domain} (категорія {category})", + "diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.
Тип: {type}
Назва: {name}
Значення: {content}", + "diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті https://doc.yunohost.org/dns_config.", + "diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) такого як .local або .test і тому не очікується, що у нього будуть актуальні записи DNS.", + "diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди yunohost dyndns update --force.", + "diagnosis_domain_expiration_error": "Строк дії деяких доменів НЕЗАБАРОМ спливе!", + "diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів", + "diagnosis_domain_expiration_not_found_details": "Відомості WHOIS для домену {domain} не містять даних про строк дії?", + "diagnosis_domain_expiration_success": "Ваші домени зареєстровані і не збираються спливати найближчим часом.", + "diagnosis_domain_expiration_warning": "Строк дії деяких доменів спливе найближчим часом!", + "diagnosis_domain_expires_in": "Строк дії {domain} спливе через {days} днів.", + "diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!", + "diagnosis_everything_ok": "Здається, для категорії '{category}' все справно!", + "diagnosis_failed": "Не вдалося отримати результат діагностики для категорії '{category}': {error}", + "diagnosis_failed_for_category": "Не вдалося провести діагностику для категорії '{category}': {error}", + "diagnosis_found_errors": "Знайдена {errors} важлива проблема (і), пов'язана з {category}!", + "diagnosis_found_errors_and_warnings": "Знайдено {errors} істотний (і) питання (и) (і {warnings} попередження (я)), що відносяться до {category}!", + "diagnosis_found_warnings": "Знайдено {warnings} пунктів, які можна поліпшити для {category}.", + "diagnosis_high_number_auth_failures": "Останнім часом сталася підозріло велика кількість помилок автентифікації. Ви можете переконатися, що fail2ban працює і правильно налаштований, або скористатися власним портом для SSH, як описано в https://doc.yunohost.org/security.", + "diagnosis_http_bad_status_code": "Схоже, що замість вашого сервера відповіла інша машина (можливо, ваш маршрутизатор).
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлено на ваш сервер .
2. На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.", + "diagnosis_http_connection_error": "Помилка з'єднання: не вдалося з'єднатися із запитуваним доменом, швидше за все, він недоступний.", + "diagnosis_http_could_not_diagnose": "Не вдалося діагностувати досяжність доменів ззовні в IPv{ipversion}.", + "diagnosis_http_could_not_diagnose_details": "Помилка: {error}", + "diagnosis_http_hairpinning_issue": "Схоже, що у вашій локальній мережі не увімкнено шпилькування (hairpinning).", + "diagnosis_http_hairpinning_issue_details": "Можливо, це пов'язано з коробкою/маршрутизатором вашого інтернет-провайдера. В результаті, люди ззовні вашої локальної мережі зможуть отримати доступ до вашого сервера, як і очікувалося, але не люди зсередини локальної мережі (як ви, ймовірно?) При використанні доменного імені або глобального IP. Можливо, ви зможете поліпшити ситуацію, глянувши https://doc.yunohost.org/dns_local_network ", + "diagnosis_http_nginx_conf_not_up_to_date": "Схоже, що конфігурація nginx цього домену була змінена вручну, що не дозволяє YunoHost визначити, чи доступний він по HTTP.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Щоб виправити становище, перевірте різницю за допомогою командного рядка, використовуючи yunohost tools regen-conf nginx --dry-run --with-diff, і якщо все в порядку, застосуйте зміни за допомогою команди yunohost tools regen-conf nginx --force.", + "diagnosis_http_ok": "Домен {domain} доступний по HTTP поза локальною мережею.", + "diagnosis_http_partially_unreachable": "Домен {domain} здається недоступним по HTTP поза локальною мережею в IPv{failed}, хоча він працює в IPv{passed}.", + "diagnosis_http_special_use_tld": "Домен {domain} базується на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він буде відкритий за межами локальної мережі.", + "diagnosis_http_timeout": "При спробі зв'язатися з вашим сервером ззовні стався тайм-аут. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 80 (і 443) неправильно перенаправлено на ваш сервер .
2. Ви також повинні переконатися, що служба nginx запущена
3.На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.", + "diagnosis_http_unreachable": "Домен {domain} здається недоступним через HTTP поза локальною мережею.", + "diagnosis_ignore_already_filtered": "(Фільтр діагностики {category} з цими критеріями вже існує)", + "diagnosis_ignore_criteria_error": "Критерії повинні мати формат ключ=значення (наприклад, домен=yolo.test)", + "diagnosis_ignore_filter_added": "Додано фільтр діагностики {category}", + "diagnosis_ignore_filter_removed": "Видалено фільтр діагностики {category}", + "diagnosis_ignore_missing_criteria": "Ви повинні надати принаймні один критерій, який є категорією діагностики, яку слід ігнорувати", + "diagnosis_ignore_no_filter_found": "(Немає такого фільтра діагностики {category} з цими критеріями для видалення)", + "diagnosis_ignore_no_issue_found": "Не знайдено жодної проблеми, що відповідає заданим критеріям.", + "diagnosis_ignored_issues": "(+ {nb_ignored} знехтувана проблема (проблеми))", + "diagnosis_ip_broken_dnsresolution": "Роздільність доменних імен, схоже, з якоїсь причини не працює… Фаєрвол блокує DNS-запити?", + "diagnosis_ip_broken_resolvconf": "Схоже, що роздільність доменних імен на вашому сервері порушено, що пов'язано з тим, що /etc/resolv.conf не вказує на 127.0.0.1.", + "diagnosis_ip_connected_ipv4": "Сервер під'єднаний до Інтернету через IPv4!", + "diagnosis_ip_connected_ipv6": "Сервер під'єднаний до Інтернету через IPv6!", + "diagnosis_ip_dnsresolution_working": "Роздільність доменних імен працює!", + "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://doc.yunohost.org/ipv6. Якщо ви не можете увімкнути IPv6 або якщо це здається вам занадто технічним, ви також можете сміливо нехтувати цим попередженням.", + "diagnosis_ip_no_ipv6_tip_important": "Зазвичай IPv6 має бути автоматично налаштований системою або вашим провайдером, якщо він доступний. В іншому випадку, можливо, вам доведеться налаштувати деякі речі вручну, як описано в документації тут: https://doc.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 повинен бути символічним посиланням на /etc/resolvconf/run/resolv.conf, що вказує на 127.0.0.1(dnsmasq). Якщо ви хочете вручну налаштувати DNS вирішувачі (resolvers), відредагуйте /etc/resolv.dnsmasq.conf.", + "diagnosis_mail_blocklist_listed_by": "Ваш IP або домен {item} знаходиться в чорному списку {blocklist_name}", + "diagnosis_mail_blocklist_ok": "IP-адреси і домени, які використовуються цим сервером, не внесені в чорний список", + "diagnosis_mail_blocklist_reason": "Причина внесення в чорний список: {reason}", + "diagnosis_mail_blocklist_reason_openresolver": "Схоже, що причина вказана як «open resolver».
Зазвичай це означає, що ваш сервер використовує не свій локальний DNS, а публічний, відкритий.
Перевірте вміст /etc/resolv.conf, він має містити nameserver 127.0.0.1.
Оскільки цей файл зазвичай генерується автоматично, не редагуйте його вручну. Перевірте налаштування DHCP або налаштування VPN, якщо ви його використовуєте, або якщо ви використовували образ Debian, створений, наприклад, постачальником VPS, знайдіть конфігурацію cloudinit.
Ви можете звернутися до каналів підтримки YunoHost, щоб отримати допомогу з цього питання.
Дослівна причина чорного списку: {reason}", + "diagnosis_mail_blocklist_website": "Після визначення причини, з якої ви потрапили в чорний список, і її усунення, ви можете попросити видалити ваш IP або домен на {blocklist_website}", + "diagnosis_mail_ehlo_bad_answer": "Не-SMTP служба відповіла на порту 25 на IPv{ipversion}", + "diagnosis_mail_ehlo_bad_answer_details": "Це може бути викликано тим, що замість вашого сервера відповідає інша машина.", + "diagnosis_mail_ehlo_could_not_diagnose": "Не вдалося діагностувати, чи доступний поштовий сервер postfix ззовні в IPv{ipversion}.", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Помилка: {error}", + "diagnosis_mail_ehlo_ok": "Поштовий сервер SMTP доступний ззовні і тому може отримувати електронні листи!", + "diagnosis_mail_ehlo_unreachable": "Поштовий сервер SMTP недоступний ззовні по IPv{ipversion}. Він не зможе отримувати листи електронної пошти.", + "diagnosis_mail_ehlo_unreachable_details": "Не вдалося відкрити з'єднання за портом 25 з вашим сервером на IPv{ipversion}. Він здається недоступним.
1. Найбільш поширеною причиною цієї проблеми є те, що порт 25 неправильно перенаправлений на ваш сервер.
2. Ви також повинні переконатися, що служба postfix запущена.
3. На більш складних установках: переконайтеся, що немає фаєрвола або зворотного проксі.", + "diagnosis_mail_ehlo_wrong": "Інший поштовий SMTP-сервер відповідає на IPv{ipversion}. Ваш сервер, ймовірно, не зможе отримувати електронні листи.", + "diagnosis_mail_ehlo_wrong_details": "EHLO, отриманий віддаленим діагностичним центром в IPv{ipversion}, відрізняється від домену вашого сервера.
Отриманий EHLO: {wrong_ehlo}
Очікуваний: {right_ehlo}< br>Найпоширенішою причиною цієї проблеми є те, що порт 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_4": "Деякі провайдери не дозволять вам налаштувати зворотний DNS (або їх функція може бути зламана…). Якщо ви відчуваєте проблеми через це, розгляньте наступні рішення:
- Деякі провайдери надають альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу подібних обмежень. Дивіться https://doc.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_details": "Спочатку спробуйте налаштувати зворотний DNS з {ehlo_domain} в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм запит у підтримку для цього).", + "diagnosis_mail_fcrdns_ok": "Ваш зворотний DNS налаштовано правильно!", + "diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.", + "diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Деякі провайдери не дозволять вам розблокувати вихідний порт 25, тому що вони не піклуються про мережевий нейтралітет (Net Neutrality).
- Деякі з них пропонують альтернативу використання ретранслятора поштового сервера, хоча це має на увазі, що ретранслятор зможе шпигувати за вашим поштовим трафіком.
- Альтернативою для захисту конфіденційності є використання VPN *з виділеним загальнодоступним IP* для обходу такого роду обмежень. Дивіться https://doc.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_never_ran_yet": "Схоже, що цей сервер був налаштований недавно, і поки немає звіту про діагностику. Вам слід почати з повної діагностики, або з вебадмініструванні, або використовуючи 'yunohost diagnosis run' з командного рядка.", + "diagnosis_no_cache": "Для категорії «{category}» ще немає кеша діагностики", + "diagnosis_package_installed_from_sury": "Деякі системні пакети мають бути зістарені у версії", + "diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені зі стороннього репозиторію під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили застосунки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити це становище, спробуйте виконати наступну команду: {cmd_to_fix}", + "diagnosis_package_security_issue_error": "Системний пакет '{package}' зараз має версію '{current_version}', що має СЕРЙОЗНУ проблему з безпекою: {title}. Рекомендується ЯК УМОГА ШВИДШЕ оновити до версії '{fixed_in_version}'. Докладніше: {more_infos_list}", + "diagnosis_package_security_issue_warning": "Системний пакет '{package}' зараз має версію '{current_version}', яка має проблему з безпекою: {title}. Рекомендується оновлення до версії '{fixed_in_version}'. Докладніше: {more_infos_list}", + "diagnosis_ports_could_not_diagnose": "Не вдалося діагностувати досяжність портів ззовні в IPv{ipversion}.", + "diagnosis_ports_could_not_diagnose_details": "Помилка: {error}", + "diagnosis_ports_forwarding_tip": "Щоб вирішити цю проблему, вам, швидше за все, потрібно налаштувати пересилання портів на вашому інтернет-маршрутизаторі, як описано в https://doc.yunohost.org/admin/get_started/post_install/dns_config/", + "diagnosis_ports_needed_by": "Відкриття цього порту необхідне для функцій {category} (служба {service})", + "diagnosis_ports_ok": "Порт {port} доступний ззовні.", + "diagnosis_ports_partially_unreachable": "Порт {port} не доступний ззовні в IPv{failed}.", + "diagnosis_ports_unreachable": "Порт {port} недоступний ззовні.", + "diagnosis_processes_killed_by_oom_reaper": "Деякі процеси було недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів:\n{kills_summary}", + "diagnosis_ram_low": "У системі наявно {available} ({available_percent}%) оперативної пам'яті (з {total}). Будьте уважні.", + "diagnosis_ram_ok": "Система все ще має {available} ({available_percent}%) оперативної пам'яті з {total}.", + "diagnosis_ram_verylow": "Система має тільки {available} ({available_percent}%) оперативної пам'яті! (з {total})", + "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_rfkill_wifi": "Карту Wi-Fi вимкнено, і системне попередження може перешкоджати встановленню додатків", + "diagnosis_rfkill_wifi_details": "Це попередження прокрадається до багатьох командних виводів, що порушує роботу деяких додатків. Зазвичай потрібно вказати код країни за допомогою команди sudo raspi-config. Ось помилка:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "Коренева файлова система має тільки {space}, що дуже тривожно! Скоріше за все, дисковий простір закінчиться дуже скоро! Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.", + "diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {space}. Можливо це нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися… Рекомендовано мати не менше 16 ГБ для кореневої файлової системи.", + "diagnosis_security_vulnerable_to_meltdown": "Схоже, що ви вразливі до критичної вразливості безпеки Meltdown", + "diagnosis_security_vulnerable_to_meltdown_details": "Щоб виправити це, вам слід оновити систему і перезавантажитися, щоб завантажити нове ядро Linux (або звернутися до вашого серверного провайдера, якщо це не спрацює). Докладніше див. на сайті https://meltdownattack.com/.", + "diagnosis_services_bad_status": "Служба {service} у стані {status} :(", + "diagnosis_services_bad_status_tip": "Ви можете спробувати перезапустити службу, а якщо це не допоможе, подивіться журнали служби в вебадмініструванні (з командного рядка це можна зробити за допомогою yunohost service restart {service} і yunohost service log {service}).", + "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_sshd_config_inconsistent_details": "Будь ласка, виконайте команду yunohost settings set security.ssh.ssh port -v ВАШ_SSH_ПОРТ, щоб визначити порт SSH, і перевіртеyunohost tools regen-conf ssh --dry-run --with-diff і yunohost tools regen-conf ssh --force, щоб скинути ваш конфіг на рекомендований YunoHost.", + "diagnosis_sshd_config_insecure": "Схоже, що конфігурація SSH була змінена вручну і є небезпечною, оскільки не містить директив 'AllowGroups' або 'AllowUsers' для обмеження доступу авторизованих користувачів.", + "diagnosis_swap_none": "В системі повністю відсутня підкачка. Ви повинні розглянути можливість додавання принаймні {recommended} обсягу підкачки, щоб уникнути ситуацій, коли системі не вистачає пам'яті.", + "diagnosis_swap_notsomuch": "Система має тільки {total} обсягу підкачки. Щоб уникнути станоаищ, коли в системі закінчується пам'ять, слід передбачити наявність не менше {recommended} обсягу підкачки.", + "diagnosis_swap_ok": "Система має {total} обсягу підкачки!", + "diagnosis_swap_tip": "Будь ласка, будьте обережні і знайте, що якщо сервер розміщує обсяг підкачки на SD-карті або SSD-накопичувачі, це може різко скоротити строк служби пристрою`.", + "diagnosis_unknown_categories": "Наступні категорії невідомі: {categories}", + "diagnosis_using_stable_codename": "apt (системний менеджер пакунків) наразі налаштовано на встановлення пакунків з кодовою назвою \"stable\", замість кодової назви поточної версії Debian (bullseye).", + "diagnosis_using_stable_codename_details": "Зазвичай це спричинено неправильним налаштуванням від вашого хостинг-провайдера. Це небезпечно, оскільки як тільки наступна версія Debian стане новою \"стабільною\", apt захоче оновити всі системні пакунки без проходження належної процедури міграції. Радимо виправити це, відредагувавши джерело apt для базового репозиторію Debian, і замінити ключове слово stable на bullseye. Відповідний конфігураційний файл має бути в /etc/apt/sources.list, або файл у /etc/apt/sources.list.d/.", + "diagnosis_using_yunohost_testing": "apt (менеджер пакунків системи) наразі налаштований на встановлення будь-якого \"тестового\" оновлення для ядра YunoHost.", + "diagnosis_using_yunohost_testing_details": "Це, ймовірно, нормально, якщо ви знаєте, що робите, але зверніть увагу на примітки до випуску, перш ніж встановлювати оновлення YunoHost! Якщо ви хочете вимкнути 'тестування' оновлень, вам слід видалити ключове слово testing з /etc/apt/sources.list.d/yunohost.list.", + "disk_space_not_sufficient_install": "Недостатньо місця на диску для встановлення цього застосунку", + "disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку", + "domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n '; ось список доменів-кандидатів: {other_domains}", + "domain_cannot_remove_main_add_new_one": "Ви не можете видалити '{domain}', так як це основний домен і ваш єдиний домен, вам потрібно спочатку додати інший домен за допомогою 'yunohost domain add ', потім встановити його як основний домен за допомогою 'yunohost domain main-domain -n ' і потім ви можете вилучити домен '{domain}' за допомогою 'yunohost domain remove {domain}'.", + "domain_cert_gen_failed": "Не вдалося утворити сертифікат", + "domain_config_acme_eligible": "Відповідність ACME", + "domain_config_acme_eligible_explain": "Здається, цей домен не готовий для сертифіката Let's Encrypt. Будь ласка, перевірте конфігурацію DNS і доступність HTTP-сервера. Розділи \"DNS-записи\" та \"Веб\" на сторінці діагностики можуть допомогти вам зрозуміти, що саме налаштовано неправильно.", + "domain_config_api_protocol": "API-протокол", + "domain_config_auth_application_key": "Ключ застосунку", + "domain_config_auth_application_secret": "Таємний ключ застосунку", + "domain_config_auth_consumer_key": "Ключ споживача", + "domain_config_auth_entrypoint": "Точка входу API", + "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_name": "Сертифікат", + "domain_config_cert_no_checks": "Нехтувати перевірками діагностики", + "domain_config_cert_renew": "Поновити сертифікат Let's Encrypt", + "domain_config_cert_renew_help": "Сертифікат буде автоматично поновлено протягом останніх 15 днів дії. Ви можете вручну поновити його, якщо хочете. (Не рекомендовано).", + "domain_config_cert_summary": "Стан сертифікату", + "domain_config_cert_summary_abouttoexpire": "Строк дії поточного сертифіката закінчується. Невдовзі його мають автоматично поновити.", + "domain_config_cert_summary_expired": "КРИТИЧНО: поточний сертифікат недійсний! HTTPS цілковито не працюватиме!", + "domain_config_cert_summary_letsencrypt": "Чудово! Ви використовуєте дійсний сертифікат Let's Encrypt!", + "domain_config_cert_summary_ok": "Гаразд, поточний сертифікат виглядає добре!", + "domain_config_cert_summary_selfsigned": "ПОПЕРЕДЖЕННЯ: поточний сертифікат є самопідписаним. Браузери відображатимуть моторошне попередження новим відвідувачам!", + "domain_config_cert_validity": "Достовірність", + "domain_config_custom_css": "Користувацька таблиця стилів CSS", + "domain_config_custom_css_help": "Це для досвідчених адміністраторів, які бажають налаштувати зовнішній вигляд порталу", + "domain_config_default_app": "Типовий застосунок", + "domain_config_default_app_help": "Користувачі будуть автоматично перенаправлятися на цей додаток при відкритті цього домену. Якщо додаток не вказано, люди будуть перенаправлені на форму входу на портал користувача.", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "Показувати список загальнодоступних додатків відвідувачам", + "domain_config_enable_public_apps_page_help": "Відвідувачі побачать сторінку «загальнодоступних додатків», коли опиняться на порталі, а не лише форму входу.", + "domain_config_feature_name": "Особливості", + "domain_config_mail_in": "Вхідні електронні листи", + "domain_config_mail_out": "Вихідні електронні листи", + "domain_config_portal_logo": "Власний логотип", + "domain_config_portal_logo_help": "Приймаються файли .svg, .png та .jpeg. Віддавайте перевагу монохромному .svg з fill: currentColor, щоб логотип адаптувався до тем.", + "domain_config_portal_name": "Додаткове налаштування порталу", + "domain_config_portal_public_intro": "Індивідуальне публічне представлення", + "domain_config_portal_public_intro_help": "Ви можете використовувати HTML, базові стилі будуть застосовані до універсальних елементів.", + "domain_config_portal_theme": "Колірна тема за замовчуванням", + "domain_config_portal_theme_help": "Користувачі можуть вибрати інше у своїх налаштуваннях.", + "domain_config_portal_tile_theme": "Тема відображення плиток додатків", + "domain_config_portal_title": "Власна назва", + "domain_config_portal_user_intro": "Вступ для користувача", + "domain_config_portal_user_intro_help": "Ви можете використовувати HTML, базові стилі будуть застосовані до універсальних елементів.", + "domain_config_search_engine": "URL-адреса пошукової системи", + "domain_config_search_engine_help": "Це додаткова функція, яка дозволяє відображати рядок пошуку на порталі (наприклад, якщо ви хочете використовувати портал YunoHost як головну сторінку браузера). Це має бути URL-адреса з порожнім рядком запиту, наприклад, `https://duckduckgo.com/?q=`, де `q=` є порожнім параметром запиту duckduckgo.", + "domain_config_search_engine_name": "Назва пошукової системи", + "domain_config_show_other_domains_apps": "Показувати додатки інших доменів", + "domain_created": "Домен створено", + "domain_creation_failed": "Неможливо створити домен {domain}: {error}", + "domain_deleted": "Домен видалено", + "domain_deletion_failed": "Неможливо видалити домен {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "Ця команда показує *рекомендовану* конфігурацію. Насправді вона не встановлює конфігурацію DNS для вас. Ви самі повинні налаштувати свою зону DNS у реєстратора відповідно до цих рекомендацій.", + "domain_dns_conf_special_use_tld": "Цей домен засновано на спеціальному домені верхнього рівня (TLD), такому як .local або .test, і тому не очікується, що він матиме актуальні записи DNS.", + "domain_dns_push_already_up_to_date": "Записи вже оновлені, нічого не потрібно робити.", + "domain_dns_push_failed": "Оновлення записів DNS зазнало невдачі.", + "domain_dns_push_failed_to_list": "Не вдалося скласти список поточних записів за допомогою API реєстратора: {error}", + "domain_dns_push_managed_in_parent_domain": "Функцією автоконфігурації DNS керує батьківський домен {parent_domain}.", + "domain_dns_push_not_applicable": "Функція автоматичної конфігурації DNS не застосовується до домену {domain}. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://doc.yunohost.org/dns_config.", + "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": "Поки що інтерфейс з API **{registrar}** не був належним чином протестований і перевірений спільнотою YunoHost. Підтримка є **дуже експериментальною** - будьте обережні!", + "domain_dns_registrar_managed_in_parent_domain": "Цей домен є піддоменом {parent_domain_link}. Конфігурацією реєстратора DNS слід керувати на панелі конфігурації {parent_domain}.", + "domain_dns_registrar_not_supported": "YunoHost не зміг автоматично виявити реєстратора, який обробляє цей домен. Вам слід вручну конфігурувати записи DNS відповідно до документації за адресою https://doc.yunohost.org/dns_config.", + "domain_dns_registrar_supported": "YunoHost автоматично визначив, що цей домен обслуговується реєстратором **{registrar}**. Якщо ви хочете, YunoHost автоматично налаштує цю DNS-зону, якщо ви надасте йому відповідні облікові дані API. Ви можете знайти документацію про те, як отримати реєстраційні дані API на цій сторінці: https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/. (Ви також можете вручну налаштувати свої DNS-записи, дотримуючись документації на https://doc.yunohost.org/dns_config)", + "domain_dns_registrar_use_auto": "Використовувати функцію автоматичного DNS", + "domain_dns_registrar_yunohost": "Цей домен є nohost.me/nohost.st/ynh.fr, тому його конфігурація DNS автоматично обробляється YunoHost без будь-якої подальшої конфігурації. (див. команду 'yunohost dyndns update')", + "domain_dyndns_already_subscribed": "Ви вже підписалися на домен DynDNS", + "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 the_app_id' або перемістіть їх на інший домен за допомогою 'yunohost app change-url the_app_id', перш ніж приступити до вилучення домену", + "domain_unknown": "Домен '{domain}' є невідомим", + "domains_available": "Доступні домени:", + "done": "Готово", + "download_bad_status_code": "{url} повернув код стану {code}", + "download_ssl_error": "Помилка SSL під час з'єднання з {url}", + "download_timeout": "Перевищено час очікування відповіді від {url}.", + "download_unknown_error": "Помилка під час завантаження даних з {url}: {error}", + "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_domain_not_provided": "DynDNS провайдер {provider} не може надати домен {domain}.", + "dyndns_ip_update_failed": "Не вдалося оновити IP-адресу в DynDNS", + "dyndns_ip_updated": "Вашу IP-адресу в DynDNS оновлено", + "dyndns_key_not_found": "DNS-ключ для домену не знайдено", + "dyndns_no_domain_registered": "Домен не зареєстровано в DynDNS", + "dyndns_no_recovery_password": "Не вказано пароль для відновлення! У разі втрати контролю над цим доменом вам необхідно звернутися до адміністратора команди YunoHost!", + "dyndns_provider_unreachable": "Неможливо зв'язатися з провайдером DynDNS {provider}: або ваш YunoHost неправильно під'єднано до Інтернету, або сервер dynette не працює.", + "dyndns_set_recovery_password_denied": "Не вдалося встановити пароль відновлення: невірний ключ", + "dyndns_set_recovery_password_failed": "Не вдалося встановити пароль для відновлення: {error}", + "dyndns_set_recovery_password_invalid_password": "Не вдалося встановити пароль для відновлення: пароль недостатньо надійний", + "dyndns_set_recovery_password_success": "Пароль для відновлення встановлено!", + "dyndns_set_recovery_password_unknown_domain": "Не вдалося встановити пароль відновлення: домен не зареєстровано", + "dyndns_subscribe_failed": "Не вдалося підписатися на домен DynDNS: {error}", + "dyndns_subscribed": "Домен DynDNS зареєстровано", + "dyndns_too_many_requests": "Сервіс DynDNS YunoHost отримала від вас занадто багато запитів, зачекайте приблизно 1 годину, перш ніж спробувати ще раз.", + "dyndns_unavailable": "Домен '{domain}' недоступний.", + "dyndns_unsubscribe_already_unsubscribed": "Домен вже відписаний", + "dyndns_unsubscribe_denied": "Не вдалося відписати домен: невірні облікові дані", + "dyndns_unsubscribe_failed": "Не вдалося скасувати підписку на домен DynDNS: {error}", + "dyndns_unsubscribed": "Домен DynDNS відписано", + "error_changing_file_permissions": "Помилка під час зміни дозволів для {path}: {error}", + "error_removing": "Помилка під час видалення {path}: {error}", + "error_writing_file": "Помилка під час запису файлу {file}: {error}", + "extracting": "Видобування…", + "field_invalid": "Неприпустиме поле '{field}'", + "file_does_not_exist": "Файл {path} не існує.", + "file_not_exist": "Файл не існує: '{path}'", + "firewall_reload_failed": "Не вдалося перезавантажити фаєрвол. Подробиці в журналі.", + "firewall_reloaded": "Фаєрвол перезавантажено", + "global_settings_reset_success": "Скинути глобальні налаштування", + "global_settings_setting_admin_strength": "Надійність пароля адміністратора", + "global_settings_setting_admin_strength_help": "Ці вимоги застосовуються лише під час ініціалізації або зміни пароля", + "global_settings_setting_antispam_name": "Антиспам", + "global_settings_setting_backup_compress_tar_archives": "Стиснення резервних копій", + "global_settings_setting_backup_compress_tar_archives_help": "При створенні нових резервних копій стискати архіви (.tar.gz) замість нестислих архівів (.tar). NB: вмикання цієї опції означає створення легших архівів резервних копій, але початкова процедура резервного копіювання буде значно довшою і важчою для CPU.", + "global_settings_setting_backup_name": "Резервне копіювання", + "global_settings_setting_dns_custom_resolvers_enabled": "Використ. користувацькі DNS-резолвери", + "global_settings_setting_dns_custom_resolvers_enabled_help": "За замовчуванням YunoHost використовує список надійних резолверів, розташованих в Європі. Досвідчені користувачі можуть замість цього вказати власні резолвери.", + "global_settings_setting_dns_custom_resolvers_list": "Адреси користувацьких резолверів", + "global_settings_setting_dns_custom_resolvers_list_help": "Список щонайменше 2 DNS-резолверів для кожного використовуваного IP-протоколу (IPv4/IPv6). Приклад: 89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "global_settings_setting_dns_exposure": "Версії IP, які слід враховувати при конфігурації та діагностиці DNS", + "global_settings_setting_dns_exposure_help": "Примітка: Це стосується лише рекомендованої конфігурації DNS і діагностичних перевірок. Це не впливає на конфігурацію системи.", + "global_settings_setting_email_name": "Ел. пошта", + "global_settings_setting_enable_blocklists": "Увімкнути списки блокування для вхідного трафіку", + "global_settings_setting_enable_blocklists_help": "Блокує сервери, перелічені spamcop.net, spamhaus.org та abuseat.org, для запобігання спаму. Однак це може спричинити проблеми з доставкою для деяких нешкідливих поштових серверів, які можуть бути перелічені цими третіми сторонами, і в такому разі пошта, надіслана з цих серверів, не буде отримана.", + "global_settings_setting_experimental_name": "Експериментальне", + "global_settings_setting_misc_name": "Інше", + "global_settings_setting_network_name": "Мережа", + "global_settings_setting_nginx_compatibility": "Сумісність NGINX", + "global_settings_setting_nginx_compatibility_help": "Компроміс між сумісністю і безпекою для вебсервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_nginx_name": "NGINX (веб-сервер)", + "global_settings_setting_nginx_redirect_to_https": "Примусово HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "Типово переспрямовувати HTTP-запити до HTTP (НЕ ВИМИКАЙТЕ, якщо ви дійсно не знаєте, що робите!)", + "global_settings_setting_password_name": "Паролі", + "global_settings_setting_passwordless_sudo": "Дозвіл адміністраторам використовувати \"sudo\" без повторного введення пароля", + "global_settings_setting_pop3_enabled": "Увімкнути POP3", + "global_settings_setting_pop3_enabled_help": "Увімкнути протокол POP3 для поштового сервера. POP3 – це старіший протокол для доступу до поштових скриньок з поштових клієнтів, він легший, але має менше функцій, ніж IMAP (увімкнено за замовчуванням)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "Дозволити користувачам редагувати свою основну адресу електронної пошти", + "global_settings_setting_portal_allow_edit_email_alias": "Дозволити користувачам додавати, видаляти та редагувати поштові псевдоніми", + "global_settings_setting_portal_allow_edit_email_alias_help": "Якщо вимкнено, їм потрібно попросити адміністраторів зробити це за них.", + "global_settings_setting_portal_allow_edit_email_forward": "Дозволити користувачам додавати, видаляти, редагувати пересилання пошти", + "global_settings_setting_portal_allow_edit_email_forward_help": "Якщо вимкнено, їм потрібно попросити адміністраторів зробити це за них.", + "global_settings_setting_portal_allow_edit_email_help": "Якщо вимкнено, їм потрібно попросити адміністраторів зробити це за них.", + "global_settings_setting_portal_name": "Портал", + "global_settings_setting_postfix_compatibility": "Сумісність Postfix", + "global_settings_setting_postfix_compatibility_help": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)", + "global_settings_setting_postfix_name": "Postfix (поштовий сервер SMTP)", + "global_settings_setting_root_access_explain": "У системах Linux \"root\" є абсолютним адміністратором. У контексті YunoHost прямий вхід в SSH від імені \"root\" типово вимкнено - за винятком локальної мережі сервера. Члени групи \"адміністратори\" можуть використовувати команду sudo, щоб діяти від імені root з командного рядка. Однак, може бути корисно мати (надійний) пароль root для налагодження системи, якщо з якихось причин звичайні адміністратори більше не можуть увійти в систему.", + "global_settings_setting_root_access_name": "Зміна пароля root", + "global_settings_setting_root_password": "Новий пароль root", + "global_settings_setting_root_password_confirm": "Новий пароль root (підтвердження)", + "global_settings_setting_security_experimental_enabled": "Експериментальні безпекові можливості", + "global_settings_setting_security_experimental_enabled_help": "Увімкнути експериментальні функції безпеки (не вмикайте це, якщо ви не знаєте, що робите!)", + "global_settings_setting_security_name": "Безпека", + "global_settings_setting_smtp_allow_ipv6": "Дозвіл IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "Дозволити використання IPv6 для отримання і надсилання листів е-пошти", + "global_settings_setting_smtp_backup_mx_domains": "Домени, які діятимуть як вторинні MX для", + "global_settings_setting_smtp_backup_mx_domains_help": "Дозволити цьому серверу діяти як резервний *вторинний* MX-домен для вказаного домену. Це означає, що якщо основний MX для домену недоступний (наприклад, через збій), листи все одно надсилатимуться на цей сервер, який зберігатиме їх максимум 20 днів і намагатиметься переслати їх до справжнього адресата після відновлення роботи. Можна вказати кілька доменів, розділених комами.", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "Білий список резервних копій SMTP-повідомлень MX", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "Якщо використовується як вторинний MX, необхідно надати вичерпний список дозволених адрес електронної пошти одержувачів (інакше листи будуть відхилені). Можна вказати кілька записів, розділених комами.", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "Увімкнути ретрансляцію SMTP", + "global_settings_setting_smtp_relay_enabled_help": "Хост SMTP-ретрансляції, який буде використовуватися для надсилання е-пошти замість цього зразка Yunohost. Корисно, якщо ви знаходитеся в одній із цих ситуацій: ваш 25 порт заблокований вашим провайдером або VPS провайдером, у вас є житловий IP в списку DUHL, ви не можете налаштувати зворотний 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-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою).", + "global_settings_setting_ssh_name": "SSH", + "global_settings_setting_ssh_password_authentication": "Парольна автентифікація", + "global_settings_setting_ssh_password_authentication_help": "Дозволити автентифікацію паролем для SSH", + "global_settings_setting_ssh_port": "SSH-порт", + "global_settings_setting_ssh_port_help": "Порт нижче 1024 є кращим, щоб запобігти спробам захоплення неадміністраторськими службами на віддаленому комп'ютері. Також слід уникати використання порту, який уже використовується, наприклад, 80 або 443.", + "global_settings_setting_tls_passthrough_enabled": "Увімкнути TLS-passthrough / переадресацію на основі SNI", + "global_settings_setting_tls_passthrough_enabled_help": "Це розширена функція для зворотного проксі-серверування всього домену на інший комп'ютер *без* розшифрування трафіку. Корисно, коли ви хочете розмістити кілька комп'ютерів за однією IP-адресою, але при цьому дозволити кожному комп'ютеру обробляти SSL-термінацію.", + "global_settings_setting_tls_passthrough_explain": "Ця функція є РОЗШИРЕНОЮ та ЕКСПЕРИМЕНТАЛЬНОЮ і призведе до значних змін у конфігурації nginx цього сервера. Будь ласка, НЕ ВИКОРИСТОВУЙТЕ її, якщо ви не знаєте, що робите! Зокрема, ви повинні знати, що fail2ban не може бути реалізований на проксі-сервері (nftables не може заборонити шкідливий трафік, оскільки всі IP-пакети виглядають як такі, що надходять з фронт-сервера). Крім того, наразі конфігурацію nginx проксі-сервера потрібно вручну налаштувати, щоб вона приймала `proxy_protocol`.", + "global_settings_setting_tls_passthrough_list": "Список переадресації", + "global_settings_setting_tls_passthrough_list_help": "Має бути список ДОМЕН;ПРИЗНАЧЕННЯ;ПОРТ, наприклад domain.tld;192.168.1.42;443 або domain.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "TLS-pass-through / переадресація на основі SNI", + "global_settings_setting_user_strength": "Надійність пароля користувача", + "global_settings_setting_user_strength_help": "Ці вимоги застосовуються лише під час ініціалізації або зміни пароля", + "global_settings_setting_webadmin_allowlist": "Білий список IP-адрес вебадміністрування", + "global_settings_setting_webadmin_allowlist_enabled": "Увімкнути білий список IP-адрес вебадміністрування", + "global_settings_setting_webadmin_allowlist_enabled_help": "Дозволити доступ до вебадмініструванні тільки деяким IP-адресам.", + "global_settings_setting_webadmin_allowlist_help": "IP-адреси, яким дозволено доступ до веб-інтерфейсу адміністратора. Дозволено нотацію CIDR.", + "global_settings_setting_webadmin_name": "Веб-інтерфейс адміністратора", + "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_be_deleted": "Група {group} не може бути видалена вручну.", + "group_cannot_edit_all_users": "Група 'all_users' не може бути відредагована вручну. Це спеціальна група, призначена для всіх користувачів, зареєстрованих в YunoHost", + "group_cannot_edit_primary_group": "Група '{group}' не може бути відредагована вручну. Це основна група, призначена тільки для одного конкретного користувача.", + "group_cannot_edit_visitors": "Група 'visitors' не може бути відредагована вручну. Це спеціальна група, що представляє анонімних відвідувачів", + "group_cannot_remove_last_admin": "Користувач '{user}' є останнім користувачем у групі 'admins' і не буде з неї видалений.", + "group_created": "Групу '{group}' створено", + "group_creation_failed": "Не вдалося створити групу '{group}': {error}", + "group_deleted": "Групу '{group}' видалено", + "group_deletion_failed": "Не вдалося видалити групу '{group}': {error}", + "group_mailalias_add": "Псевдонім електронної пошти '{mail}' буде додано до групи '{group}'", + "group_mailalias_remove": "Псевдонім електронної пошти '{mail}' буде вилучено з групи '{group}'", + "group_no_change": "Нічого не потрібно змінювати для групи '{group}'", + "group_unknown": "Група '{group}' невідома", + "group_update_aliases": "Оновлення псевдонімів для групи '{group}'", + "group_update_failed": "Не вдалося оновити групу '{group}': {error}", + "group_updated": "Групу '{group}' оновлено", + "group_user_add": "Користувача '{user}' буде додано до групи '{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}", + "hook_json_return_error": "Не вдалося розпізнати повернення з хука {path}. Помилка: {msg}. Необроблений контент: {raw_content}", + "hook_list_by_invalid": "Цю властивість не може бути використано для перерахування хуків (гачків)", + "hook_name_unknown": "Невідома назва хука '{name}'", + "installation_complete": "Установлення завершено", + "invalid_credentials": "Недійсний пароль чи ім'я користувача", + "invalid_number": "Має бути числом", + "invalid_password": "Недійсний пароль", + "invalid_regex": "Неприпустимий regex: '{regex}'", + "invalid_shell": "Недійсна оболонка: {shell}", + "invalid_url": "Помилка з'єднання із {url}… можливо, служба не працює, або ви неправильно під'єднані до Інтернету з IPv4/IPv6.", + "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_install": "Установлення застосунку '{}'", + "log_app_makedefault": "Застосунок '{}' зроблено типовим", + "log_app_remove": "Вилучення застосунку '{}'", + "log_app_upgrade": "Оновлення застосунку '{}'", + "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_diagnosis_run": "Запуск діагностики", + "log_does_exists": "Немає журналу операцій з назвою '{log}', використовуйте 'yunohost log list', щоб подивитися всі доступні журнали операцій", + "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_unsubscribe": "Скасувати реєстрацію піддомену YunoHost '{}'", + "log_dyndns_update": "Оновлення IP, пов'язаного з вашим піддоменом YunoHost '{}'", + "log_help_to_get_failed_log": "Операція '{desc}' не може бути завершена. Будь ласка, поділіться повним журналом цієї операції, використовуючи команду 'yunohost log share {name}', щоб отримати допомогу", + "log_help_to_get_log": "Щоб переглянути журнал операції '{desc}', використовуйте команду 'yunohost log show {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_regen_conf": "Перестворення системних конфігурацій '{}'", + "log_remove_on_failed_install": "Вилучення '{}' після невдалого встановлення", + "log_resource_snippet": "Надання/вилучення/оновлення ресурсу", + "log_selfsigned_cert_install": "Установлення самопідписаного сертифікату на домені '{}'", + "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_update": "Отримання доступних оновлень системи та оновлення каталогу додатків", + "log_tools_upgrade": "Оновлення системних пакетів", + "log_user_create": "Додавання користувача '{}'", + "log_user_delete": "Видалення користувача '{}'", + "log_user_group_create": "Створено групу '{}'", + "log_user_group_delete": "Видалено групу «{}»", + "log_user_group_update": "Оновлено групу '{}'", + "log_user_import": "Імпорт користувачів", + "log_user_update": "Оновлено відомості для користувача '{}'", + "mail_alias_remove_failed": "Не вдалося видалити аліас електронної пошти '{mail}'", + "mail_alias_unauthorized": "Ви не маєте права додавати псевдоніми, пов'язані з доменом '{domain}'", + "mail_already_exists": "Поштова адреса '{mail}' вже існує", + "mail_domain_unknown": "Неправильна адреса е-пошти для домену '{domain}'. Будь ласка, використовуйте домен, що адмініструється цим сервером.", + "mail_edit_operation_unauthorized": "Ви не маєте права вносити цю зміну до свого облікового запису.", + "mail_forward_remove_failed": "Не вдалося видалити переадресацію електронної пошти '{mail}'", + "mail_unavailable": "Ця адреса електронної пошти зарезервована для групи адміністраторів", + "mailbox_disabled": "Е-пошта вимкнена для користувача {user}", + "mailbox_used_space_dovecot_down": "Поштова служба Dovecot повинна бути запущена, якщо ви хочете отримати використане місце в поштовій скриньці", + "main_domain_change_failed": "Неможливо змінити основний домен", + "main_domain_changed": "Основний домен було змінено", + "migration_0027_cleaning_up": "Очищення кешу та пакетів більше не корисне…", + "migration_0027_delayed_api_restart": "API YunoHost буде автоматично перезапущено через 15 секунд. Він може бути недоступний протягом кількох секунд, після чого вам доведеться знову увійти в систему.", + "migration_0027_general_warning": "Зрештою, зверніть увагу, що ця міграція є **делікатною**. Команда YunoHost доклала всіх зусиль, щоб перевірити та протестувати її, але міграція все одно може призвести до пошкодження частин системи або її програм.\n\nТому рекомендується:\n- **Робити резервні копії** будь-яких критично важливих даних або програм. Більше інформації на https://doc.yunohost.org/backup;\n- **Будьте терплячими** після запуску міграції: залежно від вашого інтернет-з’єднання та обладнання, оновлення всього може зайняти до години;\n- **Зверніться до спільноти** на форумі, якщо вам потрібна допомога у вирішенні проблем.", + "migration_0027_main_upgrade": "Розпочато основне оновлення…", + "migration_0027_modified_files": "Зверніть увагу, що такі файли були змінені вручну та можуть бути перезаписані після оновлення: {manually_modified_files}", + "migration_0027_not_bullseye": "Поточний дистрибутив Debian не є Bullseye! Якщо ви вже виконали міграцію Bullseye -> Bookworm, то ця помилка є симптомом того, що процедура міграції не була на 100% успішною (інакше YunoHost позначив би її як завершену). Рекомендується з'ясувати, що сталося, зі службою підтримки, якій знадобиться **повний** журнал міграції, який можна знайти в Інструменти > Журнали у веб-інтерфейсі адміністратора.", + "migration_0027_not_enough_free_space": "Вільного місця у /var/ досить мало! Для виконання цієї міграції вам має бути щонайменше 1 ГБ вільного місця.", + "migration_0027_patch_yunohost_conflicts": "Застосування виправлення для вирішення проблеми конфлікту…", + "migration_0027_patching_sources_list": "Виправлення файлу sources.lists…", + "migration_0027_problematic_apps_warning": "Зверніть увагу, що було виявлено такі можливі проблемні встановлені додатки. Схоже, що вони не були встановлені з каталогу додатків YunoHost або не позначені як «робочі». Отже, не можна гарантувати, що вони все ще працюватимуть після оновлення: {problematic_apps}", + "migration_0027_start": "Розпочато міграцію до Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "Щось пішло не так під час основного оновлення, система, схоже, все ще працює на Debian Bullseye.", + "migration_0027_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед перенесенням на Bookworm.", + "migration_0027_yunohost_upgrade": "Розпочато оновлення ядра YunoHost…", + "migration_not_enough_space": "Звільніть достатньо місця в {path} для запуску міграції.", + "migration_postgresql_previous_not_installed": "PostgreSQL не встановлено на вашій системі. Нічого не потрібно робити.", + "migration_postgresql_target_not_installed": "PostgreSQL 13 встановлено, але не PostgreSQL 15!? Можливо, у вашій системі сталося щось дивне :(…", + "migration_python_venv_rebuild_broken_app": "Пропускаємо {app}, оскільки virtualenv неможливо легко перебудувати для цієї програми. Натомість слід виправити ситуацію, примусово оновивши цю програму за допомогою `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_disclaimer_base": "Після оновлення до Debian Bookworm, деякі програми Python потребують часткової перекомпіляції для конвертації до нової версії Python, що постачається з Debian (технічною мовою: потрібно пересобрати те, що називається «віртуальне середовище»). Тим часом ці програми Python можуть не працювати. YunoHost може спробувати перекомпілювати віртуальне середовище для деяких із них, як описано нижче. Для інших програм, або якщо спроба перекомпіляції не вдасться, вам потрібно буде вручну примусово оновити ці програми.", + "migration_python_venv_rebuild_disclaimer_ignored": "Віртуальні середовища не можуть бути автоматично перебудовані для цих програм. Вам потрібно примусово оновити їх, що можна зробити з командного рядка за допомогою: `yunohost app upgrade --force APP`: {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "Для таких програм буде здійснено спробу перебудови віртуального середовища (Примітка: операція може тривати деякий час!): {rebuild_apps}", + "migration_python_venv_rebuild_failed": "Не вдалося перебудувати віртуальне середовище Python для {app}. Додаток може не працювати, доки це не буде вирішено. Вам слід виправити ситуацію, примусово оновивши цей додаток за допомогою `yunohost app upgrade --force {app}`.", + "migration_python_venv_rebuild_in_progress": "Зараз намагаємося перебудувати віртуальне середовище Python для `{app}`", + "migration_0031_terms_of_services": "Ця міграція є суто інформаційним повідомленням про те, що проект YunoHost тепер публікує Умови надання послуг, пов'язані з технічними та громадськими послугами.", + "migration_0036_cleaning_up": "Очищення кешу та пакетів більше не корисне…", + "migration_0036_delayed_api_restart": "API YunoHost буде автоматично перезапущено через 15 секунд. Він може бути недоступний протягом кількох секунд, після чого вам доведеться знову увійти в систему.", + "migration_0036_general_warning": "Зрештою, зверніть увагу, що ця міграція є **делікатною**. Команда YunoHost доклала всіх зусиль, щоб перевірити та протестувати її, але міграція все одно може призвести до пошкодження частин системи або її програм.\n\nТому рекомендується:\n- **Робити резервні копії** будь-яких критично важливих даних або програм. Більше інформації на https://doc.yunohost.org/backup;\n- **Будьте терплячими** після запуску міграції: залежно від вашого інтернет-з’єднання та обладнання, оновлення всього може зайняти до години;\n- **Зверніться до спільноти** на форумі, якщо вам потрібна допомога у вирішенні проблем.", + "migration_0036_main_upgrade": "Розпочато основне оновлення…", + "migration_0036_modified_files": "Зверніть увагу, що наступні файли були змінені вручну та можуть бути перезаписані після оновлення:", + "migration_0036_not_bullseye": "Поточний дистрибутив Debian не є Bookworm! Якщо ви вже виконали міграцію Bookworm -> Trixie, то ця помилка свідчить про те, що процедура міграції не була на 100% успішною (інакше YunoHost позначив би її як завершену). Рекомендується з'ясувати, що сталося, зі службою підтримки, якій знадобиться **повний** журнал міграції, який можна знайти в розділі Інструменти > Журнали у веб-інтерфейсі адміністратора.", + "migration_0036_not_enough_free_space": "Вільного місця у /var/ досить мало! Для виконання цієї міграції вам має бути щонайменше 1 ГБ вільного місця.", + "migration_0036_patch_yunohost_dpkg": "Застосування виправлення до бази даних dpkg для вирішення проблем конфліктів…", + "migration_0036_patching_sources_list": "Виправлення файлу sources.lists…", + "migration_0036_problematic_apps_warning": "Зверніть увагу, що було виявлено такі можливі проблемні встановлені додатки. Схоже, що вони не були встановлені з каталогу YunoHost або не позначені як «робочі». Отже, не можна гарантувати, що вони все ще працюватимуть після оновлення:", + "migration_0036_start": "Розпочато міграцію до Trixie…", + "migration_0036_still_on_bookworm_after_main_upgrade": "Щось пішло не так під час основного оновлення, система, схоже, все ще працює на Debian Bookworm.", + "migration_0036_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед міграцією на Trixie.", + "migration_0036_yunohost_upgrade": "Розпочато оновлення ядра YunoHost…", + "migration_description_0027_migrate_to_bookworm": "Оновіть систему до Debian Bookworm та YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "Видаліть старі дозволи XMPP, Metronome тепер є додатком", + "migration_description_0029_postgresql_13_to_15": "Міграція баз даних з PostgreSQL 13 до 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "Відновлення застосунку Python після міграції bookworm", + "migration_description_0031_terms_of_services": "Умови використання", + "migration_description_0032_firewall_config": "Міграція файлу конфігурації внутрішнього брандмауера", + "migration_description_0033_rework_permission_infos": "Перероблено спосіб зберігання дозволів додатків", + "migration_description_0034_fix_missing_admins_aliases": "Виправлення відсутніх поштових псевдонімів для групи адміністраторів", + "migration_description_0035_fix_apps_nodejs_version": "Виправлення версій nodejs у конфігураціях systemd", + "migration_description_0036_migrate_to_trixie": "Оновити систему до Debian Trixie та YunoHost 13", + "migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і налаштування застосунків перед фактичною міграцією.", + "migration_ldap_can_not_backup_before_migration": "Не вдалося завершити резервне копіювання системи перед невдалою міграцією. Помилка: {error}", + "migration_ldap_migration_failed_trying_to_rollback": "Не вдалося перенести дані… намагаюся відкотити систему.", + "migration_ldap_rollback_success": "Система відкотилася.", + "migrations_already_ran": "Наступні міграції вже виконано: {ids}", + "migrations_dependencies_not_satisfied": "Запустіть ці міграції: '{dependencies_id}', перед міграцією {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_no_migrations_to_run": "Немає міграцій для запуску", + "migrations_no_such_migration": "Не існує міграції під назвою '{id}'", + "migrations_not_pending_cant_skip": "Наступні міграції не очікують виконання, тому не можуть бути пропущені: {ids}", + "migrations_pending_cant_rerun": "Наступні міграції ще не завершені, тому не можуть бути запущені знову: {ids}", + "migrations_running_forward": "Виконання міграції {id}…", + "migrations_skip_migration": "Пропускання міграції {id}…", + "migrations_success_forward": "Міграцію {id} завершено", + "migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Засоби → Міграції на сторінці вебадмініструванні або виконайте команду `yunohost tools migrations run`.", + "nftables_unavailable": "Ви не можете відтворювати з nftables тут. Ви перебуваєте або в контейнері, або ваше ядро не підтримує його", + "noninteractive_task": "Неінтерактивне завдання", + "not_enough_disk_space": "Недостатньо вільного місця на '{path}'", + "operation_interrupted": "Операція була вручну перервана?", + "other_available_options": "… та {n} інших доступних опцій не показано", + "password_confirmation_not_the_same": "Пароль і його підтвердження не збігаються", + "password_listed": "Цей пароль входить в число найбільш часто використовуваних паролів у світі. Будь ласка, виберіть щось неповторюваніше.", + "password_too_long": "Будь ласка, виберіть пароль коротший за 127 символів", + "password_too_simple_1": "Пароль має складатися не менше ніж з 8 символів", + "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_fullname": "Має бути дійсне повне ім’я (принаймні 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_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}", + "permission_updated": "Дозвіл '{permission}' оновлено", + "port_already_closed": "Порт {port} вже закрито", + "port_already_opened": "Порт {port} вже відкрито", + "postinstall_low_rootfsspace": "Загальне місце кореневої файлової системи становить менше 10 ГБ, що викликає занепокоєння! Швидше за все, дисковий простір закінчиться дуже скоро! Рекомендовано мати не менше 16 ГБ для кореневої файлової системи. Якщо ви хочете встановити YunoHost попри це попередження, повторно запустіть післявстановлення з параметром --force-diskspace", + "pydantic_type_error": "Недійсний тип.", + "pydantic_type_error_none_not_allowed": "Обов'язкове значення.", + "pydantic_type_error_str": "Недійсний тип, очікується рядок.", + "pydantic_value_error_color": "Недійсний колір, значення має бути іменованим або шістнадцятковим кольором.", + "pydantic_value_error_const": "Несподіване значення; виберіть між {permitted}", + "pydantic_value_error_date": "Недійсний формат дати", + "pydantic_value_error_email": "Значення не є дійсною адресою електронної пошти", + "pydantic_value_error_number_not_ge": "Значення має бути більше або дорівнювати {limit_value}.", + "pydantic_value_error_number_not_le": "Значення має бути менше або дорівнювати {limit_value}.", + "pydantic_value_error_str_regex": "Недійсний рядок; значення не відповідає шаблону '{pattern}'", + "pydantic_value_error_time": "Недійсний формат часу", + "pydantic_value_error_url_extra": "Недійсна URL-адреса, знайдено зайві символи після дійсної URL-адреси: '{extra}'", + "pydantic_value_error_url_host": "Недійсний хост URL-адреси", + "pydantic_value_error_url_port": "Порт URL недійсний, номер порту не може перевищувати 65535", + "pydantic_value_error_url_scheme": "Недійсна або відсутня схема URL-адреси", + "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 була змінена вручну, але вам потрібно явно вказати категорію 'ssh' з --force, щоб застосувати зміни.", + "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": "/! \\ Packagers! Дозвіл '{permission}' має значення show_tile 'true', тому ви не можете визначити regex URL в якості основної URL", + "regex_with_only_domain": "Ви не можете використовувати regex для домену, тільки для шляху", + "registrar_infos": "Відомості про реєстратора", + "restore_already_installed_app": "Застосунок з ID «{app}» вже встановлено", + "restore_already_installed_apps": "Наступні програми не можуть бути відновлені, тому що вони вже встановлені: {apps}", + "restore_backup_too_old": "Цей архів резервних копій не може бути відновлений, бо він отриманий з дуже старої версії YunoHost.", + "restore_cleaning_failed": "Не вдалося очистити тимчасовий каталог відновлення", + "restore_complete": "Відновлення завершено", + "restore_confirm_yunohost_installed": "Ви дійсно хочете відновити вже встановлену систему? [{answers}]", + "restore_extracting": "Вилучення потрібних файлів з архіву…", + "restore_failed": "Не вдалося відновити систему", + "restore_hook_unavailable": "Скрипт відновлення для '{part}' недоступний у вашій системі і в архіві його теж немає", + "restore_may_be_not_enough_disk_space": "Схоже, у вашій системі недостатньо місця (вільно: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin} Б)", + "restore_not_enough_disk_space": "Недостатньо місця (простір: {free_space} Б, необхідний простір: {needed_space} Б, межа безпеки: {margin} Б)", + "restore_nothings_done": "Нічого не було відновлено", + "restore_removing_tmp_dir_failed": "Неможливо видалити старий тимчасовий каталог", + "restore_running_app_script": "Відновлення додатку '{app}'…", + "restore_running_hooks": "Запуск хуків відновлення…", + "restore_system_part_failed": "Не вдалося відновити системний розділ '{part}'", + "root_password_changed": "пароль root було змінено", + "root_password_desynchronized": "Пароль адміністратора було змінено, але YunoHost не зміг поширити це на кореневий (root) пароль!", + "server_reboot": "Сервер буде перезавантажено", + "server_reboot_confirm": "Сервер буде негайно перезавантажено, ви впевнені? [{answers}]", + "server_shutdown": "Сервер буде вимкнено", + "server_shutdown_confirm": "Сервер буде негайно вимкнено, ви впевнені? [{answers}]", + "service_add_failed": "Не вдалося додати службу '{service}'", + "service_added": "Службу '{service}' було додано", + "service_already_started": "Службу '{service}' вже запущено", + "service_already_stopped": "Службу '{service}' вже зупинено", + "service_cmd_exec_failed": "Не вдалося виконати команду '{command}'", + "service_description_dnsmasq": "Обробляє роздільність доменних імен (DNS)", + "service_description_dovecot": "Дозволяє поштовим клієнтам отримувати доступ до електронної пошти (через IMAP і POP3)", + "service_description_fail2ban": "Захист від перебирання (брутфорсу) та інших видів атак з Інтернету", + "service_description_mysql": "Зберігає дані застосунків (база даних SQL)", + "service_description_nftables": "Управляє відкритими і закритими портами з'єднання зі службами", + "service_description_nginx": "Обслуговує або надає доступ до всіх вебсайтів, розміщених на вашому сервері", + "service_description_opendkim": "Підписує вихідні електронні листи за допомогою DKIM, щоб зменшувати ймовірність їх позначки як спам", + "service_description_postfix": "Використовується для надсилання та отримання е-пошти", + "service_description_postgresql": "Зберігає дані застосунків (база даних SQL)", + "service_description_redis-server": "Спеціалізована база даних, яка використовується для швидкого доступу до даних, черги завдань і зв'язку між програмами", + "service_description_slapd": "Зберігає користувачів, домени і пов'язані з ними дані", + "service_description_ssh": "Дозволяє віддалено під'єднуватися до сервера через термінал (протокол SSH)", + "service_description_yunohost-api": "Управляє взаємодією між вебінтерфейсом YunoHost і системою", + "service_description_yunohost-portal-api": "Керує взаємодією між різними веб-інтерфейсами порталу та системою", + "service_description_yunomdns": "Дозволяє вам отримати доступ до вашого сервера, використовуючи 'yunohost.local' у вашій локальній мережі", + "service_disable_failed": "Неможливо змусити службу '{service}' не запускатися під час завантаження.", + "service_disabled": "Служба '{service}' більше не буде запускатися під час завантаження системи.", + "service_enable_failed": "Неможливо змусити службу '{service}' автоматично запускатися під час завантаження.", + "service_enabled": "Служба '{service}' тепер буде автоматично запускатися під час завантаження системи.", + "service_not_reloading_because_conf_broken": "Неможливо перезавантажити/перезапустити службу '{name}', тому що її конфігурацію порушено: {errors}", + "service_reload_failed": "Не вдалося перезавантажити службу '{service}'", + "service_reload_or_restart_failed": "Не вдалося перезавантажити або перезапустити службу '{service}'", + "service_reloaded": "Служба '{service}' перезавантажена", + "service_reloaded_or_restarted": "Службу '{service}' була перезавантажено або перезапущено", + "service_remove_failed": "Не вдалося видалити службу '{service}'", + "service_removed": "Служба '{service}' вилучена", + "service_restart_failed": "Не вдалося перезапустити службу '{service}'", + "service_restarted": "Службу '{service}' перезапущено", + "service_start_failed": "Не вдалося запустити службу '{service}'", + "service_started": "Службу '{service}' запущено", + "service_stop_failed": "Неможливо зупинити службу '{service}'", + "service_stopped": "Службу '{service}' зупинено", + "service_unknown": "Невідома служба '{service}'", + "session_expired": "Сеанс закінчився", + "show_tile_cant_be_enabled_for_regex": "Ви не можете увімкнути 'show_tile' прямо зараз, тому що URL для дозволу '{permission}' являє собою регулярний вираз", + "show_tile_cant_be_enabled_for_url_not_defined": "Ви не можете увімкнути 'show_tile' прямо зараз, тому що спочатку ви повинні визначити URL для дозволу '{permission}'", + "ssowat_conf_generated": "Конфігурації SSO та порталу відновлено", + "system_upgraded": "Систему оновлено", + "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}", + "tos_dyndns_acknowledgement": "Ви вирішили зареєструвати домен DynDNS, послугу, що надається проектом YunoHost. Враховуючи, що доменні імена є важливим аспектом довгострокових цифрових послуг, нагадуємо вам уважно прочитати відповідні Умови надання послуг, зокрема розділ, що стосується цих безкоштовних доменних імен: .", + "tos_postinstall_acknowledgement": "Проект YunoHost — це команда волонтерів, які об’єднали зусилля для створення безкоштовної операційної системи для серверів під назвою YunoHost. Програмне забезпечення YunoHost опубліковано за ліцензією AGPLv3 (). У зв’язку з цим програмним забезпеченням проект адмініструє та надає кілька технічних та громадських послуг для різних цілей. Використовуючи ці послуги, ви погоджуєтеся з наступними Умовами надання послуг: .", + "unable_authenticate": "Не вдалося автентифікувати сеанс", + "unbackup_app": "{app} НЕ буде збережено", + "unexpected_error": "Щось пішло не так: {error}", + "unknown_error_reading_file": "Невідома помилка під час спроби прочитати файл {file} (причина: {error})", + "unknown_group": "Невідома група '{group}'", + "unknown_main_domain_path": "Невідомий домен або шлях для '{app}'. Вам необхідно вказати домен і шлях, щоб мати можливість вказати URL для дозволу.", + "unknown_user": "Невідомий користувач '{user}'", + "unlimit": "Квоти немає", + "unrestore_app": "{app} не буде оновлено", + "update_apt_cache_failed": "Неможливо оновити кеш APT (менеджер пакетів Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}", + "update_apt_cache_warning": "Щось пішло не так при оновленні кеша APT (менеджера пакунків Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки:\n{sourceslist}", + "updating_apt_cache": "Отримання доступних оновлень для системних пакетів…", + "upgrading_packages": "Оновлення пакетів…", + "upnp_dev_not_found": "UPnP-пристрій не знайдено", + "upnp_disabled": "UPnP вимкнено", + "upnp_enabled": "UPnP увімкнено", + "upnp_port_open_failed": "Не вдалося відкрити порт через UPnP", + "user_already_exists": "Користувач '{user}' вже існує", + "user_cannot_delete_last_admin": "Користувач '{user}' є останнім користувачем у групі 'admins' і не буде видалений.", + "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_cannot_edit_or_delete_admins": "Неможливо редагувати або видаляти '{user}' через імпорт, оскільки користувач є адміністратором", + "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_api": "API 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://doc.yunohost.org/admin.", + "migration_0036_apt_lists_file_still_exists": "Застарілий файл '{file}' все ще існує, хоча не повинен. Його буде перейменовано на '{file}.legacy_bookworm'.", + "app_db_prompt_no_app_database": "Схоже, у маніфесті цього додатка не вказано базу даних", + "app_db_prompt_type_not_supported": "Команда не підтримує цей тип бази даних: {type}", + "migration_0037_upgrade_dkim_keys_disclaimer": "Під час цієї міграції застарілі 1024-бітні ключі DKIM будуть оновлені до 2048-бітних, щоб покращити доставлення пошти. Йдеться про такі домени, і деякі з них можуть потребувати оновлення ключів DKIM у вашій зоні DNS одразу після міграції: {domains}\nВАЖЛИВО: Щоб уникнути потенційних проблем із чорним списком та доставленням, цю міграцію слід виконувати в той час, коли ваш сервер не надсилає електронну пошту. З міркувань безпеки ви можете зупинити службу Postfix перед міграцією та перезапустити її через 1 годину після оновлення ключів DKIM у ваших зонах DNS.", + "migration_0037_upgrade_dkim_keys_pending_mails": "У вашій черзі пошти є {pending_mails} листів. Щоб уникнути потенційного потрапляння до чорного списку та проблем із доставкою, цю міграцію слід виконувати в той час, коли ваш сервер не надсилає електронні листи. Ви можете тимчасово зупинити postfix та скористатися командою postsuper -d ALL, щоб звільнити чергу пошти. Щоб переглянути, які листи знаходяться в черзі, скористайтеся командою postqueue -p", + "migration_0037_upgrade_dkim_keys_failed": "Не вдалося згенерувати новий 2048-бітний ключ для {domains}.", + "migration_0037_upgrade_dkim_keys_manual_action": "Щоб завершити процес міграції, вам потрібно оновити відкриті ключі DKIM у цих зонах DNS: {domains}\nЗапустіть діагностику або скористайтеся вкладкою DNS у розділі Домени веб-панелі адміністратора або за допомогою команди yunohost domain dns suggest DOMAIN. Якщо ви вирішили зупинити postfix, не забудьте перезапустити його через 1 годину після редагування останньої зони DNS.", + "migration_description_0037_upgrade_dkim_keys": "Оновіть ключі DKIM, щоб покращити доставлення пошти" +} diff --git a/locales/uz.json b/locales/uz.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/locales/uz.json @@ -0,0 +1 @@ +{} diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json new file mode 100644 index 0000000..685dabf --- /dev/null +++ b/locales/zh_Hans.json @@ -0,0 +1,921 @@ +{ + "aborting": "正在放弃。", + "action_invalid": "无效操作 '{action}'", + "additional_urls_already_added": "附加 URL '{url}' 已添加到权限'{permission}'的附加 URL 中", + "additional_urls_already_removed": "权限'{permission}'的其他 URL 中已经删除了附加 URL'{url}'", + "admin_password": "管理员密码", + "admins": "管理员", + "all_users": "所有的 YunoHost 用户", + "already_up_to_date": "无事可做。一切都已经是最新的了。", + "app_action_broke_system": "该操作似乎破坏了以下重要服务:{services}", + "app_action_cannot_be_ran_because_required_services_down": "这些必需的服务应该正在运行以执行以下操作:{services},尝试重新启动它们以继续操作(考虑调查为什么它们出现故障)。", + "app_action_failed": "对应用{app}执行动作{action}失败", + "app_already_installed": "{app}已安装", + "app_already_installed_cant_change_url": "这个应用已经被安装。URL 不能仅仅通过这个函数来改变。在`app changeurl`中检查是否可用。", + "app_arch_not_supported": "此应用只能安装在{required}架构上,但您的服务器架构为{current}", + "app_argument_choice_invalid": "为参数'{name}': '{value}' 选择一个不在以下选项({choices})中的有效值", + "app_argument_invalid": "为参数'{name}'选择一个有效值:{error}", + "app_change_url_failed": "无法更改 {app}的 URL:{error}", + "app_change_url_identical_domains": "新旧 domain / url_path 是相同的('{domain}{path}'),无需执行任何操作。", + "app_change_url_no_script": "应用'{app_name}'尚不支持 URL 修改。也许您应该升级它。", + "app_change_url_require_full_domain": "{app} 无法移动到新的 URL,因为它需要全部域名(即路径 为 \"/\")", + "app_change_url_script_failed": "在更改 URL 脚本中发生错误", + "app_change_url_success": "{app} URL 现在为 {domain}{path}", + "app_config__core_name": "磁贴与权限", + "app_config_permission_allowed": "被允许访问的用户/组", + "app_config_permission_allowed_warn_protected": "注意:此权限是“受保护”的,因此“访客”组无法实际添加/删除到授权组中。", + "app_config_permission_description": "描述", + "app_config_permission_description_help": "如果您使用的是“描述性”门户模式,则此功能非常有用", + "app_config_permission_extraperm_section_name": "权限 '{perm}'", + "app_config_permission_label": "标签", + "app_config_permission_location": "对应于 [{absolute_url}]({absolute_url})", + "app_config_permission_logo": "自定义徽标", + "app_config_permission_logo_help": "仅支持 PNG 格式", + "app_config_permission_show_tile": "在门户中显示磁贴", + "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": "无法下载资产 '{source_id}' ({url}) 以供 {app}: {out}", + "app_full_domain_unavailable": "抱歉,此应用必须安装在其自己的域中,但其他应用已安装在域“ {domain}”上。 您可以改用专用于此应用的子域。", + "app_id_invalid": "无效 app ID", + "app_install_failed": "无法安装 {app}: {error}", + "app_install_files_invalid": "这些文件无法安装", + "app_install_script_failed": "应用安装脚本内发生错误", + "app_location_unavailable": "该 URL 不可用,或与已安装的应用冲突:\n{apps}", + "app_make_default_location_already_used": "无法将'{app}' 设置为域上的默认应用,'{other_app}'已在使用'{domain}'", + "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_manifest_install_ask_password": "选择此应用的管理密码", + "app_manifest_install_ask_path": "选择安装此应用的 URL 路径(在域名后)", + "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_properly_removed": "{app} 未正确删除", + "app_packaging_format_not_supported": "无法安装此应用,因为您的 YunoHost 版本不支持其打包格式。您应该考虑升级系统。", + "app_remove_after_failed_install": "安装失败后删除应用中…", + "app_removed": "{app} 已卸载", + "app_requirements_checking": "正在检查{app}的需求…", + "app_resource_failed": "为 {app} 配置、取消配置或更新资源失败: {error}", + "app_restore_failed": "无法还原 {app}: {error}", + "app_restore_script_failed": "应用还原脚本内部发生错误", + "app_sources_fetch_failed": "无法获取源文件,URL 是否正确?", + "app_start_backup": "正在收集要备份的文件,用于{app}…", + "app_start_install": "正在安装{app}…", + "app_start_remove": "正在删除{app}…", + "app_start_restore": "正在恢复{app}…", + "app_unknown": "未知应用", + "app_unsupported_remote_type": "应用使用的远程类型不受支持", + "app_upgrade_app_name": "现在升级{app}…", + "app_upgrade_bad_quality": "此应用目前在 YunoHost 应用目录中被标记为损坏。这可能是维护人员尝试修复问题时的临时状况。在此期间,此应用的升级功能已被禁用。", + "app_upgrade_broke_the_system": "{app} 升级看似成功但导致系统处于损坏状态,因此被视为失败。", + "app_upgrade_cli_bad_quality": "跳过 {app} 的升级,因为此应用目前在 YunoHost 应用目录中被标记为损坏。", + "app_upgrade_cli_up_to_date": "{app} 已是最新版本 ({current_version})", + "app_upgrade_cli_url_required": "{app} 不在目录中(或已不存在?),因此无法自动升级。您应使用 `yunohost app upgrade {app}` 并通过 `-u` 选项提供仓库 URL。", + "app_upgrade_cli_will_force_upgrade": "{app} 将被强制升级 ({current_version})", + "app_upgrade_cli_will_upgrade": "{app} 将从 {current_version} 升级至 {new_version}", + "app_upgrade_continuing_with_other_apps": "未能升级 {app},但仍继续升级其他应用(因为使用了 `--continue-on-failure` 参数)", + "app_upgrade_fail_requirements": "此应用有新版本可用 ({new_version}),但部分要求未满足:\n{failed_requirements}", + "app_upgrade_failed": "升级 {app} 失败:{error}", + "app_upgrade_failed_and_broke_the_system": "升级应用“{app}”失败,且导致系统处于损坏状态。", + "app_upgrade_script_failed": "应用升级脚本内部发生错误", + "app_upgrade_several_apps": "以下应用将被升级:{apps}", + "app_upgrade_some_app_failed": "某些应用无法升级", + "app_upgrade_specific_channel_msg": "请注意您当前使用 `{channel}` 作为升级源。请务必查看[此处]({pr_url})的讨论。", + "app_upgrade_up_to_date": "强制升级应用(至相同版本)有时可用于重建应用和配置。", + "app_upgrade_upgradable": "此应用可从版本 {current_version} 升级至 {new_version}", + "app_upgrade_url_required": "此应用不在目录中(或已不在?),因此您必须手动处理其升级。
在命令行中,您可以使用 `yunohost app upgrade ` 并通过 `-u` 选项提供仓库 URL。", + "app_upgraded": "{app}upgraded", + "app_yunohost_version_not_supported": "此应用需要 YunoHost >= {required},但当前安装版本为 {current}。", + "apps_already_up_to_date": "所有应用都是最新的", + "apps_catalog_failed_to_download": "无法下载{apps_catalog} 应用目录:{error}", + "apps_catalog_obsolete_cache": "应用目录缓存为空或已过时。", + "apps_catalog_update_success": "应用目录已更新!", + "apps_catalog_updating": "正在更新应用程序目录…", + "apps_confirm_partial_upgrade": "部分请求升级的应用无法升级。是否继续升级其他应用?", + "apps_no_target_can_be_upgraded": "没有可升级的应用", + "apps_upgrade_cancelled": "仍有多个其他应用待升级,但其升级已被取消(使用 `--continue-on-failure` 可继续升级):{apps}", + "ask_admin_fullname": "管理员全名", + "ask_admin_username": "管理员用户名", + "ask_dyndns_recovery_password": "DynDNS 恢复密码", + "ask_dyndns_recovery_password_explain": "请为您的 DynDNS 域名选择一个恢复密码,以备日后需要重置。", + "ask_dyndns_recovery_password_explain_during_unsubscribe": "请输入此 DynDNS 域名的恢复密码。", + "ask_dyndns_recovery_password_explain_unavailable": "此 DynDNS 域名已注册。如果您是最初注册此域名的人,可以输入恢复密码以重新获得该域名。", + "ask_fullname": "全名", + "ask_main_domain": "主域", + "ask_new_admin_password": "新的管理密码", + "ask_new_domain": "新域名", + "ask_new_path": "新路径", + "ask_password": "密码", + "ask_user_domain": "用户的电子邮件地址要使用的域", + "automatic_task": "自动任务", + "backup_abstract_method": "此备份方法尚未实现", + "backup_actually_backuping": "正在根据收集的文件创建备份档案…", + "backup_app_script_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_corrupted": "备份存档'{archive}' 似乎已损坏 : {error}", + "backup_archive_name_exists": "名称为'{name}'的备份存档已经存在。", + "backup_archive_name_unknown": "未知的本地备份档案名为'{name}'", + "backup_archive_open_failed": "无法打开备份档案", + "backup_archive_system_part_not_available": "该备份中系统部分'{part}'不可用", + "backup_archive_writing_error": "无法将要备份的文件 '{source}'(在归档文件 '{dest}' 中命名)添加到压缩归档文件 '{archive}' 中", + "backup_ask_for_copying_if_needed": "您是否要临时使用{size} MB 进行备份?(由于无法使用更有效的方法准备某些文件,因此使用这种方式。)", + "backup_before_upgrade_deleted_because_replaced_by_newer_backup": "备份 {name} 已被删除,因为它被较新的备份 {newname} 替代", + "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_hook_unknown": "备用挂钩'{hook}'未知", + "backup_method_copy_finished": "备份副本已完成", + "backup_method_custom_finished": "自定义备份方法'{method}' 已完成", + "backup_method_tar_finished": "TAR 备份存档已创建", + "backup_mount_archive_for_restore": "正在准备存档以进行恢复…", + "backup_no_file_collected": "未能收集待备份文件", + "backup_no_uncompress_archive_dir": "没有这样的未压缩存档目录", + "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_output_symlink_dir_broken": "您的存档目录'{path}' 是断开的符号链接。 也许您忘记了重新安装/装入或插入它指向的存储介质。", + "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} 没有还原脚本,您将无法自动还原该应用的备份。", + "cannot_open_file": "无法打开文件 {file}(原因:{error})", + "cannot_write_file": "无法写入文件 {file}(原因:{error})", + "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": "“Let's Encrypt”未颁发域'{domain}'的证书,无法自动续订!", + "certmanager_attempt_to_renew_valid_cert": "域'{domain}'的证书不会过期!(如果知道自己在做什么,则可以使用--force)", + "certmanager_attempt_to_replace_valid_cert": "您正在尝试覆盖域{domain}的有效证书!(使用--force 绕过)", + "certmanager_cannot_read_cert": "尝试为域 {domain}(file: {file})打开当前证书时发生错误,原因:{reason}", + "certmanager_cert_install_failed": "Let's Encrypt 证书安装失败,域名: {domains}", + "certmanager_cert_install_failed_selfsigned": "自签名证书安装失败,域名: {domains}", + "certmanager_cert_install_success": "为域'{domain}'安装“Let's Encrypt”证书", + "certmanager_cert_install_success_selfsigned": "为域 '{domain}'安装了自签名证书", + "certmanager_cert_renew_failed": "Let's Encrypt 证书续订失败,域名: {domains}", + "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_domain_dns_ip_differs_from_public_ip": "域'{domain}'的 DNS 记录与此服务器的 IP 不同。请检查诊断中的“DNS 记录”(基本)类别,以获取更多信息。如果您最近修改了 A 记录,请等待它传播(某些 DNS 传播检查器可在线获得)。 (如果您知道自己在做什么,请使用“ --no-checks”关闭这些检查。)", + "certmanager_domain_http_not_working": "域{domain}似乎无法通过 HTTP 访问。请检查诊断中的“网络”类别以获取更多信息。(如果您知道自己在做什么,请使用“ --no-checks”关闭这些检查。)", + "certmanager_domain_not_diagnosed_yet": "尚无域{domain}的诊断结果。请在诊断部分中针对“DNS 记录”和“Web”类别重新运行诊断,以检查该域是否已准备好安装“Let's Encrypt”证书。(或者,如果您知道自己在做什么,请使用“ --no-checks”关闭这些检查。)", + "certmanager_hit_rate_limit": "最近已经为此域{domain}颁发了太多的证书。请稍后再试。有关更多详细信息,请参见 https://letsencrypt.org/docs/rate-limits/", + "certmanager_no_cert_file": "无法读取域{domain}的证书文件(file: {file})", + "certmanager_self_ca_conf_file_not_found": "找不到用于自签名授权的配置文件(file: {file})", + "certmanager_unable_to_parse_self_CA_name": "无法解析自签名授权的名称 (file: {file})", + "config_action_disabled": "无法运行操作 '{action}',因为该操作已禁用,请确保满足其约束条件。帮助: {help}", + "config_action_failed": "无法运行操作 '{action}': {error}", + "config_apply_failed": "应用新配置 失败:{error}", + "config_cant_set_value_on_section": "无法在整个配置部分设置单个值。", + "config_forbidden_keyword": "关键字“{keyword}”是保留的,您不能创建或使用带有此 ID 的问题的配置面板。", + "config_forbidden_readonly_type": "类型 '{type}' 不能设置为只读,请使用其他类型来呈现该值(相关参数 ID: '{id}')。", + "config_no_panel": "未找到配置面板。", + "config_unknown_filter_key": "该过滤器钥匙“{filter_key}”有误。", + "confirm_app_install_danger": "危险!已知此应用仍处于实验阶段(如果未明确无法正常运行)! 除非您知道自己在做什么,否则可能不应该安装它。如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", + "confirm_app_install_thirdparty": "危险!该应用不是 YunoHost 的应用目录的一部分。 安装第三方应用可能会损害系统的完整性和安全性。 除非您知道自己在做什么,否则可能不应该安装它,如果此应用无法运行或无法正常使用系统,将不会提供任何支持。如果您仍然愿意承担此风险,请输入'{answers}'", + "confirm_app_install_warning": "警告:此应用可能可以运行,但未与 YunoHost 很好地集成。某些功能(例如单点登录和备份/还原)可能不可用,仍要安装吗? [{answers}] ", + "confirm_app_insufficient_ram": "此应用安装所需内存超过当前可用内存。即使此应用能够运行,其安装/升级过程需要大量内存,可能导致服务器卡顿并严重失败。若您仍愿承担此风险,请输入“{answers}”", + "confirm_notifications_read": "警告:在继续之前,您应该检查上面的应用通知,可能有重要信息需要了解。 [{answers}]", + "confirm_tos_acknowledgement": "我已经阅读并理解服务条款 [{answers}]", + "corrupted_json": "从 {ressource} 读取到损坏的 JSON:{error}", + "corrupted_toml": "从 {ressource} 读取到损坏的 TOML:{error}", + "corrupted_yaml": "从 {ressource} 读取到损坏的 YAML:{error}", + "danger": "警告:", + "diagnosis_apps_allgood": "所有已安装的应用都遵守基本的打包原则", + "diagnosis_apps_bad_quality": "此应用程序目前在 YunoHost 的应用程序目录中被标记为损坏。这可能是维护人员尝试修复问题时的暂时性问题。与此同时,该应用程序的升级已被禁用。", + "diagnosis_apps_broken": "此应用程序目前在 YunoHost 的应用程序目录中被标记为损坏。这可能是维护人员尝试修复问题时的暂时性问题。与此同时,该应用程序的升级已被禁用。", + "diagnosis_apps_deprecated_practices": "此应用的安装版本仍在使用一些较旧的,弃用的打包原则。推荐您升级它。", + "diagnosis_apps_issue": "发现应用{app}存在问题", + "diagnosis_apps_not_in_app_catalog": "此应用不在 YunoHost 的应用目录中。如果它过去有被删除过,您应该考虑卸载此应用程,因为它不会更新,并且可能会损害您系统的完整和安全性。", + "diagnosis_apps_outdated_packaging_format": "此应用使用了已弃用的打包格式,很快将不被 YunoHost 支持。您真的应该考虑升级它。", + "diagnosis_apps_outdated_ynh_requirement": "此应用安装版本仅要求 yunohost >= 2.x/3.x/4.x,通常表明其未遵循推荐的打包规范和助手工具。强烈建议进行升级。", + "diagnosis_backports_in_sources_list": "看起来 apt(软件包管理器)已配置为使用 backports 存储库。除非您真的知道自己在做什么,否则我们强烈建议您不要从 backports 安装软件包,因为这很可能在您的系统上造成不稳定或冲突。", + "diagnosis_basesystem_hardware": "服务器硬件架构为{virt} {arch}", + "diagnosis_basesystem_hardware_model": "服务器型号为 {model}", + "diagnosis_basesystem_host": "服务器正在运行 Debian {debian_version}", + "diagnosis_basesystem_kernel": "服务器正在运行 Linux kernel {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_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_description_regenconf": "系统配置", + "diagnosis_description_services": "服务状态检查", + "diagnosis_description_systemresources": "系统资源", + "diagnosis_description_web": "网页", + "diagnosis_diskusage_low": "存储器{mountpoint}(在设备{device}上)只有{free} ({free_percent}%) 的剩余空间(共{total})。请注意。", + "diagnosis_diskusage_ok": "存储器{mountpoint}(在设备{device}上)仍有 {free} ({free_percent}%) 空间(在{total}中)!", + "diagnosis_diskusage_verylow": "存储器{mountpoint}(在设备{device}上)仅剩余{free} ({free_percent}%) (剩余{total})个空间。您应该真正考虑清理一些空间!", + "diagnosis_display_tip": "要查看发现的问题,您可以转到 Webadmin 的“诊断”部分,或从命令行运行'yunohost diagnosis show --issues --human-readable'。", + "diagnosis_dns_bad_conf": "域{domain}(类别{category})的某些 DNS 记录丢失或不正确", + "diagnosis_dns_discrepancy": "以下 DNS 记录似乎未遵循建议的配置:
类型:{type}
名称:{name}
代码> 当前值:{current}期望值:{content}", + "diagnosis_dns_good_conf": "已为域{domain}(类别{category})正确配置了 DNS 记录", + "diagnosis_dns_missing_record": "根据建议的 DNS 配置,您应该添加带有以下信息的 DNS 记录。
类型:{type}
名称:{name}
值:{content}", + "diagnosis_dns_point_to_doc": "如果您需要有关配置 DNS 记录的帮助,请查看 https://doc.yunohost.org/dns_config 上的文档。", + "diagnosis_dns_specialusedomain": "域名 {domain} 是基于特殊用途的顶级域名(TLD),如 .local 或 .test,因此不指望具有实际的 DNS 记录。", + "diagnosis_dns_try_dyndns_update_force": "该域的 DNS 配置应由 YunoHost 自动管理,如果不是这种情况,您可以尝试使用 yunohost dyndns update --force强制进行更新。", + "diagnosis_domain_expiration_error": "有些域很快就会过期!", + "diagnosis_domain_expiration_not_found": "无法检查某些域的到期日期", + "diagnosis_domain_expiration_not_found_details": "域{domain}的 WHOIS 信息似乎不包含有关到期日期的信息?", + "diagnosis_domain_expiration_success": "您的域已注册,并且不会很快过期。", + "diagnosis_domain_expiration_warning": "一些域即将过期!", + "diagnosis_domain_expires_in": "{domain}在{days}天后到期。", + "diagnosis_domain_not_found_details": "域{domain}在 WHOIS 数据库中不存在或已过期!", + "diagnosis_everything_ok": "{category}看起来一切正常!", + "diagnosis_failed": "无法获取类别 '{category}'的诊断结果:{error}", + "diagnosis_failed_for_category": "诊断类别 '{category}'失败:{error}", + "diagnosis_found_errors": "发现与{category}相关的{errors}个重要问题!", + "diagnosis_found_errors_and_warnings": "发现与{category}相关的{errors}个重要问题(和{warnings}警告)!", + "diagnosis_found_warnings": "找到{warnings}项,可能需要{category}进行改进。", + "diagnosis_high_number_auth_failures": "最近出现了大量可疑的失败身份验证。您的 fail2ban 正在运行且配置正确,或使用自定义端口的 SSH 作为 https://doc.yunohost.org/admin/security。", + "diagnosis_http_bad_status_code": "它看起来像另一台机器(也许是您的互联网路由器)回答,而不是您的服务器。
1。这个问题最常见的原因是 80 端口(和 443 端口)没有正确转发到您的服务器
2.在更复杂的设置中:确保没有防火墙或反向代理的干扰。", + "diagnosis_http_connection_error": "连接错误:无法连接到请求的域,很可能无法访问。", + "diagnosis_http_could_not_diagnose": "无法诊断域是否可以从 IPv{ipversion}中从外部访问。", + "diagnosis_http_could_not_diagnose_details": "错误:{error}", + "diagnosis_http_hairpinning_issue": "您的本地网络似乎没有启用 NAT 回环功能。", + "diagnosis_http_hairpinning_issue_details": "这可能是由于您的 ISP 光猫/路由器。因此,使用域名或全局 IP 时,来自本地网络外部的人员将能够按预期访问您的服务器,但无法访问来自本地网络内部的人员(可能与您一样)。您可以通过查看 https://doc.yunohost.org/dns_local_network 来改善这种情况", + "diagnosis_http_nginx_conf_not_up_to_date": "该域的 nginx 配置似乎已被手动修改,并阻止 YunoHost 诊断它是否可以在 HTTP 上访问。", + "diagnosis_http_nginx_conf_not_up_to_date_details": "要解决这种情况,请使用yunohost tools regen-conf nginx --dry-run --with-diff从命令行检查差异,如果您同意这些更改,请使用yunohost tools regen-conf nginx --force应用更改。", + "diagnosis_http_ok": "域{domain}可以通过 HTTP 从本地网络外部访问。", + "diagnosis_http_partially_unreachable": "尽管域{domain}可以在 IPv{failed}中工作,但它似乎无法通过 HTTP 从外部网络通过 HTTP 到达 IPv{passed}。", + "diagnosis_http_special_use_tld": "域名 {domain} 是基于特殊用途的顶级域名(TLD),如 .local 或 .test,因此不指望会暴露在本地网络之外。", + "diagnosis_http_timeout": "当试图从外部联系您的服务器时,出现了超时。它似乎是不可达的。
1. 这个问题最常见的原因是 80 端口(和 443 端口)没有正确转发到您的服务器
2.您还应该确保 nginx 服务正在运行
3.对于更复杂的设置:确保没有防火墙或反向代理的干扰。", + "diagnosis_http_unreachable": "网域{domain}从本地网络外通过 HTTP 无法访问。", + "diagnosis_ignore_already_filtered": "(已经存在一个诊断 {category} 过滤器,符合这些标准)", + "diagnosis_ignore_criteria_error": "标准应为 key=value 形式(例如 domain=yolo.test)", + "diagnosis_ignore_filter_added": "已添加 {category} 诊断过滤器", + "diagnosis_ignore_filter_removed": "已移除 {category} 诊断过滤器", + "diagnosis_ignore_missing_criteria": "您应该提供至少一个标准作为要忽略的诊断类别", + "diagnosis_ignore_no_filter_found": "(没有可以移除的符合这些标准的诊断 {category} 过滤器)", + "diagnosis_ignore_no_issue_found": "未找到符合给定标准的问题。", + "diagnosis_ignored_issues": "(+ {nb_ignored} 个被忽略的问题)", + "diagnosis_ip_broken_dnsresolution": "域名解析似乎由于某种原因而被破坏…防火墙是否阻止了 DNS 请求?", + "diagnosis_ip_broken_resolvconf": "域名解析在您的服务器上似乎已损坏,这似乎与 /etc/resolv.conf 有关,但未指向 127.0.0.1 。", + "diagnosis_ip_connected_ipv4": "服务器通过 IPv4 连接到 Internet!", + "diagnosis_ip_connected_ipv6": "服务器通过 IPv6 连接到 Internet!", + "diagnosis_ip_dnsresolution_working": "域名解析正常!", + "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 并不是服务器正常运行所必需的,但是对于整个 Internet 的健康而言,则更好。通常,IPv6 应该由系统或您的提供商自动配置(如果可用)。否则,您可能需要按照此处的文档中的说明手动配置一些内容: https://doc.yunohost.org/ipv6。如果您无法启用 IPv6 或对您来说太过困难,也可以安全地忽略此警告。", + "diagnosis_ip_no_ipv6_tip_important": "IPv6 通常应由系统或您的提供商在可用时自动配置。否则,您可能需要手动配置一些内容,具体说明请参见以下文档: https://doc.yunohost.org/ipv6。", + "diagnosis_ip_not_connected_at_all": "服务器似乎根本没有连接到 Internet!?", + "diagnosis_ip_weird_resolvconf": "DNS 解析似乎可以正常工作,但是您似乎正在使用自定义的 /etc/resolv.conf 。", + "diagnosis_ip_weird_resolvconf_details": "文件 /etc/resolv.conf 应该是指向 /etc/resolvconf/run/resolv.conf 本身的符号链接,指向 127.0.0.1 (dnsmasq)。如果要手动配置 DNS 解析器,请编辑 /etc/resolv.dnsmasq.conf。", + "diagnosis_mail_blocklist_listed_by": "您的 IP 或域{item} 已在{blocklist_name}上列入黑名单", + "diagnosis_mail_blocklist_ok": "该服务器使用的 IP 和域似乎未列入黑名单", + "diagnosis_mail_blocklist_reason": "黑名单的原因是:{reason}", + "diagnosis_mail_blocklist_website": "确定列出的原因并加以修复后,请随时在{blocklist_website}上要求删除您的 IP 或域", + "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_could_not_diagnose_details": "错误:{error}", + "diagnosis_mail_ehlo_ok": "SMTP 邮件服务器可以从外部访问,因此可以接收电子邮件!", + "diagnosis_mail_ehlo_unreachable": "SMTP 邮件服务器在 IPv{ipversion}上无法从外部访问。它将无法接收电子邮件。", + "diagnosis_mail_ehlo_unreachable_details": "在 IPv{ipversion}中无法打开与您服务器的 25 端口连接。它似乎是不可达的。
1. 这个问题最常见的原因是端口 25没有正确转发到您的服务器
2.您还应该确保 postfix 服务正在运行。
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": "反向 DNS 未在 IPv{ipversion}中定义。某些电子邮件可能无法发送或被标记为垃圾邮件。", + "diagnosis_mail_fcrdns_nok_alternatives_4": "有些供应商不会让您配置您的反向 DNS(或者他们的功能可能被破坏……)。如果您因此而遇到问题,请考虑以下解决方案:
- 一些 ISP 提供了使用邮件服务器中转的选择,尽管这意味着中转将能够监视您的电子邮件流量。
- 一个有利于隐私的选择是使用 VPN*与专用公共 IP*来绕过这类限制。见https://doc.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_details": "您应该首先尝试在 Internet 路由器界面或托管服务提供商界面中使用{ehlo_domain}配置反向 DNS。(某些托管服务提供商可能会要求您为此发送支持工单)。", + "diagnosis_mail_fcrdns_ok": "您的反向 DNS 已正确配置!", + "diagnosis_mail_outgoing_port_25_blocked": "由于传出端口 25 在 IPv{ipversion}中被阻止,因此 SMTP 邮件服务器无法向其他服务器发送电子邮件。", + "diagnosis_mail_outgoing_port_25_blocked_details": "您应该首先尝试在 Internet 路由器界面或主机提供商界面中取消阻止传出端口 25。(某些托管服务提供商可能会要求您为此发送支持请求)。", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "一些供应商不会让您解除对出站端口 25 的封锁,因为他们不关心网络中立性。
- 其中一些供应商提供了使用邮件服务器中继的替代方案,尽管这意味着中继将能够监视您的电子邮件流量。
- 一个有利于隐私的替代方案是使用 VPN*,用一个专用的公共 IP*绕过这种限制。见https://doc.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} emails)", + "diagnosis_mail_queue_unavailable": "无法查询队列中待处理电子邮件的数量", + "diagnosis_mail_queue_unavailable_details": "错误:{error}", + "diagnosis_never_ran_yet": "看来这台服务器是最近安装的,还没有诊断报告可以显示。您应该首先从 Web 管理员运行完整的诊断,或者从命令行使用'yunohost diagnosis run' 。", + "diagnosis_no_cache": "尚无类别 '{category}'的诊断缓存", + "diagnosis_package_installed_from_sury": "一些系统软件包应降级", + "diagnosis_package_installed_from_sury_details": "一些软件包被无意中从一个名为 Sury 的第三方仓库安装。YunoHost 团队改进了处理这些软件包的策略,但预计一些安装了 PHP7.3 应用的设置在仍然使用 Stretch 的情况下还有一些不一致的地方。为了解决这种情况,您应该尝试运行以下命令:{cmd_to_fix}", + "diagnosis_ports_could_not_diagnose": "无法诊断端口在 IPv{ipversion}中是否可以从外部访问。", + "diagnosis_ports_could_not_diagnose_details": "错误:{error}", + "diagnosis_ports_forwarding_tip": "要解决此问题,您很可能需要按照 https://doc.yunohost.org/admin/get_started/post_install/dns_config/ 中的说明,在 Internet 路由器上配置端口转发", + "diagnosis_ports_needed_by": "{category}功能(服务{service})需要公开此端口", + "diagnosis_ports_ok": "可以从外部访问端口{port}。", + "diagnosis_ports_partially_unreachable": "无法从外部通过 IPv{failed}访问端口{port}。", + "diagnosis_ports_unreachable": "无法从外部访问端口{port}。", + "diagnosis_processes_killed_by_oom_reaper": "系统最近杀死了某些进程,因为内存不足。这通常是系统内存不足或进程占用大量内存的征兆。杀死进程的摘要:\n{kills_summary}", + "diagnosis_ram_low": "系统有 {available} ({available_percent}%) RAM 可用(共{total}个)可用。小心。", + "diagnosis_ram_ok": "系统在{total}中仍然有 {available} ({available_percent}%) RAM 可用。", + "diagnosis_ram_verylow": "系统只有 {available} ({available_percent}%) 内存可用!(在{total}中)", + "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_rfkill_wifi": "Wi-Fi 卡已禁用,系统警告可能会阻止应用程序安装", + "diagnosis_rfkill_wifi_details": "该警告经常出现在许多命令输出中,导致某些应用程序出现故障。通常需要在命令 sudo raspi-config 中指定您的国家代码。错误如下:
{rfkill_wifi_error}", + "diagnosis_rootfstotalspace_critical": "根文件系统总共只有{space},这很令人担忧!您可能很快就会用完磁盘空间!建议根文件系统至少有 16 GB。", + "diagnosis_rootfstotalspace_warning": "根文件系统总共只有{space}。这可能没问题,但要小心,因为最终您可能很快会用完磁盘空间…建议根文件系统至少有 16 GB。", + "diagnosis_security_vulnerable_to_meltdown": "您似乎容易受到 Meltdown 关键安全漏洞的影响", + "diagnosis_security_vulnerable_to_meltdown_details": "要解决此问题,您应该升级系统并重新启动以加载新的 Linux 内核(如果无法使用,请与您的服务器提供商联系)。有关更多信息,请参见 https://meltdownattack.com/。", + "diagnosis_services_bad_status": "服务{service}为 {status} :(", + "diagnosis_services_bad_status_tip": "您可以尝试重新启动服务,如果没有效果,可以看看 webadmin 中的服务日志(从命令行,您可以用yunohost service restart {service}yunohost service log {service})来做。", + "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_sshd_config_inconsistent_details": "请运行yunohost settings set security.ssh.ssh_port -v YOUR_SSH_PORT来定义 SSH 端口,并检查yunohost tools regen-conf ssh --dry-run --with-diffyunohost tools regen-conf ssh --force将您的配置重置为 YunoHost 建议。", + "diagnosis_sshd_config_insecure": "SSH 配置似乎已被手动修改,并且是不安全的,因为它不包含“AllowGroups”或“ AllowUsers”指令以限制对授权用户的访问。", + "diagnosis_swap_none": "系统根本没有交换分区。您应该考虑至少添加{recommended}交换,以避免系统内存不足的情况。", + "diagnosis_swap_notsomuch": "系统只有{total}个交换。您应该考虑至少使用{recommended},以避免系统内存不足的情况。", + "diagnosis_swap_ok": "系统有{total}个交换!", + "diagnosis_swap_tip": "请注意,如果服务器在 SD 卡或 SSD 存储器上托管交换分区,可能会大大缩短设备的预期寿命。", + "diagnosis_unknown_categories": "以下类别是未知的:{categories}", + "diagnosis_using_stable_codename": "apt(系统的包管理器)目前配置为从代号“stable”安装软件包,而不是当前 Debian 版本(bookworm)的代号。", + "diagnosis_using_stable_codename_details": "这通常是由于您的托管提供商配置不正确造成的。这是危险的,因为一旦下一个 Debian 版本成为新的“稳定版”,apt将会想要升级所有系统软件包,而不经过适当的迁移过程。建议通过编辑基础 Debian 存储库的 apt 源来解决此问题,将stable关键字替换为bookworm。相应的配置文件应为/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_install": "没有足够的磁盘空间来安装此应用", + "disk_space_not_sufficient_update": "没有足够的磁盘空间来更新此应用", + "domain_cannot_remove_main": "您不能删除'{domain}',因为它是主域,您首先需要用'yunohost domain main-domain -n '设置另一个域作为主域;这里是候选域的列表:{other_domains}", + "domain_cannot_remove_main_add_new_one": "您不能移除 '{domain}',因为它是主域和您唯一的域,您需要先用'yunohost domain add ' 添加另一个域,然后用 'yunohost domain main-domain -n ' 设置为主域,然后您可以用 'yunohost domain remove {domain}' 移除域 {domain}。", + "domain_cert_gen_failed": "无法生成证书", + "domain_config_acme_eligible": "ACME 资格", + "domain_config_acme_eligible_explain": "这个域似乎还没有准备好获取 Let's Encrypt 证书。请检查您的 DNS 配置和 HTTP 服务器的可达性。“DNS 记录”和“Web”部分在诊断页面中可以帮助您理解配置错误的地方。", + "domain_config_api_protocol": "API 协议", + "domain_config_auth_application_key": "应用程序密钥", + "domain_config_auth_application_secret": "应用程序秘密密钥", + "domain_config_auth_consumer_key": "消费者密钥", + "domain_config_auth_entrypoint": "API 入口点", + "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_name": "证书", + "domain_config_cert_no_checks": "忽略诊断检查", + "domain_config_cert_renew": "续订 Let's Encrypt 证书", + "domain_config_cert_renew_help": "证书将在有效期的最后 15 天内自动续订。如果您想,可以手动续订。(不推荐)。", + "domain_config_cert_summary": "证书状态", + "domain_config_cert_summary_abouttoexpire": "当前证书即将到期。它应该很快自动续订。", + "domain_config_cert_summary_expired": "严重:当前证书无效!HTTPS 根本无法工作!", + "domain_config_cert_summary_letsencrypt": "太好了!您正在使用有效的 Let's Encrypt 证书!", + "domain_config_cert_summary_ok": "好的,当前证书看起来良好!", + "domain_config_cert_summary_selfsigned": "警告:当前证书是自签名的。浏览器将向新访客显示一个可怕的警告!", + "domain_config_cert_validity": "有效性", + "domain_config_custom_css": "自定义 CSS 样式表", + "domain_config_custom_css_help": "这是给愿意定制门户外观的高级管理员的说明", + "domain_config_default_app": "默认应用", + "domain_config_default_app_help": "当打开此域时,人们会自动重定向到此应用。如果未指定应用,人们将重定向到门户登录表单。", + "domain_config_dns_name": "DNS", + "domain_config_enable_public_apps_page": "向访客显示公共应用列表", + "domain_config_enable_public_apps_page_help": "当访客进入门户时,他们将看到一个“公共应用”页面,而不是仅仅显示登录表单。", + "domain_config_feature_name": "功能", + "domain_config_mail_in": "收到的邮件", + "domain_config_mail_out": "发送的邮件", + "domain_config_portal_logo": "自定义 Logo", + "domain_config_portal_logo_help": "接受 .svg、.png 和 .jpeg。推荐使用单色 .svg 并带有 fill: currentColor,以便 Logo 适应主题。", + "domain_config_portal_name": "门户定制", + "domain_config_portal_public_intro": "自定义公共介绍", + "domain_config_portal_public_intro_help": "您可以使用 HTML,基本样式将应用于通用元素。", + "domain_config_portal_theme": "默认颜色主题", + "domain_config_portal_theme_help": "用户可以在其设置中选择其他主题。", + "domain_config_portal_tile_theme": "应用图块显示主题", + "domain_config_portal_title": "自定义标题", + "domain_config_portal_user_intro": "自定义用户介绍", + "domain_config_portal_user_intro_help": "您可以使用 HTML,基本样式将应用于通用元素。", + "domain_config_search_engine": "搜索引擎 URL", + "domain_config_search_engine_help": "这是一个可选功能,可以在门户中显示搜索栏(例如,如果您想将 YunoHost 门户用作浏览器的主页)。这应该是一个带有空查询字符串的 URL,例如 `https://duckduckgo.com/?q=`,其中 `q=` 是 duckduckgo 的空查询参数", + "domain_config_search_engine_name": "搜索引擎名称", + "domain_config_show_other_domains_apps": "显示其他域的应用", + "domain_created": "域已创建", + "domain_creation_failed": "无法创建域 {domain}: {error}", + "domain_deleted": "域已删除", + "domain_deletion_failed": "无法删除域 {domain}: {error}", + "domain_dns_conf_is_just_a_recommendation": "本页向您展示了*推荐的*配置。它并*不*为您配置 DNS。您有责任根据该建议在您的 DNS注册商处配置您的 DNS 区域。", + "domain_dns_conf_special_use_tld": "此域基于特殊用途的顶级域(TLD),如 .local 或 .test,因此不预计有实际的 DNS 记录。", + "domain_dns_push_already_up_to_date": "记录已是最新,无需操作。", + "domain_dns_push_failed": "更新 DNS 记录失败。", + "domain_dns_push_failed_to_list": "无法使用注册商的 API 列出当前记录:{error}", + "domain_dns_push_managed_in_parent_domain": "自动 DNS 配置功能由父域 {parent_domain} 管理。", + "domain_dns_push_not_applicable": "自动 DNS 记录功能不适用于域 {domain},您应该按照 https://doc.yunohost.org/dns_config 上的文档手动配置您的 DNS 记录。", + "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 无法自动检测处理此域的注册商。您应该根据 https://doc.yunohost.org/dns_config 的文档手动配置您的 DNS 记录。", + "domain_dns_registrar_supported": "YunoHost 自动检测到该域由注册商 **{registrar}** 处理。如果您愿意,YunoHost 将自动配置此 DNS 区域,只要您提供适当的 API 凭据。您可以在此页面上找到如何获取 API 凭据的文档:https://doc.yunohost.org/admin/get_started/providers/registrar/{registrar}/。(您也可以根据 https://doc.yunohost.org/dns_config 的文档手动配置您的 DNS 记录)", + "domain_dns_registrar_use_auto": "使用自动 DNS 功能", + "domain_dns_registrar_yunohost": "该域名为 nohost.me / nohost.st / ynh.fr,因此其 DNS 配置由 YunoHost 自动处理,无需进一步配置。(参见 'yunohost dyndns update' 命令)", + "domain_dyndns_already_subscribed": "您已经订阅了 DynDNS 域", + "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 the_app_id' 将其卸载,或使用 'yunohost app change-url the_app_id'将其移至另一个域,然后再继续删除域", + "domain_unknown": "域名 '{domain}' 不存在", + "domains_available": "可用域:", + "done": "完成", + "download_bad_status_code": "{url} 返回状态码 {code}", + "download_ssl_error": "连接到 {url} 时发生 SSL 错误", + "download_timeout": "{url} 响应时间过长,已放弃。", + "download_unknown_error": "从 {url} 下载数据时出错:{error}", + "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": "无法检查{provider}上是否可用 {domain}。", + "dyndns_domain_not_provided": "DynDNS 提供者 {provider} 无法提供域 {domain}。", + "dyndns_ip_update_failed": "无法将 IP 地址更新到 DynDNS", + "dyndns_ip_updated": "在 DynDNS 上更新了您的 IP", + "dyndns_key_not_found": "找不到该域的 DNS 密钥", + "dyndns_no_domain_registered": "没有在 DynDNS 中注册的域", + "dyndns_no_recovery_password": "未指定恢复密码!如果您失去对该域的控制,您将需要联系 YunoHost 团队的管理员!", + "dyndns_provider_unreachable": "无法联系 DynDNS 提供者 {provider}: 您的 YunoHost 未正确连接到 Internet 或 dynette 服务器已关闭。", + "dyndns_set_recovery_password_denied": "设置恢复密码失败:无效密钥", + "dyndns_set_recovery_password_failed": "设置恢复密码失败:{error}", + "dyndns_set_recovery_password_invalid_password": "设置恢复密码失败:密码强度不足", + "dyndns_set_recovery_password_success": "恢复密码已设置!", + "dyndns_set_recovery_password_unknown_domain": "设置恢复密码失败:域未注册", + "dyndns_subscribe_failed": "无法订阅 DynDNS 域:{error}", + "dyndns_subscribed": "DynDNS 域已订阅", + "dyndns_too_many_requests": "YunoHost 的 dyndns 服务收到了过多的请求,请等 1 个小时后再试。", + "dyndns_unavailable": "域'{domain}' 不可用。", + "dyndns_unsubscribe_already_unsubscribed": "域名已经取消订阅", + "dyndns_unsubscribe_denied": "取消订阅域名失败:凭据无效", + "dyndns_unsubscribe_failed": "无法取消订阅 DynDNS 域:{error}", + "dyndns_unsubscribed": "DynDNS 域已取消订阅", + "error_changing_file_permissions": "更改 {path} 权限时出错:{error}", + "error_removing": "删除 {path} 时出错:{error}", + "error_writing_file": "写入文件 {file} 时出错:{error}", + "extracting": "正在提取…", + "field_invalid": "无效的字段'{field}'", + "file_does_not_exist": "文件{path} 不存在。", + "file_not_exist": "文件不存在:'{path}'", + "firewall_reload_failed": "无法重新加载防火墙。日志中的更多信息。", + "firewall_reloaded": "重新加载防火墙", + "global_settings_reset_success": "重置全局设置", + "global_settings_setting_admin_strength": "管理员密码强度", + "global_settings_setting_admin_strength_help": "这些要求仅在初始化或更改密码时强制实施", + "global_settings_setting_antispam_name": "反垃圾邮件", + "global_settings_setting_backup_compress_tar_archives": "压缩备份", + "global_settings_setting_backup_compress_tar_archives_help": "创建新备份时,请压缩档案(.tar.gz) ,而不要压缩未压缩的档案(.tar)。注意:启用此选项意味着创建较小的备份存档,但是初始备份过程将明显更长且占用大量 CPU。", + "global_settings_setting_backup_name": "备份", + "global_settings_setting_dns_exposure": "用于 DNS 配置和诊断的 IP 版本", + "global_settings_setting_dns_exposure_help": "注意:这仅影响推荐的 DNS 配置和诊断检查。这不影响系统配置。", + "global_settings_setting_email_name": "电子邮件", + "global_settings_setting_enable_blocklists": "为入站流量启用黑名单", + "global_settings_setting_enable_blocklists_help": "阻止 spamcop.net、spamhaus.org 和 abuseat.org 列出的服务器以防止垃圾邮件。然而,这可能会导致某些被这些第三方列出的无害邮件服务器出现投递问题,在这种情况下,从这些服务器发送的邮件将无法被接收。", + "global_settings_setting_experimental_name": "实验性", + "global_settings_setting_misc_name": "其他", + "global_settings_setting_network_name": "网络", + "global_settings_setting_nginx_compatibility": "NGINX 兼容性", + "global_settings_setting_nginx_compatibility_help": "Web 服务器 NGINX 的兼容性与安全性的权衡,影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_nginx_name": "NGINX(网络服务器)", + "global_settings_setting_nginx_redirect_to_https": "强制 HTTPS", + "global_settings_setting_nginx_redirect_to_https_help": "默认情况下将 HTTP 请求重定向到 HTTPS(除非你非常清楚自己在做什么,否则不要关闭!)", + "global_settings_setting_password_name": "密码", + "global_settings_setting_passwordless_sudo": "允许管理员在不重新输入密码的情况下使用 'sudo'", + "global_settings_setting_pop3_enabled": "启用 POP3", + "global_settings_setting_pop3_enabled_help": "为邮件服务器启用 POP3 协议。POP3 是一种较旧的访问邮箱的协议,比 IMAP 更轻量,但功能较少(默认启用 IMAP)", + "global_settings_setting_pop3_name": "POP3", + "global_settings_setting_portal_allow_edit_email": "允许用户编辑其主邮箱地址", + "global_settings_setting_portal_allow_edit_email_alias": "允许用户添加、删除、编辑邮件别名", + "global_settings_setting_portal_allow_edit_email_alias_help": "如果禁用,他们需要请求管理员代为操作。", + "global_settings_setting_portal_allow_edit_email_forward": "允许用户添加、删除、编辑邮件转发", + "global_settings_setting_portal_allow_edit_email_forward_help": "如果禁用,他们需要请求管理员代为操作。", + "global_settings_setting_portal_allow_edit_email_help": "如果禁用,他们需要请求管理员代为操作。", + "global_settings_setting_portal_name": "门户", + "global_settings_setting_postfix_compatibility": "Postfix 兼容性", + "global_settings_setting_postfix_compatibility_help": "Postfix 服务器的兼容性与安全性的权衡。影响密码(以及其他与安全性有关的方面)", + "global_settings_setting_postfix_name": "Postfix(SMTP 邮件服务器)", + "global_settings_setting_root_access_explain": "在 Linux 系统中,'root' 是绝对管理员。在 YunoHost 环境中,默认情况下禁用直接 'root' SSH 登录 - 除非从服务器的本地网络。'admins' 组的成员可以使用 sudo 命令从命令行以 root 身份操作。然而,如果常规管理员无法登录,拥有一个(强健的)root 密码来调试系统可能会很有帮助。", + "global_settings_setting_root_access_name": "更改 root 密码", + "global_settings_setting_root_password": "新 root 密码", + "global_settings_setting_root_password_confirm": "新 root 密码(确认)", + "global_settings_setting_security_experimental_enabled": "实验性安全功能", + "global_settings_setting_security_experimental_enabled_help": "启用实验性安全功能(如果你不知道自己在做什么,请不要启用!)", + "global_settings_setting_security_name": "安全", + "global_settings_setting_smtp_allow_ipv6": "允许 IPv6", + "global_settings_setting_smtp_allow_ipv6_help": "允许使用 IPv6 接收和发送邮件", + "global_settings_setting_smtp_backup_mx_domains": "作为次级 MX 的域", + "global_settings_setting_smtp_backup_mx_domains_help": "允许此服务器作为列出的域的备份 *次级* MX 域。这意味着如果该域的主 MX 无法访问(例如由于停机),邮件仍将发送到此服务器,该服务器将在最多 20 天内保留邮件,并在主 MX 恢复后尝试将其转发到实际目的地。可以提供多个域,用逗号分隔。", + "global_settings_setting_smtp_backup_mx_emails_whitelisted": "SMTP 备份 MX 邮件白名单", + "global_settings_setting_smtp_backup_mx_emails_whitelisted_help": "作为次级 MX 时,必须提供允许的收件人电子邮件地址的详尽列表(否则邮件将被拒绝和丢弃)。可以提供多个条目,用逗号分隔。", + "global_settings_setting_smtp_name": "SMTP", + "global_settings_setting_smtp_relay_enabled": "启用 SMTP 中继", + "global_settings_setting_smtp_relay_enabled_help": "使用 SMTP 中继主机来代替这个 YunoHost 实例发送邮件。如果您有以下情况,就很有用:您的 25 端口被您的 ISP 或 VPS 提供商封锁,您有一个住宅 IP 列在 DUHL 上,您不能配置反向 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_name": "SSH", + "global_settings_setting_ssh_password_authentication": "密码认证", + "global_settings_setting_ssh_password_authentication_help": "允许 SSH 密码认证", + "global_settings_setting_ssh_port": "SSH 端口", + "global_settings_setting_ssh_port_help": "建议使用低于 1024 的端口,以防止远程机器上的非管理员服务进行冒充尝试。还应避免使用已在使用的端口,例如 80 或 443。", + "global_settings_setting_tls_passthrough_enabled": "启用 TLS 透传 / 基于 SNI 的转发", + "global_settings_setting_tls_passthrough_enabled_help": "这是一个高级特性,用于将整个域反向代理到另一台机器,而 *不* 解密流量。当您希望在同一 IP 后面暴露多台机器时非常有用,但仍允许每台机器处理 SSL 终止。", + "global_settings_setting_tls_passthrough_explain": "此功能是高级和实验性的,将触发此服务器的 nginx 配置的重大更改。如果您不知道自己在做什么,请 **不要** 使用它!特别是,您必须意识到无法在代理服务器上实现 fail2ban(nftables 无法禁止恶意流量,因为所有 IP 数据包看起来都来自前端服务器)。此外,目前代理服务器的 nginx 配置需要手动调整以接受 `proxy_protocol`。", + "global_settings_setting_tls_passthrough_list": "转发列表", + "global_settings_setting_tls_passthrough_list_help": "应为 DOMAIN;DESTINATION;PORT 的列表,例如 domain.tld;192.168.1.42;443 或 domain.tld;server.local;8123", + "global_settings_setting_tls_passthrough_name": "TLS 透传 / 基于 SNI 的转发", + "global_settings_setting_user_strength": "用户密码强度", + "global_settings_setting_user_strength_help": "这些要求仅在初始化或更改密码时强制执行", + "global_settings_setting_webadmin_allowlist": "Webadmin IP 白名单", + "global_settings_setting_webadmin_allowlist_enabled": "启用 Webadmin IP 白名单", + "global_settings_setting_webadmin_allowlist_enabled_help": "仅允许某些 IP 访问 webadmin。", + "global_settings_setting_webadmin_allowlist_help": "允许访问 webadmin 的 IP 地址。允许使用 CIDR 表示法。", + "global_settings_setting_webadmin_name": "Webadmin", + "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_be_deleted": "无法手动删除组{group}。", + "group_cannot_edit_all_users": "组“ all_users”不能手动编辑。这是一个特殊的组,旨在包含所有在 YunoHost 中注册的用户", + "group_cannot_edit_primary_group": "不能手动编辑 '{group}' 组。它是旨在仅包含一个特定用户的主要组。", + "group_cannot_edit_visitors": "组“访客”不能手动编辑。这是一个代表匿名访问者的特殊小组", + "group_cannot_remove_last_admin": "用户 '{user}' 是组 'admins' 中的最后一个用户,将不会被从中移除。", + "group_created": "创建了 '{group}'组", + "group_creation_failed": "无法创建组'{group}': {error}", + "group_deleted": "群组'{group}' 已删除", + "group_deletion_failed": "无法删除群组'{group}': {error}", + "group_mailalias_add": "邮箱别名 '{mail}' 将被添加到组 '{group}'", + "group_mailalias_remove": "邮箱别名 '{mail}' 将从组 '{group}' 中移除", + "group_no_change": "组 '{group}' 没有要更改的内容", + "group_unknown": "群组 '{group}' 未知", + "group_update_aliases": "更新组 '{group}' 的别名", + "group_update_failed": "无法更新群组'{group}': {error}", + "group_updated": "群组 '{group}' 已更新", + "group_user_add": "用户 '{user}' 将被添加到组 '{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}", + "hook_json_return_error": "无法读取来自钩子 {path}的返回,错误:{msg}。原始内容:{raw_content}", + "hook_list_by_invalid": "此属性不能用于列出钩子", + "hook_name_unknown": "未知的钩子名称 '{name}'", + "installation_complete": "安装完成", + "invalid_credentials": "无效的密码或用户名", + "invalid_number": "必须是数字", + "invalid_password": "无效的密码", + "invalid_regex": "无效的正则表达式:'{regex}'", + "invalid_shell": "无效的 shell: {shell}", + "invalid_url": "无法连接到 {url}… 可能是服务已关闭,或者您未正确连接到 IPv4/IPv6 互联网。", + "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": "更改'{}'应用的网址", + "log_app_config_set": "将配置应用于 '{}' 应用", + "log_app_install": "安装 '{}' 应用", + "log_app_makedefault": "将 '{}' 设为默认应用", + "log_app_remove": "删除 '{}' 应用", + "log_app_upgrade": "升级 '{}' 应用", + "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_diagnosis_run": "运行诊断", + "log_does_exists": "没有名称为'{log}'的操作日志,请使用 'yunohost log list' 查看所有可用的操作日志", + "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_unsubscribe": "取消注册 YunoHost 子域 '{}'", + "log_dyndns_update": "更新与您的 YunoHost 子域'{}'关联的 IP", + "log_help_to_get_failed_log": "操作'{desc}'无法完成。请使用命令'yunohost log share {name}' 共享此操作的完整日志以获取帮助", + "log_help_to_get_log": "要查看操作'{desc}'的日志,请使用命令'yunohost log show {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_regen_conf": "重新生成系统配置'{}'", + "log_remove_on_failed_install": "安装失败后删除 '{}'", + "log_resource_snippet": "配置/取消配置/更新资源", + "log_selfsigned_cert_install": "在 '{}'域上安装自签名证书", + "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_update": "正在获取可用的系统更新并刷新应用目录", + "log_tools_upgrade": "升级系统软件包", + "log_user_create": "添加用户'{}'", + "log_user_delete": "删除用户'{}'", + "log_user_group_create": "创建组'{}'", + "log_user_group_delete": "删除组'{}'", + "log_user_group_update": "更新组'{}'", + "log_user_import": "导入用户", + "log_user_update": "更新用户'{}'的信息", + "mail_alias_remove_failed": "无法删除电子邮件别名'{mail}'", + "mail_alias_unauthorized": "您没有权限添加与域 '{domain}' 相关的别名", + "mail_already_exists": "邮箱地址 '{mail}' 已存在", + "mail_domain_unknown": "域'{domain}'的电子邮件地址无效。请使用本服务器管理的域。", + "mail_edit_operation_unauthorized": "您未被授权为您的账号进行此更改。", + "mail_forward_remove_failed": "无法删除电子邮件转发'{mail}'", + "mail_unavailable": "此电子邮件地址保留给管理员组", + "mailbox_disabled": "用户{user}的电子邮件已关闭", + "mailbox_used_space_dovecot_down": "如果要获取使用过的邮箱空间,则必须启动 Dovecot 邮箱服务", + "main_domain_change_failed": "无法更改主域", + "main_domain_changed": "主域已更改", + "migration_0027_cleaning_up": "正在清理不再有用的缓存和软件包…", + "migration_0027_delayed_api_restart": "YunoHost API 将在 15 秒后自动重启。它可能会在几秒钟内不可用,然后您需要重新登录。", + "migration_0027_general_warning": "最后,请注意,这次迁移是 **一个微妙的操作**。YunoHost 团队已尽力审核和测试,但迁移仍可能会破坏系统或其应用的某些部分。\n\n因此,建议:\n - **对任何关键数据或应用进行备份**。更多信息请访问 https://doc.yunohost.org/backup;\n - **在迁移后耐心等待**:根据您的互联网连接和硬件,整个升级过程可能需要长达一小时才能正确完成;\n - **在论坛上联系社区**,如果您需要帮助解决问题。", + "migration_0027_main_upgrade": "正在开始主要升级…", + "migration_0027_modified_files": "请注意,发现以下文件被手动修改,可能会在升级后被覆盖:{manually_modified_files}", + "migration_0027_not_bullseye": "当前的 Debian 版本不是 Bullseye!如果您已经进行了 Bullseye -> Bookworm 的迁移,则此错误表明迁移过程未 100% 成功(否则 YunoHost 将会标记为已完成)。建议与支持团队调查发生了什么,他们将需要迁移的**完整**日志,该日志可以在 webadmin 的工具 > 日志中找到。", + "migration_0027_not_enough_free_space": "/var/中的可用空间非常低!您应该至少有 1GB 空闲空间来执行此迁移。", + "migration_0027_patch_yunohost_conflicts": "正在应用补丁以解决冲突问题…", + "migration_0027_patching_sources_list": "正在修补 sources.list 文件…", + "migration_0027_problematic_apps_warning": "请注意,检测到以下可能存在问题的已安装应用。这些应用似乎未从 YunoHost 应用目录安装,或未标记为“正常工作”。因此,无法保证它们在升级后仍能正常工作:{problematic_apps}", + "migration_0027_start": "开始迁移到 Bookworm…", + "migration_0027_still_on_bullseye_after_main_upgrade": "在主要升级期间出现了问题,系统似乎仍在 Debian Bullseye 上。", + "migration_0027_system_not_fully_up_to_date": "您的系统没有完全更新。请在运行迁移到 Bookworm 之前进行常规升级。", + "migration_0027_yunohost_upgrade": "正在开始 YunoHost 核心升级…", + "migration_not_enough_space": "请在 {path} 中腾出足够的空间以进行迁移。", + "migration_postgresql_previous_not_installed": "您的系统上未安装 PostgreSQL。无事可以做。", + "migration_postgresql_target_not_installed": "已安装 PostgreSQL 13,但未安装 PostgreSQL 15!?您的系统上可能发生了一些奇怪的事情 :(…", + "migration_python_venv_rebuild_broken_app": "跳过 {app},因为无法轻松重建虚拟环境。相反,您应该通过强制升级该应用程序来修复此情况,使用 `yunohost app upgrade --force {app}`。", + "migration_python_venv_rebuild_disclaimer_base": "在升级到 Debian Bookworm 后,一些 Python 应用程序需要部分重建以转换为 Debian 中提供的新 Python 版本(技术术语中称为:“虚拟环境”需要重新创建)。与此同时,这些 Python 应用程序可能无法工作。YunoHost 可以尝试为其中一些应用程序重建虚拟环境,具体如下。对于其他应用程序,或者如果重建尝试失败,您需要手动强制升级这些应用程序。", + "migration_python_venv_rebuild_disclaimer_ignored": "无法自动重建这些应用程序的虚拟环境。您需要为这些应用程序强制升级,这可以通过命令行完成:`yunohost app upgrade --force APP`: {ignored_apps}", + "migration_python_venv_rebuild_disclaimer_rebuild": "将尝试为以下应用程序重建虚拟环境(注意:操作可能需要一些时间!): {rebuild_apps}", + "migration_python_venv_rebuild_failed": "未能重建 {app} 的 Python 虚拟环境。只要没有解决此问题,该应用程序可能无法工作。您应该通过强制升级该应用程序来修复此情况,使用 `yunohost app upgrade --force {app}`。", + "migration_python_venv_rebuild_in_progress": "现在尝试为 `{app}` 重建 Python 虚拟环境", + "migration_0031_terms_of_services": "此迁移仅仅是关于 YunoHost 项目现在发布与技术和社区服务相关的服务条款的信息性消息。", + "migration_description_0027_migrate_to_bookworm": "将系统升级到 Debian Bookworm 和 YunoHost 12", + "migration_description_0028_delete_legacy_xmpp_permission": "删除旧的 XMPP 权限,Metronome 现在是一个应用程序", + "migration_description_0029_postgresql_13_to_15": "将数据库从 PostgreSQL 13 迁移到 15", + "migration_description_0030_rebuild_python_venv_in_bookworm": "在 Bookworm 迁移后修复 Python 应用", + "migration_description_0031_terms_of_services": "服务条款", + "migration_description_0032_firewall_config": "内部防火墙配置文件迁移", + "migration_description_0033_rework_permission_infos": "重新设计应用权限的存储方式", + "migration_description_0034_fix_missing_admins_aliases": "修复管理员组缺失的邮件别名", + "migration_description_0035_fix_apps_nodejs_version": "修复应用 systemd 配置中的 nodejs 版本", + "migration_ldap_backup_before_migration": "在实际迁移之前,请创建 LDAP 数据库和应用设置的备份。", + "migration_ldap_can_not_backup_before_migration": "迁移失败之前,无法完成系统的备份。错误:{error}", + "migration_ldap_migration_failed_trying_to_rollback": "无法迁移…试图回滚系统。", + "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_no_migrations_to_run": "无需迁移即可运行", + "migrations_no_such_migration": "没有称为 '{id}'的迁移", + "migrations_not_pending_cant_skip": "这些迁移没有待处理,因此不能跳过:{ids}", + "migrations_pending_cant_rerun": "这些迁移仍处于待处理状态,因此无法再次运行:{ids}", + "migrations_running_forward": "正在运行迁移{id}…", + "migrations_skip_migration": "正在跳过迁移{id}…", + "migrations_success_forward": "迁移 {id} 已完成", + "migrations_to_be_ran_manually": "迁移{id}必须手动运行。请转到 webadmin 页面上的工具→迁移,或运行`yunohost tools migrations run`。", + "nftables_unavailable": "您不能在这里使用 nftables。您要么在一个容器中,要么您的内核不支持它", + "noninteractive_task": "非交互式任务", + "not_enough_disk_space": "'{path}'上的可用空间不足", + "operation_interrupted": "该操作是否被手动中断?", + "other_available_options": "… 还有 {n} 个未显示的可用选项", + "password_confirmation_not_the_same": "密码及其确认不匹配", + "password_listed": "该密码是世界上最常用的密码之一。请选择一些更独特的东西。", + "password_too_long": "请选择少于 127 个字符的密码", + "password_too_simple_1": "密码长度至少为 8 个字符", + "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_fullname": "必须是有效的全名(至少 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_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}", + "permission_updated": "权限 '{permission}' 已更新", + "port_already_closed": "端口 {port} 已关闭", + "port_already_opened": "端口 {port} 已打开", + "postinstall_low_rootfsspace": "根文件系统的总空间小于 10 GB,这非常令人担忧!您可能很快就会用完磁盘空间!建议根文件系统至少有 16GB, 如果尽管出现此警告仍要安装 YunoHost,请使用--force-diskspace 重新运行 postinstall", + "pydantic_type_error": "无效类型。", + "pydantic_type_error_none_not_allowed": "值是必需的。", + "pydantic_type_error_str": "无效类型,预期为字符串。", + "pydantic_value_error_color": "不是有效的颜色,值必须是命名颜色或十六进制颜色。", + "pydantic_value_error_const": "意外值;请在 {permitted} 之间选择", + "pydantic_value_error_date": "无效的日期格式", + "pydantic_value_error_email": "值不是有效的电子邮件地址", + "pydantic_value_error_number_not_ge": "值必须大于或等于 {limit_value}。", + "pydantic_value_error_number_not_le": "值必须小于或等于 {limit_value}。", + "pydantic_value_error_str_regex": "无效字符串;值不符合模式 '{pattern}'", + "pydantic_value_error_time": "无效的时间格式", + "pydantic_value_error_url_extra": "URL 无效,在有效 URL 后发现多余字符:'{extra}'", + "pydantic_value_error_url_host": "URL 主机无效", + "pydantic_value_error_url_port": "URL 端口无效,端口不能超过 65535", + "pydantic_value_error_url_scheme": "无效或缺失的 URL 协议", + "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_confirm_yunohost_installed": "您真的要还原已经安装的系统吗? [{answers}]", + "restore_extracting": "正在从存档中提取所需文件…", + "restore_failed": "无法还原系统", + "restore_hook_unavailable": "'{part}'的恢复脚本在您的系统上和归档文件中均不可用", + "restore_may_be_not_enough_disk_space": "您的系统似乎没有足够的空间(可用空间:{free_space} B,所需空间:{needed_space} B,安全系数:{margin} B)", + "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": "root 密码已更改", + "root_password_desynchronized": "管理员密码已更改,但是 YunoHost 无法将此密码传播到 root 密码!", + "server_reboot": "服务器将重新启动", + "server_reboot_confirm": "服务器会立即重启,确定吗? [{answers}]", + "server_shutdown": "服务器将关闭", + "server_shutdown_confirm": "服务器会立即关闭,确定吗?[{answers}]", + "service_add_failed": "无法添加服务 '{service}'", + "service_added": "服务 '{service}'已添加", + "service_already_started": "服务'{service}' 已在运行", + "service_already_stopped": "服务'{service}'已被停止", + "service_cmd_exec_failed": "无法执行命令'{command}'", + "service_description_dnsmasq": "处理域名解析(DNS)", + "service_description_dovecot": "允许电子邮件客户端访问/获取电子邮件(通过 IMAP 和 POP3)", + "service_description_fail2ban": "防止来自互联网的暴力攻击和其他类型的攻击", + "service_description_mysql": "存储应用数据(SQL 数据库)", + "service_description_nftables": "管理打开和关闭服务的连接端口", + "service_description_nginx": "为您的服务器上托管的所有网站提供服务或访问", + "service_description_opendkim": "使用 DKIM 对外发送的电子邮件进行签名,从而降低被标记为垃圾邮件的可能性", + "service_description_postfix": "用于发送和接收电子邮件", + "service_description_postgresql": "存储应用数据(SQL 数据库)", + "service_description_redis-server": "用于快速数据访问,任务队列和程序之间通信的专用数据库", + "service_description_slapd": "存储用户、域名和相关信息", + "service_description_ssh": "允许您通过终端(SSH 协议)远程连接到服务器", + "service_description_yunohost-api": "管理 YunoHost Web 界面与系统之间的交互", + "service_description_yunohost-portal-api": "管理不同门户网页界面与系统之间的交互", + "service_description_yunomdns": "允许您在本地网络中使用 'yunohost.local' 访问您的服务器", + "service_disable_failed": "服务'{service}'在启动时无法启动。", + "service_disabled": "系统启动时,服务 '{service}' 将不再启动。", + "service_enable_failed": "无法使服务 '{service}'在启动时自动启动。", + "service_enabled": "现在,服务'{service}' 将在系统引导过程中自动启动。", + "service_not_reloading_because_conf_broken": "由于配置错误,未重新加载/重启服务 '{name}':{errors}", + "service_reload_failed": "无法重新加载服务'{service}'", + "service_reload_or_restart_failed": "无法重新加载或重新启动服务'{service}'", + "service_reloaded": "服务 '{service}' 已重新加载", + "service_reloaded_or_restarted": "服务'{service}'已重新加载或重新启动", + "service_remove_failed": "无法删除服务'{service}'", + "service_removed": "服务 '{service}' 已删除", + "service_restart_failed": "无法重新启动服务 '{service}'", + "service_restarted": "服务'{service}' 已重新启动", + "service_start_failed": "无法启动服务 '{service}'", + "service_started": "服务 '{service}' 已启动", + "service_stop_failed": "无法停止服务'{service}'", + "service_stopped": "服务'{service}' 已停止", + "service_unknown": "未知服务 '{service}'", + "session_expired": "会话已过期", + "show_tile_cant_be_enabled_for_regex": "您目前无法启用'show_tile',因为权限'{permission}'的 URL 是正则表达式", + "show_tile_cant_be_enabled_for_url_not_defined": "您现在无法启用 'show_tile' ,因为您必须先为权限'{permission}'定义一个 URL", + "ssowat_conf_generated": "SSO 与门户配置已重新生成", + "system_upgraded": "系统升级", + "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}", + "tos_dyndns_acknowledgement": "您选择注册一个 DynDNS 域名,这是 YunoHost 项目提供的服务。考虑到域名是长期数字服务的重要方面,我们提醒您仔细阅读相关服务条款,特别是关于这些免费域名的部分:.", + "tos_postinstall_acknowledgement": "YunoHost 项目是一个志愿者团队,他们共同努力创建一个自由的服务器操作系统,称为 YunoHost。此 YunoHost 软件在 AGPLv3 许可协议下发布()。与此软件相关,项目管理并提供多个技术和社区服务,服务用于各种目的。使用这些服务即表示您同意遵守以下服务条款:.", + "unable_authenticate": "无法验证会话", + "unbackup_app": "{app} 将不会保存", + "unexpected_error": "出乎意料的错误:{error}", + "unknown_error_reading_file": "尝试读取文件 {file} 时发生未知错误:{error}", + "unknown_group": "未知系统群组 '{group}'", + "unknown_main_domain_path": "'{app}'的域或路径未知。您需要指定一个域和一个路径,以便能够指定用于许可的 URL。", + "unknown_user": "未知系统用户 '{user}'", + "unlimit": "没有配额", + "unrestore_app": "{app} 将不会恢复", + "update_apt_cache_failed": "无法更新 APT 的缓存(Debian 的软件包管理器)。这是 sources.list 行的转储,这可能有助于确定有问题的行:\n{sourceslist}", + "update_apt_cache_warning": "更新 APT 缓存(Debian 的软件包管理器)时出了点问题。这是 sources.list 行的转储,这可能有助于确定有问题的行:\n{sourceslist}", + "updating_apt_cache": "正在获取系统软件包的可用升级…", + "upgrading_packages": "正在升级软件包…", + "upnp_dev_not_found": "找不到 UPnP 设备", + "upnp_disabled": "UPnP 已禁用", + "upnp_enabled": "UPnP 已启用", + "upnp_port_open_failed": "无法通过 UPnP 打开端口", + "user_already_exists": "用户'{user}' 已存在", + "user_cannot_delete_last_admin": "用户 '{user}' 是组 'admins' 中的最后一个用户,无法被删除。", + "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_cannot_edit_or_delete_admins": "无法通过导入编辑或删除“{user}”,因为此用户是管理员", + "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_api": "YunoHost API", + "yunohost_configured": "现在已配置 YunoHost", + "yunohost_installing": "正在安装 YunoHost…", + "yunohost_not_installed": "YunoHost 没有正确安装,请运行 'yunohost tools postinstall'", + "yunohost_postinstall_end_tip": "后期安装完成!为了最终完成您的设置,请考虑:\n -通过网络管理员的“诊断”部分(或命令行中的'yunohost diagnosis run')诊断潜在问题;\n -阅读管理文档中的“完成安装设置”和“了解 YunoHost”部分:https://doc.yunohost.org/admin.", + "app_db_prompt_no_app_database": "此应用似乎未在其清单中声明数据库", + "app_db_prompt_type_not_supported": "该命令不支持此类型的数据库:{type}", + "diagnosis_apps_security_issue_error": "应用程序 {app} 当前版本为 '{current_version}',存在一个重大安全问题:{title}。建议尽快升级到版本 '{fixed_in_version}'。更多信息:{more_infos_list}", + "diagnosis_apps_security_issue_warning": "应用程序 {app} 当前版本为 '{current_version}',存在一个中等安全问题:{title}。建议升级到 '{fixed_in_version}'。更多信息:{more_infos_list}", + "diagnosis_mail_blocklist_reason_openresolver": "看起来原因中提到了'开放解析器'。
这通常意味着您的服务器未使用本地 DNS,而是使用了公共的开放解析器。
请检查 /etc/resolv.conf 的内容,其中应包含 nameserver 127.0.0.1
由于此文件通常由系统自动生成,请勿手动编辑。请检查您的 DHCP 设置,或 VPN 设置(如果您使用了 VPN),或者如果您使用了例如 VPS 提供商制作的 Debian 镜像,请查找 cloudinit 配置。
欢迎您通过 YunoHost 支持渠道获取此问题的帮助。
黑名单原因原文为:{reason}", + "diagnosis_package_security_issue_error": "系统软件包 '{package}' 当前版本为 '{current_version}',存在一个重大安全问题:{title}。建议尽快升级到版本 '{fixed_in_version}'。更多信息:{more_infos_list}", + "diagnosis_package_security_issue_warning": "系统软件包 '{package}' 当前版本为 '{current_version}',存在一个中等安全问题:{title}。建议升级到 '{fixed_in_version}'。更多信息:{more_infos_list}", + "global_settings_setting_dns_custom_resolvers_enabled": "使用自定义 DNS 解析器", + "global_settings_setting_dns_custom_resolvers_enabled_help": "默认情况下,YunoHost 使用位于欧洲的可信解析器列表。高级用户可能希望指定自定义解析器。", + "global_settings_setting_dns_custom_resolvers_list": "自定义解析器地址", + "global_settings_setting_dns_custom_resolvers_list_help": "每个使用的 IP 协议(IPv4/IPv6)至少 2 个 DNS 解析器的列表。示例:89.234.141.66 45.67.81.23 2a00:5881:8100:1000::3 2a0c:e300::1337", + "migration_0036_cleaning_up": "正在清理不再有用的缓存和软件包…", + "migration_0036_delayed_api_restart": "YunoHost API 将在 15 秒后自动重启。可能会在几秒钟内不可用,之后您需要重新登录。", + "migration_0036_general_warning": "最后,请注意此迁移是**一项精细操作**。YunoHost 团队已尽力审查和测试,但迁移仍可能导致系统或其应用的部分功能出现问题。\n\n因此,建议:\n - **执行备份**:备份所有关键数据或应用。更多信息请访问 https://doc.yunohost.org/backup;\n - **保持耐心**:启动迁移后,根据您的互联网连接和硬件,可能需要长达一小时才能完成所有升级;\n - **寻求社区帮助**:如果遇到问题需要排查,请在论坛上联系社区。", + "migration_0036_main_upgrade": "正在启动主升级…", + "migration_0036_apt_lists_file_still_exists": "旧文件 '{file}' 仍然存在,但它不应存在。将重命名为 '{file}.legacy_bookworm'。", + "migration_0036_modified_files": "请注意,发现以下文件已被手动修改,升级后可能会被覆盖:", + "migration_0036_not_bullseye": "当前 Debian 发行版不是 Bookworm!如果您已经运行了 Bookworm -> Trixie 迁移,则此错误表明迁移过程未 100% 成功(否则 YunoHost 会将其标记为已完成)。建议与支持团队一起调查情况,他们需要迁移的**完整**日志,可以在 Web 管理界面的工具 > 日志中找到。", + "migration_0036_not_enough_free_space": "/var/ 目录可用空间不足!您应至少有 1GB 空闲空间才能运行此迁移。", + "migration_0036_patch_yunohost_dpkg": "正在对 dpkg 数据库应用补丁以解决冲突问题…", + "migration_0036_patching_sources_list": "正在修补 sources.lists 文件…", + "migration_0036_problematic_apps_warning": "请注意,检测到以下可能存在问题的已安装应用。这些应用似乎并非从 YunoHost 应用目录安装,或未被标记为'正常工作'。因此,无法保证它们在升级后仍能正常工作:", + "migration_0036_start": "正在启动迁移到 Trixie…", + "migration_0036_still_on_bookworm_after_main_upgrade": "主升级过程中出现问题,系统似乎仍处于 Debian Bookworm。", + "migration_0036_system_not_fully_up_to_date": "您的系统并非完全最新。请在运行迁移到 Trixie 之前执行常规升级。", + "migration_0036_yunohost_upgrade": "正在启动 YunoHost 核心升级…", + "migration_description_0036_migrate_to_trixie": "将系统升级到 Debian Trixie 和 YunoHost 13" +} diff --git a/maintenance/agplv3.tpl b/maintenance/agplv3.tpl new file mode 100644 index 0000000..82f3b4c --- /dev/null +++ b/maintenance/agplv3.tpl @@ -0,0 +1,16 @@ +Copyright (c) ${years} ${owner} + +This file is part of ${projectname} (see ${projecturl}) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/maintenance/autofix_locale_format.py b/maintenance/autofix_locale_format.py new file mode 100755 index 0000000..d5e2aee --- /dev/null +++ b/maintenance/autofix_locale_format.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import json +import re +import sys +import textwrap +from collections import OrderedDict +from pathlib import Path + +Locale = dict[str, str] + + +def autofix_i18n_placeholders( + reference: Locale, locale: Locale, reference_filename: str, filename: str +) -> tuple[bool, Locale]: + """ + This tries for magically fix mismatch between en.json format and other.json format + e.g. an i18n string with: + source: "Lorem ipsum {some_var}" + fr: "Lorem ipsum {une_variable}" + (ie the keyword in {} was translated but shouldnt have been) + """ + fatal_errors = False + + # We iterate over all keys/string in en.json + for key, string in reference.items(): + # Ignore check if there's no translation yet for this key + if key not in locale: + continue + + # Then we check that every "{stuff}" (for python's .format()) + # should also be in the translated string, otherwise the .format + # will trigger an exception! + subkeys_in_ref = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)] + subkeys_in_this_locale = [ + k[0] for k in re.findall(r"{(\w+)(:\w)?}", locale[key]) + ] + + if set(subkeys_in_ref) != set(subkeys_in_this_locale) and ( + len(subkeys_in_ref) == len(subkeys_in_this_locale) + ): + for i, subkey in enumerate(subkeys_in_ref): + locale[key] = locale[key].replace( + "{%s}" % subkeys_in_this_locale[i], "{%s}" % subkey + ) + + # Validate that now it's okay ? + subkeys_in_ref = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)] + subkeys_in_this_locale = [ + k[0] for k in re.findall(r"{(\w+)(:\w)?}", locale[key]) + ] + if any(k not in subkeys_in_ref for k in subkeys_in_this_locale): + errmsg = textwrap.dedent(f"""\ + ========================== + Format inconsistency for string {key} in {filename}: + {reference_filename} -> {string.encode("utf-8")} + {filename} -> {locale[key].encode("utf-8")} + Please fix it manually ! + """) + print(errmsg) + fatal_errors = True + + return fatal_errors, locale + + +def autofix_orthotypography_and_standardized_words( + locale: Locale, filename: str +) -> Locale: + godamn_spaces_of_hell = [ + "\u00a0", + "\u2000", + "\u2001", + "\u2002", + "\u2003", + "\u2004", + "\u2005", + "\u2006", + "\u2007", + "\u2008", + "\u2009", + "\u200a", + # "\u202f", + # "\u202F", + "\u3000", + ] + transformations_space = {s: " " for s in godamn_spaces_of_hell} + + transformations_misc = { + r"\.\.\.": "…", + "https ://": "https://", + } + + transformations_fr = { + "courriel": "email", + "e-mail": "email", + "Courriel": "Email", + "E-mail": "Email", + "« ": "'", + "«": "'", + " »": "'", + "»": "'", + "’": "'", + # r"$(\w{1,2})'|( \w{1,2})'": r"\1\2’", + } + + match filename: + case "en.json": + transformations = transformations_space | transformations_misc + case "fr.json": + transformations = ( + transformations_space | transformations_misc | transformations_fr + ) + case _: + transformations = {} + + for pattern, replace in transformations.items(): + for key, value in locale.items(): + locale[key] = re.sub(pattern, replace, value) + return locale + + +def remove_stale_translated_strings(reference: Locale, locale: Locale) -> Locale: + return {k: v for k, v in locale.items() if k in reference} + + +def sort_locale(locale: Locale) -> Locale: + return dict(sorted(locale.items())) + + +def main() -> None: + project_dir: Path = Path(__file__).resolve().parent.parent + locale_dir = project_dir / "locales" + + reference_file = locale_dir / "en.json" + locale_files = list(locale_dir.glob("*.json")) + locale_files.remove(reference_file) + + reference = json.load(reference_file.open()) + fatal_errors = [] + + for file in locale_files: + locale = json.load(file.open(), object_pairs_hook=OrderedDict) + + locale = autofix_orthotypography_and_standardized_words(locale, file.name) + locale = remove_stale_translated_strings(reference, locale) + errors, locale = autofix_i18n_placeholders( + reference, locale, reference_file.name, file.name + ) + if errors: + fatal_errors.append(file.name) + + # locale = sort_locale(locale) + + with file.open("w") as locale_io: + json.dump( + locale, + locale_io, + indent=4, + ensure_ascii=False, + ) + locale_io.write("\n") + + if fatal_errors: + print(f"Errors found in files: {', '.join(fatal_errors)}.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/maintenance/make_changelog.sh b/maintenance/make_changelog.sh new file mode 100755 index 0000000..1a78d8a --- /dev/null +++ b/maintenance/make_changelog.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +set -eu + +function increment_version() { + local version=$1 + local incr_type=$2 + + local major=$(awk -F. '{print $1}' <<< "$version") + local medium=$(awk -F. '{print $2}' <<< "$version") + local minor=$(awk -F. '{print $3}' <<< "$version") + local patch=$(awk -F. '{print $4}' <<< "$version") + patch=${patch:-0} + + if [[ "$incr_type" == "patch" ]] + then + echo "$major.$medium.$minor.$((patch+1))" + elif [[ "$incr_type" == "minor" ]] + then + echo "$major.$medium.$((minor+1))" + elif [[ "$incr_type" == "medium" ]] + then + echo "$major.$((medium+1)).0" + else + echo "Unhandled version increment type '$incr_type', should be either 'patch', 'minor' or 'medium'" >&2 + exit 1 + fi +} + + +RELEASE="stable" +ME=$(git config --get user.name) +EMAIL=$(git config --get user.email) + +REPO=$(head -n1 debian/changelog | awk '{print $1}') +CURRENT_VERSION=$(head -n1 debian/changelog | awk '{print $2}' | tr -d '()') +CURRENT_RELEASE_TYPE=$(head -n1 debian/changelog | awk '{print $3}' | tr -d ';') + +INCR_VERSION_TYPE="${1:-}" +if [[ -n "$INCR_VERSION_TYPE" ]] +then + NEW_VERSION="$(increment_version "$CURRENT_VERSION" "$INCR_VERSION_TYPE")" +else + NEW_VERSION="x.y.z" +fi + +RELEASE_TYPE="${2:-}" +if [[ -n "$RELEASE_TYPE" ]] +then + [[ $RELEASE_TYPE == "stable" ]] || [[ $RELEASE_TYPE == "testing" ]] || ( echo "Release type should be either 'stable' or 'testing'" >&2; exit 1; ) + NEW_RELEASE_TYPE=$RELEASE_TYPE +else + NEW_VERSION_TYPE=$CURRENT_RELEASE_TYPE +fi + + + +echo "$REPO ($NEW_VERSION) $CURRENT_RELEASE_TYPE; urgency=low" +echo "" + +PREVIOUS_TAG="debian/$CURRENT_VERSION" +COMMITS=$(git log "$PREVIOUS_TAG".. -n 10000 --first-parent --pretty=tformat:'%h') +for COMMIT in $COMMITS +do + SUBJECT="$(git show -s "$COMMIT" --pretty="%s")" + # "Regular" PRs merge commit + if grep -q "^Merge pull request #" <<< "$SUBJECT" + then + PR_LINK=$(sed -E "s@Merge .*#([0-9]+).*\$@[#\1]\(http://github.com/YunoHost/$REPO/pull/\1\)@g" <<< "$SUBJECT") + BODY="$(git show -s "$COMMIT" --pretty="%b")" + echo " - $BODY ($PR_LINK)" + # PRs merged via stash + elif grep -q " (#[0-9]*)$" <<< "$SUBJECT" + then + SUBJECT=$(sed -E "s@(.*) \(#([0-9]*)\)\$@\1 ([#\2]\(http://github.com/YunoHost/$REPO/pull/\2\))@g" <<< "$SUBJECT") + echo " - $SUBJECT" + # Other "direct" commits + else + echo " - $SUBJECT ($COMMIT)" + fi +done \ +| sed -E "/Co-authored-by: .* <.*>/d" \ +| grep -v "Translations update from Weblate" \ +| grep -v "Translated using Weblate" \ +| grep -v ":art: Format Python code" \ +| tac + +TRANSLATIONS=$(git log "$PREVIOUS_TAG"... -n 10000 --pretty=format:"%s" \ + | grep "Translated using Weblate" \ + | sed -E "s/Translated using Weblate \((.*)\)/\1/g" \ + | sort | uniq | tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g') +[[ -z "$TRANSLATIONS" ]] || echo " - i18n: Translations updated for $TRANSLATIONS" + +echo "" +CONTRIBUTORS=$(git log -n10 --pretty=format:'%Cred%h%Creset %C(bold blue)(%an) %Creset%Cgreen(%cr)%Creset - %s %C(yellow)%d%Creset' --abbrev-commit "$PREVIOUS_TAG"... -n 10000 --pretty=format:"%an" \ + | sort | uniq | grep -v "$ME" | grep -vi 'yunohost-bot\|YunoHost bot\|weblate' \ + | tr '\n' ', ' | sed -e 's/,$//g' -e 's/,/, /g') +[[ -z "$CONTRIBUTORS" ]] || echo " Thanks to all contributors <3 ! ($CONTRIBUTORS)" +echo "" +echo " -- $ME <$EMAIL> $(date -R)" +echo "" + +echo "====================================" +echo "To complete the release" +echo "====================================" +[[ -n "$INCR_VERSION_TYPE" ]] || \ + cat << EOF +- Fix the version number (or call this script with 'patch', 'minor' or 'medium' as first argument) + - 'patch' is meant for shameful bugfixes like typo breaking everything, need fix ASAP + - 'minor' for regular iterations on YunoHost with small features, minor fixes/improvements + - 'medium' typically when releasing a bunch of important, major-ish changes (not counting Debian versions which is the first number) +EOF +[[ -n "$RELEASE_TYPE" ]] || \ + echo "- Confirm that this is still a '$NEW_VERSION_TYPE' release (you can also specify 'stable' or 'testing' as second arg to this command)" + +cat << EOF +- Copypasta this new changelog to the top of debian/changelog, beware of formatting, empty line, leading/trailing spaces... +- Re-read carefully the changelog and smooth the messages to: + - cleanup bumpy syntax/formatting + - each line give a pretty good idea of what this is about without opening the commit/PR... Ideally at least prefix them with the general topic (eg 'apps:', 'dns:', 'nginx:', ...) + - possibly trim stuff that are way too technical or irrelevant (typo fixes, syntax updates from bot, purely test/quality fixes, ...) +- Conclude with: + NEW_VERSION="$NEW_VERSION" + git commit debian/changelog -m "Update changelog for \$NEW_VERSION" + git tag debian/\$NEW_VERSION + git push origin $(git branch --show-current) --tags +- Connect to the infra' 'repo' machine, in vinaigrette directory +- Edit and run the 'release' script +EOF + +# PR links can be converted to regular texts using : sed -E 's@\[(#[0-9]*)\]\([^ )]*\)@\1@g' +# Or readded with sed -E 's@#([0-9]*)@[YunoHost#\1](https://github.com/yunohost/yunohost/pull/\1)@g' | sed -E 's@\((\w+)\)@([YunoHost/\1](https://github.com/yunohost/yunohost/commit/\1))@g' diff --git a/maintenance/missing_i18n_keys.py b/maintenance/missing_i18n_keys.py new file mode 100755 index 0000000..05278f3 --- /dev/null +++ b/maintenance/missing_i18n_keys.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import argparse +import json +import re +import sys +import tomllib +from pathlib import Path +from typing import Generator + +import yaml + +############################################################################### +# Find used keys in python code # +############################################################################### + + +def find_expected_string_keys(project: Path) -> Generator[str, None, None]: + # Try to find : + # m18n.n( "foo" + # YunohostError("foo" + # YunohostValidationError("foo" + # # i18n: foo + regex_m18n = re.compile(r"m18n\.n\(\n*\s*[\"\'](\w+)[\"\']") + regex_ynherr = re.compile(r"YunohostError\(\n*\s*[\'\"](\w+)[\'\"]") + regex_ynhxerr = re.compile( + r"Yunohost(?:Validation|Authentication)Error\(\n*\s*[\'\"](\w+)[\'\"]" + ) + regex_comment = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?") + + srcdir = project / "src" + python_files: list[Path] = [ + *srcdir.rglob("*.py"), + *srcdir.rglob("*.py.disabled"), + project / "bin" / "yunohost", + ] + + for file in python_files: + content = file.read_text() + for regex in [regex_m18n, regex_ynherr, regex_ynhxerr, regex_comment]: + for match in regex.findall(content): + if not match.endswith("_"): + yield match + + # For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries) + # Also we expect to have "diagnosis_description_" for each diagnosis + regex_diagnosis = re.compile(r"[\"\'](diagnosis_[a-z]+_\w+)[\"\']") + + diagnoser_files: list[Path] = [ + *(srcdir / "diagnosers").glob("*.py"), + *(srcdir / "diagnosers").glob("*.py.disabled"), + ] + for file in diagnoser_files: + if file.name == "__init__.py": + continue + content = file.read_text() + for match in regex_diagnosis.findall(content): + if match.endswith("_"): + # Ignore some name fragments which are actually concatenated with other stuff.. + continue + yield match + + name = file.name.removesuffix(".disabled").removesuffix(".py") + yield f"diagnosis_description_{name.split('-')[-1]}" + + # For each migration, expect to find "migration_description_" + migration_files: list[Path] = [ + *(srcdir / "migrations").glob("0*.py"), + *(srcdir / "migrations").glob("0*.py.disabled"), + ] + for file in migration_files: + name = file.name.removesuffix(".disabled").removesuffix(".py") + yield f"migration_description_{name}" + + # For each default service, expect to find "service_description_" + services_yml = project / "conf" / "yunohost" / "services.yml" + for service, info in yaml.safe_load(services_yml.open("r")).items(): + if info is None: + continue + yield f"service_description_{service}" + + # For all unit operations, expect to find "log_" + # A unit operation is created either using the @is_unit_operation decorator + # or using OperationLogger( + for file in python_files: + lines = iter(file.read_text().splitlines()) + for line in lines: + if line.startswith("@is_unit_operation(") and "flash=True" not in line: + line = next(lines) + funcname = line.removeprefix("def ").split("(")[0] + yield f"log_{funcname}" + + regex_logger = re.compile(r"OperationLogger\(\n*\s*[\"\'](\w+)[\"\']") + for python_file in python_files: + content = open(python_file).read() + for match in regex_logger.findall(content): + yield f"log_{match}" + + # Keys for the actionmap ... + actionsmap_yml = project / "share" / "actionsmap.yml" + for category in yaml.safe_load(actionsmap_yml.open("r")).values(): + if "actions" not in category.keys(): + continue + for action in category["actions"].values(): + if "arguments" not in action.keys(): + continue + for argument in action["arguments"].values(): + extra = argument.get("extra") + if not extra: + continue + if "password" in extra: + yield extra["password"] + if "ask" in extra: + yield extra["ask"] + if "comment" in extra: + yield extra["comment"] + if "pattern" in extra: + yield extra["pattern"][1] + if "help" in extra: + yield extra["help"] + + # Hardcoded expected keys ... + yield "admin_password" # Not sure that's actually used nowadays... + + for method in ["tar", "copy", "custom"]: + yield "backup_applying_method_%s" % method + yield "backup_method_%s_finished" % method + + registrar_list = project / "share" / "registrar_list.toml" + registrars = tomllib.load(registrar_list.open("rb")) + supported_registrars = ["ovh", "gandi", "godaddy"] + for registrar in supported_registrars: + for key in registrars[registrar].keys(): + yield f"domain_config_{key}" + + # Domain config panel + domain_settings_with_help_key = [ + "portal_logo", + "portal_public_intro", + "portal_theme", + "portal_user_intro", + "search_engine", + "custom_css", + "dns", + "enable_public_apps_page", + ] + domain_section_with_no_name = ["app", "cert_", "mail", "registrar"] + config_domain_toml = project / "share" / "config_domain.toml" + for panel_key, panel in tomllib.load(config_domain_toml.open("rb")).items(): + if not isinstance(panel, dict): + continue + yield f"domain_config_{panel_key}_name" + for section_key, section in panel.items(): + if not isinstance(section, dict): + continue + if section_key not in domain_section_with_no_name: + yield f"domain_config_{section_key}_name" + for key, values in section.items(): + 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" + + # App config panel + app_settings_with_help_key = [ + "logo", + "description", + "force_upgrade", + ] + config_app_toml = project / "share" / "config_app.toml" + for panel_key, panel in tomllib.load(config_app_toml.open("rb")).items(): + if not isinstance(panel, dict): + continue + yield f"app_config_{panel_key}_name" + for section_key, section in panel.items(): + if not isinstance(section, dict): + continue + if section_key != "permissions": + yield f"app_config_{section_key}_name" + for key, values in section.items(): + if not isinstance(values, dict) or values.get("visible") is False: + continue + if section_key == "permissions": + key_ = f"permission_{key}" + else: + key_ = key + yield f"app_config_{key_}" + if key in app_settings_with_help_key: + yield f"app_config_{key_}_help" + + # Global settings + # Boring hard-coding because there's no simple other way idk + settings_without_help_key = [ + "passwordless_sudo", + "smtp_relay_host", + "smtp_relay_password", + "smtp_relay_port", + "smtp_relay_user", + "ssowat_panel_overlay_enabled", + "root_password", + "root_access_explain", + "root_password_confirm", + "tls_passthrough_explain", + "allow_edit_email", + "allow_edit_email_alias", + "allow_edit_email_forward", + ] + + config_global_toml = project / "share" / "config_global.toml" + for panel_key, panel in tomllib.load(config_global_toml.open("rb")).items(): + if not isinstance(panel, dict): + continue + yield f"global_settings_setting_{panel_key}_name" + for section_key, section in panel.items(): + if not isinstance(section, dict): + continue + yield f"global_settings_setting_{section_key}_name" + for key, values in section.items(): + if not isinstance(values, dict): + continue + yield f"global_settings_setting_{key}" + if key not in settings_without_help_key: + yield f"global_settings_setting_{key}_help" + + +############################################################################### +# Compare keys used and keys defined # +############################################################################### + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("mode", type=str, choices=["check", "fix"]) + parser.add_argument("--path", type=Path, help="Path to the project") + args = parser.parse_args() + + project_dir: Path = args.path or Path(__file__).resolve().parent.parent + locale_dir = project_dir / "locales" + reference_file = locale_dir / "en.json" + + expected_string_keys = set(find_expected_string_keys(project_dir)) + keys_defined_for_en = json.load(reference_file.open("r")).keys() + keys_defined = set(keys_defined_for_en) + + unused_keys = keys_defined.difference(expected_string_keys) + unused_keys = sorted(unused_keys) + + undefined_keys = expected_string_keys.difference(keys_defined) + undefined_keys = sorted(undefined_keys) + + if args.mode == "check": + # Unused keys are not too problematic, will be automatically + # removed by the other autoreformat script, + # but still informative to display them + if unused_keys: + print("Those i18n keys appears unused:") + for key in unused_keys: + print(f" - {key}") + if undefined_keys: + print("Those i18n keys should be defined in en.json:") + for key in undefined_keys: + print(f" - {key}") + sys.exit(1) + + if args.mode == "fix": + data = json.load(reference_file.open("r")) + for key in undefined_keys: + data[key] = "FIXME" + for key in unused_keys: + del data[key] + + with reference_file.open("w") as reference: + json.dump( + data, + reference, + indent=4, + ensure_ascii=False, + sort_keys=True, + ) + reference.write("\n") + + +if __name__ == "__main__": + main() diff --git a/maintenance/shfmt.sh b/maintenance/shfmt.sh new file mode 100755 index 0000000..f41407d --- /dev/null +++ b/maintenance/shfmt.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +shfmt_args=( + -i=4 + -kp # keep column alignment paddings + -sr # redirect operators will be followed by a space + -bn # binary ops like && and | may start a line + -ci # switch cases will be indented +) + +shfmt "${shfmt_args[@]}" "$@" diff --git a/maintenance/update_copyright_headers.sh b/maintenance/update_copyright_headers.sh new file mode 100755 index 0000000..21096dc --- /dev/null +++ b/maintenance/update_copyright_headers.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# To run this you'll need to: +# +# pip3 install licenseheaders + +licenseheaders \ + -o "YunoHost Contributors" \ + -n "YunoHost" \ + -u "https://yunohost.org" \ + -t ./agplv3.tpl \ + --current-year \ + -f ../src/*.py ../src/{utils,diagnosers,authenticators}/*.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5b354ee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,154 @@ +[project] +name = "yunohost" +description = "An operating system aiming to simplify as much as possible the administration of a server" +readme = "README.md" +dynamic = ["version"] +authors = [ + { name = "YunoHost", email = "yunohost@yunohost.org" } +] + +requires-python = ">=3.11" + +# These dependencies might not be up to date, please see debian/control instead +dependencies = [ + "moulinette @ git+https://github.com/yunohost/moulinette@dev", + "bottle", + "cryptography", + # "dbus-python", + "python-debian", + "dnspython", + "email-validator", + "jinja2", + "pyjwt>=1.7,<2.0", + "lexicon", + "miniupnpc", + "packaging", + "passlib", + "psutil", + "publicsuffix2", + "pydantic>=1.0,<2.0", + "pyopenssl", + # "python-ldap", + "python-magic", + "pyyaml", + "requests", + "sdbus", + "sortedcollections", + "toml", + "zeroconf", + "zmq", +] + +[dependency-groups] +tests = [ + "mypy", + "types-gevent", + "types-passlib", + "types-psutil", + "types-pyOpenSSL", + "types-pyYAML", + "types-requests", + "types-toml", + "pytest", + "mock", +] + +[tool.mypy] +follow_imports = "skip" +exclude = [ + "acme_tiny", + "migrations", + "tests" +] + +[[tool.mypy.overrides]] +# We don't provide (yet) typing in moulinette +module = "moulinette.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +# Those are the public, external modules that don't provide typing +module = ["jwt", "sortedcollections", "IPython", "ldap.*", "dbus", "sdbus.*", + "psutil", "bottle", "miniupnpc", "lexicon.*", "publicsuffix2"] +ignore_missing_imports = true + + +[tool.isort] +profile = "black" + +[tool.black] + +[tool.ty] +environment.root = ["."] + + +[tool.pytest.ini_options] +# for --cov-config see https://github.com/nedbat/coveragepy/issues/512 +addopts = "-v --cov-config=pyproject.toml" +norecursedirs = ["dist", "doc", "build", ".tox", ".eggs"] +testpaths = ["tests/"] +markers = [ + "with_system_archive_from_3p8", + "with_backup_recommended_app_installed", + "clean_opt_dir", + "with_wordpress_archive_from_3p8", + "with_legacy_app_installed", + "with_backup_recommended_app_installed_with_ynh_restore", + "with_permission_app_installed", + "other_domains", + "with_custom_domain", +] +filterwarnings = [ + "ignore::urllib3.exceptions.InsecureRequestWarning", +] + + +[tool.coverage.run] +relative_files = true +branch = true +source = ["src/"] + +[tool.coverage.report] +omit = [ + "src/vendor/*", + "/usr/lib/moulinette/yunohost/*", + "/usr/lib/python3/dist-packages/yunohost/vendor/*", +] + + +[tool.tox] +env_list = ["py311-lint", "py311-invalidcode", "py311-black-check", "py311-black-run"] + +[tool.tox.env_run_base] +skip_install = true +allowlist_externals = ["black"] + +[tool.tox.env.py311-lint] +deps = ["flake8"] +commands = [[ + "flake8", "src", "doc", "maintenance", + "--ignore", "E402,E501,E203,W503,E741", + "--exclude", "src/vendor,tests" +]] + +[tool.tox.env.py311-invalidcode] +deps = ["flake8"] +commands = [[ + "flake8", "src", "bin", "maintenance", + "--select", "F,E722,W605", + "--exclude", "src/vendor,tests" +]] + +[tool.tox.env.py311-mypy] +skip_install = false +deps = ["mypy>=1.17"] +dependency_groups = ["tests"] +commands = [["mypy", "--install-types", "--non-interactive", "src/",]] + +[tool.tox.env.py311-black-check] +deps = ["flake8"] +commands = [["black", "bin", "src", "doc", "maintenance", "tests", "--check", "--diff"]] + +[tool.tox.env.py311-black-run] +deps = ["black"] +commands = [["black", "bin", "src", "doc", "maintenance", "tests"]] diff --git a/share/100000-most-used-passwords-length8plus.txt.gz b/share/100000-most-used-passwords-length8plus.txt.gz new file mode 100644 index 0000000..6059a5a Binary files /dev/null and b/share/100000-most-used-passwords-length8plus.txt.gz differ diff --git a/share/actionsmap-portal.yml b/share/actionsmap-portal.yml new file mode 100644 index 0000000..343150f --- /dev/null +++ b/share/actionsmap-portal.yml @@ -0,0 +1,96 @@ +_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" + --mail: + help: Main email + extra: + pattern: &pattern_email + - !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ + - "pattern_email" + --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 + --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 new file mode 100755 index 0000000..4664aa0 --- /dev/null +++ b/share/actionsmap.yml @@ -0,0 +1,2412 @@ +########################################################################## +# Category/actions/arguments file +# +# +# Except for general_arguments, this file contains 3 levels +# as in this sample command line: +# +# yunohost monitor info --cpu --ram +# ^ ^ ^ ^ +# (script) | category | action | parameters +# +# +# Above example will lead to the function 'monitor_info(args)' +# in the file 'yunohost_monitor.py' with 'cpu' and 'ram' +# stored in an 'args' dictionnary. +# +# Usage: +# You can add a category at the first level, action at the second one, +# and arguments at the third one. +# If a connexion is needed for the action, don't forget to add it to +# the action parameters (ldap, repo, dns or firewall). +# +# Documentation: +# You can see all arguments settings at the argparse documentation: +# http://docs.python.org/dev/library/argparse.html +# #argparse.ArgumentParser.add_argument +# +# Don't forget to turn argument yaml style (setting: value) +# +########################################################################## + +############################# +# Global parameters # +############################# +_global: + namespace: yunohost + authentication: + api: ldap_admin + cli: null + +############################# +# User # +############################# +user: + category_help: Manage users and groups + actions: + + ### user_list() + list: + action_help: List users + api: GET /users + arguments: + --fields: + help: fields to fetch (username, fullname, mail, mail-alias, mail-forward, mailbox-quota, groups, shell, home-path) + nargs: "+" + choices: + - username + - fullname + - mail + - mail-alias + - mail-forward + - mailbox-quota + - groups + - shell + - home-path + + ### user_create() + create: + action_help: Create user + api: POST /users + arguments: + username: + help: The unique username to create + extra: + pattern: &pattern_username + - !!str ^[a-z0-9][-a-z0-9_\.]*$ + - "pattern_username" + -F: + full: --fullname + help: The full name of the user. For example 'Camille Dupont' + extra: + ask: ask_fullname + required: True + pattern: &pattern_fullname + - !!str ^([^\W_]{1,30}[ ,.'-]{0,3})+$ + - "pattern_fullname" + -p: + full: --password + help: User password + extra: + password: ask_password + required: True + pattern: &pattern_password + - !!str ^.{3,}$ + - "pattern_password" + comment: good_practices_about_user_password + -d: + full: --domain + help: Domain for the email address + extra: + pattern: &pattern_domain + - !!str ^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ + - "pattern_domain" + autocomplete: &domains_list + ynh_selector: domain list + jq_selector: '.domains[]' + use_cache: false + -q: + full: --mailbox-quota + help: Mailbox size quota + default: "0" + metavar: "{SIZE|0}" + extra: + pattern: &pattern_mailbox_quota + - !!str ^(\d+[bkMGT])|0$ + - "pattern_mailbox_quota" + -s: + full: --loginShell + help: The login shell used + default: "/bin/bash" + + + ### user_delete() + delete: + action_help: Delete user + api: DELETE /users/ + arguments: + username: + help: Username to delete + extra: + pattern: *pattern_username + autocomplete: &users_list + ynh_selector: user list --fields username + jq_selector: '.users[].username' + use_cache: false + --purge: + help: Purge user's home and mail directories + action: store_true + --force: + help: Force user deletion + action: store_true + + ### user_update() + update: + action_help: Update user informations + api: PUT /users/ + arguments: + username: + help: Username to update + extra: + autocomplete: *users_list + -F: + full: --fullname + help: The full name of the user. For example 'Camille Dupont' + extra: + pattern: *pattern_fullname + -m: + full: --mail + extra: + pattern: &pattern_email + - !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ + - "pattern_email" + -p: + full: --change-password + help: New password to set + metavar: PASSWORD + nargs: "?" + const: 0 + extra: + pattern: *pattern_password + comment: good_practices_about_user_password + --add-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" + --remove-mailforward: + help: Mailforward addresses to remove + nargs: "*" + metavar: MAIL + --add-mailalias: + help: Mail aliases to add + nargs: "*" + metavar: MAIL + extra: + pattern: *pattern_email + --remove-mailalias: + help: Mail aliases to remove + nargs: "*" + metavar: MAIL + -q: + full: --mailbox-quota + help: Mailbox size quota + metavar: "{SIZE|0}" + extra: + pattern: *pattern_mailbox_quota + -s: + full: --loginShell + help: The login shell used + default: "/bin/bash" + + ### user_info() + info: + action_help: Get user information + api: GET /users/ + arguments: + username: + help: Username or email to get information + extra: + autocomplete: *users_list + + ### user_export() + export: + action_help: Export users into CSV + api: GET /users/export + + ### user_import() + import: + action_help: Import several users from CSV + api: POST /users/import + arguments: + csvfile: + help: "CSV file with columns username, firstname, lastname, password, mail, mailbox-quota, mail-alias, mail-forward, groups (separated by coma)" + type: open + extra: + autocomplete: + zsh_completion: _files + -u: + full: --update + help: Update all existing users contained in the CSV file (by default existing users are ignored) + action: store_true + -d: + full: --delete + help: Delete all existing users that are not contained in the CSV file (by default existing users are kept) + action: store_true + + subcategories: + group: + subcategory_help: Manage user groups + actions: + ### user_group_list() + list: + action_help: List existing groups + api: GET /users/groups + arguments: + -f: + full: --full + help: Display all informations known about each groups + action: store_true + -p: + full: --include-primary-groups + help: Also display primary groups (each user has an eponym group that only contains itself) + action: store_true + default: false + + ### user_group_create() + create: + action_help: Create group + api: POST /users/groups + arguments: + groupname: + help: Name of the group to be created + extra: + pattern: &pattern_groupname + - !!str ^[a-z0-9][-a-z0-9_\.]*$ + - "pattern_groupname" + + ### user_group_delete() + delete: + action_help: Delete group + api: DELETE /users/groups/ + arguments: + groupname: + help: Name of the group to be deleted + extra: + pattern: *pattern_groupname + autocomplete: &user_groups_list + ynh_selector: user group list -s + jq_selector: '.groups[]' + use_cache: false + + ### user_group_info() + info: + action_help: Get information about a specific group + api: GET /users/groups/ + arguments: + groupname: + help: Name of the group to fetch info about + extra: + pattern: *pattern_groupname + autocomplete: *user_groups_list + + ### user_group_add() + add: + action_help: Add users to group + api: PUT /users/groups//add/ + arguments: + groupname: + help: Name of the group to add user(s) to + extra: + pattern: *pattern_groupname + autocomplete: *user_groups_list + usernames: + help: User(s) to add in the group + nargs: "*" + metavar: USERNAME + extra: + pattern: *pattern_username + autocomplete: *users_list + + ### user_group_remove() + remove: + action_help: Remove users from group + api: PUT /users/groups//remove/ + arguments: + groupname: + help: Name of the group to remove user(s) from + extra: + pattern: *pattern_groupname + autocomplete: *user_groups_list + usernames: + help: User(s) to remove from the group + nargs: "*" + metavar: USERNAME + extra: + pattern: *pattern_username + autocomplete: *users_list + + ### user_group_add_mailalias() + add-mailalias: + action_help: Add mail aliases to group + api: PUT /users/groups//aliases/ + arguments: + groupname: + help: Name of the group to add user(s) to + extra: + pattern: *pattern_groupname + autocomplete: *user_groups_list + aliases: + help: Mail aliases to add + nargs: "+" + metavar: MAIL + extra: + pattern: *pattern_email + --force: + help: Ignore warnings about special groups + action: store_true + + ### user_group_remove_mailalias() + remove-mailalias: + action_help: Remove mail aliases to group + api: DELETE /users/groups//aliases/ + arguments: + groupname: + help: Name of the group to add user(s) to + extra: + pattern: *pattern_groupname + autocomplete: *user_groups_list + aliases: + help: Mail aliases to remove + nargs: "+" + metavar: MAIL + --force: + help: Ignore warnings about special groups + action: store_true + + permission: + subcategory_help: Manage permissions + actions: + + ### user_permission_list() + list: + action_help: List permissions and corresponding accesses + api: GET /users/permissions + arguments: + apps: + help: Apps to list permission for (all by default) + nargs: "*" + extra: + autocomplete: &apps_list + ynh_selector: app list + jq_selector: '.apps[].id' + use_cache: false + -f: + full: --full + help: Display all info known about each permission, including the full user list of each group it is granted to. + action: store_true + + ### user_permission_info() + info: + action_help: Get information about a specific permission + api: GET /users/permissions/ + arguments: + permission: + help: Name of the permission to fetch info about (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: &permissions_list + ynh_selector: user permission list + jq_selector: '.permissions | keys[]' + use_cache: false + + ### user_permission_update() + update: + action_help: Manage group or user permissions + api: PUT /users/permissions/ + arguments: + permission: + help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: *permissions_list + -l: + full: --label + help: Custom label for this app / permission + -s: + full: --show_tile + help: Define if a tile will be shown in the user portal + choices: + - 'True' + - 'False' + -L: + full: --logo + help: File to use as logo for this app / permission. Only PNG are supported. + type: argparse.FileType('rb') + -d: + full: --description + help: Custom description for this app / permission + -o: + full: --order + help: Order number to be used when displaying the tiles in the user portal. Default is 100 so set this to any low value for the tile to appear first, or higher value to appear last. + type: int + -H: + full: --hide_from_public + help: Mark the tile as to be hidden from the 'public app list' (if enabled). Useful for apps such as Nextcloud that need to be exposed to be publicly exposed for desktop/mobile client to be able to connect to, but not meant to be listed for visitors. + choices: + - "True" + - "False" + + ## user_permission_add() + add: + action_help: Grant permission to group or user + api: PUT /users/permissions//add/ + arguments: + permission: + help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: *permissions_list + names: + help: Group or usernames to grant this permission to + nargs: "*" + metavar: GROUP_OR_USER + extra: + pattern: *pattern_username + autocomplete: &user_and_groups_list + ynh_selector: user group list + jq_selector: '[(.groups | keys), ([.groups[] | select(.members).members[]] | unique)] | add[]' + use_cache: false + + ## user_permission_remove() + remove: + action_help: Revoke permission to group or user + api: PUT /users/permissions//remove/ + arguments: + permission: + help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: *permissions_list + names: + help: Group or usernames to revoke this permission to + nargs: "*" + metavar: GROUP_OR_USER + extra: + pattern: *pattern_username + autocomplete: *user_and_groups_list + + ## user_permission_ldapsync() + ldapsync: + action_help: Resynchronize permissions to LDAP from app settings. This is a purely technical command, only meant to be ran if you manually modified permission settings in app, which is absolutely not recommended. + + ssh: + subcategory_help: Manage ssh access + actions: + + ### user_ssh_keys_list() + list-keys: + action_help: Show user's authorized ssh keys + api: GET /users/ssh/keys + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + autocomplete: *users_list + + ### user_ssh_keys_add() + add-key: + action_help: Add a new authorized ssh key for this user + api: POST /users/ssh/key + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + autocomplete: *users_list + key: + help: The key to be added + -c: + full: --comment + help: Optional comment about the key + + ### user_ssh_keys_remove() + remove-key: + action_help: Remove an authorized ssh key for this user + api: DELETE /users/ssh/key + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + autocomplete: *users_list + key: + help: The key to be removed + +############################# +# Domain # +############################# +domain: + category_help: Manage domains + actions: + + ### domain_list() + list: + action_help: List domains + api: GET /domains + arguments: + --exclude-subdomains: + help: Filter out domains that are obviously subdomains of other declared domains + action: store_true + --tree: + help: Display domains as a tree + action: store_true + --features: + help: List only domains with features enabled (mail_in, mail_out) + nargs: "*" + + ### domain_info() + info: + action_help: Get domain aggredated data + api: GET /domains/ + arguments: + domain: + help: Domain to check + extra: + pattern: *pattern_domain + autocomplete: *domains_list + + ### domain_add() + add: + action_help: Create a custom domain + api: POST /domains + arguments: + domain: + help: Domain name to add + extra: + pattern: *pattern_domain + --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 + --install-letsencrypt-cert: + help: If adding a subdomain of an already added domain, try to install a Let's Encrypt certificate + action: store_true + + ### domain_remove() + remove: + action_help: Delete domains + api: DELETE /domains/ + arguments: + domain: + help: Domain to delete + extra: + pattern: *pattern_domain + autocomplete: *domains_list + -r: + full: --remove-apps + help: Remove apps installed on the domain + action: store_true + -f: + full: --force + help: Do not ask confirmation to remove apps + action: store_true + --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_password + + ### domain_maindomain() + main-domain: + action_help: Check the current main domain, or change it + deprecated_alias: + - maindomain + api: PUT /domains//main + arguments: + -n: + full: --new-main-domain + help: Change the current main domain + extra: + pattern: *pattern_domain + autocomplete: *domains_list + + ### domain_url_available() + url-available: + hide_in_help: True + action_help: Check availability of a web path + api: GET /domain//urlavailable + arguments: + domain: + help: The domain for the web path (e.g. your.domain.tld) + extra: + pattern: *pattern_domain + autocomplete: *domains_list + path: + help: The path to check (e.g. /coffee) + + + ### domain_action_run() + action-run: + hide_in_help: True + action_help: Run domain action + api: PUT /domain//actions/ + arguments: + domain: + help: Domain name + extra: + autocomplete: *domains_list + action: + help: action id + -a: + full: --args + 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 + autocomplete: *domains_list + -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 + autocomplete: *domains_list + 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 + autocomplete: *domains_list + 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 + api: + - GET /domains//config + - GET /domains//config/ + arguments: + domain: + help: Domain name + extra: + autocomplete: *domains_list + key: + help: A specific panel, section or a question identifier + nargs: '?' + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" + action: store_true + + ### domain_config_set() + set: + action_help: Apply a new configuration + api: PUT /domains//config/ + arguments: + domain: + help: Domain name + extra: + autocomplete: *domains_list + key: + help: The question or form key + nargs: '?' + -v: + full: --value + help: new value + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "mail_in=0&mail_out=0") + + dns: + subcategory_help: Manage domains DNS + actions: + ### domain_dns_conf() + suggest: + action_help: Generate sample DNS configuration for a domain + api: + - GET /domains//dns + - GET /domains//dns/suggest + arguments: + domain: + help: Target domain + extra: + pattern: *pattern_domain + autocomplete: *domains_list + + ### domain_dns_push() + push: + action_help: Push DNS records to registrar + api: POST /domains//dns/push + arguments: + domain: + help: Domain name to push DNS conf for + extra: + pattern: *pattern_domain + autocomplete: *domains_list + -d: + full: --dry-run + help: Only display what's to be pushed + action: store_true + --force: + help: Also update/remove records which were not originally set by Yunohost, or which have been manually modified + action: store_true + --purge: + help: Delete all records + action: store_true + + cert: + subcategory_help: Manage domain certificates + actions: + ### certificate_status() + status: + action_help: List status of current certificates (all by default). + api: + - GET /domains//cert + - GET /domains/*/cert + arguments: + domain_list: + help: Domains to check + nargs: "*" + extra: + autocomplete: *domains_list + --full: + help: Show more details + action: store_true + + ### certificate_install() + install: + action_help: Install Let's Encrypt certificates for given domains (all by default). + api: PUT /domains//cert + arguments: + domain_list: + help: Domains for which to install the certificates + nargs: "*" + extra: + autocomplete: *domains_list + --force: + help: Install even if current certificate is not self-signed + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) + action: store_true + --self-signed: + help: Install self-signed certificate instead of Let's Encrypt + action: store_true + + ### certificate_renew() + renew: + action_help: Renew the Let's Encrypt certificates for given domains (all by default). + api: PUT /domains//cert/renew + arguments: + domain_list: + help: Domains for which to renew the certificates + nargs: "*" + extra: + autocomplete: *domains_list + --force: + help: Ignore the validity threshold (15 days) + action: store_true + --email: + help: Send an email to root with logs if some renewing fails + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) + action: store_true + +############################# +# App # +############################# +app: + category_help: Manage apps + actions: + + catalog: + action_help: Show the catalog of installable application + api: GET /apps/catalog + arguments: + -f: + full: --full + help: Display all details, including the app manifest and various other infos + action: store_true + -c: + full: --with-categories + help: Also return a list of app categories + action: store_true + -a: + full: --with-antifeatures + help: Also return a list of antifeatures categories + action: store_true + + ### app_search() + search: + action_help: Search installable apps + arguments: + string: + help: Return matching app name or description with "string" + + ### app_manifest() + manifest: + action_help: Return the manifest of a given app from the catalog, or from a remote git repo + api: GET /apps/manifest + arguments: + app: + help: Name, local path or git URL of the app to fetch the manifest of + extra: + autocomplete: &apps_catalog_list + ynh_selector: app catalog + jq_selector: '.apps | keys[]' + use_cache: True + -s: + full: --with-screenshot + help: Also return a base64 screenshot if any (API only) + action: store_true + extra: + autocomplete: + hide_in_help: True + + ### app_list() + list: + action_help: List installed apps + api: GET /apps + arguments: + -f: + full: --full + help: Display all details, including the app manifest and various other infos + action: store_true + + ### app_info() + info: + action_help: Show infos about a specific installed app + api: GET /apps/ + arguments: + app: + help: Specific app ID + extra: + autocomplete: *apps_list + -f: + full: --full + help: Display all details, including the app manifest and various other infos + action: store_true + --with-pre-upgrade-notifications: + help: Also fetch pre-upgrade notifications, if an upgrade is available (this is meant for the web API) + action: store_true + + ### app_map() + map: + action_help: Show the mapping between urls and apps + api: GET /apps/map + arguments: + -a: + full: --app + help: Specific app to map + extra: + autocomplete: *apps_list + -r: + full: --raw + help: Return complete dict + action: store_true + -u: + full: --user + help: Allowed app map for a user + extra: + pattern: *pattern_username + autocomplete: *users_list + + ### app_install() + install: + action_help: Install apps + api: POST /apps + arguments: + app: + help: Name, local path or git URL of the app to install + extra: + autocomplete: *apps_catalog_list + -l: + full: --label + help: Custom name for the app + -a: + full: --args + 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), or when the app displays a post-install notification + action: store_true + -i: + full: --ignore-yunohost-version + help: Attempt to install the app even if your YunoHost version is below the required one + action: store_true + + ### app_remove() + remove: + action_help: Remove app + api: DELETE /apps/ + arguments: + app: + help: App to remove + extra: + autocomplete: *apps_list + -p: + full: --purge + help: Also remove all application data + action: store_true + + ### app_upgrade() + upgrade: + action_help: Upgrade app + api: PUT /apps//upgrade + arguments: + app: + help: App(s) to upgrade (default all) + nargs: "*" + extra: + autocomplete: *apps_list + -u: + full: --url + help: Git url to fetch for upgrade + -f: + full: --file + help: Folder or tarball for upgrade + -F: + full: --force + help: Force the update, even though the app is up to date + action: store_true + -b: + full: --no-safety-backup + help: Disable the safety backup during upgrade + action: store_true + -c: + full: --continue-on-failure + help: Continue to upgrade apps even if one or more upgrade failed + action: store_true + -i: + full: --ignore-yunohost-version + help: Attempt to upgrade the app even if your YunoHost version is below the required one + action: store_true + + ### app_change_url() + change-url: + action_help: Change app's URL + api: PUT /apps//changeurl + arguments: + app: + help: Target app instance name + extra: + autocomplete: *apps_list + -d: + full: --domain + help: New app domain on which the application will be moved + extra: + ask: ask_new_domain + pattern: *pattern_domain + required: True + autocomplete: *domains_list + -p: + full: --path + help: New path at which the application will be moved + extra: + ask: ask_new_path + required: True + + ### app_setting() + setting: + action_help: Set or get an app setting value + api: GET /apps//settings + arguments: + app: + help: App ID + extra: + autocomplete: *apps_list + key: + help: Key to get/set + -v: + full: --value + help: Value to set + -d: + full: --delete + 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 + extra: + autocomplete: *apps_list + + ### app_db() + db: + action_help: Open an interactive database client prompt for the app + api: GET /apps//db + arguments: + app: + help: App ID + extra: + autocomplete: *apps_list + + ### app_register_url() + register-url: + hide_in_help: True + action_help: Book/register a web path for a given app + arguments: + app: + help: App which will use the web path + extra: + autocomplete: *apps_list + domain: + help: The domain on which the app should be registered (e.g. your.domain.tld) + extra: + autocomplete: *domains_list + path: + help: The path to be registered (e.g. /coffee) + + + ### app_makedefault() + makedefault: + hide_in_help: True + action_help: Redirect domain root to an app + api: PUT /apps//default + arguments: + app: + help: App name to put on domain root + extra: + autocomplete: *apps_list + -d: + full: --domain + help: Specific domain to put app on (the app domain by default) + extra: + autocomplete: *domains_list + -u: + full: --undo + help: Undo redirection + action: store_true + + ### app_dismiss_notification + dismiss-notification: + hide_in_help: True + action_help: Dismiss post_install or post_upgrade notification + api: PUT /apps//dismiss_notification/ + arguments: + app: + help: App ID to dismiss notification for + extra: + autocomplete: *apps_list + name: + help: Notification name, either post_install or post_upgrade + choices: + - post_install + - post_upgrade + + ### app_ssowatconf() + ssowatconf: + action_help: Regenerate SSOwat configuration file + + ### app_change_label() + change-label: + action_help: Change app label + api: PUT /apps//label + arguments: + app: + help: App ID + extra: + autocomplete: *apps_list + new_label: + help: New app label + + subcategories: + + action: + subcategory_help: Handle apps actions + actions: + + ### app_action_list() + list: + action_help: List app actions + api: GET /apps//actions + arguments: + app: + help: App name + extra: + autocomplete: *apps_list + + ### app_action_run() + run: + action_help: Run app action + api: PUT /apps//actions/ + arguments: + app: + help: App name + extra: + autocomplete: *apps_list + action: + help: action id + -a: + full: --args + help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") + + config: + subcategory_help: Applications configuration panel + actions: + + ### app_config_get() + get: + action_help: Display an app configuration + api: + - GET /apps//config + - GET /apps//config/ + arguments: + app: + help: App name + extra: + autocomplete: *apps_list + key: + help: A specific panel, section or a question identifier + nargs: '?' + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" + action: store_true + --core: + help: Get the 'core' config for this app, such as permissions stuff. This option is only meant for API. + action: store_true + + ### app_config_set() + set: + action_help: Apply a new configuration + api: PUT /apps//config/ + arguments: + app: + help: App name + extra: + autocomplete: *apps_list + key: + help: The question or panel key + nargs: '?' + -v: + full: --value + help: new value + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "domain=domain.tld&path=/path") + -f: + full: --args-file + help: YAML or JSON file with key/value couples + type: open + --core: + help: Set the 'core' config for this app, such as permissions stuff. This option is only meant for API. + action: store_true + +############################# +# Backup # +############################# +backup: + category_help: Manage backups + actions: + + ### backup_create() + create: + action_help: Create a backup local archive. If neither --apps or --system are given, this will backup all apps and all system parts. If only --apps if given, this will only backup apps and no system parts. Similarly, if only --system is given, this will only backup system parts and no apps. + api: POST /backups + arguments: + -n: + full: --name + help: Name of the backup archive + extra: + pattern: &pattern_backup_archive_name + - !!str ^[\w\-\._]{1,50}(?/restore + arguments: + name: + help: Name or path of the backup archive + extra: + autocomplete: &backups_list + ynh_selector: backup list + jq_selector: '.archives[]' + use_cache: false + --system: + help: List of system parts to restore (or all if none is given) + nargs: "*" + --apps: + help: List of application names to restore (or all if none is given) + nargs: "*" + extra: + autocomplete: *apps_list + --force: + help: Force restauration on an already installed system + action: store_true + --no-remove-on-failure: + help: For app only, debug option to avoid removing the app on a failed restore + action: store_true + + ### backup_list() + list: + action_help: List available local backup archives + api: GET /backups + arguments: + -i: + full: --with-info + help: Show backup information for each archive + action: store_true + -H: + full: --human-readable + help: Print sizes in human readable format + action: store_true + + ### backup_info() + info: + action_help: Show info about a local backup archive + api: GET /backups/ + arguments: + name: + help: Name or path of the backup archive + extra: + autocomplete: *backups_list + -d: + full: --with-details + help: Show additional backup information + action: store_true + -H: + full: --human-readable + help: Print sizes in human readable format + action: store_true + + ### backup_download() + download: + hide_in_help: True + action_help: (API only) Request to download the file + api: GET /backups//download + arguments: + name: + help: Name of the local backup archive + + ### backup_delete() + delete: + action_help: Delete a backup archive + api: DELETE /backups/ + arguments: + name: + help: Name of the archive to delete + extra: + pattern: *pattern_backup_archive_name + autocomplete: *backups_list + +############################# +# Settings # +############################# +settings: + category_help: Manage YunoHost global settings + actions: + + ### settings_list() + list: + action_help: list all entries of the settings + api: GET /settings + arguments: + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + + ### settings_get() + get: + action_help: get an entry value in the settings + api: GET /settings/ + arguments: + key: + help: Settings key + extra: + autocomplete: &settings_list + ynh_selector: settings list + jq_selector: '. | keys[]' + use_cache: false + -f: + full: --full + help: Display all details (meant to be used by the API) + action: store_true + -e: + full: --export + help: Only export key/values, meant to be reimported using "config set --args-file" + action: store_true + + ### settings_set() + set: + action_help: set an entry value in the settings + api: PUT /settings/ + arguments: + key: + help: The question or form key + nargs: '?' + extra: + autocomplete: *settings_list + -v: + full: --value + help: new value + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "mail_in=0&mail_out=0") + + ### settings_reset_all() + reset-all: + action_help: reset all settings to their default value + api: DELETE /settings + + ### settings_reset() + reset: + action_help: set an entry value to its default one + api: DELETE /settings/ + arguments: + key: + help: Settings key + extra: + autocomplete: *settings_list + +############################# +# Service # +############################# +service: + category_help: Manage services + actions: + + ### service_add() + add: + action_help: Add a service + arguments: + name: + help: Service name to add + -d: + full: --description + help: Description of the service + -l: + full: --log + help: Absolute path to log file to display + nargs: "+" + extra: + autocomplete: + zsh_completion: _files + --test_status: + help: Specify a custom bash command to check the status of the service. Note that it only makes sense to specify this if the corresponding systemd service does not return the proper information already. + --test_conf: + help: Specify a custom bash command to check if the configuration of the service is valid or broken, similar to nginx -t. + --needs_exposed_ports: + help: A list of ports that needs to be publicly exposed for the service to work as intended. + nargs: "+" + type: int + metavar: PORT + -n: + full: --need_lock + help: Use this option to prevent deadlocks if the service does invoke yunohost commands. + action: store_true + + ### service_remove() + remove: + action_help: Remove a service + arguments: + name: + help: Service name to remove + extra: + autocomplete: &services_list + ynh_selector: service status + jq_selector: '. | keys[]' + use_cache: false + + ### service_start() + start: + action_help: Start one or more services + api: PUT /services//start + arguments: + names: + help: Service name to start + nargs: "+" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_stop() + stop: + action_help: Stop one or more services + api: PUT /services//stop + arguments: + names: + help: Service name to stop + nargs: "+" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_reload() + reload: + action_help: Reload one or more services + arguments: + names: + help: Service name to reload + nargs: "+" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_restart() + restart: + action_help: Restart one or more services. If the services are not running yet, they will be started. + api: PUT /services//restart + arguments: + names: + help: Service name to restart + nargs: "+" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_reload_or_restart() + reload_or_restart: + action_help: Reload one or more services if they support it. If not, restart them instead. If the services are not running yet, they will be started. + arguments: + names: + help: Service name to reload or restart + nargs: "+" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_enable() + enable: + action_help: Enable one or more services + api: PUT /services//enable + arguments: + names: + help: Service name to enable + nargs: "+" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_disable() + disable: + action_help: Disable one or more services + api: PUT /services//disable + arguments: + names: + help: Service name to disable + nargs: "+" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_status() + status: + action_help: Show status information about one or more services (all by default) + api: + - GET /services + - GET /services/ + arguments: + names: + help: Service name to show + nargs: "*" + metavar: NAME + extra: + autocomplete: *services_list + + ### service_log() + log: + action_help: Log every log files of a service + api: GET /services//log + arguments: + name: + help: Service name to log + extra: + autocomplete: *services_list + -n: + full: --number + help: Number of lines to display + default: 50 + type: int + +############################# +# Firewall # +############################# +firewall: + category_help: Manage firewall rules + actions: + + ### firewall_list() + list: + action_help: List all firewall rules + api: GET /firewall + arguments: + -r: + full: --raw + help: Return the complete YAML dict + action: store_true + -p: + full: --protocol + help: "If not raw, protocol type to list (tcp/udp)" + choices: + - tcp + - udp + nargs: "?" + default: tcp + -f: + full: --forwarded + help: If not raw, list UPnP forwarded ports instead of open ports + action: store_true + + ### firewall_is_open() + is-open: + action_help: Returns whether the port is open or not. + api: GET /firewall// + arguments: + port: + help: Port or range of ports to check + extra: + pattern: &pattern_port_or_range + - !!str ((^|(?!\A):)([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){1,2}?$ + - "pattern_port_or_range" + -p: + full: --protocol + help: "Protocol type (tcp/udp)" + choices: + - tcp + - udp + default: tcp + + ### firewall_open() + open: + action_help: Allow connections on a port + api: PUT /firewall//open/ + arguments: + port: + help: Port or range of ports to open + extra: + pattern: *pattern_port_or_range + -p: + full: --protocol + help: "Protocol type (tcp/udp)" + choices: + - tcp + - udp + default: tcp + comment: + help: A reason for the port to be open (like the app's name) + default: "" + --upnp: + help: Add forwarding of this port with UPnP + action: store_true + --no-reload: + help: Do not reload firewall rules + action: store_true + + ### firewall_close() + close: + action_help: Disallow connections on a port + api: PUT /firewall//close/ + arguments: + port: + help: Port or range of ports to close + extra: + pattern: *pattern_port_or_range + -p: + full: --protocol + help: "Protocol type (tcp/udp)" + choices: + - tcp + - udp + default: tcp + --upnp-only: + help: Only remove forwarding of this port with UPnP + action: store_true + --no-reload: + help: Do not reload firewall rules + action: store_true + + ### firewall_delete() + delete: + action_help: Unregister a port from YunoHost + api: PUT /firewall//delete/ + arguments: + port: + help: Port or range of ports to delete + extra: + pattern: *pattern_port_or_range + -p: + full: --protocol + help: "Protocol type (tcp/udp)" + choices: + - tcp + - udp + default: tcp + --no-reload: + help: Do not reload firewall rules + action: store_true + + ### firewall_allow() + allow: + action_help: Allow connections on a port + api: PUT /firewall//allow/ + arguments: + protocol: + help: "Protocol type to allow (TCP/UDP/Both)" + choices: + - TCP + - UDP + - Both + default: TCP + port: + help: Port or range of ports to open + extra: + pattern: *pattern_port_or_range + -4: + full: --ipv4-only + help: Only add a rule for IPv4 connections + action: store_true + -6: + full: --ipv6-only + help: Only add a rule for IPv6 connections + action: store_true + --no-upnp: + help: Do not add forwarding of this port with UPnP + action: store_true + --no-reload: + help: Do not reload firewall rules + action: store_true + + ### firewall_disallow() + disallow: + action_help: Disallow connections on a port + api: PUT /firewall//disallow/ + arguments: + protocol: + help: "Protocol type to allow (TCP/UDP/Both)" + choices: + - TCP + - UDP + - Both + default: TCP + port: + help: Port or range of ports to close + extra: + pattern: *pattern_port_or_range + -4: + full: --ipv4-only + help: Only remove the rule for IPv4 connections + action: store_true + -6: + full: --ipv6-only + help: Only remove the rule for IPv6 connections + action: store_true + --upnp-only: + help: Only remove forwarding of this port with UPnP + action: store_true + --no-reload: + help: Do not reload firewall rules + action: store_true + + ### firewall_upnp() + upnp: + action_help: Manage port forwarding using UPnP + api: PUT /firewall/upnp/ + arguments: + action: + choices: + - enable + - disable + - status + nargs: "?" + default: status + --no-refresh: + help: Do not refresh port forwarding + action: store_true + + + ### firewall_reload() + reload: + action_help: Reload all firewall rules + arguments: + --skip-upnp: + help: Do not refresh port forwarding using UPnP + action: store_true + + ### firewall_stop() + stop: + action_help: Remove all the firewall rules + +############################# +# DynDNS # +############################# +dyndns: + 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 ( deprecated, use 'yunohost domain dyndns subscribe' instead ) + extra: + pattern: *pattern_domain + autocomplete: *domains_list + -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 + arguments: + -d: + full: --domain + help: Full domain to update + extra: + pattern: *pattern_domain + autocomplete: *domains_list + -f: + full: --force + help: Force the update (for debugging only) + action: store_true + -D: + full: --dry-run + help: Only display the generated zone + action: store_true + +############################# +# Tools # +############################# +tools: + category_help: Specific tools + actions: + + ### tools_rootpw() + rootpw: + action_help: Change root password + api: PUT /rootpw + arguments: + -n: + full: --new-password + extra: + password: ask_new_admin_password + pattern: *pattern_password + required: True + comment: good_practices_about_admin_password + + ### tools_maindomain() + maindomain: + action_help: Check the current main domain, or change it + arguments: + -n: + full: --new-main-domain + help: Change the current main domain + extra: + pattern: *pattern_domain + autocomplete: *domains_list + + ### tools_postinstall() + postinstall: + action_help: YunoHost post-install + api: POST /postinstall + authentication: + # We need to be able to run the postinstall without being authenticated, otherwise we can't run the postinstall + api: null + arguments: + -d: + full: --domain + help: YunoHost main domain + extra: + ask: ask_main_domain + pattern: *pattern_domain + required: True + autocomplete: *domains_list + -u: + full: --username + help: Username for the first (admin) user. For example 'camille' + extra: + ask: ask_admin_username + pattern: *pattern_username + required: True + autocomplete: *users_list + -F: + full: --fullname + help: The full name for the first (admin) user. For example 'Camille Dupont' + extra: + ask: ask_admin_fullname + required: True + pattern: *pattern_fullname + -p: + full: --password + help: YunoHost admin password + extra: + password: ask_new_admin_password + pattern: *pattern_password + required: True + comment: good_practices_about_admin_password + --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 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 + --i-have-read-terms-of-services: + help: Automatically reply to the terms of services prompt, for example for non-interactive installations + action: store_true + + + update_norefresh: + # This exists mainly for the API, such that there's an explicit read-only (GET) route + # vs the old/regular/legacy(?)/ambiguous PUT /update route ... + hide_in_help: True + action_help: List available system/apps updates (without refreshing caches) + api: GET /update + + ### tools_update() + update: + action_help: YunoHost update + api: PUT /update/ + arguments: + target: + help: What to update, "apps" (application catalog) or "system" (fetch available package upgrades, equivalent to apt update), "all" for both + choices: + - apps + - system + - all + nargs: "?" + metavar: TARGET + default: all + --no-refresh: + help: Does not run apt update or fetch the apps catalog, only list upgradable packages and apps + action: store_true + + ### tools_upgrade() + upgrade: + action_help: YunoHost upgrade + api: PUT /upgrade/ + arguments: + target: + help: What to upgrade, either "apps" (all apps) or "system" (all system packages) + choices: + - apps + - system + nargs: "?" + + ### tools_shell() + shell: + action_help: Launch a development shell + arguments: + -c: + help: python command to execute + full: --command + + ### tools_basic_space_cleanup() + basic-space-cleanup: + action_help: Basic space cleanup (apt, journalctl, system and YunoHost logs, ...) + + ### tools_shutdown() + shutdown: + action_help: Shutdown the server + api: PUT /shutdown + arguments: + -f: + help: skip the shutdown confirmation + full: --force + action: store_true + + ### tools_reboot() + reboot: + action_help: Reboot the server + api: PUT /reboot + arguments: + -f: + help: skip the reboot confirmation + full: --force + action: store_true + + ### tools_regen_conf() + regen-conf: + action_help: Regenerate the configuration file(s) + api: + - PUT /regenconf + - PUT /regenconf/ + arguments: + names: + help: Categories to regenerate configuration of (all by default) + nargs: "*" + metavar: NAME + -d: + full: --with-diff + help: Show differences in case of configuration changes + action: store_true + -f: + full: --force + help: Override all manual modifications in configuration files + action: store_true + -n: + full: --dry-run + help: Show what would have been regenerated + action: store_true + -p: + full: --list-pending + help: List pending configuration files and exit + action: store_true + + ### tools_versions() + versions: + action_help: Display YunoHost's packages versions + api: GET /versions + + subcategories: + + migrations: + subcategory_help: Manage migrations + actions: + + ### tools_migrations_list() + list: + action_help: List migrations + api: GET /migrations + arguments: + --pending: + help: list only pending migrations + action: store_true + --done: + help: list only migrations already performed + action: store_true + + ### tools_migrations_run() + run: + action_help: Run migrations + api: + - PUT /migrations + - PUT /migrations/ + deprecated_alias: + - migrate + arguments: + targets: + help: Migrations to run (all pendings by default) + nargs: "*" + extra: + autocomplete: &migrations_list + ynh_selector: tools migrations list + jq_selector: '.migrations[].id' + use_cache: false + --skip: + help: Skip specified migrations (to be used only if you know what you are doing) + action: store_true + --force-rerun: + help: Re-run already-ran specified migration (to be used only if you know what you are doing) + action: store_true + --auto: + help: Automatic mode, won't run manual migrations (to be used only if you know what you are doing) + action: store_true + --accept-disclaimer: + help: Accept disclaimers of migrations (please read them before using this option) + action: store_true + + ### tools_migrations_state() + state: + action_help: Show current migrations state + +############################# +# Hook # +############################# +hook: + category_help: Manage hooks + actions: + + ### hook_add() + add: + action_help: Store hook script to filesystem + arguments: + app: + help: App to link with + extra: + autocomplete: *apps_list + file: + help: Script to add + extra: + autocomplete: + zsh_completion: _files + + ### hook_remove() + remove: + action_help: Remove hook scripts from filesystem + arguments: + app: + help: Scripts related to app will be removed + extra: + autocomplete: *apps_list + + ### hook_info() + info: + hide_in_help: false + action_help: Get information about a given hook + arguments: + action: + help: Action name + choices: &hook_action_choices + - post_user_create + - post_user_delete + - post_user_update + - post_app_addaccess + - post_app_removeaccess + - post_domain_add + - post_domain_remove + - post_cert_update + - custom_dns_rules + - post_app_change_url + - post_app_upgrade + - post_app_install + - post_app_remove + - backup + - restore + - backup_method + - post_iptable_rules + - conf_regen + name: + help: Hook name + extra: + autocomplete: &hooks_list_case + ynh_selector: hook list + jq_selector: '.hooks[]' + depends: previous + use_cache: true + + ### hook_list() + list: + action_help: List available hooks for an action + api: GET /hooks/ + arguments: + action: + help: Action name + choices: *hook_action_choices + -l: + full: --list-by + help: Property to list hook by + choices: + - name + - priority + - folder + default: name + -i: + full: --show-info + help: Show hook information + action: store_true + + ### hook_callback() + callback: + hide_in_help: True + action_help: Execute all scripts binded to an action + arguments: + action: + help: Action name + choices: *hook_action_choices + -n: + full: --hooks + help: List of hooks names to execute + nargs: "*" + -a: + full: --args + help: Ordered list of arguments to pass to the scripts + nargs: "*" + -d: + full: --chdir + help: The directory from where the scripts will be executed + + ### hook_exec() + exec: + hide_in_help: True + action_help: Execute hook from a file with arguments + arguments: + path: + help: Path of the script to execute + extra: + autocomplete: + zsh_completion: _files + + -a: + full: --args + help: Ordered list of arguments to pass to the script + nargs: "*" + --raise-on-error: + help: Raise if the script returns a non-zero exit code + action: store_true + -d: + full: --chdir + help: The directory from where the script will be executed + +############################# +# Log # +############################# +log: + category_help: Manage debug logs + actions: + + ### log_list() + list: + action_help: List logs + api: GET /logs + arguments: + -l: + full: --limit + help: Maximum number of operations to list (default to 50) + type: int + default: 50 + -d: + full: --with-details + help: Show additional infos (e.g. operation success) but may significantly increase command time. Consider using --limit in combination with this. + action: store_true + -s: + full: --with-suboperations + help: Include metadata about operations that are not the main operation but are sub-operations triggered by another ongoing operation... (e.g. initializing groups/permissions when installing an app) + action: store_true + + ### log_show() + show: + action_help: Display a log content + api: GET /logs/ + deprecated_alias: + - display + arguments: + path: + help: Log file which to display the content + extra: + autocomplete: &logs_list + ynh_selector: log list + jq_selector: '.operation[].name' + use_cache: false + -n: + full: --number + help: Number of lines to display + default: 50 + type: int + --share: + help: (Deprecated, see yunohost log share) Share the full log using yunopaste + action: store_true + -i: + full: --filter-irrelevant + help: Do not show some lines deemed not relevant (like set +x or helper argument parsing) + action: store_true + -s: + full: --with-suboperations + help: Include metadata about sub-operations of this operation... (e.g. initializing groups/permissions when installing an app) + action: store_true + + ### log_share() + share: + action_help: Share the full log on yunopaste (alias to show --share) + api: GET /logs//share + arguments: + path: + help: Log file to share + extra: + autocomplete: *logs_list + +############################# +# Diagnosis # +############################# +diagnosis: + category_help: Look for possible issues on the server + actions: + + list: + action_help: List diagnosis categories + api: GET /diagnosis/categories + + show: + action_help: Show most recents diagnosis results + api: GET /diagnosis + arguments: + categories: + help: Diagnosis categories to display (all by default) + nargs: "*" + extra: + autocomplete: &diagnosis_list + ynh_selector: diagnosis list + jq_selector: '.categories[]' + use_cache: false + --full: + help: Display additional information + action: store_true + --issues: + help: Only display issues + action: store_true + --share: + help: Share the logs using yunopaste + action: store_true + --human-readable: + help: Show a human-readable output + action: store_true + + get: + action_help: Low-level command to fetch raw data and status about a specific diagnosis test + api: GET /diagnosis/ + arguments: + category: + help: Diagnosis category to fetch results from + extra: + autocomplete: *diagnosis_list + item: + help: "List of criteria describing the test. Must correspond exactly to the 'meta' infos in 'yunohost diagnosis show'" + metavar: CRITERIA + nargs: "*" + + run: + action_help: Run diagnosis + api: PUT /diagnosis/run + arguments: + categories: + help: Diagnosis categories to run (all by default) + nargs: "*" + extra: + autocomplete: *diagnosis_list + --force: + help: Ignore the cached report even if it is still 'fresh' + action: store_true + --except-if-never-ran-yet: + help: Don't run anything if diagnosis never ran yet ... (this is meant to be used by the webadmin) + action: store_true + --email: + help: Send an email to root with issues found (this is meant to be used by cron job) + action: store_true + + ignore: + action_help: Configure some diagnosis results to be ignored and therefore not considered as actual issues + 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=mail'" + nargs: "*" + metavar: CRITERIA + --list: + help: List active ignore filters + action: store_true + + unignore: + action_help: Configure some diagnosis results to be unignored and therefore considered as actual issues + api: PUT /diagnosis/unignore + arguments: + --filter: + help: Remove a filter (it should be an existing filter as listed with "ignore --list") + nargs: "*" + metavar: CRITERIA + + +############################# +# Storage # +############################# +storage: + category_help: Manage hard-drives, filesystem, pools + subcategories: + disk: + subcategory_help: Manage et get infos about hard-drives + actions: + # storage_disks_list + list: + action_help: List hard-drives currently attached to this system optionnaly with infos + api: GET /storage/disk/list + arguments: + -H: + full: --human-readable + help: Print informations in a human-readable format + action: store_true + --human-readable-size: + help: Print sizes in a human-readable format + action: store_true + -i: + full: --with-info + help: Get all informations for each archive + action: store_true + # storage_disks_info + info: + action_help: Get hard-drive information + api: GET /storage/disk/info/ + arguments: + -H: + full: --human-readable + help: Print informations in a human-readable format + action: store_true + --human-readable-size: + help: Print sizes in a human-readable format + action: store_true diff --git a/share/config_app.toml b/share/config_app.toml new file mode 100644 index 0000000..9300cce --- /dev/null +++ b/share/config_app.toml @@ -0,0 +1,35 @@ +version = "1.0" +i18n = "app_config" + +[_core] + + # This is duplicated for each permission and named "permission_main", "permission_admin" etc. + # Keys are also made unique by prefixing them with "permission_main" / "permission_admin", + # for example the label key is "permission_{permid}_label" + [_core.permissions] + + [_core.permissions.url] + type = "url" + visible = false + + [_core.permissions.location] + type = "markdown" + # Core automatically add a "visible=false" if url is empty (but since the keys are dynamic, can't do it here) + + [_core.permissions.label] + type = "string" + + [_core.permissions.description] + type = "string" + + [_core.permissions.show_tile] + type = "boolean" + # Core automatically add a "visible=false" if url is empty (but since the keys are dynamic, can't do it here) + + [_core.permissions.logo] + type = "file" + accept = ["image/png"] + + [_core.permissions.allowed] + type = "tags" + # Core automatically add a "readonly" if protected is true diff --git a/share/config_domain.toml b/share/config_domain.toml new file mode 100644 index 0000000..3218c2a --- /dev/null +++ b/share/config_domain.toml @@ -0,0 +1,125 @@ +version = "1.0" +i18n = "domain_config" + +[feature] + + [feature.mail] + [feature.mail.mail_out] + type = "boolean" + default = 1 + + [feature.mail.mail_in] + type = "boolean" + default = 1 + + [feature.app] + [feature.app.default_app] + type = "app" + filter = "is_webapp" + default = "_none" + + [feature.portal] + # Only available for "topest" domains + + [feature.portal.enable_public_apps_page] + type = "boolean" + default = false + + [feature.portal.show_other_domains_apps] + type = "boolean" + default = true + + [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", "omg", "legacy", "black", "synthwave", "halloween", "coffee", "cupcake", "cyberpunk", "valentine", "nord"] + default = "system" + + [feature.portal.portal_tile_theme] + type = "select" + optional = false + choices = ["descriptive", "simple", "periodic"] + default = "simple" + + [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.portal.custom_css] + # NB: this is wrote into "/usr/share/yunohost/portal/customassets/{domain}.custom.css" + type = "text" + +[dns] + + [dns.registrar] + # This part is automatically generated in DomainConfigPanel + +[cert] + + [cert.cert_] + # The section has a different id than 'cert' otherwise it ends up with an unecessary "name" because it's defined for the panel (in i18n.json) + + [cert.cert_.cert_summary] + type = "alert" + # Automatically filled by DomainConfigPanel + + [cert.cert_.cert_validity] + type = "number" + readonly = true + visible = "false" + # Automatically filled by DomainConfigPanel + + [cert.cert_.cert_issuer] + type = "string" + visible = false + # Automatically filled by DomainConfigPanel + + [cert.cert_.acme_eligible] + type = "boolean" + visible = false + # Automatically filled by DomainConfigPanel + + [cert.cert_.acme_eligible_explain] + type = "alert" + style = "warning" + visible = "acme_eligible == false || acme_eligible == null" + + [cert.cert_.cert_no_checks] + type = "boolean" + default = false + visible = "acme_eligible == false || acme_eligible == null" + + [cert.cert_.cert_install] + type = "button" + icon = "star" + style = "success" + visible = "cert_issuer != 'letsencrypt'" + enabled = "acme_eligible || cert_no_checks" + + [cert.cert_.cert_renew] + type = "button" + icon = "refresh" + style = "warning" + visible = "cert_issuer == 'letsencrypt'" + enabled = "acme_eligible || cert_no_checks" diff --git a/share/config_global.toml b/share/config_global.toml new file mode 100644 index 0000000..cbdb4f4 --- /dev/null +++ b/share/config_global.toml @@ -0,0 +1,210 @@ +version = "1.0" +i18n = "global_settings_setting" + +[security] + [security.password] + + [security.password.admin_strength] + type = "select" + choices.1 = "Require at least 8 chars" + choices.2 = "ditto, but also require at least one digit, one lower and one upper char" + choices.3 = "ditto, but also require at least one special char" + choices.4 = "ditto, but also require at least 12 chars" + default = "1" + + [security.password.user_strength] + type = "select" + choices.1 = "Require at least 8 chars" + choices.2 = "ditto, but also require at least one digit, one lower and one upper char" + choices.3 = "ditto, but also require at least one special char" + choices.4 = "ditto, but also require at least 12 chars" + default = "1" + + [security.password.passwordless_sudo] + type = "boolean" + # The actual value is dynamically computed by checking the sudoOption of cn=admins,ou=sudo + default = false + + [security.ssh] + + [security.ssh.ssh_compatibility] + type = "select" + choices.intermediate = "Intermediate (compatible with older softwares)" + choices.modern = "Modern (recommended)" + default = "modern" + + [security.ssh.ssh_port] + type = "number" + default = 22 + + [security.ssh.ssh_password_authentication] + type = "boolean" + default = true + + [security.nginx] + [security.nginx.nginx_redirect_to_https] + type = "boolean" + default = true + + [security.nginx.nginx_compatibility] + type = "select" + choices.intermediate = "Intermediate (compatible with Firefox 27, Android 4.4.2, Chrome 31, Edge, IE 11, Opera 20, and Safari 9)" + choices.modern = "Modern (compatible with Firefox 63, Android 10.0, Chrome 70, Edge 75, Opera 57, and Safari 12.1)" + default = "intermediate" + + [security.postfix] + + [security.postfix.postfix_compatibility] + type = "select" + choices.intermediate = "Intermediate (allows TLS 1.2)" + choices.modern = "Modern (TLS 1.3 only)" + default = "intermediate" + + [security.webadmin] + [security.webadmin.webadmin_allowlist_enabled] + type = "boolean" + default = false + + [security.webadmin.webadmin_allowlist] + type = "tags" + visible = "webadmin_allowlist_enabled" + optional = true + default = "" + + [security.portal] + [security.portal.portal_allow_edit_email] + type = "boolean" + default = false + + [security.portal.portal_allow_edit_email_alias] + type = "boolean" + default = true + + [security.portal.portal_allow_edit_email_forward] + type = "boolean" + default = true + + [security.root_access] + [security.root_access.root_access_explain] + type = "alert" + style = "info" + icon = "info" + + [security.root_access.root_password] + type = "password" + optional = true + default = "" + + [security.root_access.root_password_confirm] + type = "password" + optional = true + default = "" + + [security.experimental] + [security.experimental.security_experimental_enabled] + type = "boolean" + default = false + +[email] + [email.pop3] + [email.pop3.pop3_enabled] + type = "boolean" + default = false + + [email.smtp] + [email.smtp.smtp_allow_ipv6] + type = "boolean" + default = true + + [email.smtp.smtp_relay_enabled] + type = "boolean" + default = false + + [email.smtp.smtp_relay_host] + type = "string" + default = "" + optional = true + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_port] + type = "number" + default = 587 + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_user] + type = "string" + default = "" + optional = true + visible="smtp_relay_enabled" + + [email.smtp.smtp_relay_password] + type = "password" + default = "" + optional = true + visible="smtp_relay_enabled" + help = "" # This is empty string on purpose, otherwise the core automatically set the 'good_practice_admin_password' string here which is not relevant, because the admin is not actually "choosing" the password ... + + [email.smtp.smtp_backup_mx_domains] + type = "string" + default = "" + optional = true + + [email.smtp.smtp_backup_mx_emails_whitelisted] + type = "string" + default = "" + optional = true + visible = "smtp_backup_mx_domains" + + [email.antispam] + [email.antispam.enable_blocklists] + type = "boolean" + default = true + +[misc] + + [misc.backup] + [misc.backup.backup_compress_tar_archives] + type = "boolean" + default = false + + [misc.network] + + [misc.network.dns_exposure] + type = "select" + choices.both = "Both" + choices.ipv4 = "IPv4 Only" + choices.ipv6 = "IPv6 Only" + default = "both" + + [misc.network.dns_custom_resolvers_enabled] + type = "boolean" + default = false + + [misc.network.dns_custom_resolvers_list] + type = "tags" + # Regex is + + # Regex from Jonas Jared Jacek + pattern.regexp = '^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,7}:|:(:[0-9A-Fa-f]{1,4}){1,7}|([0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,5}(:[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){1,4}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){1,3}(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){1,2}(:[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(:[0-9A-Fa-f]{1,4}){1,6}|:(:[0-9A-Fa-f]{1,4}){1,6}))(,?((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,7}:|:(:[0-9A-Fa-f]{1,4}){1,7}|([0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,5}(:[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){1,4}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){1,3}(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){1,2}(:[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(:[0-9A-Fa-f]{1,4}){1,6}|:(:[0-9A-Fa-f]{1,4}){1,6})))+$' + pattern.error = "You should specify a valid resolver Ipv4/Ipv6, such as 89.234.141.66 or 2a00:5881:8100:1000::3" + default = "" + visible = "dns_custom_resolvers_enabled" + + [misc.tls_passthrough] + + [misc.tls_passthrough.tls_passthrough_enabled] + type = "boolean" + default = false + + [misc.tls_passthrough.tls_passthrough_explain] + type = "alert" + style = "info" + icon = "info" + visible = "tls_passthrough_enabled" + + [misc.tls_passthrough.tls_passthrough_list] + type = "tags" + # Regex is just ;; + pattern.regexp = '^((([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}));(([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?([^\W_]{2,}|[0-9]{1,3})));[0-9]{1,}(?:,|$))+' + pattern.error = "You should specify a list of items formatted as DOMAIN;DESTINATION;DESTPORT, such as yolo.test;192.168.1.42;443" + default = "" + visible = "tls_passthrough_enabled" diff --git a/share/dnsbl_list.yml b/share/dnsbl_list.yml new file mode 100644 index 0000000..b6ac45f --- /dev/null +++ b/share/dnsbl_list.yml @@ -0,0 +1,182 @@ +# Used by GAFAM +- name: Spamhaus ZEN + dns_server: zen.spamhaus.org + website: https://www.spamhaus.org/zen/ + ipv4: true + ipv6: true + domain: false + non_blocklisted_return_code: [] +- name: Barracuda Reputation Block List + dns_server: b.barracudacentral.org + website: https://barracudacentral.org/rbl/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: Hostkarma + dns_server: hostkarma.junkemailfilter.com + website: https://ipadmin.junkemailfilter.com/remove.php + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: ['127.0.0.1', '127.0.0.5', '127.0.1.1'] +- name: ImproWare IP based spamlist + dns_server: spamrbl.imp.ch + website: https://antispam.imp.ch/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: ImproWare IP based wormlist + dns_server: wormrbl.imp.ch + website: https://antispam.imp.ch/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: Backscatterer.org + dns_server: ips.backscatterer.org + website: http://www.backscatterer.org/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: inps.de + dns_server: dnsbl.inps.de + website: http://dnsbl.inps.de/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: LASHBACK + dns_server: ubl.unsubscore.com + website: https://blacklist.lashback.com/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: Mailspike.org + dns_server: bl.mailspike.net + website: http://www.mailspike.net/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: NiX Spam + dns_server: ix.dnsbl.manitu.net + website: http://www.dnsbl.manitu.net/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: REDHAWK + dns_server: access.redhawk.org + website: https://www.redhawk.org/SpamHawk/query.php + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: SORBS Open SMTP relays + dns_server: smtp.dnsbl.sorbs.net + website: http://www.sorbs.net/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: SORBS Spamhost (last 28 days) + dns_server: recent.spam.dnsbl.sorbs.net + website: http://www.sorbs.net/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: SORBS Spamhost (last 48 hours) + dns_server: new.spam.dnsbl.sorbs.net + website: http://www.sorbs.net/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: SpamCop Blocking List + dns_server: bl.spamcop.net + website: https://www.spamcop.net/bl.shtml + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: Spam Eating Monkey SEM-BACKSCATTER + dns_server: backscatter.spameatingmonkey.net + website: https://spameatingmonkey.com/services + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: Spam Eating Monkey SEM-BLACK + dns_server: bl.spameatingmonkey.net + website: https://spameatingmonkey.com/services + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: Spam Eating Monkey SEM-IPV6BL + dns_server: bl.ipv6.spameatingmonkey.net + website: https://spameatingmonkey.com/services + ipv4: false + ipv6: true + domain: false + non_blocklisted_return_code: [] +- name: SpamRATS! all + dns_server: all.spamrats.com + website: http://www.spamrats.com/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: PSBL (Passive Spam Block List) + dns_server: psbl.surriel.com + website: http://psbl.surriel.com/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: SWINOG + dns_server: dnsrbl.swinog.ch + website: https://antispam.imp.ch/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: GBUdb Truncate + dns_server: truncate.gbudb.net + website: http://www.gbudb.com/truncate/index.jsp + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: Weighted Private Block List + dns_server: db.wpbl.info + website: http://www.wpbl.info/ + ipv4: true + ipv6: false + domain: false + non_blocklisted_return_code: [] +- name: AntiCaptcha.NET IPv6 + dns_server: dnsbl6.anticaptcha.net + website: http://anticaptcha.net/ + ipv4: false + ipv6: true + domain: false + non_blocklisted_return_code: [] +- name: Suomispam Blacklist + dns_server: bl.suomispam.net + website: http://suomispam.net/ + ipv4: true + ipv6: true + domain: false + non_blocklisted_return_code: [] +- name: NordSpam + dns_server: bl.nordspam.com + website: https://www.nordspam.com/ + ipv4: true + ipv6: true + domain: false diff --git a/share/ffdhe2048.pem b/share/ffdhe2048.pem new file mode 100644 index 0000000..9b182b7 --- /dev/null +++ b/share/ffdhe2048.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- diff --git a/share/html/502.html b/share/html/502.html new file mode 100644 index 0000000..bef0275 --- /dev/null +++ b/share/html/502.html @@ -0,0 +1,20 @@ + + + +502 Bad Gateway + + + +

502 Bad Gateway

+

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

+

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

+

Thank you for using YunoHost.

+ + diff --git a/share/registrar_list.toml b/share/registrar_list.toml new file mode 100644 index 0000000..bc3defd --- /dev/null +++ b/share/registrar_list.toml @@ -0,0 +1,826 @@ +[aliyun] + [aliyun.auth_key_id] + type = "string" + redact = true + + [aliyun.auth_secret] + type = "string" + redact = true + +[arvancloud] + [arvancloud.auth_token] + type = "string" + redact = true + +[aurora] + [aurora.auth_api_key] + type = "string" + redact = true + + [aurora.auth_secret_key] + type = "string" + redact = true + +[azure] + [azure.auth_client_id] + type = "string" + redact = true + + [azure.auth_client_secret] + type = "string" + redact = true + + [azure.auth_tenant_id] + type = "string" + redact = true + + [azure.auth_subscription_id] + type = "string" + redact = true + + [azure.resource_group] + type = "string" + redact = true + +[cloudflare] + [cloudflare.auth_username] + type = "string" + redact = true + + [cloudflare.auth_token] + type = "string" + redact = true + + [cloudflare.zone_id] + type = "string" + redact = true + +[cloudns] + [cloudns.auth_id] + type = "string" + redact = true + + [cloudns.auth_subid] + type = "string" + redact = true + + [cloudns.auth_subuser] + type = "string" + redact = true + + [cloudns.auth_password] + type = "password" + + [cloudns.weight] + type = "number" + + [cloudns.port] + type = "number" + +[cloudxns] + [cloudxns.auth_username] + type = "string" + redact = true + + [cloudxns.auth_token] + type = "string" + redact = true + +[conoha] + [conoha.auth_region] + type = "string" + redact = true + + [conoha.auth_token] + type = "string" + redact = true + + [conoha.auth_username] + type = "string" + redact = true + + [conoha.auth_password] + type = "password" + + [conoha.auth_tenant_id] + type = "string" + redact = true + +[constellix] + [constellix.auth_username] + type = "string" + redact = true + + [constellix.auth_token] + type = "string" + redact = true + +[ddns] + [ddns.auth_token] + type = "string" + redacte = true + + [ddns.ddns_server] + type = "string" + redact = true + +[devnomads] + [devnomads.auth_token] + type = "string" + redact = true + +[digitalocean] + [digitalocean.auth_token] + type = "string" + redact = true + +[dinahosting] + [dinahosting.auth_username] + type = "string" + redact = true + + [dinahosting.auth_password] + type = "password" + +[directadmin] + [directadmin.auth_password] + type = "password" + + [directadmin.auth_username] + type = "string" + redact = true + + [directadmin.endpoint] + type = "string" + redact = true + +[dnsimple] + [dnsimple.auth_token] + type = "string" + redact = true + + [dnsimple.auth_username] + type = "string" + redact = true + + [dnsimple.auth_password] + type = "password" + + [dnsimple.auth_2fa] + type = "string" + redact = true + +[dnsmadeeasy] + [dnsmadeeasy.auth_username] + type = "string" + redact = true + + [dnsmadeeasy.auth_token] + type = "string" + redact = true + +[dnspark] + [dnspark.auth_username] + type = "string" + redact = true + + [dnspark.auth_token] + type = "string" + redact = true + +[dnspod] + [dnspod.auth_username] + type = "string" + redact = true + + [dnspod.auth_token] + type = "string" + redact = true + +[dnsservices] + [dnsservices.auth_username] + type = "string" + redact = true + + [dnsservices.auth_password] + type = "string" + +[dreamhost] + [dreamhost.auth_token] + type = "string" + redact = true + +[duckdns] + [duckdns.auth_token] + type = "string" + redact = true + +[dynu] + [dynu.auth_token] + type = "string" + redact = true + +[easydns] + [easydns.auth_username] + type = "string" + redact = true + + [easydns.auth_token] + type = "string" + redact = true + +[easyname] + [easyname.auth_username] + type = "string" + redact = true + + [easyname.auth_password] + type = "password" + +[euserv] + [euserv.auth_username] + type = "string" + redact = true + + [euserv.auth_password] + type = "password" + +[exoscale] + [exoscale.auth_key] + type = "string" + redact = true + + [exoscale.auth_secret] + type = "string" + redact = true + +[flexibleengine] + [flexibleengine.auth_token] + type = "string" + redact = true + + [flexibleengine.zone_id] + type = "string" + redact = true + +[gandi] + [gandi.auth_token] + type = "string" + redact = true + + [gandi.api_protocol] + type = "select" + choices.rpc = "RPC" + choices.rest = "REST" + default = "rpc" + visible = "false" + +[gehirn] + [gehirn.auth_token] + type = "string" + redact = true + + [gehirn.auth_secret] + type = "string" + redact = true + +[glesys] + [glesys.auth_username] + type = "string" + redact = true + + [glesys.auth_token] + type = "string" + redact = true + +[godaddy] + [godaddy.auth_key] + type = "string" + redact = true + + [godaddy.auth_secret] + type = "string" + redact = true + +[googleclouddns] + [goggleclouddns.auth_service_account_info] + type = "string" + redact = true + +[gransy] + [gransy.auth_username] + type = "string" + redact = true + + [gransy.auth_password] + type = "password" + +[gratisdns] + [gratisdns.auth_username] + type = "string" + redact = true + + [gratisdns.auth_password] + type = "password" + +[henet] + [henet.auth_username] + type = "string" + redact = true + + [henet.auth_password] + type = "password" + +[hetzner] + [hetzner.auth_token] + type = "string" + redact = true + +[hostingde] + [hostingde.auth_token] + type = "string" + redact = true + +[hover] + [hover.auth_username] + type = "string" + redact = true + + [hover.auth_password] + type = "password" + + [hover.auth_totp_secret] + type = "string" + redact = true + +[infoblox] + [infoblox.auth_user] + type = "string" + redact = true + + [infoblox.auth_psw] + type = "password" + + [infoblox.ib_view] + type = "string" + redact = true + + [infoblox.ib_host] + type = "string" + redact = true + +[infomaniak] + [infomaniak.auth_token] + type = "string" + redact = true + +[internetbs] + [internetbs.auth_key] + type = "string" + redact = true + + [internetbs.auth_password] + type = "string" + redact = true + +[inwx] + [inwx.auth_username] + type = "string" + redact = true + + [inwx.auth_password] + type = "password" + +[ionos] + [ionos.api_key] + type = "string" + redact = true + +[joker] + [joker.auth_token] + type = "string" + redact = true + +[linode] + [linode.auth_token] + type = "string" + redact = true + +[linode4] + [linode4.auth_token] + type = "string" + redact = true + +[localzone] + [localzone.filename] + type = "string" + redact = true + +[luadns] + [luadns.auth_username] + type = "string" + redact = true + + [luadns.auth_token] + type = "string" + redact = true + +[memset] + [memset.auth_token] + type = "string" + redact = true + +[misaka] + [misaka.auth_token] + type = "string" + redact = true + +[mythicbeasts] + [mythicbeasts.auth_username] + type = "string" + redact = true + + [mythicbeasts.auth_password] + type = "password" + + [mythicbeasts.auth_token] + type = "string" + redact = true + +[namecheap] + [namecheap.auth_token] + type = "string" + redact = true + + [namecheap.auth_username] + type = "string" + redact = true + + [namecheap.auth_client_ip] + type = "string" + redact = true + + [namecheap.auth_sandbox] + type = "string" + redact = true + +[namecom] + [namecom.auth_username] + type = "string" + redact = true + + [namecom.auth_token] + type = "string" + redact = true + +[namesilo] + [namesilo.auth_token] + type = "string" + redact = true + +[netcup] + [netcup.auth_customer_id] + type = "string" + redact = true + + [netcup.auth_api_key] + type = "string" + redact = true + + [netcup.auth_api_password] + type = "string" + redact = true + +[nfsn] + [nfsn.auth_username] + type = "string" + redact = true + + [nfsn.auth_token] + type = "string" + redact = true + +[njalla] + [njalla.auth_token] + type = "string" + redact = true + +[nsone] + [nsone.auth_token] + type = "string" + redact = true + +[oci] + [oci.auth_config_file] + type = "string" + redact = true + + [oci.auth_profile] + type = "string" + redact = true + + [oci.auth_user] + type = "string" + redact = true + + [oci.auth_tenancy] + type = "string" + redact = true + + [oci.auth_fingerprint] + type = "string" + redact = true + + [oci.auth_key_content] + type = "string" + redact = true + + [oci.auth_key_file] + type = "string" + redact = true + + [oci.auth_pass_phrase] + type = "password" + + [oci.auth_region] + type = "string" + redact = true + + [oci.auth_type] + type = "select" + choices = ["api_key", "instance_principal"] + default = "api_key" + +[onapp] + [onapp.auth_username] + type = "string" + redact = true + + [onapp.auth_token] + type = "string" + redact = true + + [onapp.auth_server] + type = "string" + redact = true + +[online] + [online.auth_token] + type = "string" + redact = true + +[ovh] + [ovh.auth_entrypoint] + type = "select" + choices = ["ovh-eu", "ovh-ca", "ovh-us", "soyoustart-eu", "soyoustart-ca", "kimsufi-eu", "kimsufi-ca"] + default = "ovh-eu" + + [ovh.auth_application_key] + type = "string" + redact = true + + [ovh.auth_application_secret] + type = "string" + redact = true + + [ovh.auth_consumer_key] + type = "string" + redact = true + +[plesk] + [plesk.auth_username] + type = "string" + redact = true + + [plesk.auth_password] + type = "password" + + [plesk.plesk_server] + type = "string" + redact = true + +[pointhq] + [pointhq.auth_username] + type = "string" + redact = true + + [pointhq.auth_token] + type = "string" + redact = true + +[porkbun] + [porkbun.auth_key] + type = "string" + redact = true + + [porkbun.auth_secret] + type = "string" + redact = true + +[powerdns] + [powerdns.auth_token] + type = "string" + redact = true + + [powerdns.pdns_server] + type = "string" + redact = true + + [powerdns.pdns_server_id] + type = "string" + redact = true + + [powerdns.pdns_disable_notify] + type = "boolean" + +[qcloud] + [qcloud.secret_id] + type = "string" + redact = true + + [qcloud.secret_key] + type = "string" + redact = true + +[rackspace] + [rackspace.auth_account] + type = "string" + redact = true + + [rackspace.auth_username] + type = "string" + redact = true + + [rackspace.auth_api_key] + type = "string" + redact = true + + [rackspace.auth_token] + type = "string" + redact = true + + [rackspace.sleep_time] + type = "string" + redact = true + +[rage4] + [rage4.auth_username] + type = "string" + redact = true + + [rage4.auth_token] + type = "string" + redact = true + +[rcodezero] + [rcodezero.auth_token] + type = "string" + redact = true + +[regfish] + [regfish.auth_api_key] + type = "string" + redact = true + +[route53] + [route53.auth_access_key] + type = "string" + redact = true + + [route53.auth_access_secret] + type = "string" + redact = true + + [route53.private_zone] + type = "string" + redact = true + + [route53.zone_id] + type = "string" + redact = true + + [route53.auth_username] + type = "string" + redact = true + + [route53.auth_token] + type = "string" + redact = true + +[safedns] + [safedns.auth_token] + type = "string" + redact = true + +[sakuracloud] + [sakuracloud.auth_token] + type = "string" + redact = true + + [sakuracloud.auth_secret] + type = "string" + redact = true + +[scaleway] + [scaleway.auth_secret_key] + type = "string" + redact = true + +[softlayer] + [softlayer.auth_username] + type = "string" + redact = true + + [softlayer.auth_api_key] + type = "string" + redact = true + +[timeweb] + [timeweb.auth_token] + type = "string" + redact = true + +[transip] + [transip.auth_username] + type = "string" + redact = true + + [transip.auth_api_key] + type = "string" + redact = true + +[ultradns] + [ultradns.auth_token] + type = "string" + redact = true + + [ultradns.auth_username] + type = "string" + redact = true + + [ultradns.auth_password] + type = "password" + +[valuedomain] + [valuedomain.auth_token] + type = "string" + redact = true + +[vercel] + [vercel.auth_token] + type = "string" + redact = true + +[vultr] + [vultr.auth_token] + type = "string" + redact = true + +[wedos] + [wedos.auth_username] + type = "string" + + [wedos.auth_pass] + type = "password" + +[yandex] + [yandex.auth_token] + type = "string" + redact = true + +[yandexcloud] + [yandexcloud.auth_token] + type = "string" + redact = true + + [yandexcloud.dns_zone_id] + type = "string" + redact = true + + [yandexcloud.cloud_id] + type = "string" + redact = true + + [yandexcloud.folder_id] + type = "string" + redact = true + +[zeit] + [zeit.auth_token] + type = "string" + redact = true + +[zilore] + [zilore.auth_key] + type = "string" + redact = true + +[zonomi] + [zonomy.auth_token] + type = "string" + redact = true + + [zonomy.auth_entrypoint] + type = "string" + redact = true + diff --git a/share/yunohost-nftables-hooks b/share/yunohost-nftables-hooks new file mode 100755 index 0000000..0bdb960 --- /dev/null +++ b/share/yunohost-nftables-hooks @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +HOOK_FOLDER="/usr/share/yunohost/hooks/" +CUSTOM_HOOK_FOLDER="/etc/yunohost/hooks.d/" + +run_parts() { + dir="$1" + if [ -d "$dir" ]; then + run-parts "$dir" + fi +} + +pre_or_post="${1:-pre}" + +run_parts "$HOOK_FOLDER/${pre_or_post}_nftables" +run_parts "$CUSTOM_HOOK_FOLDER/${pre_or_post}_nftables" + +# This one is legacy, apps should use the new ${pre_or_post}_nftables hook +run_parts "$HOOK_FOLDER/${pre_or_post}_iptable_rules" +run_parts "$CUSTOM_HOOK_FOLDER/${pre_or_post}_iptable_rules" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..d048825 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import sys +from typing import TYPE_CHECKING, Literal, NoReturn + +if TYPE_CHECKING: + import argparse + + from moulinette.core import MoulinetteLock + +from pathlib import Path + +import moulinette +from moulinette import m18n +from moulinette.interfaces.cli import colorize, get_locale + +from .utils.logging import init_logging + + +def is_installed() -> bool: + """Returns whether YunoHost is installed on the system.""" + return os.path.isfile("/etc/yunohost/installed") + + +def cli( + debug: bool, + quiet: bool, + output_as: str, + timeout: int | None, + args: list[str], + parser: "argparse.ArgumentParser", +) -> NoReturn: + """Entry point for YunoHost CLI""" + init_logging(interface="cli", debug=debug, quiet=quiet) + + # Check that YunoHost is installed + if not is_installed(): + check_command_is_valid_before_postinstall(args) + + ret = moulinette.cli( + args, + actionsmap="/usr/share/yunohost/actionsmap.yml", + locales_dir="/usr/share/yunohost/locales/", + output_as=output_as, + timeout=timeout, + top_parser=parser, + ) + sys.exit(ret) + + +def api(debug: bool, host: str, port: int, actionsmap: str | None = None) -> NoReturn: + """Entry point for YunoHost API server""" + actionsmap = actionsmap or "/usr/share/yunohost/actionsmap.yml" + path = Path(actionsmap).resolve() + if path.exists(): + actionsmap = str(path) + + 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() -> dict[Literal["installed"], bool]: + return {"installed": is_installed()} + + # FIXME : someday, maybe find a way to disable route /postinstall if + # postinstall already done ... + + ret = moulinette.api( + host=host, + port=port, + actionsmap=actionsmap, + locales_dir="/usr/share/yunohost/locales/", + routes={("GET", "/installed"): is_installed_api}, + allowed_cors_origins=allowed_cors_origins, + ) + sys.exit(ret) + + +def portalapi(debug: bool, host: str, port: int) -> NoReturn: + """Entry point for YunoHost Portal API server""" + 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) + + +def check_command_is_valid_before_postinstall(args: list[str]) -> None: + """Asserts if the given command is valid before running postinstall, or exits 1""" + allowed_if_not_postinstalled = [ + "tools postinstall", + "tools versions", + "tools shell", + "backup list", + "backup restore", + "log display", + ] + + if len(args) < 2 or (args[0] + " " + args[1] not in allowed_if_not_postinstalled): + init_i18n() + print(colorize(m18n.g("error"), "red") + " " + m18n.n("yunohost_not_installed")) + sys.exit(1) + + +def init( + interface: str = "cli", + debug: bool = False, + quiet: bool = False, + logdir: str = "/var/log/yunohost", +) -> "MoulinetteLock": + """ + This is a small util function ONLY meant to be used to initialize a Yunohost + context when ran from tests or from scripts. + """ + init_logging(interface=interface, debug=debug, quiet=quiet, logdir=logdir) + init_i18n() + from moulinette.core import MoulinetteLock + + lock = MoulinetteLock("yunohost", timeout=30) + lock.acquire() + return lock + + +def init_i18n() -> None: + """ + Initialize the i18n locale dir and locale. + This should only be called when not willing to go through moulinette.cli + or moulinette.api but still willing to call m18n.n/g... + """ + m18n.set_locales_dir("/usr/share/yunohost/locales/") + m18n.set_locale(get_locale()) diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..edde179 --- /dev/null +++ b/src/app.py @@ -0,0 +1,2705 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import time +from logging import getLogger +from pathlib import Path +from shutil import rmtree +from typing import ( + TYPE_CHECKING, + Any, + Iterator, + Literal, + NotRequired, + Required, + TypedDict, + Union, + cast, +) + +from moulinette import Moulinette, m18n + +from .app_catalog import ( # noqa + APPS_CATALOG_LOGOS, + _load_apps_catalog, + app_catalog, # Unused but imported because it's exposed via Moulinette + app_search, # Unused but imported because it's exposed via Moulinette +) +from .log import OperationLogger, is_flash_unit_operation, is_unit_operation +from .utils.app_utils import ( + APPS_SETTING_PATH, + AppManifest, + AppNotificationsDict, + AppRequirementCheckResult, + _ask_confirmation, + _assert_is_installed, + _assert_no_conflicting_apps, + _assert_system_is_sane_for_app, + _check_manifest_requirements, + _display_notifications, + _extract_app, + _filter_and_hydrate_notifications, + _get_app_settings, + _get_manifest_of_app, + _guess_webapp_path_requirement, + _hydrate_app_template, + _installed_apps, + _is_installed, + _make_environment_for_app_script, + _make_tmp_workdir_for_app, + _parse_app_version, + _set_app_settings, + _validate_webpath_requirement, +) +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import ( + chmod, + chown, + cp, + read_file, + rm, + write_to_file, +) + +if TYPE_CHECKING: + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + + from .utils.configpanel import ConfigPanelModel, RawConfig, RawSettings + from .utils.form import FormModel + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.app")) +else: + logger = getLogger("yunohost.app") + + +PORTAL_SETTINGS_DIR = "/etc/yunohost/portal" +APP_FILES_TO_COPY = [ + "manifest.json", + "manifest.toml", + "config_panel.toml", + "scripts", + "conf", + "hooks", + "doc", +] + + +class AppInfo(TypedDict, total=False): + id: Required[str] + name: Required[str] + description: Required[str] + version: Required[str] + domain_path: str + logo: str | None + upgrade: "AppUpgradeInfos" + settings: dict[str, Any] + setting_path: str + manifest: AppManifest + from_catalog: dict[str, Any] + is_webapp: bool + is_default: bool + supports_change_url: bool + supports_backup_restore: bool + supports_multi_instance: bool + supports_config_panel: bool + supports_purge: bool + permissions: dict[str, Any] + label: str + notifications: dict[str, dict[str, str]] + + +def app_list(full: bool = False) -> dict[Literal["apps"], list[AppInfo]]: + """ + List installed apps + """ + + out = [] + for app_id in sorted(_installed_apps()): + try: + app_info_dict = app_info(app_id, full=full) + except Exception as e: + logger.error(f"Failed to read info for {app_id} : {e}") + continue + out.append(app_info_dict) + + return {"apps": out} + + +def app_info( + app: str, + full: bool = False, + with_upgrade_infos: bool = False, + with_pre_upgrade_notifications: bool = False, + with_settings: bool = False, +) -> AppInfo: + from tempfile import TemporaryDirectory + + from .domain import _get_raw_domain_settings + from .permission import user_permission_list + from .utils.app_utils import ( + APPS_TMP_WORKDIRS, + _get_app_label, + _git_clone_light, + _notification_is_dismissed, + _parse_app_doc_and_notifications, + ) + from .utils.i18n import _value_for_locale + + _assert_is_installed(app) + + local_manifest = _get_manifest_of_app(app) + settings = _get_app_settings(app) + main_perm = settings.get("_permissions", {}).get("main", {}) + + ret: AppInfo = { + "id": app, + "name": _get_app_label(app, local_manifest), + "description": main_perm.get("description") + or _value_for_locale(local_manifest["description"]), + "version": local_manifest.get("version", "-"), + } + + if "domain" in settings and "path" in settings: + ret["domain_path"] = settings["domain"] + settings["path"] + + if full or with_upgrade_infos: + ret["upgrade"] = _app_upgrade_infos(app, current_version=ret["version"]) + + if ( + "upgrade" in ret + and with_pre_upgrade_notifications + and ret["upgrade"]["status"] not in ["up_to_date", "url_required"] + ): + url = ret["upgrade"]["url"] + specific_channel = ret["upgrade"]["specific_channel"] + new_revision = ret["upgrade"]["new_revision"] + + tmp_notifications: AppNotificationsDict = {} + if url and new_revision: + try: + with TemporaryDirectory(prefix="app_", dir=APPS_TMP_WORKDIRS) as d: + _git_clone_light( + d, url, branch=specific_channel, revision=new_revision + ) + _, tmp_notifications = _parse_app_doc_and_notifications(Path(d)) + except Exception as e: + logger.warning( + f"Failed to check pre-upgrade notifications for {app} : {e}", + exc_info=True, + ) + + if tmp_notifications.get("PRE_UPGRADE"): + ret["upgrade"]["notifications"] = _filter_and_hydrate_notifications( + tmp_notifications["PRE_UPGRADE"], + ret["version"], + settings, + ) + + if full or with_settings: + ret["settings"] = settings + + if not full: + return ret + + ret["manifest"] = local_manifest + + base_app_id = app.split("__")[0] + ret["from_catalog"] = _load_apps_catalog()["apps"].get(base_app_id, {}) + + # Check if $app.png exists in the app logo folder, this is a trick to be able to easily customize the logo + # of an app just by creating $app.png (instead of the hash.png) in the corresponding folder + if (Path(APPS_CATALOG_LOGOS) / f"{app}.png").exists(): + ret["logo"] = app + else: + ret["logo"] = main_perm.get("logo_hash") or ret["from_catalog"].get("logo_hash") # type: ignore[typeddict-item] + + # Hydrate app notifications and doc + rendered_doc: dict[str, dict[str, str]] = {} + for pagename, content_per_lang in ret["manifest"]["doc"].items(): + for lang, content in content_per_lang.items(): + 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"] = { + k: v + for k, v in ret["manifest"]["notifications"].items() + if not _notification_is_dismissed(k, settings) + } + + # Hydrate notifications (also filter uneeded post_upgrade notification based on version) + for step, notifications in ret["manifest"]["notifications"].items(): + rendered_notifications: dict[str, dict[str, str]] = {} + for name, content_per_lang in notifications.items(): + for lang, content in content_per_lang.items(): + 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 settings["domain"] and "path" in settings + ) + + if ret["is_webapp"]: + ret["is_default"] = ( + _get_raw_domain_settings(settings["domain"]).get("default_app") == app + ) + + setting_path = Path(APPS_SETTING_PATH) / app + ret["supports_change_url"] = (setting_path / "scripts" / "change_url").exists() + ret["supports_backup_restore"] = ( + setting_path / "scripts" / "backup" + ).exists() and (setting_path / "scripts" / "restore").exists() + ret["supports_multi_instance"] = local_manifest.get("integration", {}).get( + "multi_instance", False + ) + ret["supports_config_panel"] = (setting_path / "config_panel.toml").exists() + ret["supports_purge"] = ( + local_manifest["packaging_format"] >= 2 + and local_manifest["resources"].get("data_dir") is not None + ) + + ret["permissions"] = user_permission_list( + full=True, absolute_urls=True, apps=[app] + )["permissions"] + + # FIXME: this is the same stuff as "name" ... maybe we should get rid of "name" or "label" ? + ret["label"] = ret["name"] + + return ret + + +class AppUpgradeInfos(TypedDict): + status: Literal[ + "upgradable", "up_to_date", "url_required", "bad_quality", "fail_requirements" + ] + message: str + url: str | None + current_version: str + new_version: str | None + new_revision: str | None + requirements: dict[str, "AppRequirementCheckResult"] | None + specific_channel: str | None + specific_channel_message: str | None + notifications: NotRequired[dict[str, str]] + + +def _app_upgrade_infos(app: str, current_version: str | None = None) -> AppUpgradeInfos: + base_app_id = app.split("__")[0] + app_in_catalog = _load_apps_catalog()["apps"].get(base_app_id, {}) + + # current_version can be provided to avoid re-reading the manifest from scratch (eg when in app_info) + # Otherwise we read it here + if current_version is None: + current_version = _get_manifest_of_app(app).get("version", "0~ynh0") + + assert current_version + + if not app_in_catalog or "git" not in app_in_catalog: + return { + "status": "url_required", + "message": m18n.n("app_upgrade_url_required"), + "url": None, + "current_version": current_version, + "new_version": None, + "new_revision": None, + "requirements": None, + "specific_channel": None, + "specific_channel_message": None, + } + + url = app_in_catalog["git"]["url"] + current_revision = _get_app_settings(app).get("current_revision", "?")[:7] + available_upgrade_channels = app_in_catalog.get("alternative_branches", {}) + specific_channel: str | None = _get_app_settings(app).get("upgrade_channel") + specific_channel_pr_url: str | None = None + specific_channel_message: str | None = None + manifest_in_catalog = app_in_catalog.get("manifest", {}) + + if specific_channel and specific_channel in available_upgrade_channels: + channel = available_upgrade_channels[specific_channel] + ahead = channel["ahead"] + if ahead: + level = channel["level"] + new_revision = channel["revision"] + new_version = channel["version"] + specific_channel_pr_url = channel["pr_url"] + specific_channel_message = m18n.n( + "app_upgrade_specific_channel_msg", + channel=specific_channel, + pr_url=specific_channel_pr_url, + ) + else: + logger.debug( + f"Ignoring specific upgrade channel '{specific_channel}' for '{app}', because it's not currently ahead of the default branch. The default branch will be used instead." + ) + specific_channel = None + elif specific_channel: + logger.warning( + f"Unknown specific upgrade channel '{specific_channel}' for '{app}'. Falling back to default." + ) + specific_channel = None + + if specific_channel is None: + new_version = manifest_in_catalog.get("version", "0~ynh0") + new_revision = app_in_catalog.get("git", {}).get("revision", "?") + level = app_in_catalog.get("level", -1) + + # Do not advertise upgrades for bad-quality apps + if ( + not (isinstance(level, int) and level >= 5) + or app_in_catalog.get("state") != "working" + ): + return { + "status": "bad_quality", + "message": m18n.n("app_upgrade_bad_quality"), + "url": url, + "current_version": current_version, + "new_version": None, + "new_revision": None, + "requirements": None, + "specific_channel": specific_channel, + "specific_channel_message": specific_channel_message, + } + + if _parse_app_version(current_version) >= _parse_app_version(new_version) and ( + specific_channel is None or new_revision == current_revision + ): + return { + "status": "up_to_date", + "message": m18n.n( + "app_upgrade_up_to_date", current_version=current_version + ), + "url": None, + "current_version": current_version, + "new_version": new_version, + "new_revision": new_revision, + "requirements": None, + "specific_channel": specific_channel, + "specific_channel_message": specific_channel_message, + } + + # Not sure when this happens exactly considering we checked for ">=" before ... + # maybe that's for "legacy versions" that do not respect the X.Y~ynhZ syntax + # + # Update: well it does cover the alternative upgrade channel now (typically testing) + # where the version may not have been bumped but we want a way to advertise it anyway + # and distinguish the two versions, using the commit id + if current_version == new_version: + current_version += f" ({current_revision or '?'})" + new_version = f"{new_version} ({new_revision[:7]})" + else: + new_version = new_version + + # Check requirements + + requirements = { + r["id"]: r + for r in _check_manifest_requirements( + manifest_in_catalog, action="upgrade", app=app + ) + } + pass_requirements = all(r["passed"] for r in requirements.values()) + failed_requirements = " ; ".join( + [r["error"] for r in requirements.values() if not r["passed"]] + ) + + status: Literal["upgradable", "fail_requirements"] = ( + "upgradable" if pass_requirements else "fail_requirements" + ) + return { + "status": status, + # i18n: app_upgrade_upgradable + # i18n: app_upgrade_fail_requirements + "message": m18n.n( + f"app_upgrade_{status}", + current_version=current_version, + new_version=new_version, + failed_requirements=failed_requirements, + ), + "url": url, + "current_version": current_version, + "new_version": new_version, + "new_revision": new_revision, + "requirements": requirements, + "specific_channel": specific_channel, + "specific_channel_message": specific_channel_message, + } + + +def app_map( + app: str | None = None, raw: bool = False, user: str | None = None +) -> dict[str, Any]: + """ + Returns a map of url <-> app id such as : + + { + "domain.tld/foo": "foo__2", + "domain.tld/mail: "rainloop", + "other.tld/": "bar", + "sub.other.tld/pwet": "pwet", + } + + When using "raw", the structure changes to : + + { + "domain.tld": { + "/foo": {"label": "App foo", "id": "foo__2"}, + "/mail": {"label": "Rainloop", "id: "rainloop"}, + }, + "other.tld": { + "/": {"label": "Bar", "id": "bar"}, + }, + "sub.other.tld": { + "/pwet": {"label": "Pwet", "id": "pwet"} + } + } + """ + + from .permission import AppPermInfos, user_permission_list + + apps = [] + result: dict[str, Any] = {} + + if app is not None: + _assert_is_installed(app) + apps = [ + app, + ] + else: + apps = _installed_apps() + + permissions = user_permission_list(full=True, absolute_urls=True, apps=apps)[ + "permissions" + ] + for app in apps: + app_settings = _get_app_settings(app) + if not app_settings: + continue + if "domain" not in app_settings: + continue + if "path" not in app_settings: + # we assume that an app that doesn't have a path doesn't have an HTTP api + continue + # This 'no_sso' settings sound redundant to not having $path defined .... + # At least from what I can see, all apps using it don't have a path defined ... + if ( + "no_sso" in app_settings + ): # I don't think we need to check for the value here + continue + # Users must at least have access to the main permission to have access to extra permissions + if user: + if not app + ".main" in permissions: + logger.warning( + f"Uhoh, no main permission was found for app {app} ... sounds like an app was only partially removed due to another bug :/" + ) + continue + main_perm = permissions[app + ".main"] + if user not in main_perm["corresponding_users"]: + continue + + this_app_perms: dict[str, AppPermInfos] = { + p: i # type: ignore + for p, i in permissions.items() + if p.startswith(app + ".") and (i["url"] or i["additional_urls"]) # type: ignore + } + + for perm_info in this_app_perms.values(): + # If we're building the map for a specific user, check the user + # actually is allowed for this specific perm + if user and user not in perm_info["corresponding_users"]: + continue + + perm_label = perm_info["label"] + perm_all_urls = list( + filter(None, [perm_info["url"], *perm_info["additional_urls"]]) + ) + + for url in perm_all_urls: + # Here, we decide to completely ignore regex-type urls ... + # Because : + # - displaying them in regular "yunohost app map" output creates + # a pretty big mess when there are multiple regexes for the same + # app ? (c.f. for example lufi) + # - it doesn't really make sense when checking app conflicts to + # compare regexes ? (Or it could in some cases but ugh ?) + # + if url.startswith("re:"): + continue + + if not raw: + result[url] = perm_label + else: + if "/" in url: + perm_domain, perm_path = url.split("/", 1) + perm_path = "/" + perm_path + else: + perm_domain = url + perm_path = "/" + if perm_domain not in result: + result[perm_domain] = {} + result[perm_domain][perm_path] = {"label": perm_label, "id": app} + + return result + + +@is_unit_operation() +def app_change_url( + operation_logger: "OperationLogger", app: str, domain: str, path: str +) -> None: + """ + Modify the URL at which an application is installed. + + Keyword argument: + app -- Taget app instance name + domain -- New app domain on which the application will be moved + path -- New path at which the application will be move + + """ + from .hook import hook_callback, hook_exec_with_script_debug_if_failure + from .service import service_reload_or_restart + from .utils.form import DomainOption, WebPathOption + + _assert_is_installed(app) + + if not os.path.exists( + os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url") + ): + raise YunohostValidationError("app_change_url_no_script", app_name=app) + + old_domain = app_setting(app, "domain") + old_path = app_setting(app, "path") + + assert isinstance(old_domain, str) + assert isinstance(old_path, str) + + # Normalize path and domain format + + domain = DomainOption.normalize(domain) + old_domain = DomainOption.normalize(old_domain) + path = WebPathOption.normalize(path) + old_path = WebPathOption.normalize(old_path) + + if (domain, path) == (old_domain, old_path): + raise YunohostValidationError( + "app_change_url_identical_domains", domain=domain, path=path + ) + + app_setting_path = os.path.join(APPS_SETTING_PATH, app) + path_requirement = _guess_webapp_path_requirement(app_setting_path) + _validate_webpath_requirement( + {"domain": domain, "path": path}, path_requirement, ignore_app=app + ) + if path_requirement == "full_domain" and path != "/": + raise YunohostValidationError("app_change_url_require_full_domain", app=app) + + tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) + + # Prepare env. var. to pass to script + env_dict = _make_environment_for_app_script( + app, workdir=tmp_workdir_for_app, action="change_url" + ) + + env_dict["YNH_APP_OLD_DOMAIN"] = old_domain + env_dict["YNH_APP_OLD_PATH"] = old_path + env_dict["YNH_APP_NEW_DOMAIN"] = domain + env_dict["YNH_APP_NEW_PATH"] = path + + env_dict["old_domain"] = old_domain + env_dict["old_path"] = old_path + env_dict["new_domain"] = domain + env_dict["new_path"] = path + env_dict["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" + + if domain != old_domain: + operation_logger.related_to.append(("domain", old_domain)) + operation_logger.extra.update({"env": env_dict}) + operation_logger.start() + + old_nginx_conf_path = f"/etc/nginx/conf.d/{old_domain}.d/{app}.conf" + new_nginx_conf_path = f"/etc/nginx/conf.d/{domain}.d/{app}.conf" + old_nginx_conf_backup = None + if not os.path.exists(old_nginx_conf_path): + logger.warning( + f"Current nginx config file {old_nginx_conf_path} doesn't seem to exist ... wtf ?" + ) + else: + old_nginx_conf_backup = read_file(old_nginx_conf_path) + + change_url_script = os.path.join(tmp_workdir_for_app, "scripts/change_url") + + # Execute App change_url script + change_url_failed = True + try: + ( + change_url_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + change_url_script, + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_change_url_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_change_url_failed", app=app, error=e + ), + ) + finally: + rmtree(tmp_workdir_for_app) + + if change_url_failed: + logger.warning("Restoring initial nginx config file") + if old_nginx_conf_path != new_nginx_conf_path and os.path.exists( + new_nginx_conf_path + ): + rm(new_nginx_conf_path, force=True) + if old_nginx_conf_backup: + write_to_file(old_nginx_conf_path, old_nginx_conf_backup) + service_reload_or_restart("nginx") + + # restore values modified by app_checkurl + # see begining of the function + app_setting(app, "domain", value=old_domain) + app_setting(app, "path", value=old_path) + raise YunohostError(failure_message_with_debug_instructions, raw_msg=True) + else: + # make sure the domain/path setting are propagated + app_setting(app, "domain", value=domain) + app_setting(app, "path", value=path) + + app_ssowatconf() + + service_reload_or_restart("nginx") + + logger.success( + m18n.n("app_change_url_success", app=app, domain=domain, path=path) + ) + + hook_callback("post_app_change_url", env=env_dict) + + +def app_upgrade( + app: str | list[str] = [], + url: str | None = None, + file: str | None = None, + force: bool = False, + no_safety_backup: bool = False, + continue_on_failure: bool = False, + ignore_yunohost_version: bool = False, +) -> ( + None + | dict[Literal["success", "failed", "cancelled"], Any] + | dict[Literal["notifications"], dict[Literal["POST_UPGRADE"], dict[str, str]]] +): + """ + Upgrade app + + Keyword argument: + app -- App(s) to upgrade (default all) + url -- Git url to fetch for upgrade + file -- Folder or tarball for upgrade + no_safety_backup -- Disable the safety backup during upgrade + + """ + from .backup import ( + backup_create, + backup_delete, + backup_list, + backup_restore, + ) + from .hook import ( + hook_add, + hook_callback, + hook_exec_with_script_debug_if_failure, + hook_remove, + ) + from .permission import _sync_permissions_with_ldap + from .regenconf import manually_modified_files + from .utils.legacy import _patch_legacy_helpers + from .utils.system import free_space_in_directory + + # "app" is a bad name for this arg but meh that's legacy and possibly can't easily be changed + # (and actually a bit relevant in terms of what's shown in --help) + raw_requested_targets = app + if not raw_requested_targets: + requested_targets = _installed_apps() + elif isinstance(raw_requested_targets, str): + requested_targets = [raw_requested_targets] + else: + requested_targets = raw_requested_targets.copy() + # Remove possible duplicates + requested_targets = [ + app_ + for i, app_ in enumerate(requested_targets) + if app_ not in requested_targets[:i] + ] + # Abort if any of those app is in fact not installed.. + for app_ in requested_targets: + _assert_is_installed(app_) + + # Check if disk space available + if free_space_in_directory("/") <= 512 * 1000 * 1000: + raise YunohostValidationError("disk_space_not_sufficient_update") + + def _check_upgrade_targets( + requested_targets: list[str], + ) -> Iterator[tuple[str, str, str]]: + if (url or file) and len(requested_targets) > 1 and not isinstance(file, dict): + raise YunohostValidationError( + "You provided an url or file to 'yunohost app upgrade' with several targets to upgrade ... it's unclear what to do with this. Please provide a single target when specifying a file or url to 'yunohost app upgrade'!", + raw_msg=True, + ) + + for app_ in requested_targets: + logger.debug(f"Checking upgradability for {app_}") + upgrade_infos = _app_upgrade_infos(app_) + status = upgrade_infos["status"] + current_version = upgrade_infos.get("current_version") + new_version = upgrade_infos.get("new_version") + + new_app_src: str + if file and isinstance(file, dict): + # We use this hack to test chained upgrades in unit/functional tests + new_app_src = file[app_] + elif file: + new_app_src = file + elif url: + new_app_src = url + elif status == "url_required": + logger.warning(m18n.n("app_upgrade_cli_url_required", app=app_)) + continue + elif upgrade_infos.get("specific_channel"): + assert upgrade_infos["url"] + assert upgrade_infos["new_revision"] + new_app_src = ( + upgrade_infos["url"] + "/tree/" + upgrade_infos["new_revision"] + ) + else: + # Use the infos from the catalog + app_base_id = app_.split("__")[0] + new_app_src = app_base_id + + if file or url: + # It's a bit brutal because we'll do it again later ... + # ... but using -f or -u is supposed to remain occasional and not the nominal case + # so doesnt feel worth it to optimize (and there's also cache mechanism for _git_clone under the hood) + new_manifest, extracted_app_folder = _extract_app(new_app_src) + new_version = new_manifest.get("version", "?") + msg = m18n.n( + "app_upgrade_cli_will_upgrade", + app=app_, + current_version=current_version, + new_version=new_version, + ) + yield (app_, msg, new_app_src) + elif status in ["upgradable", "fail_requirements"]: + # We allow "fail_requirements" here because the requirements are re-checked later with a possibility to bypass them + # (so effectivly they're checked twice but meh) + msg = m18n.n( + "app_upgrade_cli_will_upgrade", + app=app_, + current_version=current_version, + new_version=new_version, + ) + yield (app_, msg, new_app_src) + elif status == "up_to_date": + if force: + msg = m18n.n( + "app_upgrade_cli_will_force_upgrade", + app=app_, + current_version=current_version, + ) + yield (app_, msg, new_app_src) + else: + logger.info( + m18n.n( + "app_upgrade_cli_up_to_date", + app=app_, + current_version=current_version, + ) + ) + elif status == "bad_quality": + logger.warning(m18n.n("app_upgrade_cli_bad_quality", app=app_)) + else: + logger.error(f"Unknown upgrade status '{status}' for {app_} !?") + + def _check_manifest_requirements_with_option_to_bypass(app_, new_manifest): + # Check requirements + failed_requirements = { + r["id"]: r + for r in _check_manifest_requirements( + new_manifest, action="upgrade", app=app_ + ) + if not r["passed"] + } + for id_, check in failed_requirements.items(): + logger.warning(app_ + ": " + check["error"]) + if id_ == "ram": + # i18n: confirm_app_insufficient_ram + _ask_confirmation("confirm_app_insufficient_ram", force=force) + elif id_ == "required_yunohost_version" and ignore_yunohost_version: + pass + else: + return False + return True + + def _upgrade_single_app( + app_, current_manifest, new_manifest, workdir, no_safety_backup + ) -> dict: + logger.info(m18n.n("app_upgrade_app_name", app=app_)) + + # Get current_version and new version + app_new_version_raw = new_manifest.get("version", "?") + assert isinstance(app_new_version_raw, str) + app_new_version = _parse_app_version(app_new_version_raw) + + app_current_version_raw = current_manifest.get("version", "?") + assert isinstance(app_current_version_raw, str) + app_current_version = _parse_app_version(app_current_version_raw) + + # Manage upgrade type and avoid any upgrade if there is nothing to do + # (LEGACY hmpf, we should get rid of this somehow ...) + upgrade_type = "UNKNOWN" + if "~ynh" in str(app_current_version_raw) and "~ynh" in str( + app_new_version_raw + ): + if app_current_version > app_new_version: + upgrade_type = "DOWNGRADE" + elif app_current_version == app_new_version: + upgrade_type = "UPGRADE_SAME" + else: + 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" + else: + upgrade_type = "UPGRADE_APP" + + if new_manifest["packaging_format"] >= 2: + if no_safety_backup: + # FIXME: i18n + logger.warning( + "Skipping the creation of a backup prior to the upgrade." + ) + else: + # FIXME: i18n + logger.info("Creating a safety backup prior to the upgrade") + + # Switch between pre-upgrade1 or pre-upgrade2 + safety_backup_name = f"{app_}-pre-upgrade1" + other_safety_backup_name = f"{app_}-pre-upgrade2" + if safety_backup_name in backup_list()["archives"]: + safety_backup_name = f"{app_}-pre-upgrade2" + other_safety_backup_name = f"{app_}-pre-upgrade1" + + 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_], system=None) + except Exception as e: + raise YunohostError( + f"Aborting the upgrade, because a safety backup could not be created ({e})", + raw_msg=True, + ) + 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 + if other_safety_backup_name in backup_list()["archives"]: + backup_delete(other_safety_backup_name, display_success=False) + logger.info( + m18n.n( + "backup_before_upgrade_deleted_because_replaced_by_newer_backup", + name=other_safety_backup_name, + newname=safety_backup_name, + ) + ) + else: + # Is this needed ? Shouldn't backup_create report an expcetion if backup failed ? + raise YunohostError( + "Uhoh the safety backup failed ?! Aborting the upgrade process.", + raw_msg=True, + ) + + _assert_system_is_sane_for_app(new_manifest, "pre") + + # We'll check that the app didn't brutally edit some system configuration + manually_modified_files_before_install = manually_modified_files() + + # Attempt to patch legacy helpers ... + _patch_legacy_helpers(workdir) + + # Prepare env. var. to pass to script + env_dict = _make_environment_for_app_script( + app_, workdir=workdir, action="upgrade" + ) + + env_dict_more = { + "YNH_APP_UPGRADE_TYPE": upgrade_type, + "YNH_APP_MANIFEST_VERSION": str(app_new_version_raw), + "YNH_APP_CURRENT_VERSION": str(app_current_version_raw), + } + + if new_manifest["packaging_format"] < 2: + env_dict_more["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0" + + env_dict.update(env_dict_more) + + # Start register change on system + related_to = [("app", app_)] + operation_logger = OperationLogger("app_upgrade", related_to, env=env_dict) + operation_logger.start() + + hook_callback("pre_app_upgrade", env=env_dict) + + if new_manifest["packaging_format"] >= 2: + from .utils.resources import AppResourceManager + + AppResourceManager( + app_, + wanted=new_manifest, + current=current_manifest, + workdir=workdir, + ).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + action="upgrade", + ) + + # Boring stuff : the resource upgrade may have added/remove/updated setting + # so we need to reflect this in the env_dict used to call the actual upgrade script x_x + # Or: the old manifest may be in v1 and the new in v2, so force to add the setting in env + env_dict = _make_environment_for_app_script( + app_, + workdir=workdir, + action="upgrade", + force_include_app_settings=True, + ) + env_dict.update(env_dict_more) + + # Execute the app upgrade script + upgrade_failed = True + try: + ( + upgrade_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + workdir + "/scripts/upgrade", + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_upgrade_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_upgrade_failed", app=app_, error=e + ), + ) + finally: + # If upgrade failed, try to restore the safety backup + if ( + upgrade_failed + and new_manifest["packaging_format"] >= 2 + and not no_safety_backup + ): + logger.warning( + "Upgrade failed ... attempting to restore the safety backup (Yunohost first need to remove the app for this) ..." + ) + + app_remove(app_, force_workdir=workdir) + backup_restore(name=safety_backup_name, apps=[app_], force=True) + if not _is_installed(app_): + logger.error( + "Uhoh ... Yunohost failed to restore the app to the way it was before the failed upgrade :|" + ) + + # Whatever happened (install success or failure) we check if it broke the system + # and warn the user about it + try: + broke_the_system = False + _assert_system_is_sane_for_app(new_manifest, "post") + except Exception as e: + broke_the_system = True + logger.error(m18n.n("app_upgrade_failed", app=app_, error=str(e))) + failure_message_with_debug_instructions = operation_logger.error(str(e)) + + # We'll check that the app didn't brutally edit some system configuration + manually_modified_files_after_install = manually_modified_files() + manually_modified_files_by_app = set( + manually_modified_files_after_install + ) - set(manually_modified_files_before_install) + if manually_modified_files_by_app: + logger.error( + "Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + + "\n - ".join(manually_modified_files_by_app) + ) + + # 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_, "update_time", now) + app_setting( + app_, + "current_revision", + new_manifest.get("remote", {}).get("revision", "?"), + ) + + # Clean hooks and add new ones + hook_remove(app_) + if "hooks" in os.listdir(workdir): + for hook in os.listdir(workdir + "/hooks"): + hook_add(app_, workdir + "/hooks/" + hook) + + app_setting_path = os.path.join(APPS_SETTING_PATH, app_) + + # 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(workdir, file_to_copy)): + cp( + f"{workdir}/{file_to_copy}", + f"{app_setting_path}/{file_to_copy}", + recursive=True, + ) + + # Clean and set permissions + rmtree(workdir) + chmod(app_setting_path, 0o600) + chmod(f"{app_setting_path}/settings.yml", 0o400) + chown(app_setting_path, "root", recursive=True) + + if upgrade_failed and broke_the_system: + raise YunohostError("app_upgrade_failed_and_broke_the_system", app=app_) + elif broke_the_system: + raise YunohostError("app_upgrade_broke_the_system", app=app_) + elif upgrade_failed: + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True + ) + + # So much win + logger.success(m18n.n("app_upgraded", app=app_)) + + # Format post-upgrade notifications + if new_manifest["notifications"]["POST_UPGRADE"]: + # Get updated settings to hydrate notifications + settings = _get_app_settings(app_) + post_upgrade_notifications = _filter_and_hydrate_notifications( + new_manifest["notifications"]["POST_UPGRADE"], + current_version=app_current_version_raw, + data=settings, + ) + if Moulinette.interface.type == "cli": + # ask for simple confirm + _display_notifications(post_upgrade_notifications, force=force) + else: + post_upgrade_notifications = {} + + # Reset the dismiss flag for post upgrade notification + app_setting(app_, "_dismiss_notification_post_upgrade", delete=True) + + hook_callback("post_app_upgrade", env=env_dict) + operation_logger.success() + + _sync_permissions_with_ldap() + + return post_upgrade_notifications + + # + # + # Start of the actual "multi" app upgrade flow... + # + # + + actual_targets = list(_check_upgrade_targets(requested_targets)) + actual_targets_with_manifests_and_workdir = [] + + # Fetch app dirs and check requirements before actually launching upgrades + for app_, msg, new_app_src in actual_targets: + new_manifest, workdir = _extract_app(new_app_src) + ok = _check_manifest_requirements_with_option_to_bypass(app_, new_manifest) + if ok: + actual_targets_with_manifests_and_workdir.append( + (app_, msg, _get_manifest_of_app(app_), new_manifest, workdir) + ) + + # If we were asked to upgrade everything + if not raw_requested_targets: + # Everything is already ok? Success ! + if not actual_targets: + logger.success(m18n.n("apps_already_up_to_date")) + if Moulinette.interface.type == "api": + return {"notifications": {"POST_UPGRADE": {}}} # type: ignore + else: + return None + else: + # We'll proceed - or ask confirmation if some apps did not pass requirements + pass + if not actual_targets_with_manifests_and_workdir: + raise YunohostValidationError("apps_no_target_can_be_upgraded") + + # Before going in, display the list of what's actually gonna be upgraded + # (though no need to do this if the goal is to upgrade a single app, it's gonna be redundant with the info message in the loop) + if not raw_requested_targets or len(requested_targets) > 1: + todolist = [ + msg for _, msg, _, _, _ in actual_targets_with_manifests_and_workdir + ] + logger.info( + m18n.n( + "app_upgrade_several_apps", apps="\n - " + ("\n - ".join(todolist)) + ) + ) + + # If some apps did not pass requirements, ask confirmation + some_target_didnt_pass_requirements = len( + actual_targets_with_manifests_and_workdir + ) < len(actual_targets) + if some_target_didnt_pass_requirements: + # i18n: apps_confirm_partial_upgrade + _ask_confirmation("apps_confirm_partial_upgrade", kind="soft") + + # Display pre-upgrade notifications and ask for simple confirm + # (On the webadmin, it's already handled by the front so we only do this in CLI) + if Moulinette.interface.type == "cli": + for ( + app_, + _, + current_manifest, + new_manifest, + _, + ) in actual_targets_with_manifests_and_workdir: + if new_manifest["notifications"]["PRE_UPGRADE"]: + notifications = _filter_and_hydrate_notifications( + new_manifest["notifications"]["PRE_UPGRADE"], + current_version=current_manifest.get("version"), + data=_get_app_settings(app_), + ) + _display_notifications(notifications, force=force) + + post_upgrade_notifications: dict[str, str] = {} + pending_apps = [target[0] for target in actual_targets_with_manifests_and_workdir] + failed_to_upgrade_apps: dict[str, str] = {} + successful_apps: list[str] = [] + for ( + app_, + _, + current_manifest, + new_manifest, + workdir, + ) in actual_targets_with_manifests_and_workdir: + pending_apps.remove(app_) + + try: + post_upgrade_notifications = _upgrade_single_app( + app_, current_manifest, new_manifest, workdir, no_safety_backup + ) + except YunohostError as e: + # If upgrading a single app : re-raise the Exception + if raw_requested_targets and len(requested_targets) == 1: + raise e + + failed_to_upgrade_apps[app_] = str(e) + logger.error(e) + + broke_the_system = "broke_the_system" in e.key + # FIXME if "pending_apps" etc + if broke_the_system and continue_on_failure and pending_apps: + logger.warning( + "Option --continue-on-failure was provided, but all remaining upgrades are cancelled anyway because it looks like the app broke the system" + ) + continue_on_failure = False + if continue_on_failure and pending_apps: + logger.warning( + m18n.n("app_upgrade_continuing_with_other_apps", app=app_) + ) + continue + else: + if pending_apps: + logger.warning( + m18n.n("apps_upgrade_cancelled", apps=", ".join(pending_apps)) + ) + break + else: + successful_apps.append(app_) + + if Moulinette.interface.type == "api": + # FIXME : in fact post_upgrade_notifications is only the notification from the last app x_x + # I guess we didnt notice so far because the app only upgrades a single app at a time... + return {"notifications": {"POST_UPGRADE": post_upgrade_notifications}} # type: ignore[return-value] + else: + if len(actual_targets_with_manifests_and_workdir) > 1: + result_dict: dict[Literal["success", "failed", "cancelled"], Any] = {} + if successful_apps: + result_dict["success"] = successful_apps + if failed_to_upgrade_apps: + result_dict["failed"] = failed_to_upgrade_apps + if pending_apps: + result_dict["cancelled"] = pending_apps + return result_dict + else: + return None + + +def app_manifest(app: str, with_screenshot: bool = False) -> AppManifest: + from .utils.form import parse_raw_options + + manifest, extracted_app_folder = _extract_app(app) + + 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": + import base64 + + manifest["screenshot"] = None + screenshots_folder = os.path.join(extracted_app_folder, "doc", "screenshots") + + if os.path.exists(screenshots_folder): + with os.scandir(screenshots_folder) as it: + for entry in it: + ext = os.path.splitext(entry.name)[1].replace(".", "").lower() + if entry.is_file() and ext in ("png", "jpg", "jpeg", "webp", "gif"): + with open(entry.path, "rb") as img_file: + data = base64.b64encode(img_file.read()).decode("utf-8") + manifest["screenshot"] = ( + f"data:image/{ext};charset=utf-8;base64,{data}" + ) + break + + rmtree(extracted_app_folder) + + manifest["requirements"] = { + r["id"]: r + for r in _check_manifest_requirements( + manifest, action="install", app=manifest["id"] + ) + } + return manifest + + +@is_unit_operation() +def app_install( + operation_logger: "OperationLogger", + app: str, + label: str | None = None, + args: str | None = None, + no_remove_on_failure: bool = False, + force: bool = False, + ignore_yunohost_version: bool = False, +) -> None | dict[Literal["notifications"], dict[str, str]]: + """ + Install apps + + Keyword argument: + app -- Name, local path or git URL of the app to install + label -- Custom name for the app + args -- Serialize arguments for app installation + no_remove_on_failure -- Debug option to avoid removing the app on a failed installation + force -- Do not ask for confirmation when installing experimental / low-quality apps + """ + + from .hook import ( + hook_add, + hook_callback, + hook_exec, + hook_exec_with_script_debug_if_failure, + hook_remove, + ) + from .log import OperationLogger + from .permission import ( + _sync_permissions_with_ldap, + permission_create, + permission_delete, + user_permission_list, + ) + from .regenconf import manually_modified_files + from .user import user_list + from .utils.app_utils import _confirm_app_install, _next_instance_number_for_app + from .utils.form import ask_questions_and_parse_answers + from .utils.legacy import _patch_legacy_helpers + from .utils.system import free_space_in_directory + + # Check if disk space available + if free_space_in_directory("/") <= 512 * 1000 * 1000: + raise YunohostValidationError("disk_space_not_sufficient_install") + + _confirm_app_install(app, force) + manifest, extracted_app_folder = _extract_app(app) + + # Display pre_install notices in cli mode + if manifest["notifications"]["PRE_INSTALL"] and Moulinette.interface.type == "cli": + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["PRE_INSTALL"] + ) + _display_notifications(notifications, force=force) + + packaging_format = manifest["packaging_format"] + + # Check ID + if "id" not in manifest or "__" in manifest["id"] or "." in manifest["id"]: + raise YunohostValidationError("app_id_invalid") + + app_id = manifest["id"] + + instance_number = _next_instance_number_for_app(app_id) + if instance_number > 1: + # Change app_id to the forked app id + app_instance_name = app_id + "__" + str(instance_number) + else: + app_instance_name = app_id + + if app_instance_name in user_list()["users"].keys(): + raise YunohostValidationError( + f"There is already a YunoHost user called {app_instance_name} ...", + raw_msg=True, + ) + + # Check requirements + failed_requirements = { + r["id"]: r + for r in _check_manifest_requirements( + manifest, action="upgrade", app=app_instance_name + ) + if not r["passed"] + } + for id_, check in failed_requirements.items(): + if id_ == "ram": + logger.warning(check["error"]) + _ask_confirmation("confirm_app_insufficient_ram", force=force) + elif id_ == "required_yunohost_version" and ignore_yunohost_version: + logger.warning(check["error"]) + else: + raise YunohostValidationError(check["error"], raw_msg=True) + + _assert_system_is_sane_for_app(manifest, "pre") + + app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) + + # Retrieve arguments list for install script + raw_options = manifest["install"] + options, form = ask_questions_and_parse_answers(raw_options, prefilled_answers=args) + parsedargs = form.dict(exclude_none=True) + + # Validate domain / path availability for webapps + # (ideally this should be handled by the resource system for manifest v >= 2 + path_requirement = _guess_webapp_path_requirement(extracted_app_folder) + _validate_webpath_requirement(parsedargs, path_requirement) + + if packaging_format < 2: + # Attempt to patch legacy helpers ... + _patch_legacy_helpers(extracted_app_folder) + + # We'll check that the app didn't brutally edit some system configuration + manually_modified_files_before_install = manually_modified_files() + + operation_logger.related_to = [ + s for s in operation_logger.related_to if s[0] != "app" + ] + operation_logger.related_to.append(("app", app_id)) + operation_logger.start() + + logger.info(m18n.n("app_start_install", app=app_id)) + + # Create app directory + if os.path.exists(app_setting_path): + rmtree(app_setting_path) + os.makedirs(app_setting_path) + + # Hotfix for bug in the webadmin while we fix the actual issue :D + if label == "undefined": + label = None + + # Set initial app settings + app_settings = { + "id": app_instance_name, + "install_time": int(time.time()), + "current_revision": manifest.get("remote", {}).get("revision", "?"), + } + + if label: + app_settings["label"] = label + + # If packaging_format v2+, save all install options as settings + if packaging_format >= 2: + 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 option.type == "password": + continue + + app_settings[option.id] = form[option.id] + + _set_app_settings(app_instance_name, app_settings) + + # Move scripts and manifest to the right place + for file_to_copy in APP_FILES_TO_COPY: + if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): + cp( + f"{extracted_app_folder}/{file_to_copy}", + f"{app_setting_path}/{file_to_copy}", + recursive=True, + ) + + if packaging_format >= 2: + from .utils.resources import AppResourceManager + + try: + AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + action="install", + ) + except (KeyboardInterrupt, EOFError, Exception) as e: + rmtree(app_setting_path) + raise e + else: + # Initialize the main permission for the app + # The permission is initialized with no url associated, and with tile disabled + # For web app, the root path of the app will be added as url and the tile + # will be enabled during the app install. C.f. 'app_register_url()' below + # or the webpath resource + permission_create( + app_instance_name + ".main", + allowed=["all_users"], + show_tile=False, + protected=False, + ) + + # Prepare env. var. to pass to script + env_dict = _make_environment_for_app_script( + app_instance_name, + args=parsedargs, + workdir=extracted_app_folder, + action="install", + ) + + # If packaging_format v2+, save all install options as settings + if packaging_format >= 2: + for option in options: + # Reinject user-provider passwords which are not in the app settings + # (cf a few line before) + 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 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}) + + # Execute the app install script + install_failed = True + try: + ( + install_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + os.path.join(extracted_app_folder, "scripts/install"), + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_install_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_install_failed", app=app_id, error=e + ), + ) + finally: + # If success so far, validate that app didn't break important stuff + if not install_failed: + try: + broke_the_system = False + _assert_system_is_sane_for_app(manifest, "post") + except Exception as e: + broke_the_system = True + logger.error(m18n.n("app_install_failed", app=app_id, error=str(e))) + failure_message_with_debug_instructions = operation_logger.error(str(e)) + + # We'll check that the app didn't brutally edit some system configuration + manually_modified_files_after_install = manually_modified_files() + manually_modified_files_by_app = set( + manually_modified_files_after_install + ) - set(manually_modified_files_before_install) + if manually_modified_files_by_app: + logger.error( + "Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + + "\n - ".join(manually_modified_files_by_app) + ) + # Actually forbid this for app packaging >= 2 + if packaging_format >= 2: + broke_the_system = True + + # If the install failed or broke the system, we remove it + if install_failed or broke_the_system: + # This option is meant for packagers to debug their apps more easily + if no_remove_on_failure: + raise YunohostError( + f"The installation of {app_id} failed, but was not cleaned up as requested by --no-remove-on-failure.", + raw_msg=True, + ) + else: + logger.warning(m18n.n("app_remove_after_failed_install")) + + # Setup environment for remove script + env_dict_remove = _make_environment_for_app_script( + app_instance_name, workdir=extracted_app_folder, action="remove" + ) + + # Execute remove script + operation_logger_remove = OperationLogger( + "remove_on_failed_install", + [("app", app_instance_name)], + env=env_dict_remove, + ) + operation_logger_remove.start() + + # Try to remove the app + try: + remove_retcode = hook_exec( + os.path.join(extracted_app_folder, "scripts/remove"), + args=[app_instance_name], + env=env_dict_remove, + )[0] + + # Here again, calling hook_exec could fail miserably, or get + # manually interrupted (by mistake or because script was stuck) + # In that case we still want to proceed with the rest of the + # removal (permissions, /etc/yunohost/apps/{app} ...) + except (KeyboardInterrupt, EOFError, Exception): + remove_retcode = -1 + import traceback + + logger.error( + m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + ) + + if packaging_format >= 2: + from .utils.resources import AppResourceManager + + AppResourceManager( + app_instance_name, wanted={}, current=manifest + ).apply(rollback_and_raise_exception_if_failure=False, action="remove") + else: + # Remove all permission in LDAP + for permission_name in user_permission_list()["permissions"].keys(): + if permission_name.startswith(app_instance_name + "."): + permission_delete(permission_name, force=True, sync_perm=False) + + if remove_retcode != 0: + msg = m18n.n("app_not_properly_removed", app=app_instance_name) + logger.warning(msg) + operation_logger_remove.error(msg) + else: + try: + _assert_system_is_sane_for_app(manifest, "post") + except Exception as e: + operation_logger_remove.error(e) + else: + operation_logger_remove.success() + + # Clean tmp folders + rmtree(app_setting_path) + rmtree(extracted_app_folder) + + _sync_permissions_with_ldap() + app_ssowatconf() + + raise YunohostError(failure_message_with_debug_instructions, raw_msg=True) + + # Clean hooks and add new ones + hook_remove(app_instance_name) + if "hooks" in os.listdir(extracted_app_folder): + for file in os.listdir(extracted_app_folder + "/hooks"): + hook_add(app_instance_name, extracted_app_folder + "/hooks/" + file) + + # Clean and set permissions + rmtree(extracted_app_folder) + chmod(app_setting_path, 0o600) + chmod(f"{app_setting_path}/settings.yml", 0o400) + chown(app_setting_path, "root", recursive=True) + + logger.success(m18n.n("installation_complete")) + + # Get the generated settings to hydrate notifications + settings = _get_app_settings(app_instance_name) + notifications = _filter_and_hydrate_notifications( + manifest["notifications"]["POST_INSTALL"], data=settings + ) + + # Display post_install notices in cli mode + if notifications and Moulinette.interface.type == "cli": + _display_notifications(notifications, force=force) + + # Call postinstall hook + hook_callback("post_app_install", env=env_dict) + + # Return hydrated post install notif for API + if Moulinette.interface.type == "api": + return {"notifications": notifications} + else: + return None + + +@is_unit_operation() +def app_remove( + operation_logger: "OperationLogger", + app: str, + purge: bool = False, + force_workdir: str | None = None, +) -> None: + """ + Remove app + + Keyword arguments: + app -- App(s) to delete + purge -- Remove with all app data + force_workdir -- Special var to force the working directoy to use, in context such as remove-after-failed-upgrade or remove-after-failed-restore + """ + from .domain import _get_raw_domain_settings, domain_config_set, domain_list + from .hook import hook_callback, hook_exec, hook_remove + from .permission import ( + _sync_permissions_with_ldap, + permission_delete, + user_permission_list, + ) + from .utils.legacy import _patch_legacy_helpers + + _assert_is_installed(app) + + operation_logger.start() + + logger.info(m18n.n("app_start_remove", app=app)) + app_setting_path = os.path.join(APPS_SETTING_PATH, app) + + # Attempt to patch legacy helpers ... + _patch_legacy_helpers(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 + # and also get the new manifest + # It's especially important during v1->v2 app format transition where the + # setting names change (e.g. install_dir instead of final_path) and + # running the old remove script doesnt make sense anymore ... + tmp_workdir_for_app = _make_tmp_workdir_for_app() + os.system(f"cp -a {force_workdir}/* {tmp_workdir_for_app}/") + else: + tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) + + manifest = _get_manifest_of_app(tmp_workdir_for_app) + + remove_script = f"{tmp_workdir_for_app}/scripts/remove" + + env_dict = {} + env_dict = _make_environment_for_app_script( + app, workdir=tmp_workdir_for_app, action="remove" + ) + env_dict["YNH_APP_PURGE"] = str(1 if purge else 0) + + operation_logger.extra.update({"env": env_dict}) + operation_logger.flush() + + try: + ret = hook_exec(remove_script, env=env_dict)[0] + # Here again, calling hook_exec could fail miserably, or get + # manually interrupted (by mistake or because script was stuck) + # In that case we still want to proceed with the rest of the + # removal (permissions, /etc/yunohost/apps/{app} ...) + except (KeyboardInterrupt, EOFError, Exception): + ret = -1 + import traceback + + logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc())) + finally: + rmtree(tmp_workdir_for_app) + + packaging_format = manifest["packaging_format"] + if packaging_format >= 2: + from .utils.resources import AppResourceManager + + AppResourceManager(app, wanted={}, current=manifest).apply( + rollback_and_raise_exception_if_failure=False, + purge_data_dir=purge, + action="remove", + ) + else: + # Remove all permission in LDAP + for permission_name in user_permission_list(apps=[app])["permissions"].keys(): + permission_delete(permission_name, force=True, sync_perm=False) + + if purge and os.path.exists(f"/var/log/{app}"): + rmtree(f"/var/log/{app}") + + if os.path.exists(app_setting_path): + rmtree(app_setting_path) + + hook_remove(app) + + for domain in domain_list()["domains"]: + if _get_raw_domain_settings(domain).get("default_app") == app: + domain_config_set(domain, "feature.app.default_app", "_none") + + if ret == 0: + logger.success(m18n.n("app_removed", app=app)) + hook_callback("post_app_remove", env=env_dict) + else: + logger.warning(m18n.n("app_not_properly_removed", app=app)) + + _sync_permissions_with_ldap() + _assert_system_is_sane_for_app(manifest, "post") + + +@is_unit_operation() +def app_makedefault( + operation_logger: "OperationLogger", + app: str, + domain: str | None = None, + undo: bool = False, +) -> None: + """ + Redirect domain root to an app + + Keyword argument: + app + domain + + """ + from .domain import _assert_domain_exists, domain_config_set + + app_settings = _get_app_settings(app) + app_domain = app_settings["domain"] + + if domain is None: + domain = app_domain + + _assert_domain_exists(domain) + + operation_logger.related_to.append(("domain", domain)) + + operation_logger.start() + + if undo: + domain_config_set(domain, "feature.app.default_app", "_none") + else: + domain_config_set(domain, "feature.app.default_app", app) + + +def app_setting( + app: str, + key: str, + value: str | int | dict[str, Any] | None = None, + delete: bool = False, +) -> str | int | dict[str, Any] | None: + """ + Set or get an app setting value + + Keyword argument: + value -- Value to set + app -- App ID + key -- Key to get/set + delete -- Delete the key + + """ + app_settings = _get_app_settings(app) or {} + + # GET + if value is None and not delete: + return app_settings.get(key, None) + + # DELETE + if delete: + if key in app_settings: + del app_settings[key] + else: + # Don't call _set_app_settings to avoid unecessary writes... + return None + + # SET + else: + app_settings[key] = value + + _set_app_settings(app, app_settings) + + return None + + +def app_shell(app: str) -> None: + """ + Open an interactive shell with the app environment already loaded + + Keyword argument: + app -- App ID + + """ + import subprocess + + env = _make_environment_for_app_script(app) + env["PATH"] = os.environ["PATH"] + env["YNH_APP_BASEDIR"] = os.path.join(APPS_SETTING_PATH, app) + env["TERM"] = os.environ.get("TERM", "xterm-256color") + subprocess.run( + [ + "/bin/bash", + "-c", + "source /usr/share/yunohost/helpers && ynh_spawn_app_shell " + app, + ], + env=env, + ) + + +def app_db(app): + """ + Open an interactive DB pompt for the app + + Keyword argument: + app -- App ID + + """ + import subprocess + import tempfile + + _assert_is_installed(app) + local_manifest = _get_manifest_of_app(app) + settings = _get_app_settings(app) + + db_in_manifest = local_manifest["resources"].get("database", None) + if not db_in_manifest: + raise YunohostValidationError("app_db_prompt_no_app_database") + + type = db_in_manifest.get("type", None) + database = settings["db_name"] + user = settings["db_user"] + password = settings["db_pwd"] + + if type == "postgresql": + password_file_content = f"localhost:5432:{database}:{user}:{password}" + elif type == "mysql": + password_file_content = f""" +[client] +user={user} +password={password} +""" + else: + raise YunohostValidationError("app_db_prompt_type_not_supported", type=type) + + # NOTE: Postgresql (and probably mariadb too) requires the password file to + # be a regular one. We can't work with pipes. + # Let's create a temp file that is destroyed when quitting + with tempfile.NamedTemporaryFile() as fp: + fp.write(password_file_content.encode()) + fp.flush() + + env_by_type = {"mysql": {}, "postgresql": {"PGPASSFILE": fp.name}} + command = { + "mysql": ["mysql", f"--defaults-file={fp.name}", database], + "postgresql": ["psql", f"--user={user}"], + } + subprocess.run(command[type], env=env_by_type[type]) + + +def app_register_url(app: str, domain: str, path: str) -> None: + """ + Book/register a web path for a given app + + Keyword argument: + app -- App which will use the web 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 .permission import ( + _sync_permissions_with_ldap, + permission_url, + user_permission_update, + ) + from .utils.form import DomainOption, WebPathOption + + domain = DomainOption.normalize(domain) + path = WebPathOption.normalize(path) + + # We cannot change the url of an app already installed simply by changing + # the settings... + + if _is_installed(app): + settings = _get_app_settings(app) + if "path" in settings.keys() and "domain" in settings.keys(): + raise YunohostValidationError("app_already_installed_cant_change_url") + + # Check the url is available + _assert_no_conflicting_apps(domain, path, ignore_app=app) + + app_setting(app, "domain", value=domain) + app_setting(app, "path", value=path) + + # Initially, the .main permission is created with no url at all associated + # When the app register/books its web url, we also add the url '/' + # (meaning the root of the app, domain.tld/path/) + # and enable the tile to the SSO, and both of this should match 95% of apps + # For more specific cases, the app is free to change / add urls or disable + # the tile using the permission helpers. + permission_url(app + ".main", url="/", sync_perm=False) + user_permission_update(app + ".main", show_tile=True, sync_perm=False) + _sync_permissions_with_ldap() + + +def app_ssowatconf() -> None: + """ + Regenerate SSOwat configuration file + + """ + from .domain import ( + _get_domain_portal_dict, + _get_raw_domain_settings, + domain_list, + ) + from .permission import AppPermInfos, user_permission_list + from .settings import settings_get + from .utils.file_utils import read_json, write_to_json + + domain_portal_dict = _get_domain_portal_dict() + + domains = domain_list()["domains"] + portal_domains = domain_list(exclude_subdomains=True)["domains"] + all_permissions: dict[str, AppPermInfos] = user_permission_list( # type: ignore + full=True, ignore_system_perms=True, absolute_urls=True + )["permissions"] + + permissions = { + "core_skipped": { + "users": [], + "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] + + [ + 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.*$", + ], + } + } + + # FIXME : this could be handled by nginx's regen conf to further simplify ssowat's code ... + redirected_urls = {} + for domain in domains: + default_app: str | None = _get_raw_domain_settings(domain).get("default_app") + + if ( + default_app is not None + and default_app != "_none" + and _is_installed(default_app) + ): + app_settings = _get_app_settings(default_app) + app_domain = app_settings["domain"] + app_path = app_settings["path"] + + # Prevent infinite redirect loop... + if domain + "/" != app_domain + app_path: + redirected_urls[domain + "/"] = app_domain + app_path + elif bool( + _get_raw_domain_settings(domain).get("enable_public_apps_page", False) + ): + redirected_urls[domain + "/"] = domain_portal_dict[domain] + + # Will organize apps by portal domain + portal_domains_apps: dict[str, dict[str, dict]] = { + domain: {} for domain in portal_domains + } + + # This check is to prevent an issue during postinstall if the catalog cant + # be initialized (because of offline postinstall) and it's not a big deal + # because there's no app yet (this is only used to get the default logo for + # the app + if os.path.exists("/etc/yunohost/installed"): + apps_catalog = _load_apps_catalog()["apps"] + else: + apps_catalog = {} + + # New permission system + for perm_name, perm_info in all_permissions.items(): + uris = list( + filter(None, [perm_info.get("url"), *perm_info.get("additional_urls", [])]) + ) + # Ignore permissions for which there's no url defined + if not uris: + continue + + app_id = perm_name.split(".")[0] + app_settings = _get_app_settings(app_id) + + if perm_info["auth_header"]: + if 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-with-password" + else: + auth_header = False + + permissions[perm_name] = { + "users": perm_info["corresponding_users"], + "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... + # ... But some apps live caldav/webdav need this to not break external clients x_x + apps_that_need_external_auth_maybe = [ + "agendav", + "baikal", + "ihatemoney", + "keeweb", + "monica", + "my_webdav", + "nextcloud", + "owncloud", + "paheko", + "radicale", + "tracim", + "vikunja", + "z-push", + ] + protect_against_basic_auth_spoofing = app_settings.get( + "protect_against_basic_auth_spoofing" + ) + if protect_against_basic_auth_spoofing is not None: + permissions[perm_name]["protect_against_basic_auth_spoofing"] = ( + protect_against_basic_auth_spoofing + not in [False, "False", "false", "0", 0] + ) + elif app_id.split("__")[0] in apps_that_need_external_auth_maybe: + 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 + + local_manifest = _get_manifest_of_app(app_id) + + 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": perm_info.get("description") + or local_manifest["description"], + "order": perm_info.get("order", 100), + } + + if perm_info.get("hide_from_public"): + app_portal_info["hide_from_public"] = True + + # Logo may be customized via the perm setting, otherwise we use the default logo that we fetch from the catalog infos + app_base_id = app_id.split("__")[0] + # Use the perm logo, or the main-perm logo, or the default logo from catalog + logo_hash = ( + perm_info.get("logo_hash") + or all_permissions[f"{app_id}.main"].get("logo_hash") + or apps_catalog.get(app_base_id, {}).get("logo_hash") + ) + if logo_hash: + app_portal_info["logo"] = f"/yunohost/sso/applogos/{logo_hash}.png" + + portal_domains_apps[app_portal_domain][perm_name] = app_portal_info + + conf_dict: dict[str, str | dict] = { + "cookie_secret_file": "/etc/yunohost/.ssowat_cookie_secret", + "session_folder": "/var/cache/yunohost-portal/sessions", + "cookie_name": "yunohost.portal", + "redirected_urls": redirected_urls, + "domain_portal_urls": domain_portal_dict, + "permissions": permissions, + } + + write_to_json("/etc/ssowat/conf.json", conf_dict, sort_keys=True, indent=4) # type: ignore[arg-type] + + # Generate a file per possible portal with available apps + portal_email_settings = { + k: v + for k, v in settings_get("security.portal", export=True).items() + if "allow_edit_email" in k + } + for domain, apps in portal_domains_apps.items(): + portal_settings = {} + + # If possible, load the existing file + portal_settings_path = Path(PORTAL_SETTINGS_DIR) / f"{domain}.json" + if portal_settings_path.exists(): + this_domain_portal_settings: dict[str, Any] = read_json( + str(portal_settings_path) + ) # type: ignore[assignment] + portal_settings.update(this_domain_portal_settings) + + # Update with the new settings + portal_settings.update(portal_email_settings) + + # 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, # type: ignore[arg-type] + 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")) + + +@is_flash_unit_operation() +def app_change_label(app: str, new_label: str) -> None: + _assert_is_installed(app) + + app_setting(app, "label", new_label) + + # FIXME: we kinda have redundant stuff between the label key on the main perm, and the label key at top level ... + # or at least this operation should also change the label in the main perm to be consistent ... + + +def app_action_list(app: str) -> None: + AppConfigPanel, _ = _get_AppConfigPanel() + return AppConfigPanel(app).list_actions() + + +def app_action_run( + app: str, action: str, args: str | None = None, args_file=None, core: bool = False +) -> None: + if action.startswith("_core"): + core = True + if core: + _assert_is_installed(app) + + from .utils.form import parse_prefilled_values + + parsedargs = parse_prefilled_values(args) + + _, _, action = action.split(".") + if action == "force_upgrade": + app_upgrade(app, force=True) + elif action == "upgrade": + app_upgrade(app) + elif action == "change_url": + app_change_url( + app, parsedargs["change_url_domain"], parsedargs["change_url_path"] + ) + elif action == "uninstall": + app_remove(app, purge=parsedargs.get("purge", False)) + else: + raise YunohostValidationError("Unknown app action {action}", raw_msg=True) + return + else: + operation_logger = OperationLogger("app_action_run", [("app", app)]) + AppConfigPanel, _ = _get_AppConfigPanel() + config_panel = AppConfigPanel(app) + return config_panel.run_action( + action, args=args, args_file=args_file, operation_logger=operation_logger + ) + + +def app_config_get( + app: str, + key: str = "", + full: bool = False, + export: bool = False, + core: bool = False, +): + """ + Display an app configuration in classic, full or export mode + """ + if full and export: + raise YunohostValidationError( + "You can't use --full and --export together.", raw_msg=True + ) + + if full: + mode = "full" + elif export: + mode = "export" + else: + mode = "classic" + + AppConfigPanel, AppCoreConfigPanel = _get_AppConfigPanel() + try: + config_ = AppConfigPanel(app) if core is False else AppCoreConfigPanel(app) + return config_.get(key, mode) + except YunohostValidationError as e: + if Moulinette.interface.type == "api" and e.key == "config_no_panel": + # Be more permissive when no config panel found + return {} + else: + raise + + +@is_unit_operation() +def app_config_set( + operation_logger: "OperationLogger", + app: str, + key: str | None = None, + value: Any = None, + args: str | None = None, + args_file=None, + core: bool = False, +) -> None: + """ + Apply a new app configuration + """ + + AppConfigPanel, AppCoreConfigPanel = _get_AppConfigPanel() + config_ = AppConfigPanel(app) if core is False else AppCoreConfigPanel(app) + + return config_.set(key, value, args, args_file, operation_logger=operation_logger) + + +def _get_AppConfigPanel(): + from .utils.configpanel import ConfigPanel + + 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 = True + + def _get_raw_settings(self) -> "RawSettings": + return self._call_config_script("show") + + def _apply( + self, + form: "FormModel", + config: "ConfigPanelModel", + 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 + errors = return_content.get("validation_errors") + if errors: + for key, message in errors.items(): + raise YunohostValidationError( + "app_argument_invalid", + name=key, + error=message, + ) + + 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) + + def _call_config_script( + self, action: str, env: dict[str, Any] | None = None + ) -> dict[str, Any]: + from .hook import hook_exec + + 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) + + # 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) + app_script_env = _make_environment_for_app_script( + app, workdir=app_setting_path + ) + app_script_env.update(env) + app_script_env["YNH_APP_CONFIG_PANEL_OPTIONS_TYPES_AND_BINDS"] = ( + self._dump_options_types_and_binds() + ) + + ret, values = hook_exec(config_script, args=[action], env=app_script_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_partial_raw_config(self): + raw_config = super()._get_partial_raw_config() + + self._compute_binds(raw_config) + + return raw_config + + def _compute_binds(self, raw_config): + """ + This compute the 'bind' statement for every option + In particular to handle __FOOBAR__ syntax + and to handle the fact that bind statements may be defined panel-wide or section-wide + """ + + settings = _get_app_settings(self.entity) + + for panel_id, panel in raw_config.items(): + if not isinstance(panel, dict): + continue + bind_panel = panel.get("bind") + for section_id, 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: + selector, bind_panel_file = bind_panel.split(":") + if ">" in bind_section: + bind_section = bind_section + bind_panel_file + else: + bind_section = selector + bind_section + bind_panel_file + for option_id, option in section.items(): + if not isinstance(option, dict): + continue + bind = option.get("bind") + if not bind: + if bind_section: + bind = bind_section + else: + bind = "settings" + elif bind[-1] == ":" and bind_section and ":" in bind_section: + selector, bind_file = bind_section.split(":") + if ">" in bind: + bind = bind + bind_file + else: + bind = selector + bind + bind_file + if ( + bind == "settings" + and option.get("type", "string") == "file" + ): + bind = "null" + if option.get("type", "string") == "button": + bind = "null" + + option["bind"] = _hydrate_app_template(bind, settings) + + def _dump_options_types_and_binds(self): + raw_config = self._get_partial_raw_config() + lines = [] + for panel_id, panel in raw_config.items(): + if not isinstance(panel, dict): + continue + for section_id, section in panel.items(): + if not isinstance(section, dict): + continue + for option_id, option in section.items(): + if not isinstance(option, dict): + continue + lines.append( + "|".join( + [ + option_id, + option.get("type", "string"), + option["bind"], + ] + ) + ) + return "\n".join(lines) + + class AppCoreConfigPanel(ConfigPanel): + entity_type = "app" + + def __init__(self, entity) -> None: + self.entity = entity + self.config_path = self.config_path_tpl.format( + entity=entity, entity_type=self.entity_type + ) + + def _get_raw_config(self) -> "RawConfig": + from .user import user_group_list, user_list + + raw_config = super()._get_raw_config() + i18n_prefix = raw_config["i18n"] + + perm_config_template = raw_config["_core"].pop("permissions") + special_groups = {g: m18n.n(g) for g in ["visitors", "all_users", "admins"]} + regular_groups = { + g: g.title() + for g in list( + user_group_list(include_primary_groups=False)["groups"].keys() + ) + if g not in special_groups + } + users = { + username: infos["fullname"] + for username, infos in user_list(fields=["fullname"])["users"].items() + } + + groups = {**special_groups, **regular_groups, **users} + + perm_config_template["allowed"]["choices"] = list(groups.keys()) + + # i18n tweaks + for k, v in perm_config_template.items(): + v["ask"] = m18n.n(f"{i18n_prefix}_permission_{k}") + if m18n.key_exists(f"{i18n_prefix}_permission_{k}_help"): + v["help"] = m18n.n(f"{i18n_prefix}_permission_{k}_help") + + settings = _get_app_settings(self.entity) + perms_that_are_not_main = list(settings.get("_permissions", {}).keys()) + if "main" in perms_that_are_not_main: + perms_that_are_not_main.remove("main") + for perm in ["main"] + perms_that_are_not_main: + # Prefix every key with "permission_{perm}_" to make the key unique + this_perm_config = { + f"permission_{perm}_{k}": v for k, v in perm_config_template.items() + } + raw_config["_core"][f"permission_{perm}"] = this_perm_config + if perm != "main": + # i18n: app_config_permission_extraperm_section_name + section_name = m18n.n( + f"{i18n_prefix}_permission_extraperm_section_name", perm=perm + ) + raw_config["_core"][f"permission_{perm}"]["collapsed"] = True + raw_config["_core"][f"permission_{perm}"]["name"] = section_name + + return raw_config + + def _get_raw_settings(self) -> "RawSettings": + from .permission import AppPermInfos, user_permission_list + + perms: dict[str, AppPermInfos] = user_permission_list( + full=True, apps=[self.entity] + )["permissions"] # type: ignore + app_settings = _get_app_settings(self.entity) + perms_as_app_settings = app_settings.get("_permissions", {}) + + domain, path = app_settings.get("domain"), app_settings.get("path") + + raw_settings = {} + + for perm, infos in perms.items(): + perm = perm.split(".")[1] + if perm == "main": + label = ( + perms_as_app_settings.get(perm, {}).get("label") + or app_settings.get("label") + or self.entity.title() + ) + else: + label = perms_as_app_settings.get(perm, {}).get("label") or perm + raw_settings[f"permission_{perm}_label"] = label + raw_settings[f"permission_{perm}_description"] = infos.get( + "description", "" + ) + raw_settings[f"permission_{perm}_show_tile"] = infos["show_tile"] + if infos.get("url") and domain and path: + absolute_url = ( + f"https://{domain}{path.rstrip('/')}{infos.get('url')}" + ) + raw_settings[f"permission_{perm}_location"] = { + "ask": m18n.n( + "app_config_permission_location", absolute_url=absolute_url + ) + } + else: + raw_settings[f"permission_{perm}_location"] = {"visible": False} + raw_settings[f"permission_{perm}_show_tile"] = { + "value": False, + "visible": False, + } + raw_settings[f"permission_{perm}_url"] = infos.get("url") or "" + if infos.get("logo_hash"): + raw_settings[f"permission_{perm}_logo"] = ( + f"{APPS_CATALOG_LOGOS}/{infos.get('logo_hash')}.png" + ) + else: + raw_settings[f"permission_{perm}_logo"] = "" + raw_settings[f"permission_{perm}_allowed"] = ",".join(infos["allowed"]) + if infos.get("protected"): + raw_settings[f"permission_{perm}_allowed"] = { + "value": raw_settings[f"permission_{perm}_allowed"], + "help": m18n.n("app_config_permission_allowed_warn_protected"), + } + + return raw_settings + + def _apply( + self, + form: "FormModel", + config: "ConfigPanelModel", + previous_settings: dict[str, Any], + exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None] = None, + ) -> None: + from .user import ( + user_permission_add, + user_permission_remove, + user_permission_update, + ) + + next_settings = { + k: v for k, v in form.dict().items() if previous_settings.get(k) != v + } + + perm_changes: dict[str, dict[str, Any]] = {} + for next_setting, value in next_settings.items(): + # next_setting is something like 'permission_main_logo' + _, perm, key = next_setting.split("_", 2) + if perm not in perm_changes: + perm_changes[perm] = {} + if key == "logo" and value.strip(): + value = open(value, "rb") + perm_changes[perm][key] = value + + for perm, new_infos in perm_changes.items(): + new_allowed = ( + new_infos.pop("allowed") if "allowed" in new_infos else None + ) + + if new_infos: + user_permission_update(f"{self.entity}.{perm}", **new_infos) + if new_allowed is not None: + old_allowed = previous_settings.get( + f"permission_{perm}_allowed", "" + ) + old_allowed_set = ( + set(old_allowed.split(",")) if old_allowed else set() + ) + new_allowed_set = ( + set(new_allowed.split(",")) if new_allowed else set() + ) + to_add = list(set(new_allowed_set) - set(old_allowed_set)) + to_remove = list(set(old_allowed_set) - set(new_allowed_set)) + if to_add: + user_permission_add(f"{self.entity}.{perm}", to_add) + if to_remove: + user_permission_remove(f"{self.entity}.{perm}", to_remove) + + return AppConfigPanel, AppCoreConfigPanel + + +@is_flash_unit_operation() +def app_dismiss_notification(app: str, name: Literal["post_install", "post_upgrade"]): + assert isinstance(name, str) + name_ = name.lower() + assert name_ in ["post_install", "post_upgrade"] + _assert_is_installed(app) + + app_setting(app, f"_dismiss_notification_{name_}", value="1") + + +def regen_mail_app_user_config_for_dovecot_and_postfix( + only: Literal["dovecot", "postfix"] | None = None, +) -> None: + dovecot = True if only in [None, "dovecot"] else False + postfix = True if only in [None, "postfix"] else False + + from .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") + ret = os.system(f"postmap {app_senders_map} 2>/dev/null") + if ret != 0: + logger.error(f"Uhoh, failed to run 'postmap {app_senders_map}' ?!") + 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 new file mode 100644 index 0000000..ea436ae --- /dev/null +++ b/src/app_catalog.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import hashlib +import os +import re +from logging import getLogger +from pathlib import Path +from typing import Any, Literal, NotRequired, TypedDict + +from moulinette import m18n + +from .utils.error import YunohostError +from .utils.file_utils import download_json, mkdir, read_json, read_yaml, write_to_json +from .utils.i18n import _value_for_locale + +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: list[dict[Literal["id", "url"], str]] = [ + {"id": "default", "url": APPS_CATALOG_DEFAULT_URL} +] +SECURITY_INDEX_SUPPORTED_VERSION = 1 + + +class SecurityIssueInfos(TypedDict): + date: str + title: str + more_infos: ( + str | list[str] + ) # typically an URL, for example a forum thread or github issue + fixed_in_version: ( + str | dict[Literal["bookworm", "trixie"], str] + ) # eg "1.2.3~ynh1" or "2.2.27-3+deb9u5" + level: Literal["warning", "error"] + + +class AppCatalog(TypedDict): + apps: dict[str, dict[str, Any]] + categories: NotRequired[list[dict[str, Any]]] + antifeatures: NotRequired[list[dict[str, Any]]] + security: NotRequired[ + dict[Literal["apps", "system"], dict[str, list[SecurityIssueInfos]]] + ] + from_api_version: NotRequired[int] + + +def app_catalog( + full: bool = False, with_categories: bool = False, with_antifeatures: bool = False +) -> AppCatalog: + """ + Return a dict of apps available to installation from Yunohost's app catalog + """ + + from .utils.app_utils import _installed_apps + + # Get app list from catalog cache + catalog = _load_apps_catalog() + installed_apps = set(_installed_apps()) + + # Trim info for apps if not using --full + for app, infos in catalog["apps"].items(): + infos["installed"] = app in installed_apps + + infos["manifest"]["description"] = _value_for_locale( + infos["manifest"]["description"] + ) + + if not full: + catalog["apps"][app] = { + "description": infos["manifest"]["description"], + "level": infos["level"], + } + + _catalog: AppCatalog = {"apps": catalog["apps"]} + + if with_categories: + for category in catalog["categories"]: + category["title"] = _value_for_locale(category["title"]) + category["description"] = _value_for_locale(category["description"]) + for subtags in category.get("subtags", []): + subtags["title"] = _value_for_locale(subtags["title"]) + + if not full: + catalog["categories"] = [ + {"id": c["id"], "description": c["description"]} + for c in catalog["categories"] + ] + + _catalog["categories"] = catalog["categories"] + + if with_antifeatures: + for antifeature in catalog["antifeatures"]: + antifeature["title"] = _value_for_locale(antifeature["title"]) + antifeature["description"] = _value_for_locale(antifeature["description"]) + + if not full: + catalog["antifeatures"] = [ + {"id": a["id"], "description": a["description"]} + for a in catalog["antifeatures"] + ] + + _catalog["antifeatures"] = catalog["antifeatures"] + + return _catalog + + +def app_search(string: str) -> dict[Literal["apps"], dict[str, Any]]: + """ + Return a dict of apps whose description or name match the search string + """ + + # Retrieve a simple dict listing all apps + catalog_of_apps = app_catalog() + + # Selecting apps according to a match in app name or description + matching_apps = {} + for app in catalog_of_apps["apps"].items(): + if re.search(string, app[0], flags=re.IGNORECASE) or re.search( + string, app[1]["description"], flags=re.IGNORECASE + ): + matching_apps[app[0]] = app[1] + + return {"apps": matching_apps} + + +def _read_apps_catalog_list() -> list[dict[Literal["id", "url"], str]]: + """ + 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 [] # type: ignore[return-value] + except Exception as e: + raise YunohostError( + f"Could not read the apps_catalog list ... : {e}", raw_msg=True + ) + + +def _actual_apps_catalog_api_url(base_url: str) -> str: + return f"{base_url}/v{APPS_CATALOG_API_VERSION}/apps.json" + + +def _actual_apps_catalog_security_url(base_url: str) -> str: + return f"{base_url}/v{APPS_CATALOG_API_VERSION}/security.json" + + +def _update_apps_catalog() -> None: + """ + Fetches the json for each apps_catalog and update the cache + + apps_catalog_list is for example : + [ {"id": "default", "url": "https://app.yunohost.org/default/"} ] + + Then for each apps_catalog, the actual json URL to be fetched is like : + https://app.yunohost.org/default/vX/apps.json + + And store it in : + /var/cache/yunohost/repo/default.json + """ + + apps_catalog_list = _read_apps_catalog_list() + + logger.info(m18n.n("apps_catalog_updating")) + + # Create cache folder if needed + if not os.path.exists(APPS_CATALOG_CACHE): + logger.debug("Initialize folder for apps catalog cache") + mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root") + + if not os.path.exists(APPS_CATALOG_LOGOS): + mkdir(APPS_CATALOG_LOGOS, mode=0o755, parents=True, uid="root") + + for apps_catalog in apps_catalog_list: + if apps_catalog["url"] is None: + continue + + apps_catalog_id = apps_catalog["id"] + actual_api_url = _actual_apps_catalog_api_url(apps_catalog["url"]) + + # Fetch the json + try: + apps_catalog_content: AppCatalog = download_json(actual_api_url) # type: ignore[assignment] + except Exception as e: + raise YunohostError( + "apps_catalog_failed_to_download", + apps_catalog=apps_catalog_id, + error=str(e), + ) + + # Remember the apps_catalog api version for later + apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION + + # Save the apps_catalog data in the cache + cache_file = f"{APPS_CATALOG_CACHE}/{apps_catalog_id}.json" + try: + write_to_json(cache_file, apps_catalog_content) # type: ignore[arg-type] + except Exception as e: + raise YunohostError( + f"Unable to write cache data for {apps_catalog_id} apps_catalog : {e}", + raw_msg=True, + ) + + # Download missing app logos + logos_to_download = [] + for app, infos in apps_catalog_content["apps"].items(): + logo_hash = infos.get("logo_hash") + if not logo_hash or os.path.exists(f"{APPS_CATALOG_LOGOS}/{logo_hash}.png"): + continue + logos_to_download.append(logo_hash) + + if len(logos_to_download) > 20: + logger.info( + f"(Will fetch {len(logos_to_download)} logos, this may take a couple minutes)" + ) + + from multiprocessing.pool import ThreadPool + + import requests + + def fetch_logo(logo_hash: str) -> bool: + try: + r = requests.get( + f"{apps_catalog['url']}/v{APPS_CATALOG_API_VERSION}/logos/{logo_hash}.png", + timeout=10, + ) + assert r.status_code == 200, ( + f"Got status code {r.status_code}, expected 200" + ) + if hashlib.sha256(r.content).hexdigest() != logo_hash: + raise Exception( + f"Found inconsistent hash while downloading logo {logo_hash}" + ) + open(f"{APPS_CATALOG_LOGOS}/{logo_hash}.png", "wb").write(r.content) + return True + except Exception as e: + logger.debug(f"Failed to download logo {logo_hash} : {e}") + return False + + results = ThreadPool(8).imap_unordered(fetch_logo, logos_to_download) + for result in results: + # Is this even needed to iterate on the results ? + pass + + logger.success(m18n.n("apps_catalog_update_success")) # type: ignore + + +_apps_catalog_cache_timestamp: float = 0 +_apps_catalog_cache: AppCatalog | None = None + + +def _load_apps_catalog() -> AppCatalog: + """ + Read all the apps catalog cache files and build a single dict (merged_catalog) + corresponding to all known apps and categories + """ + + timestamps = [] + catalog_conf = Path(APPS_CATALOG_CONF) + if catalog_conf.exists(): + stats = catalog_conf.stat() + timestamps.append(stats.st_mtime) + timestamps.append(stats.st_ctime) + for f in Path(APPS_CATALOG_CACHE).glob("*.json"): + stats = f.stat() + timestamps.append(stats.st_mtime) + timestamps.append(stats.st_ctime) + + timestamp = max(timestamps) if timestamps else 0 + global _apps_catalog_cache + global _apps_catalog_cache_timestamp + if _apps_catalog_cache and timestamp <= _apps_catalog_cache_timestamp: + return _apps_catalog_cache + + merged_catalog: AppCatalog = {"apps": {}, "categories": [], "antifeatures": []} + + for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]: + # Let's load the json from cache for this catalog + cache_file = Path(APPS_CATALOG_CACHE) / (apps_catalog_id + ".json") + + apps_catalog_content: AppCatalog | None + try: + apps_catalog_content = ( + read_json(str(cache_file)) if cache_file.exists() else None # type: ignore[assignment] + ) + except Exception as e: + raise YunohostError( + f"Unable to read cache for apps_catalog {cache_file} : {e}", + raw_msg=True, + ) + + # Check that the version of the data matches version .... + # ... otherwise it means we updated yunohost in the meantime + # and need to update the cache for everything to be consistent + if ( + not apps_catalog_content + or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION + ): + logger.info(m18n.n("apps_catalog_obsolete_cache")) + _update_apps_catalog() + apps_catalog_content = read_json(str(cache_file)) # type: ignore[assignment] + + assert apps_catalog_content is not None + + del apps_catalog_content["from_api_version"] + + # Add apps from this catalog to the output + for app, info in apps_catalog_content["apps"].items(): + # (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ... + # in which case we keep only the first one found) + if app in merged_catalog["apps"]: + other_catalog = merged_catalog["apps"][app]["repository"] + logger.warning( + f"Duplicate app {app} found between apps catalog {apps_catalog_id} and {other_catalog}" + ) + continue + + if info.get("level") == "?": + info["level"] = -1 + + # FIXME: we may want to autoconvert all v0/v1 manifest to v2 here + # so that everything is consistent in terms of APIs, datastructure format etc + info["repository"] = apps_catalog_id + merged_catalog["apps"][app] = info + + # Annnnd categories + antifeatures + # (we use .get here, only because the dev catalog doesnt include the categories/antifeatures keys) + merged_catalog["categories"] += apps_catalog_content.get("categories", []) + merged_catalog["antifeatures"] += apps_catalog_content.get("antifeatures", []) + + # Save as cache for next call + _apps_catalog_cache = merged_catalog + _apps_catalog_cache_timestamp = timestamp + + return merged_catalog + + +def _load_security_issues_list() -> dict[ + Literal["apps", "system"], dict[str, list[SecurityIssueInfos]] +]: + apps_issues: dict[str, list[SecurityIssueInfos]] = {} + system_issues: dict[str, list[SecurityIssueInfos]] = {} + + for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]: + cache_file = Path(APPS_CATALOG_CACHE) / (apps_catalog_id + ".json") + if not cache_file.exists(): + continue + content: AppCatalog + try: + content = read_json(str(cache_file)) or {} # type: ignore[assignment] + assert content is not None, f"Uhoh, read json from {cache_file} is None!?" + except Exception as e: + raise YunohostError( + f"Unable to read cache for security issue list {cache_file} : {e}", + raw_msg=True, + ) + + security_index: ( + dict[Literal["apps", "system"], dict[str, list[SecurityIssueInfos]]] | None + ) = content.get("security") + security_index_version: int | None = ( + security_index.get("version") if security_index else None # type: ignore + ) # type: ignore[index,call-overload] + # Even if not mentioned in the typing, security_index has a "version" key + if security_index_version != SECURITY_INDEX_SUPPORTED_VERSION: + if security_index_version is None: + logger.warning( + f"Catalog '{apps_catalog_id}' is missing the security vulnerability index. Please contact the people maintaing this catalog." + ) + elif security_index_version > SECURITY_INDEX_SUPPORTED_VERSION: + logger.warning( + f"In catalog {apps_catalog_id}, the security index info is too recent. You should probably upgrade YunoHost." + ) + elif security_index_version < SECURITY_INDEX_SUPPORTED_VERSION: + logger.warning( + f"Security index format from catalog '{apps_catalog_id}' is too old, please contact the people maintaining it to upgrade it to the recent standards?" + ) + continue + + assert security_index is not None + + for app, entries in security_index["apps"].items(): + if app not in apps_issues: + apps_issues[app] = [] + apps_issues[app] += entries + + for package, entries in security_index["system"].items(): + if package not in system_issues: + system_issues[package] = [] + system_issues[package] += entries + + return {"apps": apps_issues, "system": system_issues} diff --git a/src/authenticators/ldap_admin.py b/src/authenticators/ldap_admin.py new file mode 100644 index 0000000..4f2f330 --- /dev/null +++ b/src/authenticators/ldap_admin.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import hashlib +import logging +import os +import time +from collections.abc import Mapping +from functools import cache +from pathlib import Path +from typing import Any, Literal + +import jwt +import ldap +import ldap.sasl +from moulinette import m18n +from moulinette.authentication import BaseAuthenticator + +from ..utils.error import YunohostAuthenticationError, YunohostError +from ..utils.ldap import _get_ldap_interface +from ..utils.misc import random_ascii + +logger = logging.getLogger("yunohost.authenticators.ldap_admin") + +SESSION_SECRET_PATH = Path("/etc/yunohost/.admin_cookie_secret") +SESSION_FOLDER = Path("/var/cache/yunohost/sessions") +SESSION_VALIDITY = 3 * 24 * 3600 # 3 days + + +@cache +def SESSION_SECRET() -> str: + # Only load this once actually requested to avoid boring issues like + # "secret doesnt exists yet" (before postinstall) and therefore service + # miserably fail to start + return SESSION_SECRET_PATH.read_text().strip() + + +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: str) -> str: + return hashlib.shake_256(data.encode()).hexdigest(20) + + +class Authenticator(BaseAuthenticator): # type: ignore + name = "ldap_admin" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + + def _authenticate_credentials( + self, credentials: str | None = None + ) -> dict[Literal["user"], str]: + try: + admins = ( + _get_ldap_interface() + .search(ADMIN_GROUP, attrs=["memberUid"])[0] + .get("memberUid", []) + ) + except ldap.SERVER_DOWN: + # ldap is down, attempt to restart it before really failing + logger.warning(m18n.n("ldap_server_is_down_restart_it")) + os.system("systemctl restart slapd") + time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted + + # Force-reset existing LDAP interface + from ..utils import ldap as ldaputils + + ldaputils._destroy_ldap_interface() + + try: + admins = ( + _get_ldap_interface() + .search(ADMIN_GROUP, attrs=["memberUid"])[0] + .get("memberUid", []) + ) + except ldap.SERVER_DOWN: + raise YunohostError("ldap_server_down") + + if credentials is None: + raise YunohostError("invalid_credentials") + + try: + uid, password = credentials.split(":", 1) + except ValueError: + raise YunohostError("invalid_credentials") + + # Here we're explicitly using set() which are handled as hash tables + # and should prevent timing attacks to find out the admin usernames? + if uid not in set(admins): + raise YunohostError("invalid_credentials") + + dn = AUTH_DN.format(uid=uid) + + def _reconnect() -> ldap.ldapobject.SimpleLDAPObject: + con = ldap.ldapobject.ReconnectLDAPObject( + LDAP_URI, retry_max=10, retry_delay=0.5 + ) + con.simple_bind_s(dn, password) + return con + + try: + con = _reconnect() + except ldap.INVALID_CREDENTIALS: + raise YunohostError("invalid_credentials") + except ldap.SERVER_DOWN: + # ldap is down, attempt to restart it before really failing + logger.warning(m18n.n("ldap_server_is_down_restart_it")) + os.system("systemctl restart slapd") + time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted + + try: + con = _reconnect() + except ldap.SERVER_DOWN: + raise YunohostError("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 != dn: + raise YunohostError( + f"Not logged with the appropriate identity ? Found {who}, expected {dn} !?", + 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() + + return {"user": uid} + + def set_session_cookie(self, infos: dict[str, str]) -> None: + from bottle import response + + assert isinstance(infos, dict) + assert "user" 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) + + response.set_cookie( + "yunohost.admin", + jwt.encode(infos, SESSION_SECRET(), algorithm="HS256"), + secure=True, + httponly=True, + path="/yunohost/api", + samesite="strict", + ) + + # Create the session file (expiration mechanism) + session_file = SESSION_FOLDER / infos["id"] + session_file.touch(exist_ok=True) + + def get_session_cookie( + self, raise_if_no_session_exists: bool = True + ) -> Mapping[str, Any]: + from bottle import request, response + + try: + token = request.get_cookie("yunohost.admin", default="").encode() + infos = jwt.decode( + token, + SESSION_SECRET(), + algorithms="HS256", + options={"require": ["id", "user"]}, + ) + except Exception: + if raise_if_no_session_exists: + raise YunohostAuthenticationError("unable_authenticate") + # Boring fix because Moulinette wants to get the session ID during 'emit' called by the logger etc, for example during postinstall where we aint logged yet, and we don't want to crash just for this... (to be removed on 12.1) + else: + return {"id": None} + + if not infos: + raise YunohostAuthenticationError("unable_authenticate") + + self.purge_expired_session_files() + session_file = SESSION_FOLDER / infos["id"] + if not session_file.exists(): + response.delete_cookie("yunohost.admin", path="/yunohost/api") + raise YunohostAuthenticationError("session_expired") + + # Otherwise, we 'touch' the file to extend the validity + session_file.touch(exist_ok=True) + + return infos # type: ignore + + def delete_session_cookie(self) -> None: + from bottle import response + + try: + infos = self.get_session_cookie() + session_file = 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.admin", path="/yunohost/api") + + def purge_expired_session_files(self) -> None: + for session_file in 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: str) -> None: + for file in 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 0000000..3d67201 --- /dev/null +++ b/src/authenticators/ldap_ynhuser.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import base64 +import hashlib +import logging +import os +import time +from collections.abc import Mapping +from functools import cache +from pathlib import Path +from typing import Any + +import jwt +import ldap +import ldap.filter +import ldap.sasl +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from moulinette import m18n +from moulinette.authentication import BaseAuthenticator + +from ..utils.error import YunohostAuthenticationError, YunohostError +from ..utils.file_utils import read_json +from ..utils.ldap import _get_ldap_interface +from ..utils.misc import random_ascii + +logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser") + +SESSION_SECRET_PATH = Path("/etc/yunohost/.ssowat_cookie_secret") +SESSION_FOLDER = Path("/var/cache/yunohost-portal/sessions") +SESSION_VALIDITY = 3 * 24 * 3600 # 3 days + + +@cache +def SESSION_SECRET() -> str: + # Only load this once actually requested to avoid boring issues like + # "secret doesnt exists yet" (before postinstall) and therefore service + # miserably fail to start + return SESSION_SECRET_PATH.read_text().strip() + + +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[str, Any]] = {} +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() + portal_settings: dict[str, Any] = read_json(str(portal_settings_path)) # type: ignore + for infos in portal_settings["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_list = user_result[0]["mail"] + if len(user_mail_list) != 1: + logger.error( + f"User {user} found, but has the wrong number of email addresses: {user_mail_list}" + ) + return False + + user_mail = user_mail_list[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: str) -> str: + 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: str) -> str: + 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: bytes = p.update(data_padded) + p.finalize() + return data.decode() + + +def short_hash(data: str) -> str: + return hashlib.shake_256(data.encode()).hexdigest(20) + + +class Authenticator(BaseAuthenticator): # type: ignore + name = "ldap_ynhuser" + + def _authenticate_credentials( + self, credentials: str | None = None + ) -> dict[str, str]: + from bottle import request + + if credentials is None: + raise YunohostError("invalid_credentials") + + try: + username, password = credentials.split(":", 1) + except ValueError: + raise YunohostError("invalid_credentials") + + username = ldap.filter.escape_filter_chars(username) + # Search username, if user give a mail instead + if "@" in username: + user = _get_ldap_interface().search("ou=users", f"mail={username}", ["uid"]) + if len(user) != 0: + username = user[0]["uid"][0] + + def _reconnect() -> ldap.ldapobject.SimpleLDAPObject: + 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() + + ldap_user_infos = _get_ldap_interface().search( + "ou=users", f"uid={username}", attrs=["cn", "mail"] + )[0] + + if not user_is_allowed_on_domain(username, request.get_header("host")): + raise YunohostAuthenticationError("unable_authenticate") + + return { + "user": username, + "pwd": encrypt(password), + "email": ldap_user_infos["mail"][0], + "fullname": ldap_user_infos["cn"][0], + } + + def set_session_cookie(self, infos: dict[str, Any]) -> None: + from bottle import request, response + + assert isinstance(infos, dict) + assert "user" in infos + assert "pwd" in infos + assert "email" in infos + assert "fullname" 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="/", + samesite="lax" if not is_dev else None, + domain=f".{request.get_header('host')}", + max_age=SESSION_VALIDITY + - 600, # remove 1 minute such that cookie expires on the browser slightly sooner on browser side, just to help desimbuigate edge case near the expiration limit + ) + + # Create the session file (expiration mechanism) + session_file = SESSION_FOLDER / infos["id"] + session_file.touch(exist_ok=True) + + def get_session_cookie(self, decrypt_pwd: bool = False) -> Mapping[str, Any]: + 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 = 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(exist_ok=True) + + is_dev = Path("/etc/yunohost/.portal-api-allowed-cors-origins").exists() + + # We also re-set the cookie such that validity is also extended on browser side + response.set_cookie( + "yunohost.portal", + request.get_cookie( + "yunohost.portal" + ), # Reuse the same token to avoid recomputing stuff (saves a bit of CPU / delay I suppose?) + secure=True, + httponly=True, + path="/", + samesite="lax" if not is_dev else None, + domain=f".{request.get_header('host')}", + max_age=SESSION_VALIDITY + - 600, # remove 1 minute such that cookie expires on the browser slightly sooner on browser side, just to help desimbuigate edge case near the expiration limit + ) + + if decrypt_pwd: + infos["pwd"] = decrypt(infos["pwd"]) + + return infos # type: ignore + + def delete_session_cookie(self) -> None: + from bottle import response + + try: + infos = self.get_session_cookie() + session_file = 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) -> None: + for session_file in 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: str) -> None: + for file in 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 new file mode 100644 index 0000000..ea59869 --- /dev/null +++ b/src/backup.py @@ -0,0 +1,2588 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import csv +import json +import os +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile +import time +from collections import OrderedDict +from datetime import datetime +from functools import reduce +from glob import glob +from logging import getLogger +from typing import TYPE_CHECKING, cast + +from moulinette import Moulinette, m18n + +from .hook import ( + CUSTOM_HOOK_FOLDER, + hook_add, + hook_callback, + hook_exec, + hook_exec_with_script_debug_if_failure, + hook_info, + hook_list, + hook_remove, +) +from .log import OperationLogger, is_flash_unit_operation, is_unit_operation +from .regenconf import regen_conf +from .tools import ( + _tools_migrations_run_after_system_restore, + _tools_migrations_run_before_app_restore, + tools_postinstall, +) +from .utils.app_utils import ( + APPS_SETTING_PATH, + _get_manifest_of_app, + _is_installed, + _make_environment_for_app_script, + _make_tmp_workdir_for_app, +) +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import ( + chmod, + chown, + mkdir, + read_file, + rm, +) +from .utils.misc import random_ascii +from .utils.process import check_output +from .utils.system import ( + binary_to_human, + free_space_in_directory, + get_ynh_package_version, + space_used_by_directory, +) + +BACKUP_PATH = "/home/yunohost.backup" +ARCHIVES_PATH = f"{BACKUP_PATH}/archives" +APP_MARGIN_SPACE_SIZE = 100 # In MB +CONF_MARGIN_SPACE_SIZE = 10 # IN MB +POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB +MB_ALLOWED_TO_ORGANIZE = 10 + +if TYPE_CHECKING: + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.backup")) +else: + logger = getLogger("yunohost.backup") + + +class BackupRestoreTargetsManager: + """ + BackupRestoreTargetsManager manage the targets + in BackupManager and RestoreManager + """ + + def __init__(self): + self.targets = {} + self.results = {"system": {}, "apps": {}} + + def set_result(self, category, element, value): + """ + Change (or initialize) the current status/result of a given target. + + Args: + category -- The category of the target + + element -- The target for which to change the status/result + + value -- The new status/result, among "Unknown", "Success", + "Warning", "Error" and "Skipped" + """ + + levels = ["Unknown", "Success", "Warning", "Error", "Skipped"] + + assert value in levels + + if element not in self.results[category].keys(): + self.results[category][element] = value + else: + currentValue = self.results[category][element] + if levels.index(currentValue) > levels.index(value): + return + else: + self.results[category][element] = value + + def set_wanted( + self, + category, + wanted_targets, + available_targets, + error_if_wanted_target_is_unavailable, + ): + """ + Define and validate targets to be backuped or to be restored (list of + system parts, apps..). The wanted targets are compared and filtered + with respect to the available targets. If a wanted targets is not + available, a call to "error_if_wanted_target_is_unavailable" is made. + + Args: + category -- The category (apps or system) for which to set the + targets ; + + wanted_targets -- List of targets which are wanted by the user. Can be + "None" or [], corresponding to "No targets" or "All + targets" ; + + available_targets -- List of targets which are really available ; + + error_if_wanted_target_is_unavailable + -- Callback for targets which are not available. + """ + + # If no targets wanted, set as empty list + if wanted_targets is None: + self.targets[category] = [] + + # If all targets wanted, use all available targets + elif wanted_targets == []: + self.targets[category] = available_targets + + # If the user manually specified which targets to backup, we need to + # validate that each target is actually available + else: + self.targets[category] = [ + part for part in wanted_targets if part in available_targets + ] + + # Display an error for each target asked by the user but which is + # unknown + unavailable_targets = [ + part for part in wanted_targets if part not in available_targets + ] + + for target in unavailable_targets: + self.set_result(category, target, "Skipped") + error_if_wanted_target_is_unavailable(target) + + # For target with no result yet (like 'Skipped'), set it as unknown + if self.targets[category] is not None: + for target in self.targets[category]: + self.set_result(category, target, "Unknown") + + return self.list(category, exclude=["Skipped"]) + + def list(self, category, include=None, exclude=None): + """ + List targets in a given category. + + The list is filtered with a whitelist (include) or blacklist (exclude) + with respect to the current 'result' of the target. + """ + + assert (include and isinstance(include, list) and not exclude) or ( + exclude and isinstance(exclude, list) and not include + ) + + if include: + return [ + target + for target in self.targets[category] + if self.results[category][target] in include + ] + + if exclude: + return [ + target + for target in self.targets[category] + if self.results[category][target] not in exclude + ] + + +class BackupManager: + """ + This class collect files to backup in a list and apply one or several + backup method on it. + + The list contains dict with source and dest properties. The goal of this csv + is to list all directories and files which need to be backup in this + archive. The `source` property is the path of the source (dir or file). + The `dest` property is the path where it could be placed in the archive. + + The list is filled by app backup scripts and system/user backup hooks. + Files located in the work_dir are automatically added. + + With this list, "backup methods" are able to apply their backup strategy on + data listed in it. It's possible to tar each path (tar methods), to mount + each dir into the work_dir, to copy each files (copy method) or to call a + custom method (via a custom script). + + Note: some future backups methods (like borg) are not able to specify a + different place than the original path. That's why the ynh_restore_file + helpers use primarily the SOURCE_PATH as argument. + + Public properties: + info (getter) + work_dir (getter) # FIXME currently it's not a getter + is_tmp_work_dir (getter) + paths_to_backup (getter) # FIXME not a getter and list is not protected + name (getter) # FIXME currently it's not a getter + size (getter) # FIXME currently it's not a getter + + Public methods: + add(self, method) + set_system_targets(self, system_parts=[]) + set_apps_targets(self, apps=[]) + collect_files(self) + backup(self) + + Usage: + backup_manager = BackupManager(name="mybackup", description="bkp things") + + # Add backup method to apply + backup_manager.add('copy', output_directory='/mnt/local_fs') + backup_manager.add('tar', output_directory='/mnt/remote_fs') + + # Define targets to be backuped + backup_manager.set_system_targets(["data"]) + backup_manager.set_apps_targets(["wordpress"]) + + # Collect files to backup from targets + backup_manager.collect_files() + + # Apply backup methods + backup_manager.backup() + """ + + def __init__(self, name=None, description="", methods=[], work_dir=None): + """ + BackupManager constructor + + Args: + name -- (string) The name of this backup (without spaces). If + None, the name will be generated (default: None) + + description -- (string) A description for this future backup archive + (default: '') + + work_dir -- (None|string) A path where prepare the archive. If None, + temporary work_dir will be created (default: None) + """ + self.description = description or "" + self.created_at = int(time.time()) + self.apps_return = {} + self.system_return = {} + self.paths_to_backup = [] + self.size_details = {"system": {}, "apps": {}} + self.targets = BackupRestoreTargetsManager() + + # Define backup name if needed + if not name: + name = self._define_backup_name() + self.name = name + + # Define working directory if needed and initialize it + self.work_dir = work_dir + if self.work_dir is None: + self.work_dir = os.path.join(BACKUP_PATH, "tmp", name) + self._init_work_dir() + + # Initialize backup methods + self.methods = [ + BackupMethod.create(method, self, repo=work_dir) for method in methods + ] + + # + # Misc helpers # + # + + @property + def info(self): + """(Getter) Dict containing info about the archive being created""" + return { + "description": self.description, + "created_at": self.created_at, + "size": self.size, + "size_details": self.size_details, + "apps": self.apps_return, + "system": self.system_return, + "from_yunohost_version": get_ynh_package_version("yunohost")["version"], + } + + @property + def is_tmp_work_dir(self): + """(Getter) Return true if the working directory is temporary and should + be clean at the end of the backup""" + return self.work_dir == os.path.join(BACKUP_PATH, "tmp", self.name) + + def __repr__(self): + return json.dumps(self.info) + + def _define_backup_name(self): + """Define backup name + + Return: + (string) A backup name created from current date 'YYMMDD-HHMMSS' + """ + # FIXME: case where this name already exist + return time.strftime("%Y%m%d-%H%M%S", time.gmtime()) + + def _init_work_dir(self): + """Initialize preparation directory + + Ensure the working directory exists and is empty + """ + + # FIXME replace isdir by exists ? manage better the case where the path + # exists + if not os.path.isdir(self.work_dir): + mkdir(self.work_dir, 0o750, parents=True) + elif self.is_tmp_work_dir: + logger.debug( + "temporary directory for backup '%s' already exists... attempting to clean it", + self.work_dir, + ) + + # Try to recursively unmount stuff (from a previously failed backup ?) + if not _recursive_umount(self.work_dir): + raise YunohostValidationError("backup_output_directory_not_empty") + else: + # If umount succeeded, remove the directory (we checked that + # we're in /home/yunohost.backup/tmp so that should be okay... + # c.f. method clean() which also does this) + rm(self.work_dir, recursive=True, force=True) + mkdir(self.work_dir, 0o750, parents=True) + + # + # Backup target management # + # + + def set_system_targets(self, system_parts=[]): + """ + Define and validate targetted apps to be backuped + + Args: + system_parts -- (list) list of system parts which should be backuped. + If empty list, all system will be backuped. If None, + no system parts will be backuped. + """ + + def unknown_error(part): + logger.error(m18n.n("backup_hook_unknown", hook=part)) + + self.targets.set_wanted( + "system", system_parts, hook_list("backup")["hooks"], unknown_error + ) + + def set_apps_targets(self, apps=[]): + """ + Define and validate targetted apps to be backuped + + Args: + apps -- (list) list of apps which should be backuped. If given an empty + list, all apps will be backuped. If given None, no apps will be + backuped. + """ + + def unknown_error(app): + logger.error(m18n.n("unbackup_app", app=app)) + + target_list = self.targets.set_wanted( + "apps", apps, os.listdir("/etc/yunohost/apps"), unknown_error + ) + + # Additionnaly, we need to check that each targetted app has a + # backup and restore scripts + + for app in target_list: + app_script_folder = f"/etc/yunohost/apps/{app}/scripts" + backup_script_path = os.path.join(app_script_folder, "backup") + restore_script_path = os.path.join(app_script_folder, "restore") + + if not os.path.isfile(backup_script_path): + logger.warning(m18n.n("backup_with_no_backup_script_for_app", app=app)) + self.targets.set_result("apps", app, "Skipped") + + elif not os.path.isfile(restore_script_path): + logger.warning(m18n.n("backup_with_no_restore_script_for_app", app=app)) + self.targets.set_result("apps", app, "Warning") + + # + # Management of files to backup / "The CSV" # + # + + def _import_to_list_to_backup(self, tmp_csv): + """ + Commit collected path from system hooks or app scripts + + Args: + tmp_csv -- (string) Path to a temporary csv file with source and + destinations column to add to the list of paths to backup + """ + _call_for_each_path(self, BackupManager._add_to_list_to_backup, tmp_csv) + + def _add_to_list_to_backup(self, source, dest=None): + """ + Mark file or directory to backup + + This method add source/dest couple to the "paths_to_backup" list. + + Args: + source -- (string) Source path to backup + + dest -- (string) Destination path in the archive. If it ends by a + slash the basename of the source path will be added. If None, + the source path will be used, so source files will be set up + at the same place and with same name than on the system. + (default: None) + + Usage: + self._add_to_list_to_backup('/var/www/wordpress', 'sources') + # => "wordpress" dir will be move and rename as "sources" + + self._add_to_list_to_backup('/var/www/wordpress', 'sources/') + # => "wordpress" dir will be put inside "sources/" and won't be renamed + + """ + if dest is None: + dest = source + source = os.path.join(self.work_dir, source) + if dest.endswith("/"): + dest = os.path.join(dest, os.path.basename(source)) + self.paths_to_backup.append({"source": source, "dest": dest}) + + def _write_csv(self): + """ + Write the backup list into a CSV + + The goal of this csv is to list all directories and files which need to + be backup in this archive. For the moment, this CSV contains 2 columns. + The first column `source` is the path of the source (dir or file). The + second `dest` is the path where it could be placed in the archive. + + This CSV is filled by app backup scripts and system/user hooks. + Files in the work_dir are automatically added. + + With this CSV, "backup methods" are able to apply their backup strategy + on data listed in it. It's possible to tar each path (tar methods), to + mount each dir into the work_dir, to copy each files (copy methods) or + a custom method (via a custom script). + + Note: some future backups methods (like borg) are not able to specify a + different place than the original path. That's why the ynh_restore_file + helpers use primarily the SOURCE_PATH as argument. + + Error: + backup_csv_creation_failed -- Raised if the CSV couldn't be created + backup_csv_addition_failed -- Raised if we can't write in the CSV + """ + self.csv_path = os.path.join(self.work_dir, "backup.csv") + try: + self.csv_file = open(self.csv_path, "a") + self.fieldnames = ["source", "dest"] + self.csv = csv.DictWriter( + self.csv_file, fieldnames=self.fieldnames, quoting=csv.QUOTE_ALL + ) + except (IOError, OSError, csv.Error): + logger.error(m18n.n("backup_csv_creation_failed")) + + for row in self.paths_to_backup: + try: + self.csv.writerow(row) + except csv.Error: + logger.error(m18n.n("backup_csv_addition_failed")) + self.csv_file.close() + + # + # File collection from system parts and apps # + # + + def collect_files(self): + """ + Collect all files to backup, write its into a CSV and create a + info.json file + + Files to backup are listed by system parts backup hooks and by backup + app scripts that have been defined with the set_targets() method. + + Some files or directories inside the working directory are added by + default: + + info.json -- info about the archive + backup.csv -- a list of paths to backup + apps/ -- some apps generate here temporary files to backup (like + database dump) + conf/ -- system configuration backup scripts could generate here + temporary files to backup + data/ -- system data backup scripts could generate here temporary + files to backup + hooks/ -- restore scripts associated to system backup scripts are + copied here + """ + + self._collect_system_files() + self._collect_apps_files() + + # Check if something has been saved ('success' or 'warning') + successfull_apps = self.targets.list("apps", include=["Success", "Warning"]) + successfull_system = self.targets.list("system", include=["Success", "Warning"]) + + if not successfull_apps and not successfull_system: + rm(self.work_dir, True, True) + raise YunohostError("backup_no_file_collected") + + # Add unlisted files from backup tmp dir + self._add_to_list_to_backup("backup.csv") + self._add_to_list_to_backup("info.json") + for app in self.apps_return.keys(): + self._add_to_list_to_backup(f"apps/{app}") + if os.path.isdir(os.path.join(self.work_dir, "conf")): + self._add_to_list_to_backup("conf") + if os.path.isdir(os.path.join(self.work_dir, "data")): + self._add_to_list_to_backup("data") + + # Write CSV file + self._write_csv() + + # Calculate total size + self._compute_backup_size() + + # Create backup info file + with open(f"{self.work_dir}/info.json", "w") as f: + f.write(json.dumps(self.info)) + + def _get_env_var(self, app=None): + """ + Define environment variables for apps or system backup scripts. + + Args: + app -- (string|None) The instance name of the app we want the variable + environment. If you want a variable environment for a system backup + script keep None. (default: None) + + Return: + (Dictionnary) The environment variables to apply to the script + """ + env_var = {} + + _, tmp_csv = tempfile.mkstemp(prefix="backupcsv_") + env_var["YNH_BACKUP_DIR"] = self.work_dir + env_var["YNH_BACKUP_CSV"] = tmp_csv + + if app is not None: + env_var.update(_make_environment_for_app_script(app, action="backup")) + env_var["YNH_APP_BACKUP_DIR"] = os.path.join( + self.work_dir, "apps", app, "backup" + ) + + return env_var + + def _collect_system_files(self): + """ + List file to backup for each selected system part + + This corresponds to scripts in data/hooks/backup/ (system hooks) and + to those in /etc/yunohost/hooks.d/backup/ (user hooks) + + Environment variables: + YNH_BACKUP_DIR -- The backup working directory (in + "/home/yunohost.backup/tmp/BACKUPNAME" or could be + defined by the user) + YNH_BACKUP_CSV -- A temporary CSV where the script whould list paths toi + backup + """ + + system_targets = self.targets.list("system", exclude=["Skipped"]) + + # If nothing to backup, return immediately + if system_targets == []: + return + + logger.debug(m18n.n("backup_running_hooks")) + + # Prepare environnement + env_dict = self._get_env_var() + + # Actual call to backup scripts/hooks + + ret = hook_callback( + "backup", + system_targets, + args=[self.work_dir], + env=env_dict, + chdir=self.work_dir, + ) + + ret_succeed = { + hook: [ + path for path, result in infos.items() if result["state"] == "succeed" + ] + for hook, infos in ret.items() + if any(result["state"] == "succeed" for result in infos.values()) + } + ret_failed = { + hook: [ + path for path, result in infos.items() if result["state"] == "failed" + ] + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + } + + if list(ret_succeed.keys()) != []: + self.system_return = ret_succeed + + # Add files from targets (which they put in the CSV) to the list of + # files to backup + self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) + + # Save restoration hooks for each part that suceeded (and which have + # a restore hook available) + + restore_hooks_dir = os.path.join(self.work_dir, "hooks", "restore") + if not os.path.exists(restore_hooks_dir): + mkdir(restore_hooks_dir, mode=0o700, parents=True, uid="root") + + restore_hooks = hook_list("restore")["hooks"] + + for part in ret_succeed.keys(): + if part in restore_hooks: + part_restore_hooks = hook_info("restore", part)["hooks"] + for hook in part_restore_hooks: + self._add_to_list_to_backup(hook["path"], "hooks/restore/") + self.targets.set_result("system", part, "Success") + else: + logger.warning(m18n.n("restore_hook_unavailable", hook=part)) + self.targets.set_result("system", part, "Warning") + + for part in ret_failed.keys(): + logger.error(m18n.n("backup_system_part_failed", part=part)) + self.targets.set_result("system", part, "Error") + + def _collect_apps_files(self): + """Prepare backup for each selected apps""" + + apps_targets = self.targets.list("apps", exclude=["Skipped"]) + + for app_instance_name in apps_targets: + self._collect_app_files(app_instance_name) + + def _collect_app_files(self, app): + """ + List files to backup for the app into the paths_to_backup dict. + + If the app backup script fails, paths from this app already listed for + backup aren't added to the general list and will be ignored + + Environment variables: + YNH_BACKUP_DIR -- The backup working directory (in + "/home/yunohost.backup/tmp/BACKUPNAME" or could be + defined by the user) + YNH_BACKUP_CSV -- A temporary CSV where the script whould list paths toi + backup + YNH_APP_BACKUP_DIR -- The directory where the script should put + temporary files to backup like database dump, + files in this directory don't need to be added to + the temporary CSV. + YNH_APP_ID -- The app id (eg wordpress) + YNH_APP_INSTANCE_NAME -- The app instance name (eg wordpress__3) + YNH_APP_INSTANCE_NUMBER -- The app instance number (eg 3) + + + Args: + app -- (string) an app instance name (already installed) to backup + """ + + from .app import app_info + + app_setting_path = os.path.join(APPS_SETTING_PATH, app) + + # Prepare environment + env_dict = self._get_env_var(app) + env_dict["YNH_APP_BASEDIR"] = os.path.join( + self.work_dir, "apps", app, "settings" + ) + tmp_app_bkp_dir = env_dict["YNH_APP_BACKUP_DIR"] + settings_dir = os.path.join(self.work_dir, "apps", app, "settings") + + logger.info(m18n.n("app_start_backup", app=app)) + tmp_workdir_for_app = _make_tmp_workdir_for_app(app=app) + try: + # Prepare backup directory for the app + mkdir(tmp_app_bkp_dir, 0o700, True, uid="root") + + # Copy the app settings to be able to call _common.sh + shutil.copytree(app_setting_path, settings_dir) + + hook_exec( + f"{tmp_workdir_for_app}/scripts/backup", + raise_on_error=True, + chdir=tmp_app_bkp_dir, + env=env_dict, + )[0] + + self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) + + except Exception as e: + logger.debug(e) + abs_tmp_app_dir = os.path.join(self.work_dir, "apps/", app) + shutil.rmtree(abs_tmp_app_dir, ignore_errors=True) + logger.error(m18n.n("backup_app_script_failed", app=app)) + self.targets.set_result("apps", app, "Error") + else: + # Add app info + i = app_info(app) + self.apps_return[app] = { + "version": i["version"], + "name": i["name"], + "description": i["description"], + } + self.targets.set_result("apps", app, "Success") + + # Remove tmp files in all situations + finally: + shutil.rmtree(tmp_workdir_for_app) + rm(env_dict["YNH_BACKUP_CSV"], force=True) + + # + # Actual backup archive creation / method management # + # + + def backup(self): + """Apply backup methods""" + + for method in self.methods: + method_name = ( + method.method if hasattr(method, "method") else method.method_name + ) + logger.debug( + m18n.n( + "backup_applying_method_" + method.method_name, + method=method_name, + ) + ) + method.mount_and_backup() + logger.debug( + m18n.n( + "backup_method_" + method.method_name + "_finished", + method=method_name, + ) + ) + + def _compute_backup_size(self): + """ + Compute backup global size and details size for each apps and system + parts + + Update self.size and self.size_details + + Note: currently, these sizes are the size in this archive, not really + the size of needed to restore the archive. To know the size needed to + restore we should consider apt/npm/pip dependencies space and database + dump restore operations. + + Return: + (int) The global size of the archive in bytes + """ + # FIXME Database dump will be loaded, so dump should use almost the + # double of their space + # FIXME Some archive will set up dependencies, those are not in this + # size info + self.size = 0 + for system_key in self.system_return: + self.size_details["system"][system_key] = 0 + for app_key in self.apps_return: + self.size_details["apps"][app_key] = 0 + + for row in self.paths_to_backup: + if row["dest"] == "info.json": + continue + + size = space_used_by_directory(row["source"], follow_symlinks=False) + + # Add size to apps details + splitted_dest = row["dest"].split("/") + category = splitted_dest[0] + if category == "apps": + for app_key in self.apps_return: + if row["dest"].startswith("apps/" + app_key): + self.size_details["apps"][app_key] += size + break + + # OR Add size to the correct system element + elif category == "data" or category == "conf": + for system_key in self.system_return: + if row["dest"].startswith(system_key.replace("_", "/")): + self.size_details["system"][system_key] += size + break + + self.size += size + + return self.size + + +class RestoreManager: + """ + RestoreManager allow to restore a past backup archive + + Currently it's a tar file, but it could be another kind of archive + + Public properties: + info (getter)i # FIXME + work_dir (getter) # FIXME currently it's not a getter + name (getter) # FIXME currently it's not a getter + success (getter) + result (getter) # FIXME + + Public methods: + set_targets(self, system_parts=[], apps=[]) + restore(self) + + Usage: + restore_manager = RestoreManager(name) + + restore_manager.set_targets(None, ['wordpress__3']) + + restore_manager.restore() + + if restore_manager.success: + logger.success(m18n.n('restore_complete')) + + return restore_manager.result + """ + + def __init__(self, name, method="tar", no_remove_on_failure=False): + """ + RestoreManager constructor + + Args: + name -- (string) Archive name + method -- (string) Method name to use to mount the archive + """ + from packaging import version + + # Retrieve and open the archive + # FIXME this way to get the info is not compatible with copy or custom + # backup methods + self.info = backup_info(name, with_details=True) + + from_version = self.info.get("from_yunohost_version", "") + # Remove any '~foobar' in the version ... c.f ~alpha, ~beta version during + # early dev for next debian version + from_version = re.sub(r"~\w+", "", from_version) + + if not from_version or version.parse(from_version) < version.parse("4.2.0"): + raise YunohostValidationError("restore_backup_too_old") + + self.archive_path = self.info["path"] + self.name = name + self.method = BackupMethod.create(method, self) + self.targets = BackupRestoreTargetsManager() + self.no_remove_on_failure = no_remove_on_failure + + # + # Misc helpers # + # + + @property + def success(self): + successful_apps = self.targets.list("apps", include=["Success", "Warning"]) + successful_system = self.targets.list("system", include=["Success", "Warning"]) + + return len(successful_apps) != 0 or len(successful_system) != 0 + + def _read_info_files(self): + """ + Read the info file from inside an archive + """ + # Retrieve backup info + info_file = os.path.join(self.work_dir, "info.json") + try: + with open(info_file, "r") as f: + self.info = json.load(f) + + # Historically, "system" was "hooks" + if "system" not in self.info.keys(): + self.info["system"] = self.info["hooks"] + except IOError: + logger.debug("unable to load '%s'", info_file, exc_info=1) + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=self.archive_path + ) + else: + logger.debug( + "restoring from backup '%s' created on %s", + self.name, + datetime.utcfromtimestamp(self.info["created_at"]), + ) + + def _postinstall_if_needed(self): + """ + Post install yunohost if needed + """ + # Check if YunoHost is installed + if not os.path.isfile("/etc/yunohost/installed"): + # Retrieve the domain from the backup + try: + with open(f"{self.work_dir}/conf/ynh/current_host", "r") as f: + domain = f.readline().rstrip() + except IOError: + logger.debug( + "unable to retrieve current_host from the backup", exc_info=1 + ) + # FIXME include the current_host by default ? + raise YunohostError( + "The main domain name cannot be retrieved from inside the archive, and is needed to perform the postinstall", + raw_msg=True, + ) + + logger.debug("executing the post-install...") + + # Use a dummy password which is not gonna be saved anywhere + # because the next thing to happen should be that a full restore of the LDAP db will happen + tools_postinstall( + domain, + "tmpadmin", + "Tmp Admin", + password=random_ascii(70), + ignore_dyndns=True, + overwrite_root_password=False, + ) + + def clean(self): + """ + End a restore operations by cleaning the working directory and + regenerate ssowat conf (if some apps were restored) + """ + from .app import app_ssowatconf + from .permission import _sync_permissions_with_ldap + + _sync_permissions_with_ldap() + app_ssowatconf() + + if os.path.ismount(self.work_dir): + ret = subprocess.call(["umount", self.work_dir]) + if ret != 0: + logger.warning(m18n.n("restore_cleaning_failed")) + rm(self.work_dir, recursive=True, force=True) + + # + # Restore target manangement # + # + + def set_system_targets(self, system_parts=[]): + """ + Define system parts that will be restored + + Args: + system_parts -- (list) list of system parts which should be restored. + If an empty list if given, restore all system part in + the archive. If None is given, no system will be restored. + """ + + def unknown_error(part): + logger.error(m18n.n("backup_archive_system_part_not_available", part=part)) + + target_list = self.targets.set_wanted( + "system", system_parts, self.info["system"].keys(), unknown_error + ) + + # Now we need to check that the restore hook is actually available for + # all targets we want to restore + + # These are the hooks on the current installation + available_restore_system_hooks = hook_list("restore")["hooks"] + + custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore") + mkdir(custom_restore_hook_folder, 755, parents=True, force=True) + + for system_part in target_list: + # By default, we'll use the restore hooks on the current install + # if available + + # FIXME: so if the restore hook exist we use the new one and not + # the one from backup. So hook should not break compatibility.. + + if system_part in available_restore_system_hooks: + continue + + # Otherwise, attempt to find it (or them?) in the archive + + # If we didn't find it, we ain't gonna be able to restore it + if ( + system_part not in self.info["system"] + or "paths" not in self.info["system"][system_part] + or len(self.info["system"][system_part]["paths"]) == 0 + ): + logger.error(m18n.n("restore_hook_unavailable", part=system_part)) + self.targets.set_result("system", system_part, "Skipped") + continue + + hook_paths = self.info["system"][system_part]["paths"] + hook_paths = [f"hooks/restore/{os.path.basename(p)}" for p in hook_paths] + + # Otherwise, add it from the archive to the system + # FIXME: Refactor hook_add and use it instead + for hook_path in hook_paths: + logger.debug( + "Adding restoration script '%s' to the system " + "from the backup archive '%s'", + hook_path, + self.archive_path, + ) + self.method.copy(hook_path, custom_restore_hook_folder) + + def set_apps_targets(self, apps=[]): + """ + Define and validate targetted apps to be restored + + Args: + apps -- (list) list of apps which should be restored. If [] is given, + all apps in the archive will be restored. If None is given, + no apps will be restored. + """ + + def unknown_error(app): + logger.error(m18n.n("backup_archive_app_not_found", app=app)) + + to_be_restored = self.targets.set_wanted( + "apps", apps, self.info["apps"].keys(), unknown_error + ) + + # If all apps to restore are already installed, stop right here. + # Otherwise, if at least one app can be restored, we keep going on + # because those which can be restored will indeed be restored + already_installed = [app for app in to_be_restored if _is_installed(app)] + if already_installed != []: + if already_installed == to_be_restored: + raise YunohostValidationError( + "restore_already_installed_apps", apps=", ".join(already_installed) + ) + else: + logger.warning( + m18n.n( + "restore_already_installed_apps", + apps=", ".join(already_installed), + ) + ) + + # + # Archive mounting # + # + + def mount(self): + """ + Mount the archive. We avoid copy to be able to restore on system without + too many space. + + Use the mount method from the BackupMethod instance and read info about + this archive + """ + + self.work_dir = os.path.join(BACKUP_PATH, "tmp", self.name) + + if os.path.ismount(self.work_dir): + logger.debug("An already mounting point '%s' already exists", self.work_dir) + ret = subprocess.call(["umount", self.work_dir]) + if ret == 0: + subprocess.call(["rmdir", self.work_dir]) + logger.debug(f"Unmount dir: {self.work_dir}") + else: + raise YunohostError("restore_removing_tmp_dir_failed") + elif os.path.isdir(self.work_dir): + logger.debug( + "temporary restore directory '%s' already exists", self.work_dir + ) + ret = subprocess.call(["rm", "-Rf", self.work_dir]) + if ret == 0: + logger.debug(f"Delete dir: {self.work_dir}") + else: + raise YunohostError("restore_removing_tmp_dir_failed") + + mkdir(self.work_dir, parents=True) + + self.method.mount() + + self._read_info_files() + + # + # Space computation / checks # + # + + def _compute_needed_space(self): + """ + Compute needed space to be able to restore + + Return: + size -- (int) needed space to backup in bytes + margin -- (int) margin to be sure the backup don't fail by missing space + in bytes + """ + system = self.targets.list("system", exclude=["Skipped"]) + apps = self.targets.list("apps", exclude=["Skipped"]) + restore_all_system = system == self.info["system"].keys() + restore_all_apps = apps == self.info["apps"].keys() + + # If complete restore operations (or legacy archive) + margin = CONF_MARGIN_SPACE_SIZE * 1024 * 1024 + if (restore_all_system and restore_all_apps) or "size_details" not in self.info: + size = self.info["size"] + if ( + "size_details" not in self.info + or self.info["size_details"]["apps"] != {} + ): + margin = APP_MARGIN_SPACE_SIZE * 1024 * 1024 + # Partial restore don't need all backup size + else: + size = 0 + if system is not None: + for system_element in system: + size += self.info["size_details"]["system"][system_element] + + # TODO how to know the dependencies size ? + if apps is not None: + for app in apps: + size += self.info["size_details"]["apps"][app] + margin = APP_MARGIN_SPACE_SIZE * 1024 * 1024 + + if not os.path.isfile("/etc/yunohost/installed"): + size += POSTINSTALL_ESTIMATE_SPACE_SIZE * 1024 * 1024 + return (size, margin) + + def assert_enough_free_space(self): + """ + Check available disk space + """ + + free_space = free_space_in_directory(BACKUP_PATH) + + (needed_space, margin) = self._compute_needed_space() + if free_space >= needed_space + margin: + return True + elif free_space > needed_space: + # TODO Add --force options to avoid the error raising + raise YunohostValidationError( + "restore_may_be_not_enough_disk_space", + free_space=free_space, + needed_space=needed_space, + margin=margin, + ) + else: + raise YunohostValidationError( + "restore_not_enough_disk_space", + free_space=free_space, + needed_space=needed_space, + margin=margin, + ) + + # + # "Actual restore" (reverse step of the backup collect part) # + # + + def restore(self): + """ + Restore the archive + + Restore system parts and apps after mounting the archive, checking free + space and postinstall if needed + """ + + try: + self._postinstall_if_needed() + + self._restore_system() + self._restore_apps() + except Exception as e: + raise YunohostError( + f"The following critical error happened during restoration: {e}", + raw_msg=True, + ) + finally: + self.clean() + + def _restore_system(self): + """Restore user and system parts""" + + system_targets = self.targets.list("system", exclude=["Skipped"]) + + # If nothing to restore, return immediately + if system_targets == []: + return + + from . import domain + from .app import app_ssowatconf + from .permission import _sync_permissions_with_ldap + + # Start register change on system + operation_logger = OperationLogger("backup_restore_system") + operation_logger.start() + + logger.debug(m18n.n("restore_running_hooks")) + + env_dict = { + "YNH_BACKUP_DIR": self.work_dir, + "YNH_BACKUP_CSV": os.path.join(self.work_dir, "backup.csv"), + } + operation_logger.extra["env"] = env_dict + operation_logger.flush() + ret = hook_callback( + "restore", + system_targets, + args=[self.work_dir], + env=env_dict, + chdir=self.work_dir, + ) + + ret_succeed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "succeed" for result in infos.values()) + ] + ret_failed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + ] + + for part in ret_succeed: + self.targets.set_result("system", part, "Success") + + error_part = [] + for part in ret_failed: + logger.error(m18n.n("restore_system_part_failed", part=part)) + self.targets.set_result("system", part, "Error") + error_part.append(part) + + if ret_failed: + operation_logger.error( + m18n.n("restore_system_part_failed", part=", ".join(error_part)) + ) + else: + operation_logger.success() + + domain.domain_list_cache = {} + + regen_conf() + + _tools_migrations_run_after_system_restore( + backup_version=self.info["from_yunohost_version"] + ) + + _sync_permissions_with_ldap() + app_ssowatconf() + + def _restore_apps(self): + """Restore all apps targeted""" + + apps_targets = self.targets.list("apps", exclude=["Skipped"]) + + for app in apps_targets: + self._restore_app(app) + + def _restore_app(self, app_instance_name): + """ + Restore an app + + Environment variables: + YNH_BACKUP_DIR -- The backup working directory (in + "/home/yunohost.backup/tmp/BACKUPNAME" or could be + defined by the user) + YNH_BACKUP_CSV -- A temporary CSV where the script whould list paths to + backup + YNH_APP_BACKUP_DIR -- The directory where the script should put + temporary files to backup like database dump, + files in this directory don't need to be added to + the temporary CSV. + YNH_APP_ID -- The app id (eg wordpress) + YNH_APP_INSTANCE_NAME -- The app instance name (eg wordpress__3) + YNH_APP_INSTANCE_NUMBER -- The app instance number (eg 3) + + Args: + app_instance_name -- (string) The app name to restore (no app with this + name should be already install) + """ + from .app import app_remove + from .utils.legacy import _patch_legacy_helpers + + def copytree(src, dst, symlinks=False, ignore=None): + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if os.path.isdir(s): + shutil.copytree(s, d, symlinks, ignore) + else: + shutil.copy2(s, d) + + # Check if the app is not already installed + if _is_installed(app_instance_name): + logger.error(m18n.n("restore_already_installed_app", app=app_instance_name)) + self.targets.set_result("apps", app_instance_name, "Error") + return + + # Start register change on system + related_to = [("app", app_instance_name)] + operation_logger = OperationLogger("backup_restore_app", related_to) + operation_logger.start() + + logger.info(m18n.n("app_start_restore", app=app_instance_name)) + + app_dir_in_archive = os.path.join(self.work_dir, "apps", app_instance_name) + app_backup_in_archive = os.path.join(app_dir_in_archive, "backup") + app_settings_in_archive = os.path.join(app_dir_in_archive, "settings") + app_scripts_in_archive = os.path.join(app_settings_in_archive, "scripts") + + # Attempt to patch legacy helpers... + _patch_legacy_helpers(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) + + # Check if the app has a restore script + app_restore_script_in_archive = os.path.join(app_scripts_in_archive, "restore") + if not os.path.isfile(app_restore_script_in_archive): + logger.warning(m18n.n("unrestore_app", app=app_instance_name)) + self.targets.set_result("apps", app_instance_name, "Warning") + return + + try: + # Restore app settings + app_settings_new_path = os.path.join( + "/etc/yunohost/apps/", app_instance_name + ) + app_scripts_new_path = os.path.join(app_settings_new_path, "scripts") + shutil.copytree(app_settings_in_archive, app_settings_new_path) + chmod(app_settings_new_path, 0o400, 0o400, True) + chown(app_scripts_new_path, "root", None, True) + + # Copy the app scripts to a writable temporary folder + tmp_workdir_for_app = _make_tmp_workdir_for_app() + copytree(app_scripts_in_archive, tmp_workdir_for_app) + chmod(tmp_workdir_for_app, 0o700, 0o700, True) + chown(tmp_workdir_for_app, "root", None, True) + restore_script = os.path.join(tmp_workdir_for_app, "restore") + + _tools_migrations_run_before_app_restore( + backup_version=self.info["from_yunohost_version"], + app_id=app_instance_name, + app_backup_in_archive=app_backup_in_archive, + ) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + msg = m18n.n("app_restore_failed", app=app_instance_name, error=error) + logger.error(msg) + operation_logger.error(msg) + + self.targets.set_result("apps", app_instance_name, "Error") + + # Cleanup + shutil.rmtree(app_settings_new_path, ignore_errors=True) + shutil.rmtree(tmp_workdir_for_app, ignore_errors=True) + + return + + logger.debug(m18n.n("restore_running_app_script", app=app_instance_name)) + + # Prepare env. var. to pass to script + # FIXME : workdir should be a tmp workdir + app_workdir = os.path.join(self.work_dir, "apps", app_instance_name, "settings") + env_dict = _make_environment_for_app_script( + app_instance_name, workdir=app_workdir, action="restore" + ) + env_dict.update( + { + "YNH_BACKUP_DIR": self.work_dir, + "YNH_BACKUP_CSV": os.path.join(self.work_dir, "backup.csv"), + "YNH_APP_BACKUP_DIR": os.path.join( + self.work_dir, "apps", app_instance_name, "backup" + ), + } + ) + + operation_logger.extra["env"] = env_dict + operation_logger.flush() + + manifest = _get_manifest_of_app(app_settings_in_archive) + if manifest["packaging_format"] >= 2: + from .utils.resources import AppResourceManager + + AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( + rollback_and_raise_exception_if_failure=True, + operation_logger=operation_logger, + action="restore", + ) + + # Execute the app install script + restore_failed = True + try: + ( + restore_failed, + failure_message_with_debug_instructions, + ) = hook_exec_with_script_debug_if_failure( + restore_script, + chdir=app_backup_in_archive, + env=env_dict, + operation_logger=operation_logger, + error_message_if_script_failed=m18n.n("app_restore_script_failed"), + error_message_if_failed=lambda e: m18n.n( + "app_restore_failed", app=app_instance_name, error=e + ), + ) + finally: + if not restore_failed: + self.targets.set_result("apps", app_instance_name, "Success") + operation_logger.success() + + # Clean hooks and add new ones + hook_remove(app_instance_name) + if "hooks" in os.listdir(app_settings_in_archive): + for hook in os.listdir(app_settings_in_archive + "/hooks"): + hook_add( + app_instance_name, + app_settings_in_archive + "/hooks/" + hook, + ) + + # Cleaning temporary scripts directory + shutil.rmtree(tmp_workdir_for_app, ignore_errors=True) + + # 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") + + # Cleaning temporary scripts directory + shutil.rmtree(tmp_workdir_for_app, ignore_errors=True) + + if not self.no_remove_on_failure: + app_remove(app_instance_name, force_workdir=app_workdir) + else: + logger.error( + f"The restore of {app_instance_name} failed, but was not cleaned up as requested by --no-remove-on-failure." + ) + + logger.error(failure_message_with_debug_instructions) + + +# +# Backup methods # +# +class BackupMethod: + """ + BackupMethod is an abstract class that represents a way to backup and + restore a list of files. + + Daughters of this class can be used by a BackupManager or RestoreManager + instance. Some methods are meant to be used by BackupManager and others + by RestoreManager. + + BackupMethod has a factory method "create" to initialize instances. + + Currently, there are 3 BackupMethods implemented: + + CopyBackupMethod + ---------------- + This method corresponds to a raw (uncompressed) copy of files to a location, + and (could?) reverse the copy when restoring. + + TarBackupMethod + --------------- + This method compresses all files to backup in a .tar archive. When + restoring, it untars the required parts. + + CustomBackupMethod + ------------------ + This one use a custom bash scrip/hook "backup_method" to do the + backup/restore operations. A user can add his own hook inside + /etc/yunohost/hooks.d/backup_method/ + + Public properties: + method_name + + Public methods: + mount_and_backup(self) + mount(self) + create(cls, method, **kwargs) + + Usage: + method = BackupMethod.create("tar", backup_manager) + method.mount_and_backup() + #or + method = BackupMethod.create("copy", restore_manager) + method.mount() + """ + + @classmethod + def create(cls, method, manager, **kwargs): + """ + Factory method to create instance of BackupMethod + + Args: + method -- (string) The method name of an existing BackupMethod. If the + name is unknown the CustomBackupMethod will be tried + *args -- Specific args for the method, could be the repo target by the + method + + Return a BackupMethod instance + """ + known_methods = {c.method_name: c for c in BackupMethod.__subclasses__()} + backup_method = known_methods.get(method, CustomBackupMethod) + return backup_method(manager, method=method, **kwargs) + + def __init__(self, manager, repo=None, **kwargs): + """ + BackupMethod constructors + + Note it is an abstract class. You should use the "create" class method + to create instance. + + Args: + repo -- (string|None) A string that represent the repo where put or + get the backup. It could be a path, and in future a + BackupRepository object. If None, the default repo is used : + /home/yunohost.backup/archives/ + """ + self.manager = manager + self.repo = ARCHIVES_PATH if repo is None else repo + + @property + def method_name(self): + """Return the string name of a BackupMethod (eg "tar" or "copy")""" + raise YunohostError("backup_abstract_method") + + @property + def name(self): + """Return the backup name""" + return self.manager.name + + @property + def work_dir(self): + """ + Return the working directory + + For a BackupManager, it is the directory where we prepare the files to + backup + + For a RestoreManager, it is the directory where we mount the archive + before restoring + """ + return self.manager.work_dir + + def need_mount(self): + """ + Return True if this backup method need to organize path to backup by + binding its in the working directory before to backup its. + + Indeed, some methods like tar or copy method don't need to organize + files before to add it inside the archive, but others like borgbackup + are not able to organize directly the files. In this case we have the + choice to organize in the working directory before to put in the archive + or to organize after mounting the archive before the restoring + operation. + + The default behaviour is to return False. To change it override the + method. + + Note it's not a property because some overrided methods could do long + treatment to get this info + """ + return False + + def mount_and_backup(self): + """ + Run the backup on files listed by the BackupManager instance + + This method shouldn't be overrided, prefer overriding self.backup() and + self.clean() + """ + if self.need_mount(): + self._organize_files() + + try: + self.backup() + finally: + self.clean() + + def mount(self): + """ + Mount the archive from RestoreManager instance in the working directory + + This method should be extended. + """ + pass + + def clean(self): + """ + Umount sub directories of working dirextories and delete it if temporary + """ + if self.need_mount(): + if not _recursive_umount(self.work_dir): + raise YunohostError("backup_cleaning_failed") + + if self.manager.is_tmp_work_dir: + rm(self.work_dir, True, True) + + def _check_is_enough_free_space(self): + """ + Check free space in repository or output directory before to backup + """ + # TODO How to do with distant repo or with deduplicated backup ? + backup_size = self.manager.size + + free_space = free_space_in_directory(self.repo) + + if free_space < backup_size: + logger.debug( + "Not enough space at %s (free: %s / needed: %d)", + self.repo, + free_space, + backup_size, + ) + raise YunohostValidationError("not_enough_disk_space", path=self.repo) + + def _organize_files(self): + """ + Mount all csv src in their related path + + The goal is to organize the files app by app and hook by hook, before + custom backup method or before the restore operation (in the case of an + unorganize archive). + + The usage of binding could be strange for a user because the du -sb + command will return that the working directory is big. + """ + paths_needed_to_be_copied = [] + for path in self.manager.paths_to_backup: + src = path["source"] + + if self.manager is RestoreManager: + # TODO Support to run this before a restore (and not only before + # backup). To do that RestoreManager.unorganized_work_dir should + # be implemented + src = os.path.join(self.unorganized_work_dir, src) + + dest = os.path.join(self.work_dir, path["dest"]) + if dest == src: + continue + dest_dir = os.path.dirname(dest) + + # Be sure the parent dir of destination exists + if not os.path.isdir(dest_dir): + mkdir(dest_dir, parents=True) + + # For directory, attempt to mount bind + if os.path.isdir(src): + mkdir(dest, parents=True, force=True) + + try: + subprocess.check_call(["mount", "--rbind", src, dest]) + subprocess.check_call(["mount", "-o", "remount,ro,bind", dest]) + except Exception: + logger.warning(m18n.n("backup_couldnt_bind", src=src, dest=dest)) + # To check if dest is mounted, use /proc/mounts that + # escape spaces as \040 + raw_mounts = read_file("/proc/mounts").strip().split("\n") + mounts = [m.split()[1] for m in raw_mounts] + mounts = [m.replace("\\040", " ") for m in mounts] + if dest in mounts: + subprocess.check_call(["umount", "-R", dest]) + else: + # Success, go to next file to organize + continue + + # For files, create a hardlink + elif os.path.isfile(src) or os.path.islink(src): + # Can create a hard link only if files are on the same fs + # (i.e. we can't if it's on a different fs) + if os.stat(src).st_dev == os.stat(dest_dir).st_dev: + # Don't hardlink /etc/cron.d files to avoid cron bug + # 'NUMBER OF HARD LINKS > 1' see #1043 + cron_path = os.path.abspath("/etc/cron") + "." + if not os.path.abspath(src).startswith(cron_path): + try: + os.link(src, dest) + except Exception as e: + # This kind of situation may happen when src and dest are on different + # logical volume ... even though the st_dev check previously match... + # E.g. this happens when running an encrypted hard drive + # where everything is mapped to /dev/mapper/some-stuff + # yet there are different devices behind it or idk ... + logger.warning( + f"Could not link {src} to {dest} ({e}) ... falling back to regular copy." + ) + else: + # Success, go to next file to organize + continue + + # If mountbind or hardlink couldnt be created, + # prepare a list of files that need to be copied + paths_needed_to_be_copied.append(path) + + if len(paths_needed_to_be_copied) == 0: + return + # Manage the case where we are not able to use mount bind abilities + # It could be just for some small files on different filesystems or due + # to mounting error + + # Compute size to copy + size = sum( + space_used_by_directory(path["source"], follow_symlinks=False) + for path in paths_needed_to_be_copied + ) + size /= 1024 * 1024 # Convert bytes to megabytes + + # Ask confirmation for copying + if size > MB_ALLOWED_TO_ORGANIZE: + # Check if we're in an interactive terminal + is_interactive = ( + sys.stdout.isatty() if hasattr(sys.stdout, "isatty") else False + ) + + if is_interactive: + i = Moulinette.prompt( + m18n.n( + "backup_ask_for_copying_if_needed", + answers="y/N", + size=str(size), + ) + ) + if i != "y" and i != "Y": + raise YunohostError("backup_unable_to_organize_files") + else: + # In non-interactive mode, accept automatically with a warning + logger.warning( + f"Copying {size:.1f} MB without confirmation (non-interactive mode)" + ) + + # Copy unbinded path + logger.debug(m18n.n("backup_copying_to_organize_the_archive", size=str(size))) + for path in paths_needed_to_be_copied: + dest = os.path.join(self.work_dir, path["dest"]) + if os.path.isdir(path["source"]): + shutil.copytree(path["source"], dest, symlinks=True) + else: + shutil.copy(path["source"], dest) + + +class CopyBackupMethod(BackupMethod): + """ + This class just do an uncompress copy of each file in a location, and + could be the inverse for restoring + """ + + method_name = "copy" + + def backup(self): + """Copy prepared files into a the repo""" + # Check free space in output + self._check_is_enough_free_space() + + for path in self.manager.paths_to_backup: + source = path["source"] + dest = os.path.join(self.repo, path["dest"]) + if source == dest: + logger.debug("Files already copyed") + return + + dest_parent = os.path.dirname(dest) + if not os.path.exists(dest_parent): + mkdir(dest_parent, 0o700, True) + + if os.path.isdir(source): + shutil.copytree(source, dest) + else: + shutil.copy(source, dest) + + def mount(self): + """ + Mount the uncompress backup in readonly mode to the working directory + """ + # FIXME: This code is untested because there is no way to run it from + # the ynh cli + super(CopyBackupMethod, self).mount() + + if not os.path.isdir(self.repo): + raise YunohostError("backup_no_uncompress_archive_dir") + + mkdir(self.work_dir, parents=True) + ret = subprocess.call(["mount", "-r", "--rbind", self.repo, self.work_dir]) + if ret == 0: + return + + logger.warning( + "Could not mount the backup in readonly mode with --rbind ... Unmounting" + ) + # FIXME : Does this stuff really works ? '&&' is going to be interpreted as an argument for mounpoint here ... Not as a classical '&&' ... + subprocess.call( + ["mountpoint", "-q", self.work_dir, "&&", "umount", "-R", self.work_dir] + ) + raise YunohostError("backup_cant_mount_uncompress_archive") + + def copy(self, file, target): + shutil.copy(file, target) + + +class TarBackupMethod(BackupMethod): + method_name = "tar" + + @property + def _archive_file(self): + from .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" + ): + return os.path.join(self.repo, self.name + ".tar.gz") + + f = os.path.join(self.repo, self.name + ".tar") + if os.path.exists(f + ".gz"): + f += ".gz" + return f + + def backup(self): + """ + Compress prepared files + + It adds the info.json in /home/yunohost.backup/archives and if the + compress archive isn't located here, add a symlink to the archive to. + """ + + if not os.path.exists(self.repo): + mkdir(self.repo, 0o750, parents=True) + + # Check free space in output + self._check_is_enough_free_space() + + # Open archive file for writing + try: + tar = tarfile.open( + self._archive_file, + "w:gz" if self._archive_file.endswith(".gz") else "w", + ) + except Exception: + logger.debug( + "unable to open '%s' for writing", self._archive_file, exc_info=1 + ) + raise YunohostError("backup_archive_open_failed") + + # Add files to the archive + try: + for path in self.manager.paths_to_backup: + # Add the "source" into the archive and transform the path into + # "dest" + tar.add(path["source"], arcname=path["dest"]) + except IOError: + logger.error( + m18n.n( + "backup_archive_writing_error", + source=path["source"], + archive=self._archive_file, + dest=path["dest"], + ), + exc_info=1, + ) + raise YunohostError("backup_creation_failed") + finally: + tar.close() + + # Move info file + shutil.copy( + os.path.join(self.work_dir, "info.json"), + os.path.join(ARCHIVES_PATH, self.name + ".info.json"), + ) + + # If backuped to a non-default location, keep a symlink of the archive + # to that location + link = os.path.join(ARCHIVES_PATH, self.name + ".tar") + if not os.path.isfile(link): + os.symlink(self._archive_file, link) + + def mount(self): + """ + Mount the archive. We avoid intermediate copies to be able to restore on system with low free space. + """ + super(TarBackupMethod, self).mount() + + # Mount the tarball + logger.debug(m18n.n("restore_extracting")) + try: + tar = tarfile.open( + self._archive_file, + "r:gz" if self._archive_file.endswith(".gz") else "r", + ) + except Exception: + logger.debug( + "cannot open backup archive '%s'", self._archive_file, exc_info=1 + ) + raise YunohostError("backup_archive_open_failed") + + try: + files_in_archive = tar.getnames() + except (IOError, EOFError, tarfile.ReadError) as e: + raise YunohostError( + "backup_archive_corrupted", archive=self._archive_file, error=str(e) + ) + + if "info.json" in tar.getnames(): + leading_dot = "" + tar.extract("info.json", path=self.work_dir) + elif "./info.json" in files_in_archive: + leading_dot = "./" + tar.extract("./info.json", path=self.work_dir) + else: + logger.debug( + "unable to retrieve 'info.json' inside the archive", exc_info=1 + ) + tar.close() + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=self._archive_file + ) + + if "backup.csv" in files_in_archive: + tar.extract("backup.csv", path=self.work_dir) + elif "./backup.csv" in files_in_archive: + tar.extract("./backup.csv", path=self.work_dir) + else: + # Old backup archive have no backup.csv file + pass + + # Extract system parts backup + conf_extracted = False + + system_targets = self.manager.targets.list("system", exclude=["Skipped"]) + apps_targets = self.manager.targets.list("apps", exclude=["Skipped"]) + + for system_part in system_targets: + # Caution: conf_ynh_currenthost helpers put its files in + # conf/ynh + if system_part.startswith("conf_"): + if conf_extracted: + continue + system_part = "conf/" + conf_extracted = True + else: + system_part = system_part.replace("_", "/") + "/" + subdir_and_files = [ + tarinfo + for tarinfo in tar.getmembers() + if tarinfo.name.startswith(leading_dot + system_part) + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) + subdir_and_files = [ + tarinfo + for tarinfo in tar.getmembers() + if tarinfo.name.startswith(leading_dot + "hooks/restore/") + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) + + # Extract apps backup + for app in apps_targets: + subdir_and_files = [ + tarinfo + for tarinfo in tar.getmembers() + if tarinfo.name.startswith(leading_dot + "apps/" + app) + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) + + tar.close() + + def copy(self, file, target): + tar = tarfile.open( + self._archive_file, "r:gz" if self._archive_file.endswith(".gz") else "r" + ) + file_to_extract = tar.getmember(file) + # Remove the path + file_to_extract.name = os.path.basename(file_to_extract.name) + tar.extract(file_to_extract, path=target) + tar.close() + + +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 + /etc/yunohost/hooks.d/backup_method/ + """ + + method_name = "custom" + + def __init__(self, manager, repo=None, method=None, **kwargs): + super(CustomBackupMethod, self).__init__(manager, repo) + self.args = kwargs + self.method = method + self._need_mount = None + + def need_mount(self): + """Call the backup_method hook to know if we need to organize files""" + if self._need_mount is not None: + return self._need_mount + + ret = hook_callback( + "backup_method", [self.method], args=self._get_args("need_mount") + ) + ret_succeed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "succeed" for result in infos.values()) + ] + self._need_mount = True if ret_succeed else False + return self._need_mount + + def backup(self): + """ + Launch a custom script to backup + """ + + ret = hook_callback( + "backup_method", [self.method], args=self._get_args("backup") + ) + + ret_failed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + ] + if ret_failed: + raise YunohostError("backup_custom_backup_error") + + def mount(self): + """ + Launch a custom script to mount the custom archive + """ + super(CustomBackupMethod, self).mount() + ret = hook_callback( + "backup_method", [self.method], args=self._get_args("mount") + ) + + ret_failed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + ] + if ret_failed: + raise YunohostError("backup_custom_mount_error") + + def _get_args(self, action): + """Return the arguments to give to the custom script""" + return [ + action, + self.work_dir, + self.name, + self.repo, + self.manager.size, + self.manager.description, + ] + + +# +# "Front-end" # +# + + +@is_unit_operation() +def backup_create( + operation_logger, + name=None, + description=None, + methods=[], + output_directory=None, + system=[], + apps=[], + dry_run=False, +): + """ + Create a backup local archive + + Keyword arguments: + name -- Name of the backup archive + description -- Short description of the backup + method -- Method of backup to use + output_directory -- Output directory for the backup + system -- List of system elements to backup + apps -- List of application names to backup + """ + + # TODO: Add a 'clean' argument to clean output directory + + # + # Validate / parse arguments # + # + + # Validate there is no archive with the same name + if name and name in backup_list()["archives"]: + raise YunohostValidationError("backup_archive_name_exists", name=name) + + # By default we backup using the tar method + if not methods: + methods = ["tar"] + + # Validate output_directory option + if output_directory: + output_directory = os.path.abspath(output_directory) + + # Check for forbidden folders + if output_directory.startswith(ARCHIVES_PATH) or re.match( + r"^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$", + output_directory, + ): + raise YunohostValidationError("backup_output_directory_forbidden") + + if "copy" in methods: + if not output_directory: + raise YunohostValidationError("backup_output_directory_required") + # Check that output directory is empty + elif os.path.isdir(output_directory) and os.listdir(output_directory): + raise YunohostValidationError("backup_output_directory_not_empty") + + # If no --system or --apps given, backup everything + if system is None and apps is None: + system = [] + apps = [] + + # + # Intialize # + # + + operation_logger.start() + + # Create yunohost archives directory if it does not exists + _create_archive_dir() + + # Initialize backup manager + + backup_manager = BackupManager( + name, description, methods=methods, work_dir=output_directory + ) + + # Add backup targets (system and apps) + + backup_manager.set_system_targets(system) + backup_manager.set_apps_targets(apps) + + for app in backup_manager.targets.list("apps", exclude=["Skipped"]): + operation_logger.related_to.append(("app", app)) + operation_logger.flush() + + # + # Collect files and put them in the archive # + # + + # Collect files to be backup (by calling app backup script / system hooks) + backup_manager.collect_files() + + if dry_run: + return { + "size": backup_manager.size, + "size_details": backup_manager.size_details, + } + + # Apply backup methods on prepared files + logger.info(m18n.n("backup_actually_backuping")) + logger.info( + m18n.n( + "backup_create_size_estimation", + size=binary_to_human(backup_manager.size) + "B", + ) + ) + backup_manager.backup() + + logger.success(m18n.n("backup_created", name=backup_manager.name)) + operation_logger.success() + + return { + "name": backup_manager.name, + "size": backup_manager.size, + "results": backup_manager.targets.results, + } + + +def backup_restore(name, system=[], apps=[], force=False, no_remove_on_failure=False): + """ + Restore from a local backup archive + + Keyword argument: + name -- Name of the local backup archive + force -- Force restauration on an already installed system + system -- List of system parts to restore + apps -- List of application names to restore + no_remove_on_failure -- Only for apps, avoid to remove the app in case of the restore fail. + Mainly useful for debug + """ + + # + # Validate / parse arguments # + # + + # If no --system or --apps given, restore everything + if system is None and apps is None: + system = [] + apps = [] + + # + # Initialize # + # + + restore_manager = RestoreManager(name, no_remove_on_failure=no_remove_on_failure) + + restore_manager.set_system_targets(system) + restore_manager.set_apps_targets(apps) + + restore_manager.assert_enough_free_space() + + # + # Add validation if restoring system parts on an already-installed system + # + + 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: + try: + # Ask confirmation for restoring + i = Moulinette.prompt( + m18n.n("restore_confirm_yunohost_installed", answers="y/N") + ) + except NotImplementedError: + pass + else: + if i == "y" or i == "Y": + force = True + if not force: + raise YunohostError("restore_failed") + + # + # Mount the archive then call the restore for each system part / app # + # + + logger.info(m18n.n("backup_mount_archive_for_restore")) + restore_manager.mount() + restore_manager.restore() + + # Check if something has been restored + if restore_manager.success: + logger.success(m18n.n("restore_complete")) + else: + raise YunohostError("restore_nothings_done") + + return restore_manager.targets.results + + +def backup_list(with_info=False, human_readable=False): + """ + List available local backup archives + + Keyword arguments: + with_info -- Show backup information for each archive + human_readable -- Print sizes in human readable format + + """ + # Get local archives sorted according to last modification time + # (we do a realpath() to resolve symlinks) + archives = glob(f"{ARCHIVES_PATH}/*.tar.gz") + glob(f"{ARCHIVES_PATH}/*.tar") + archives = {os.path.realpath(archive) for archive in archives} + archives = {archive for archive in archives if os.path.exists(archive)} + archives = sorted(archives, key=lambda x: os.path.getctime(x)) + # Extract only filename without the extension + + def remove_extension(f): + if f.endswith(".tar.gz"): + return os.path.basename(f)[: -len(".tar.gz")] + else: + return os.path.basename(f)[: -len(".tar")] + + archives = [remove_extension(f) for f in archives] + + if with_info: + d = OrderedDict() + for archive in archives: + try: + d[archive] = backup_info(archive, human_readable=human_readable) + except YunohostError as e: + logger.warning(str(e)) + except Exception: + import traceback + + trace_ = "\n" + traceback.format_exc() + logger.warning(f"Could not check infos for archive {archive}: {trace_}") + + archives = d + + return {"archives": archives} + + +def backup_download(name): + if Moulinette.interface.type != "api": + logger.error( + "This option is only meant for the API/webadmin and doesn't make sense for the command line." + ) + return + + archive_file = f"{ARCHIVES_PATH}/{name}.tar" + + # Check file exist (even if it's a broken symlink) + 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) + + # If symlink, retrieve the real path + if os.path.islink(archive_file): + archive_file = os.path.realpath(archive_file) + + # Raise exception if link is broken (e.g. on unmounted external storage) + if not os.path.exists(archive_file): + raise YunohostValidationError( + "backup_archive_broken_link", path=archive_file + ) + + # We return a raw bottle HTTPresponse (instead of serializable data like + # list/dict, ...), which is gonna be picked and used directly by moulinette + from bottle import static_file + + archive_folder, archive_file_name = archive_file.rsplit("/", 1) + return static_file(archive_file_name, archive_folder, download=archive_file_name) + + +def backup_info(name, with_details=False, human_readable=False): + """ + Get info about a local backup archive + + Keyword arguments: + name -- Name of the local backup archive + with_details -- Show additional backup information + human_readable -- Print sizes in human readable format + + """ + original_name = name + + if name.endswith(".tar.gz"): + name = name[: -len(".tar.gz")] + elif name.endswith(".tar"): + name = name[: -len(".tar")] + + archive_file = f"{ARCHIVES_PATH}/{name}.tar" + + # Check file exist (even if it's a broken symlink) + if not os.path.lexists(archive_file): + archive_file += ".gz" + if not os.path.lexists(archive_file): + # 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): + archive_file = os.path.realpath(archive_file) + + # Raise exception if link is broken (e.g. on unmounted external storage) + if not os.path.exists(archive_file): + raise YunohostValidationError( + "backup_archive_broken_link", path=archive_file + ) + + info_file = f"{ARCHIVES_PATH}/{name}.info.json" + + if not os.path.exists(info_file): + tar = tarfile.open( + archive_file, "r:gz" if archive_file.endswith(".gz") else "r" + ) + info_dir = info_file + ".d" + + try: + files_in_archive = tar.getnames() + except (IOError, EOFError, tarfile.ReadError) as e: + raise YunohostError( + "backup_archive_corrupted", archive=archive_file, error=str(e) + ) + + try: + if "info.json" in files_in_archive: + tar.extract("info.json", path=info_dir) + elif "./info.json" in files_in_archive: + tar.extract("./info.json", path=info_dir) + else: + raise KeyError + except KeyError: + logger.debug( + "unable to retrieve '%s' inside the archive", info_file, exc_info=1 + ) + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=archive_file + ) + else: + shutil.move(os.path.join(info_dir, "info.json"), info_file) + finally: + tar.close() + os.rmdir(info_dir) + + try: + with open(info_file) as f: + # Retrieve backup info + info = json.load(f) + except Exception: + logger.debug("unable to load '%s'", info_file, exc_info=1) + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=archive_file + ) + + # Retrieve backup size + size = info.get("size", 0) + if not size: + tar = tarfile.open( + archive_file, "r:gz" if archive_file.endswith(".gz") else "r" + ) + size = reduce( + lambda x, y: getattr(x, "size", x) + getattr(y, "size", y), tar.getmembers() + ) + tar.close() + if human_readable: + size = binary_to_human(size) + "B" + + result = { + "path": archive_file, + "created_at": datetime.utcfromtimestamp(info["created_at"]), + "description": info["description"], + "size": size, + } + + if with_details: + system_key = "system" + # Historically 'system' was 'hooks' + if "hooks" in info.keys(): + system_key = "hooks" + + if "size_details" in info.keys(): + for category in ["apps", "system"]: + for name, key_info in info[category].items(): + if category == "system": + info[category][name] = key_info = {"paths": key_info} + else: + info[category][name] = key_info + + if name in info["size_details"][category].keys(): + key_info["size"] = info["size_details"][category][name] + if human_readable: + key_info["size"] = binary_to_human(key_info["size"]) + "B" + else: + key_info["size"] = -1 + if human_readable: + key_info["size"] = "?" + + result["apps"] = info["apps"] + result["system"] = info[system_key] + result["from_yunohost_version"] = info.get("from_yunohost_version") + return result + + +@is_flash_unit_operation() +def backup_delete(name, display_success: bool = True): + if name not in backup_list()["archives"]: + raise YunohostValidationError("backup_archive_name_unknown", name=name) + + hook_callback("pre_backup_delete", args=[name]) + + archive_file = f"{ARCHIVES_PATH}/{name}.tar" + if not os.path.exists(archive_file) and os.path.exists(archive_file + ".gz"): + archive_file += ".gz" + info_file = f"{ARCHIVES_PATH}/{name}.info.json" + + files_to_delete = [archive_file, info_file] + + # To handle the case where archive_file is in fact a symlink + if os.path.islink(archive_file): + actual_archive = os.path.realpath(archive_file) + files_to_delete.append(actual_archive) + + for backup_file in files_to_delete: + if not os.path.exists(backup_file): + continue + try: + os.remove(backup_file) + except Exception: + logger.debug("unable to delete '%s'", backup_file, exc_info=True) + logger.warning(m18n.n("backup_delete_error", path=backup_file)) + + hook_callback("post_backup_delete", args=[name]) + + # "display success" is here because when running the + # safety-backup-before-upgrade, yunohost will delete the previous safety + # upgrade and we don't really want it to trigger a success or toast saying + # that some backup was deleted and is counter intuitive... + if display_success: + logger.success(m18n.n("backup_deleted", name=name)) + + +# +# Misc helpers # +# + + +def _create_archive_dir(): + """Create the YunoHost archives directory if doesn't exist""" + if not os.path.isdir(ARCHIVES_PATH): + if os.path.lexists(ARCHIVES_PATH): + raise YunohostError("backup_output_symlink_dir_broken", path=ARCHIVES_PATH) + + # Create the archive folder, with 'admins' as groupowner, such that + # people can scp archives out of the server + mkdir(ARCHIVES_PATH, mode=0o770, parents=True, gid="admins") + + +def _call_for_each_path(self, callback, csv_path=None): + """Call a callback for each path in csv""" + if csv_path is None: + csv_path = self.csv_path + with open(csv_path, "r") as backup_file: + backup_csv = csv.DictReader(backup_file, fieldnames=["source", "dest"]) + for row in backup_csv: + callback(self, row["source"], row["dest"]) + + +def _recursive_umount(directory): + """ + Recursively umount sub directories of a directory + + Args: + directory -- a directory path + """ + mount_lines = check_output("mount").split("\n") + + points_to_umount = [ + line.split(" ")[2] + for line in mount_lines + if len(line) >= 3 and line.split(" ")[2].startswith(os.path.realpath(directory)) + ] + + everything_went_fine = True + for point in reversed(points_to_umount): + ret = subprocess.call(["umount", point]) + if ret != 0: + everything_went_fine = False + logger.warning(m18n.n("backup_cleaning_failed", point)) + continue + + return everything_went_fine diff --git a/src/certificate.py b/src/certificate.py new file mode 100644 index 0000000..b15be93 --- /dev/null +++ b/src/certificate.py @@ -0,0 +1,918 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import shutil +import subprocess +import sys +from datetime import datetime +from glob import glob +from logging import getLogger +from typing import TYPE_CHECKING, Any, cast + +from moulinette import m18n + +from .diagnosis import Diagnoser +from .log import OperationLogger +from .regenconf import regen_conf +from .service import _run_service_command +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import chmod, chown, read_file +from .utils.network import get_public_ip +from .utils.process import check_output +from .vendor.acme_tiny.acme_tiny import get_crt as sign_certificate + +if TYPE_CHECKING: + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.certmanager")) +else: + logger = getLogger("yunohost.certmanager") + +CERT_FOLDER = "/etc/yunohost/certs/" +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" + +SSL_DIR = "/usr/share/yunohost/ssl" + +KEY_SIZE = 4096 + +VALIDITY_LIMIT = 15 # days + +# For prod +PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v02.api.letsencrypt.org" + +# +# Front-end stuff # +# + + +def certificate_status( + domains: list[str], full: bool = False +) -> dict[str, dict[str, Any]]: + """ + Print the status of certificate for given domains (all by default) + + Keyword argument: + domains -- Domains to be checked + full -- Display more info about the certificates + """ + + from .domain import ( + _assert_domain_exists, + _get_parent_domain_of, + domain_list, + ) + + # If no domains given, consider all yunohost domains + if domains == []: + domains = domain_list()["domains"] # type: ignore[assignment] + # Else, validate that yunohost knows the domains given + else: + for domain in domains: + _assert_domain_exists(domain) + + certificates = {} + + for domain in domains: + status = _get_status(domain) + + if not full: + del status["subject"] + del status["CA_name"] + + if full: + try: + _check_domain_is_ready_for_ACME(domain) + status["ACME_eligible"] = True + except YunohostError as e: + if e.key == "certmanager_domain_not_diagnosed_yet": + status["ACME_eligible"] = None # = unknown status + else: + status["ACME_eligible"] = False + + # Check if a wildcard is setup for the ipv4/ipv6 A/AAAA records on the topest domain + parent_domain = _get_parent_domain_of(domain, return_self=True, topest=True) + dns_extra = Diagnoser.get_cached_report( + "dnsrecords", item={"domain": parent_domain, "category": "extra"} + ).get("data", {}) + has_wildcards = [ + v == "OK" for k, v in dns_extra.items() if k.startswith("A") + ] + status["has_wildcards"] = len(has_wildcards) > 0 and all(has_wildcards) + + del status["domain"] + certificates[domain] = status + + return {"certificates": certificates} + + +def certificate_install( + domain_list: list[str], + force: bool = False, + no_checks: bool = False, + self_signed: bool = False, +) -> None: + """ + Install a Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domain_list -- Domains on which to install certificates + force -- Install even if current certificate is not self-signed + no-check -- Disable some checks about the reachability of web server + before attempting the install + self-signed -- Instal self-signed certificates instead of Let's Encrypt + """ + + if self_signed: + _certificate_install_selfsigned(domain_list, force) + else: + _certificate_install_letsencrypt(domain_list, force, no_checks) + + +def _certificate_install_selfsigned(domain_list, force=False): + failed_cert_install = [] + for domain in domain_list: + operation_logger = OperationLogger( + "selfsigned_cert_install", [("domain", domain)], args={"force": force} + ) + + # Paths of files and folder we'll need + date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") + new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-selfsigned" + + conf_template = os.path.join(SSL_DIR, "openssl.cnf") + + csr_file = os.path.join(SSL_DIR, "certs", "yunohost_csr.pem") + conf_file = os.path.join(new_cert_folder, "openssl.cnf") + key_file = os.path.join(new_cert_folder, "key.pem") + crt_file = os.path.join(new_cert_folder, "crt.pem") + ca_file = os.path.join(new_cert_folder, "ca.pem") + + # Check we ain't trying to overwrite a good cert ! + current_cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") + if not force and os.path.isfile(current_cert_file): + status = _get_status(domain) + + if status["style"] == "success": + raise YunohostValidationError( + "certmanager_attempt_to_replace_valid_cert", domain=domain + ) + + operation_logger.start() + + # Create output folder for new certificate stuff + os.makedirs(new_cert_folder) + + # Create our conf file, based on template, replacing the occurences of + # "yunohost.org" with the given domain + with open(conf_file, "w") as f, open(conf_template, "r") as template: + for line in template: + f.write(line.replace("yunohost.org", domain)) + + # Use OpenSSL command line to create a certificate signing request, + # and self-sign the cert + commands = [ + f"openssl req -new -config {conf_file} -out {csr_file} -keyout {key_file} -nodes -batch", + f"openssl ca -config {conf_file} -days 3650 -in {csr_file} -out {crt_file} -batch", + ] + + for command in commands: + p = subprocess.Popen( + command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + + out, _ = p.communicate() + + out = out.decode("utf-8") + + if p.returncode != 0: + logger.warning(out) + raise YunohostError("domain_cert_gen_failed") + else: + logger.debug(out) + + # Link the CA cert (not sure it's actually needed in practice though, + # since we append it at the end of crt.pem. For instance for Let's + # Encrypt certs, we only need the crt.pem and key.pem) + os.symlink(SELF_CA_FILE, ca_file) + + # Append ca.pem at the end of crt.pem + with open(ca_file, "r") as ca_pem, open(crt_file, "a") as crt_pem: + crt_pem.write("\n") + crt_pem.write(ca_pem.read()) + + # Set appropriate permissions + _set_permissions(new_cert_folder, "root", "root", 0o755) + _set_permissions(key_file, "root", "ssl-cert", 0o640) + _set_permissions(crt_file, "root", "ssl-cert", 0o640) + _set_permissions(conf_file, "root", "root", 0o600) + + # Actually enable the certificate we created + _enable_certificate(domain, new_cert_folder) + + # Check new status indicate a recently created self-signed certificate + status = _get_status(domain) + + if status and status["CA_type"] == "selfsigned" and status["validity"] > 3648: + logger.success( + m18n.n("certmanager_cert_install_success_selfsigned", domain=domain) + ) + operation_logger.success() + else: + msg = f"Installation of self-signed certificate installation for {domain} failed !" + failed_cert_install.append(domain) + logger.error(msg) + logger.error(status) + operation_logger.error(msg) + + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed_selfsigned", + domains=",".join(failed_cert_install), + ) + + +def _certificate_install_letsencrypt(domains, force=False, no_checks=False): + from .domain import _assert_domain_exists, domain_list + + if not os.path.exists(ACCOUNT_KEY_FILE): + _generate_account_key() + + # If no domains given, consider all yunohost domains with self-signed + # certificates + if domains == []: + for domain in domain_list()["domains"]: + status = _get_status(domain) + if status["CA_type"] != "selfsigned": + continue + + domains.append(domain) + + # Else, validate that yunohost knows the domains given + else: + for domain in domains: + _assert_domain_exists(domain) + + # Is it self-signed? + status = _get_status(domain) + if not force and status["CA_type"] != "selfsigned": + raise YunohostValidationError( + "certmanager_domain_cert_not_selfsigned", domain=domain + ) + + # Actual install steps + failed_cert_install = [] + for domain in domains: + if not no_checks: + try: + _check_domain_is_ready_for_ACME(domain) + except Exception as e: + logger.error(e) + continue + + logger.info("Now attempting install of certificate for domain %s!", domain) + + operation_logger = OperationLogger( + "letsencrypt_cert_install", + [("domain", domain)], + args={"force": force, "no_checks": no_checks}, + ) + operation_logger.start() + + try: + _fetch_and_enable_new_certificate(domain, no_checks=no_checks) + except Exception as e: + msg = f"Certificate installation for {domain} failed !\nException: {e}" + logger.error(msg) + operation_logger.error(msg) + if no_checks: + logger.error( + f"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." + ) + failed_cert_install.append(domain) + else: + logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) + + operation_logger.success() + + if failed_cert_install: + raise YunohostError( + "certmanager_cert_install_failed", domains=",".join(failed_cert_install) + ) + + +def certificate_renew( + domains: list[str], + force: bool = False, + no_checks: bool = False, + email: bool = False, +) -> None: + """ + Renew Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domains -- Domains for which to renew the certificates + force -- Ignore the validity threshold (15 days) + no-check -- Disable some checks about the reachability of web server + before attempting the renewing + email -- Emails root if some renewing failed + """ + + from .domain import _assert_domain_exists, domain_list + + # If no domains given, consider all yunohost domains with Let's Encrypt + # certificates + if domains == []: + for domain in domain_list()["domains"]: + # Does it have a Let's Encrypt cert? + status = _get_status(domain) + if status["CA_type"] != "letsencrypt": + continue + + # Does it expire soon? + if status["validity"] > VALIDITY_LIMIT and not force: + continue + + # Check ACME challenge configured for given domain + if not _check_acme_challenge_configuration(domain): + logger.warning( + m18n.n("certmanager_acme_not_configured_for_domain", domain=domain) + ) + continue + + domains.append(domain) + + if len(domains) == 0 and not email: + logger.info("No certificate needs to be renewed.") + + # Else, validate the domain list given + else: + for domain in domains: + # Is it in Yunohost domain list? + _assert_domain_exists(domain) + + status = _get_status(domain) + + # Does it expire soon? + if status["validity"] > VALIDITY_LIMIT and not force: + raise YunohostValidationError( + "certmanager_attempt_to_renew_valid_cert", domain=domain + ) + + # Does it have a Let's Encrypt cert? + if status["CA_type"] != "letsencrypt": + raise YunohostValidationError( + "certmanager_attempt_to_renew_nonLE_cert", domain=domain + ) + + # Check ACME challenge configured for given domain + if not _check_acme_challenge_configuration(domain): + raise YunohostValidationError( + "certmanager_acme_not_configured_for_domain", domain=domain + ) + + # Actual renew steps + failed_cert_install = [] + for domain in domains: + if not no_checks: + try: + _check_domain_is_ready_for_ACME(domain) + except Exception as e: + logger.error(e) + if email: + logger.error("Sending email with details to root ...") + _email_renewing_failed(domain, e) + continue + + logger.info("Now attempting renewing of certificate for domain %s !", domain) + + operation_logger = OperationLogger( + "letsencrypt_cert_renew", + [("domain", domain)], + args={ + "force": force, + "no_checks": no_checks, + "email": email, + }, + ) + operation_logger.start() + + try: + _fetch_and_enable_new_certificate(domain, no_checks=no_checks) + except Exception as e: + import traceback + from io import StringIO + + stack = StringIO() + traceback.print_exc(file=stack) + msg = f"Certificate renewing for {domain} failed!" + if no_checks: + msg += f"\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain {domain}." + logger.error(msg) + operation_logger.error(msg) + logger.error(stack.getvalue()) + logger.error(str(e)) + + failed_cert_install.append(domain) + + if email: + logger.error("Sending email with details to root ...") + _email_renewing_failed(domain, msg + "\n" + str(e), stack.getvalue()) + else: + logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) + operation_logger.success() + + if failed_cert_install: + raise YunohostError( + "certmanager_cert_renew_failed", domains=",".join(failed_cert_install) + ) + + +# +# Back-end stuff # +# + + +def _email_renewing_failed(domain, exception_message, stack=""): + from_ = f"certmanager@{domain} (Certificate Manager)" + to_ = "root" + subject_ = f"Certificate renewing attempt for {domain} failed!" + + logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") + message = f"""\ +From: {from_} +To: {to_} +Subject: {subject_} + + +An attempt for renewing the certificate for domain {domain} failed with the following +error : + +{exception_message} +{stack} + +Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to +investigate : + +{logs} + +-- Certificate Manager +""" + + try: + import smtplib + + smtp = smtplib.SMTP("localhost") + smtp.sendmail(from_, [to_], message.encode("utf-8")) + smtp.quit() + except Exception as e: + # Dont miserably crash the whole auto renew cert when one renewal fails ... + # cf boring cases like https://github.com/YunoHost/issues/issues/2102 + logger.exception(f"Failed to send mail about cert renewal failure ... : {e}") + + +def _check_acme_challenge_configuration(domain): + domain_conf = f"/etc/nginx/conf.d/{domain}.conf" + return "include /etc/nginx/conf.d/acme-challenge.conf.inc" in read_file(domain_conf) + + +def _fetch_and_enable_new_certificate(domain, no_checks=False): + if not os.path.exists(ACCOUNT_KEY_FILE): + _generate_account_key() + + # Make sure tmp folder exists + logger.debug("Making sure tmp folders exists...") + + if not os.path.exists(WEBROOT_FOLDER): + os.makedirs(WEBROOT_FOLDER) + + if not os.path.exists(TMP_FOLDER): + os.makedirs(TMP_FOLDER) + + _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0o650) + _set_permissions(TMP_FOLDER, "root", "root", 0o640) + + # Regen conf for dnsmasq if needed + _regen_dnsmasq_if_needed() + + # Prepare certificate signing request + logger.debug("Prepare key and certificate signing request (CSR) for %s...", domain) + + domain_key_file = f"{TMP_FOLDER}/{domain}.pem" + _generate_key(domain_key_file) + _set_permissions(domain_key_file, "root", "ssl-cert", 0o640) + + _prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER) + + # Sign the certificate + logger.debug("Now using ACME Tiny to sign the certificate...") + + domain_csr_file = f"{TMP_FOLDER}/{domain}.csr" + + try: + signed_certificate = sign_certificate( + ACCOUNT_KEY_FILE, + domain_csr_file, + WEBROOT_FOLDER, + log=logger, + disable_check=no_checks, + CA=PRODUCTION_CERTIFICATION_AUTHORITY, + ) + except ValueError as e: + if "urn:acme:error:rateLimited" in str(e): + raise YunohostError("certmanager_hit_rate_limit", domain=domain) + else: + logger.error(str(e)) + raise YunohostError("certmanager_cert_signing_failed") + + except Exception as e: + logger.error(str(e)) + + raise YunohostError("certmanager_cert_signing_failed") + + # Now save the key and signed certificate + logger.debug("Saving the key and signed certificate...") + + # Create corresponding directory + date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") + + new_cert_folder = f"{CERT_FOLDER}/{domain}-history/{date_tag}-letsencrypt" + + os.makedirs(new_cert_folder) + + _set_permissions(new_cert_folder, "root", "root", 0o655) + + # Move the private key + domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem") + shutil.move(domain_key_file, domain_key_file_finaldest) + _set_permissions(domain_key_file_finaldest, "root", "ssl-cert", 0o640) + + # Write the cert + domain_cert_file = os.path.join(new_cert_folder, "crt.pem") + + with open(domain_cert_file, "w") as f: + f.write(signed_certificate) + + _set_permissions(domain_cert_file, "root", "ssl-cert", 0o640) + + _enable_certificate(domain, new_cert_folder) + + # Check the status of the certificate is now good + status_style = _get_status(domain)["style"] + + if status_style != "success": + raise YunohostError( + "certmanager_certificate_fetching_or_enabling_failed", domain=domain + ) + + +def _prepare_certificate_signing_request(domain, key_file, output_folder): + from OpenSSL import crypto # lazy loading this module for performance reasons + + from .hook import hook_callback + + # Init a request + csr = crypto.X509Req() + + # Set the domain + csr.get_subject().CN = domain + + 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 sanlist: + subsanlist = [f"DNS:{sub}.{domain}" for sub in sanlist if "." not in sub] + # This is meant for situation such as cryptpad where we need to be able to have a cert for sandbox-domain.tld (with a dash, not just sandbox.domain.tld) + domainsanlist = [f"DNS:{domain}" for domain in sanlist if "." in domain] + sanlist = ", ".join(subsanlist + domainsanlist) + csr.add_extensions( + [ + crypto.X509Extension( + b"subjectAltName", + False, + sanlist.encode("utf-8"), + ) + ] + ) + + # Set the key + with open(key_file, "rt") as f: + key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) + + csr.set_pubkey(key) + + # Sign the request + csr.sign(key, "sha256") + + # Save the request in tmp folder + csr_file = output_folder + domain + ".csr" + logger.debug("Saving to %s.", csr_file) + + with open(csr_file, "wb") as f: + f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) + + +def _get_status(domain): + cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") + + if not os.path.isfile(cert_file): + raise YunohostError("certmanager_no_cert_file", domain=domain, file=cert_file) + + from OpenSSL import crypto # lazy loading this module for performance reasons + + try: + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) + except Exception as exception: + import traceback + + traceback.print_exc(file=sys.stdout) + raise YunohostError( + "certmanager_cannot_read_cert", + domain=domain, + file=cert_file, + reason=exception, + ) + + cert_subject = cert.get_subject().CN + cert_issuer = cert.get_issuer().CN + organization_name = cert.get_issuer().O + valid_up_to = datetime.strptime( + cert.get_notAfter().decode("utf-8"), "%Y%m%d%H%M%SZ" + ) + days_remaining = (valid_up_to - datetime.utcnow()).days + + # Identify that a domain's cert is self-signed if the cert dir + # is actually a symlink to a dir ending with -selfsigned + if os.path.realpath(os.path.join(CERT_FOLDER, domain)).endswith("-selfsigned"): + CA_type = "selfsigned" + elif organization_name == "Let's Encrypt": + CA_type = "letsencrypt" + else: + CA_type = "other" + + if days_remaining <= 0: + style = "danger" + summary = "expired" + elif CA_type == "selfsigned": + style = "warning" + summary = "selfsigned" + elif days_remaining < VALIDITY_LIMIT: + style = "warning" + summary = "abouttoexpire" + elif CA_type == "other": + style = "success" + summary = "ok" + elif CA_type == "letsencrypt": + style = "success" + summary = "letsencrypt" + else: + # shouldnt happen, because CA_type can be only selfsigned, letsencrypt, or other + style = "" + summary = "wat" + + return { + "domain": domain, + "subject": cert_subject, + "CA_name": cert_issuer, + "CA_type": CA_type, + "validity": days_remaining, + "style": style, + "summary": summary, + } + + +# +# Misc small stuff ... # +# + + +def _generate_account_key(): + logger.debug("Generating account key ...") + _generate_key(ACCOUNT_KEY_FILE) + _set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0o400) + + +def _generate_key(destination_path): + from OpenSSL import crypto # lazy loading this module for performance reasons + + k = crypto.PKey() + k.generate_key(crypto.TYPE_RSA, KEY_SIZE) + + with open(destination_path, "wb") as f: + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) + + +def _set_permissions(path, user, group, permissions): + chown(path, user, group) + chmod(path, permissions) + + +def _enable_certificate(domain, new_cert_folder): + logger.debug("Enabling the certificate for domain %s ...", domain) + + live_link = os.path.join(CERT_FOLDER, domain) + + # If a live link (or folder) already exists + if os.path.exists(live_link): + # If it's not a link ... expect if to be a folder + if not os.path.islink(live_link): + # Backup it and remove it + _backup_current_cert(domain) + shutil.rmtree(live_link) + # Else if it's a link, simply delete it + elif os.path.lexists(live_link): + os.remove(live_link) + + os.symlink(new_cert_folder, live_link) + + logger.debug("Restarting services...") + + 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) + # We also regenconf for postfix to propagate the SNI hash map thingy + regen_conf(names=["nginx", "postfix"]) + + _run_service_command("reload", "nginx") + _run_service_command("restart", "dovecot") + + from .hook import hook_callback + + hook_callback("post_cert_update", args=[domain]) + + +def _backup_current_cert(domain): + logger.debug("Backuping existing certificate for domain %s", domain) + + cert_folder_domain = os.path.join(CERT_FOLDER, domain) + + date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") + backup_folder = f"{cert_folder_domain}-backups/{date_tag}" + + shutil.copytree(cert_folder_domain, backup_folder) + + +def _check_domain_is_ready_for_ACME(domain): + from .dns import _get_dns_zone_for_domain + from .domain import _get_parent_domain_of + from .utils.dns import is_yunohost_dyndns_domain + + httpreachable = ( + Diagnoser.get_cached_report( + "web", item={"domain": domain}, warn_if_no_cache=False + ) + or {} + ) + + parent_domain = _get_parent_domain_of(domain, return_self=True, topest=True) + + dnsrecords = ( + Diagnoser.get_cached_report( + "dnsrecords", + item={"domain": parent_domain, "category": "basic"}, + warn_if_no_cache=False, + ) + or {} + ) + + base_dns_zone = _get_dns_zone_for_domain(domain) + record_name = ( + domain.replace(f".{base_dns_zone}", "") if domain != base_dns_zone else "@" + ) + + # Stupid edge case for subdomains of ynh dyndns domains ... + # ... related to the fact that we don't actually check subdomains for + # dyndns domains because we assume that there's already the wildcard doing + # the job, hence no "A:foobar" ... Instead, just check that the parent domain + # is correctly configured. + if is_yunohost_dyndns_domain(parent_domain): + record_name = "@" + + A_record_status = dnsrecords.get("data", {}).get(f"A:{record_name}") + AAAA_record_status = dnsrecords.get("data", {}).get(f"AAAA:{record_name}") + + # Fallback to wildcard in case no result yet for the DNS name? + if not A_record_status: + A_record_status = dnsrecords.get("data", {}).get("A:*") + if not AAAA_record_status: + AAAA_record_status = dnsrecords.get("data", {}).get("AAAA:*") + + if ( + not httpreachable + or not dnsrecords.get("data") + or (A_record_status, AAAA_record_status) == (None, None) + ): + raise YunohostValidationError( + "certmanager_domain_not_diagnosed_yet", domain=domain + ) + + # Check that DNS record matches public IP + # in particular we want at least one to be "OK" but neither to be "WRONG" + # (in particular Let's Encrypt will fail if there's an incorrect IPv6 despite a correct IPv4) + # Also we can live with a "MISSING" IPv4 or IPv6 record assuming the other one is OK + # (for example when there's theoretically an IPv6 on the machine, but the admins didnt define the AAAA record) + statuses = [A_record_status, AAAA_record_status] + if "WRONG" in statuses or "OK" not in statuses: + raise YunohostValidationError( + "certmanager_domain_dns_ip_differs_from_public_ip", domain=domain + ) + + # Check if domain seems to be accessible through HTTP? + if not httpreachable.get("status") == "SUCCESS": + raise YunohostValidationError( + "certmanager_domain_http_not_working", domain=domain + ) + + +# FIXME / TODO : ideally this should not be needed. There should be a proper +# mechanism to regularly check the value of the public IP and trigger +# corresponding hooks (e.g. dyndns update and dnsmasq regen-conf) +def _regen_dnsmasq_if_needed(): + """ + Update the dnsmasq conf if some IPs are not up to date... + """ + + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) + + do_regen = False + + # For all domain files in DNSmasq conf... + domainsconf = glob("/etc/dnsmasq.d/*.*") + for domainconf in domainsconf: + # Look for the IP, it's in the lines with this format : + # host-record=the.domain.tld,11.22.33.44 + for line in open(domainconf).readlines(): + if not line.startswith("host-record"): + continue + ip = line.strip().split(",")[-1] + + # Compared found IP to current IPv4 / IPv6 + # IPv6 IPv4 + if (":" in ip and ip != ipv6) or (ip != ipv4): + do_regen = True + break + + if do_regen: + break + + if do_regen: + regen_conf(["dnsmasq"]) + + +def _name_self_CA(): + ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") + + if not os.path.exists(ca_conf): + logger.warning(m18n.n("certmanager_self_ca_conf_file_not_found", file=ca_conf)) + return "" + + with open(ca_conf) as f: + lines = f.readlines() + + for line in lines: + if line.startswith("commonName_default"): + return line.split()[2] + + logger.warning(m18n.n("certmanager_unable_to_parse_self_CA_name", file=ca_conf)) + return "" + + +def _tail(n, file_path): + return check_output(f"tail -n {n} '{file_path}'") diff --git a/src/diagnosers/00-basesystem.py b/src/diagnosers/00-basesystem.py new file mode 100644 index 0000000..59c822e --- /dev/null +++ b/src/diagnosers/00-basesystem.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import json +import logging +import os +import re +import subprocess +from collections.abc import Generator +from typing import Any + +from ..app_catalog import SecurityIssueInfos, _load_security_issues_list +from ..diagnosis import Diagnoser +from ..utils.file_utils import read_file, read_json, write_to_json +from ..utils.process import check_output +from ..utils.system import ( + debian_version, + dpkg_compare_version, + dpkg_list_installed_packages, + dpkg_package_version, + system_arch, + system_virt, + ynh_packages_version, +) + +logger = logging.getLogger("yunohost.diagnosis") + + +class MyDiagnoser(Diagnoser): # type: ignore + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 600 + dependencies: list[str] = [] + + def run(self) -> Generator[dict[str, Any], None, None]: + virt = system_virt() + if virt.lower() == "none": + virt = "bare-metal" + + # Detect arch + arch = system_arch() + hardware = dict( + meta={"test": "hardware"}, + status="INFO", + data={"virt": virt, "arch": arch}, + summary="diagnosis_basesystem_hardware", + ) + + # Also possibly the board / hardware name + if os.path.exists("/proc/device-tree/model"): + model = read_file("/proc/device-tree/model").strip().replace("\x00", "") + hardware["data"]["model"] = model # type: ignore + hardware["details"] = ["diagnosis_basesystem_hardware_model"] + elif os.path.exists("/sys/devices/virtual/dmi/id/sys_vendor"): + model = read_file("/sys/devices/virtual/dmi/id/sys_vendor").strip() + if os.path.exists("/sys/devices/virtual/dmi/id/product_name"): + product_name = read_file( + "/sys/devices/virtual/dmi/id/product_name" + ).strip() + model = f"{model} {product_name}" + hardware["data"]["model"] = model # type: ignore + hardware["details"] = ["diagnosis_basesystem_hardware_model"] + + yield hardware + + # Kernel version + kernel_version = read_file("/proc/sys/kernel/osrelease").strip() + yield dict( + meta={"test": "kernel"}, + data={"kernel_version": kernel_version}, + status="INFO", + summary="diagnosis_basesystem_kernel", + ) + + # Debian release + yield dict( + meta={"test": "host"}, + data={"debian_version": debian_version()}, + status="INFO", + summary="diagnosis_basesystem_host", + ) + + # Yunohost packages versions + # We check if versions are consistent (e.g. all 3.6 and not 3 packages with 3.6 and the other with 3.5) + # This is a classical issue for upgrades that failed in the middle + # (or people upgrading half of the package because they did 'apt upgrade' instead of 'dist-upgrade') + # Here, ynh_core_version is for example "3.5.4.12", so [:3] is "3.5" and we check it's the same for all packages + ynh_packages = ynh_packages_version() + ynh_core_version = ynh_packages["yunohost"]["version"] + consistent_versions = all( + infos["version"][:3] == ynh_core_version[:3] + for infos in ynh_packages.values() + ) + ynh_version_details = [ + ( + "diagnosis_basesystem_ynh_single_version", + { + "package": package, + "version": infos["version"], + "repo": infos["repo"], + }, + ) + for package, infos in ynh_packages.items() + ] + + yield dict( + meta={"test": "ynh_versions"}, + data={ + "main_version": ynh_core_version, + "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" + ), + details=ynh_version_details, + ) + + if self.is_vulnerable_to_meltdown(): + yield dict( + meta={"test": "meltdown"}, + status="ERROR", + summary="diagnosis_security_vulnerable_to_meltdown", + details=["diagnosis_security_vulnerable_to_meltdown_details"], + ) + + bad_sury_packages = list(self.bad_sury_packages()) + if bad_sury_packages: + cmd_to_fix = "apt install --allow-downgrades " + " ".join( + [f"{package}={version}" for package, version in bad_sury_packages] + ) + yield dict( + meta={"test": "packages_from_sury"}, + data={"cmd_to_fix": cmd_to_fix}, + status="WARNING", + summary="diagnosis_package_installed_from_sury", + details=["diagnosis_package_installed_from_sury_details"], + ) + + if self.backports_in_sources_list(): + yield dict( + meta={"test": "backports_in_sources_list"}, + status="WARNING", + summary="diagnosis_backports_in_sources_list", + ) + + # Using yunohost testing channel + if ( + os.system( + "grep -q '^\\s*deb\\s*.*yunohost.org.*\\stesting' /etc/apt/sources.list /etc/apt/sources.list.d/*" + ) + == 0 + ): + yield dict( + meta={"test": "apt_yunohost_channel"}, + status="WARNING", + summary="diagnosis_using_yunohost_testing", + details=["diagnosis_using_yunohost_testing_details"], + ) + + # Apt being mapped to 'stable' (instead of 'buster/bullseye/bookworm/trixie/...') + # will cause the machine to spontaenously upgrade everything as soon as next debian is released ... + # Note that we grep this from the policy for libc6, because it's hard to know exactly which apt repo + # is configured (it may not be simply debian.org) + if ( + os.system( + "apt policy libc6 2>/dev/null | grep '^\\s*500' | awk '{print $3}' | tr '/' ' ' | awk '{print $1}' | grep -q 'stable'" + ) + == 0 + ): + yield dict( + meta={"test": "apt_debian_codename"}, + status="WARNING", + summary="diagnosis_using_stable_codename", + details=["diagnosis_using_stable_codename_details"], + ) + + if self.number_of_recent_auth_failure() > 750: + yield dict( + meta={"test": "high_number_auth_failure"}, + status="WARNING", + summary="diagnosis_high_number_auth_failures", + ) + + rfkill_wifi = self.rfkill_wifi() + if len(rfkill_wifi) > 0: + yield dict( + meta={"test": "rfkill_wifi"}, + status="ERROR", + summary="diagnosis_rfkill_wifi", + details=["diagnosis_rfkill_wifi_details"], + data={"rfkill_wifi_error": rfkill_wifi}, + ) + + yield from self.security_issues() + + def bad_sury_packages(self) -> Generator[tuple[str, str], None, None]: + packages_to_check = ["openssl", "libssl1.1", "libssl-dev"] + for package in packages_to_check: + cmd = "dpkg --list | grep '^ii' | grep gbp | grep -q -w %s" % package + # If version currently installed is not from sury, nothing to report + if os.system(cmd) != 0: + continue + + cmd = ( + "LC_ALL=C apt policy %s 2>&1 | grep http -B1 | tr -d '*' | grep '+deb' | grep -v 'gbp' | head -n 1 | awk '{print $1}'" + % package + ) + version_to_downgrade_to = check_output(cmd) + yield (package, version_to_downgrade_to) + + def backports_in_sources_list(self) -> bool: + cmd = "grep -q -nr '^ *deb .*-backports' /etc/apt/sources.list*" + return os.system(cmd) == 0 + + def number_of_recent_auth_failure(self) -> int: + # Those syslog facilities correspond to auth and authpriv + # c.f. https://unix.stackexchange.com/a/401398 + # and https://wiki.archlinux.org/title/Systemd/Journal#Facility + cmd = "journalctl -q SYSLOG_FACILITY=10 SYSLOG_FACILITY=4 --since '1day ago' | grep 'authentication failure' | wc -l" + + n_failures = check_output(cmd) + try: + return int(n_failures) + except Exception: + logger.warning( + "Failed to parse number of recent auth failures, expected an int, got '%s'" + % n_failures + ) + return -1 + + def is_vulnerable_to_meltdown(self) -> bool: + # meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754 + + # We use a cache file to avoid re-running the script so many times, + # which can be expensive (up to around 5 seconds on ARM) + # and make the admin appear to be slow (c.f. the calls to diagnosis + # from the webadmin) + # + # The cache is in /tmp and shall disappear upon reboot + # *or* we compare it to dpkg.log modification time + # such that it's re-ran if there was package upgrades + # (e.g. from yunohost) + cache_file = "/tmp/yunohost-meltdown-diagnosis" + dpkg_log = "/var/log/dpkg.log" + if os.path.exists(cache_file): + if not os.path.exists(dpkg_log) or os.path.getmtime( + cache_file + ) > os.path.getmtime(dpkg_log): + logger.debug( + "Using cached results for meltdown checker, from %s" % cache_file + ) + return read_json(cache_file)[0]["VULNERABLE"] # type: ignore + + # script taken from https://github.com/speed47/spectre-meltdown-checker + # script commit id is store directly in the script + SCRIPT_PATH = "/usr/lib/python3/dist-packages/yunohost/vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh" + + # '--variant 3' corresponds to Meltdown + # example output from the script: + # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] + try: + logger.debug("Running meltdown vulnerability checker") + call = subprocess.Popen( + "bash %s --batch json --variant 3" % SCRIPT_PATH, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # TODO / FIXME : here we are ignoring error messages ... + # in particular on RPi2 and other hardware, the script complains about + # "missing some kernel info (see -v), accuracy might be reduced" + # Dunno what to do about that but we probably don't want to harass + # users with this warning ... + output_bytes, _ = call.communicate() + output = output_bytes.decode() + assert call.returncode in (0, 2, 3), "Return code: %s" % call.returncode + + # If there are multiple lines, sounds like there was some messages + # in stdout that are not json >.> ... Try to get the actual json + # stuff which should be the last line + output = output.strip() + if "\n" in output: + logger.debug("Original meltdown checker output : %s" % output) + output = output.split("\n")[-1] + + CVEs = json.loads(output) + assert len(CVEs) == 1 + assert CVEs[0]["NAME"] == "MELTDOWN" + except Exception as e: + import traceback + + traceback.print_exc() + logger.warning( + "Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" + % e + ) + raise Exception(f"Command output for failed meltdown check: '{output}'") + + logger.debug( + "Writing results from meltdown checker to cache file, %s" % cache_file + ) + write_to_json(cache_file, CVEs) + return CVEs[0]["VULNERABLE"] # type: ignore + + def rfkill_wifi(self) -> str: + if os.path.isfile("/etc/profile.d/wifi-check.sh"): + cmd = "bash /etc/profile.d/wifi-check.sh" + return check_output(cmd) # type: ignore + else: + return "" + + def security_issues(self): + installed_packages = dpkg_list_installed_packages() + security_issues_list_per_pkg: dict[str, list[SecurityIssueInfos]] = ( + _load_security_issues_list()["system"] + ) + for package, issues in security_issues_list_per_pkg.items(): + if package not in installed_packages and package != "kernel": + continue + + if package != "kernel": + current_version = dpkg_package_version(package) + else: + # NOT equivalent to uname -r ... we are looking for the + # "mainline" kernel version, not the debian kernel version, + # cf issues#2803 + raw_kernel_comment = read_file("/proc/sys/kernel/version").strip() + version_matches = re.findall(r"\s[0-9]\S+", raw_kernel_comment) + if len(version_matches) != 1: + logger.warning( + f"Unable to extract mainline kernel version from the kernel info '{raw_kernel_comment}' ... Therefore YunoHost will be unable to check for security issues related to the kernel. Please try to report this message to the YunoHost team to improve the situation" + ) + continue + current_version = version_matches[0].strip() + # RPi have their mainline kernel version number somehow + # starting with "1:" which messes up the version comparison + # later + if ":" in current_version: + current_version = current_version.split(":")[1] + + # FIXME : so far this whole kernel check is not reliable on every setup + # because not every context has the kernel from Debian : + # - RPI ships their own kernel possibly with more recent + # versions than the standard Debian setup + # - LXC/containers use the kernel from the host, which may be + # in a totally different distribution therefore we can't just + # expect the kernel version to be related to the debian version + # we're running (e.g. 6.1.x for Bookworm, 6.12.x for Trixie) + + for issue in issues: + raw_fixed_in_version = issue["fixed_in_version"] + if isinstance(raw_fixed_in_version, dict): + if debian_version() not in raw_fixed_in_version: + logger.warning( + f"Not able to check versions in which security issue is fixed for package '{package}' (no version specified for Debian {debian_version()})" + ) + continue + fixed_in_version = raw_fixed_in_version[debian_version()] + else: + fixed_in_version = raw_fixed_in_version + + if dpkg_compare_version(current_version, fixed_in_version) >= 0: + # installed version is >= to the version which fixes the issue, therefore there's no issue to report + continue + + level = "error" if issue["level"] == "danger" else "warning" + if isinstance(issue["more_infos"], list): + more_infos_list = ", ".join(issue["more_infos"]) + else: + more_infos_list = issue["more_infos"] + yield dict( + meta={"package": package}, + status=level.upper(), + # i18n: diagnosis_package_security_issue_warning + # i18n: diagnosis_package_security_issue_error + summary=f"diagnosis_package_security_issue_{level}", + data={ + **issue, + "fixed_in_version": fixed_in_version, + "more_infos_list": more_infos_list, + "current_version": current_version, + }, + ) diff --git a/src/diagnosers/10-ip.py b/src/diagnosers/10-ip.py new file mode 100644 index 0000000..f3faa4f --- /dev/null +++ b/src/diagnosers/10-ip.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import logging +import os +import random +import re +from collections.abc import Generator +from typing import Any + +from ..diagnosis import Diagnoser +from ..settings import settings_get +from ..utils.file_utils import download_text, read_file +from ..utils.network import get_network_interfaces +from ..utils.process import check_output + +logger = logging.getLogger("yunohost.diagnosis") + + +class MyDiagnoser(Diagnoser): # type: ignore + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 600 + dependencies: list[str] = [] + + def run(self) -> Generator[dict[str, Any], None, None]: + # ############################################################ # + # PING : Check that we can ping outside at least in ipv4 or v6 # + # ############################################################ # + + can_ping_ipv4 = self.can_ping_outside(4) + can_ping_ipv6 = self.can_ping_outside(6) + + if not can_ping_ipv4 and not can_ping_ipv6: + yield dict( + meta={"test": "ping"}, + status="ERROR", + summary="diagnosis_ip_not_connected_at_all", + ) + # Not much else we can do if there's no internet at all + return + + # ###################################################### # + # DNS RESOLUTION : Check that we can resolve domain name # + # (later needed to talk to ip. and ip6.yunohost.org) # + # ###################################################### # + + can_resolve_dns = self.can_resolve_dns() + + # In every case, we can check that resolvconf seems to be okay + # (symlink managed by resolvconf service + pointing to dnsmasq) + good_resolvconf = self.good_resolvconf() + + # If we can't resolve domain names at all, that's a pretty big issue ... + # If it turns out that at the same time, resolvconf is bad, that's probably + # the cause of this, so we use a different message in that case + if not can_resolve_dns: + yield dict( + meta={"test": "dnsresolv"}, + status="ERROR", + 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, + # still warn that we're using a weird resolv conf ... + elif not good_resolvconf: + yield dict( + meta={"test": "dnsresolv"}, + status="WARNING", + summary="diagnosis_ip_weird_resolvconf", + details=["diagnosis_ip_weird_resolvconf_details"], + ) + else: + yield dict( + meta={"test": "dnsresolv"}, + status="SUCCESS", + summary="diagnosis_ip_dnsresolution_working", + ) + + # ##################################################### # + # IP DIAGNOSIS : Check that we're actually able to talk # + # to a web server to fetch current IPv4 and v6 # + # ##################################################### # + + ipv4 = self.get_public_ip(4) if can_ping_ipv4 else None + ipv6 = self.get_public_ip(6) if can_ping_ipv6 else None + + network_interfaces = get_network_interfaces() + + def get_local_ip(version: str) -> dict[str, str] | str | None: + local_ip: dict[str, str] = { + iface: addr[version].split("/")[0] + for iface, addr in network_interfaces.items() + if version in addr + } + if not local_ip: + return None + elif len(local_ip): + return next(iter(local_ip.values())) + else: + return local_ip + + def is_ipvx_important(x: int) -> bool: + return settings_get("misc.network.dns_exposure") in ["both", f"ipv{x}"] + + yield dict( + meta={"test": "ipv4"}, + data={"global": ipv4, "local": get_local_ip("ipv4")}, + status=( + "SUCCESS" if ipv4 else "ERROR" if is_ipvx_important(4) else "WARNING" + ), + summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", + details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, + ) + + 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" + ) + ), + 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" + ) + ] + ), + ) + + # TODO / FIXME : add some attempt to detect ISP (using whois ?) ? + + def can_ping_outside(self, protocol: int = 4) -> bool | None: + assert protocol in [ + 4, + 6, + ], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr( + protocol + ) + + # We can know that ipv6 is not available directly if this file does not exists + if protocol == 6 and not os.path.exists("/proc/net/if_inet6"): + return False + + # If we are indeed connected in ipv4 or ipv6, we should find a default route + routes = check_output("ip -%s route show table all" % protocol).split("\n") + + def is_default_route(r: str) -> bool: + # Typically the default route starts with "default" + # But of course IPv6 is more complex ... e.g. on internet cube there's + # no default route but a /3 which acts as a default-like route... + # e.g. 2000:/3 dev tun0 ... + return r.startswith("default") or ( + ":" in r and re.match(r".*/[0-3]$", r.split()[0]) is not None + ) + + if not any(is_default_route(r) for r in routes): + logger.debug( + "No default route for IPv%s, so assuming there's no IP address for that version" + % protocol + ) + return None + + # We use the resolver file as a list of well-known, trustable (ie not google ;)) IPs that we can ping + resolver_file = "/usr/share/yunohost/conf/dnsmasq/plain/resolv.dnsmasq.conf" + resolvers = [ + r.split(" ")[1] + for r in read_file(resolver_file).split("\n") + if r.startswith("nameserver") + ] + + if protocol == 4: + resolvers = [r for r in resolvers if ":" not in r] + if protocol == 6: + resolvers = [r for r in resolvers if ":" in r] + + assert resolvers != [], ( + f"Uhoh, need at least one IPv{protocol} DNS resolver in {resolver_file} ..." + ) + + # So let's try to ping the first 4~5 resolvers (shuffled) + # If we succesfully ping any of them, we conclude that we are indeed connected + def ping(protocol: int, target: str) -> bool: + prot = "" if protocol == 4 else "6" + command = f"ping{prot} -c1 -W 3 {target} >/dev/null 2>/dev/null" + return os.system(command) == 0 + + random.shuffle(resolvers) + return any(ping(protocol, resolver) for resolver in resolvers[:5]) + + def can_resolve_dns(self) -> bool: + return os.system("dig +short ipv4.yunohost.org >/dev/null 2>/dev/null") == 0 + + def good_resolvconf(self) -> bool: + content = read_file("/etc/resolv.conf").strip().split("\n") + # Ignore comments and empty lines + content = [ + line.strip() + for line in content + if line.strip() + and not line.strip().startswith("#") + and not line.strip().startswith("search") + ] + # We should only find a "nameserver 127.0.0.1" + return len(content) == 1 and content[0].split() == ["nameserver", "127.0.0.1"] + + def get_public_ip(self, protocol: int = 4) -> str | None: + # FIXME - TODO : here we assume that DNS resolution for ip4/6.yunohost.org is working + # but if we want to be able to diagnose DNS resolution issues independently from + # internet connectivity, we gotta rely on fixed IPs first.... + + assert protocol in [ + 4, + 6, + ], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr( + protocol + ) + + url = f"https://ipv{protocol}.yunohost.org" + + try: + return download_text(url, timeout=30).strip() # type: ignore + except Exception as e: + logger.debug(f"Could not get public IPv{protocol} : {e}") + return None diff --git a/src/diagnosers/12-dnsrecords.py b/src/diagnosers/12-dnsrecords.py new file mode 100644 index 0000000..594f8a7 --- /dev/null +++ b/src/diagnosers/12-dnsrecords.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import logging +import os +import re +from collections.abc import Collection, Generator +from datetime import datetime, timedelta +from typing import Any + +from publicsuffix2 import PublicSuffixList + +from ..diagnosis import Diagnoser +from ..dns import ( + DNSRecord, + _build_dns_conf, + _get_dns_zone_for_domain, + _get_relative_name_for_dns_zone, +) +from ..domain import _get_maindomain, domain_list +from ..utils.dns import ( + YNH_DYNDNS_DOMAINS, + dig, + is_special_use_tld, + is_yunohost_dyndns_domain, +) +from ..utils.process import check_output + +logger = logging.getLogger("yunohost.diagnosis") + + +class MyDiagnoser(Diagnoser): # type: ignore + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 600 + dependencies: list[str] = ["ip"] + + def run(self) -> Generator[dict[str, Any], None, None]: + main_domain = _get_maindomain() + + major_domains = domain_list(exclude_subdomains=True)["domains"] + for domain in major_domains: + logger.debug("Diagnosing DNS conf for %s" % domain) + + for report in self.check_domain( + domain, + domain == main_domain, + ): + yield report + + # Check if a domain buy by the user will expire soon + psl = PublicSuffixList() + domains_from_registrar = [ + psl.get_public_suffix(domain) for domain in major_domains + ] + domains_from_registrar = [ + domain + for domain in domains_from_registrar + if domain is not None and "." in domain + ] + domains_from_registrar_set = set(domains_from_registrar) - set( + YNH_DYNDNS_DOMAINS + ["netlib.re"] + ) + for report in self.check_expiration_date(domains_from_registrar_set): + yield report + + def check_domain( + self, domain: str, is_main_domain: bool + ) -> Generator[dict[str, Any], None, None]: + if is_special_use_tld(domain): + yield dict( + meta={"domain": domain}, + data={}, + status="INFO", + summary="diagnosis_dns_specialusedomain", + ) + return + + base_dns_zone = _get_dns_zone_for_domain(domain) + basename = _get_relative_name_for_dns_zone(domain, base_dns_zone) + + expected_configuration = _build_dns_conf( + domain, include_empty_AAAA_if_no_ipv6=True + ) + + categories = ["basic", "mail", "extra"] + + for category in categories: + records = expected_configuration[category] + discrepancies = [] + results = {} + + for r in records: + id_ = r["type"] + ":" + r["name"] + fqdn = r["name"] + "." + base_dns_zone if r["name"] != "@" else domain + + # Ugly hack to not check mail records for subdomains stuff, + # otherwise will end up in a shitstorm of errors for people with many subdomains... + # Should find a cleaner solution in the suggested conf... + if r["type"] in ["MX", "TXT"] and fqdn not in [ + domain, + f"mail._domainkey.{domain}", + f"_dmarc.{domain}", + ]: + continue + + r["current"] = self.get_current_record(fqdn, r["type"]) + if r["content"] == "@": + r["content"] = domain + "." + elif r["type"] == "CNAME": + r["content"] = r["content"] # + f".{base_dns_zone}." + + if self.current_record_match_expected(r): + results[id_] = "OK" + else: + if r["current"] is None: + results[id_] = "MISSING" + discrepancies.append(("diagnosis_dns_missing_record", r)) + else: + results[id_] = "WRONG" + discrepancies.append(("diagnosis_dns_discrepancy", r)) + + def its_important() -> bool: + # Every mail DNS records are important for main domain + # For other domain, we only report it as a warning for now... + if is_main_domain and category == "mail": + return True + elif category == "basic": + # A bad or missing A record is critical ... + # And so is a wrong AAAA record + # (However, a missing AAAA record is acceptable) + if ( + results[f"A:{basename}"] != "OK" + or results[f"AAAA:{basename}"] == "WRONG" + ): + return True + + return False + + if discrepancies: + status = "ERROR" if its_important() else "WARNING" + summary = "diagnosis_dns_bad_conf" + else: + status = "SUCCESS" + summary = "diagnosis_dns_good_conf" + + # If status is okay and there's actually no expected records + # (e.g. XMPP disabled) + # then let's not yield any diagnosis line + if not records and status == "SUCCESS": + continue + + output = dict( + meta={"domain": domain, "category": category}, + data=results, + status=status, + summary=summary, + ) + + if discrepancies: + # For ynh-managed domains (nohost.me etc...), tell people to try to "yunohost dyndns update --force" + if is_yunohost_dyndns_domain(domain): + output["details"] = ["diagnosis_dns_try_dyndns_update_force"] + # Otherwise point to the documentation + else: + output["details"] = ["diagnosis_dns_point_to_doc"] + output["details"] += discrepancies # type: ignore + + yield output + + def get_current_record(self, fqdn: str, type_: str) -> str | list[str] | None: + success, answers = dig(fqdn, type_, resolvers="force_external") + + if success != "ok": + return None + else: + if type_ == "TXT" and isinstance(answers, list): + for part in answers: + if part.startswith('"v=spf1'): + return part # type: ignore + return answers[0] if len(answers) == 1 else answers # type: ignore + + def current_record_match_expected(self, r: DNSRecord) -> bool: + if r["content"] is not None and r["current"] is None: + return False + if r["content"] is None and r["current"] is not None: + return False + elif isinstance(r["current"], list): + return False + + if r["type"] == "TXT": + # Split expected/current + # from "v=DKIM1; k=rsa; p=hugekey;" + # to a set like {'v=DKIM1', 'k=rsa', 'p=...'} + # Additionally, for DKIM, because the key is pretty long, + # some DNS registrar sometime split it into several pieces like this: + # "p=foo" "bar" (with a space and quotes in the middle)... + assert r["content"] is not None and r["current"] is not None + expected = set( + r["content"].replace('" "', "").strip(';" ').replace(";", " ").split() + ) + current = set( + r["current"].replace('" "', "").strip(';" ').replace(";", " ").split() + ) + + # For SPF, ignore parts starting by ip4: or ip6: + if "v=spf1" in r["content"]: + current = { + part + for part in current + if not part.startswith("ip4:") and not part.startswith("ip6:") + } + if "v=DMARC1" in r["content"]: + for param in current: + if "=" not in param: + return False + key, value = param.split("=", 1) + if key == "p": + return value in ["none", "quarantine", "reject"] + return expected == current + elif r["type"] == "MX": + # For MX, we want to ignore the priority + assert r["content"] is not None and r["current"] is not None + expected_str = r["content"].split()[-1] + current_str = r["current"].split()[-1] + return expected_str == current_str + elif r["type"] == "CAA": + # For CAA, check only the last item, ignore the 0 / 128 nightmare + assert r["content"] is not None and r["current"] is not None + expected_str = r["content"].split()[-1] + current_str = r["current"].split()[-1] + return expected_str == current_str + else: + return r["current"] == r["content"] + + def check_expiration_date( + self, domains: Collection[str] + ) -> Generator[dict[str, Any], None, None]: + """ + Alert if expiration date of a domain is soon + """ + details: dict[str, Any] = { + "not_found": [], + "error": [], + "warning": [], + "success": [], + } + + for domain in domains: + expire_date = self.get_domain_expiration(domain) + + if isinstance(expire_date, str): + status_ns, _ = dig(domain, "NS", resolvers="force_external") + status_a, _ = dig(domain, "A", resolvers="force_external") + if "ok" not in [status_ns, status_a]: + # i18n: diagnosis_domain_not_found_details + details["not_found"].append( + ( + "diagnosis_domain_%s_details" % (expire_date), + {"domain": domain}, + ) + ) + else: + logger.debug("Dyndns domain: %s" % (domain)) + continue + + expire_in = expire_date - datetime.now() + + alert_type = "success" + if expire_in <= timedelta(15): + alert_type = "error" + elif expire_in <= timedelta(45): + alert_type = "warning" + + args = { + "domain": domain, + "days": expire_in.days - 1, + "expire_date": str(expire_date), + } + details[alert_type].append(("diagnosis_domain_expires_in", args)) + + for alert_type in ["success", "error", "warning", "not_found"]: + if details[alert_type]: + if alert_type == "not_found": + meta = {"test": "domain_not_found"} + else: + meta = {"test": "domain_expiration"} + # Allow to ignore specifically a single domain + if len(details[alert_type]) == 1: + meta["domain"] = details[alert_type][0][1]["domain"] + + # i18n: diagnosis_domain_expiration_not_found + # i18n: diagnosis_domain_expiration_error + # i18n: diagnosis_domain_expiration_warning + # i18n: diagnosis_domain_expiration_success + # i18n: diagnosis_domain_expiration_not_found_details + yield dict( + meta=meta, + data={}, + status=( + alert_type.upper() if alert_type != "not_found" else "WARNING" + ), + summary="diagnosis_domain_expiration_" + alert_type, + details=details[alert_type], + ) + + def get_domain_expiration(self, domain: str) -> datetime | str: + """ + Return the expiration datetime of a domain or None + """ + command = "whois -H %s || echo failed" % (domain) + out = check_output(command).split("\n") + + # Reduce output to determine if whois answer is equivalent to NOT FOUND + filtered_out = [ + line + for line in out + if re.search(r"^[a-zA-Z0-9 ]{4,25}:", line, re.IGNORECASE) + and not re.match(r">>> Last update of whois", line, re.IGNORECASE) + and not re.match(r"^NOTICE:", line, re.IGNORECASE) + and not re.match(r"^%%", line, re.IGNORECASE) + and not re.match(r'"https?:"', line, re.IGNORECASE) + ] + + # If there is less than 7 lines, it's NOT FOUND response + if len(filtered_out) <= 6: + return "not_found" + + for line in out: + match = re.search(r"Expir.+(\d{4}-\d{2}-\d{2})", line, re.IGNORECASE) + if match is not None: + return datetime.strptime(match.group(1), "%Y-%m-%d") + + match = re.search(r"Expir.+(\d{2}-\w{3}-\d{4})", line, re.IGNORECASE) + if match is not None: + return datetime.strptime(match.group(1), "%d-%b-%Y") + + return "expiration_not_found" diff --git a/src/diagnosers/14-ports.py b/src/diagnosers/14-ports.py new file mode 100644 index 0000000..dc65b84 --- /dev/null +++ b/src/diagnosers/14-ports.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +from collections.abc import Generator +from typing import Any + +from ..diagnosis import Diagnoser +from ..service import _get_services +from ..settings import settings_get + + +class MyDiagnoser(Diagnoser): # type: ignore + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 600 + dependencies: list[str] = ["ip"] + + def run(self) -> Generator[dict[str, Any], None, None]: + # TODO: report a warning if port 53 or 5353 is exposed to the outside world... + + # This dict is something like : + # { 80: "nginx", + # 25: "postfix", + # 443: "nginx" + # ... } + ports = {} + services = _get_services() + for service, infos in services.items(): + for port in infos.get("needs_exposed_ports", []): + ports[port] = service + + ipversions = [] + ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} + if ( + ipv4.get("status") == "SUCCESS" + or settings_get("misc.network.dns_exposure") != "ipv6" + ): + ipversions.append(4) + + # To be discussed: we could also make this check dependent on the + # existence of an AAAA record... + ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {} + if ipv6.get("status") == "SUCCESS": + ipversions.append(6) + + # Fetch test result for each relevant IP version + results = {} + for ipversion in ipversions: + try: + r = Diagnoser.remote_diagnosis( + "check-ports", data={"ports": list(ports)}, ipversion=ipversion + ) + results[ipversion] = r["ports"] + except Exception as e: + yield dict( + meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion}, + data={"error": str(e)}, + status="WARNING", + summary="diagnosis_ports_could_not_diagnose", + details=["diagnosis_ports_could_not_diagnose_details"], + ) + continue + + ipversions = list(results.keys()) + if not ipversions: + return + + for port, service in sorted(ports.items()): + port = str(port) + category = services[service].get("category", "[?]") + + # If both IPv4 and IPv6 (if applicable) are good + if all(results[ipversion].get(port) is True for ipversion in ipversions): + yield dict( + meta={"port": port}, + data={"service": service, "category": category}, + status="SUCCESS", + summary="diagnosis_ports_ok", + details=["diagnosis_ports_needed_by"], + ) + # If both IPv4 and IPv6 (if applicable) are failed + elif all( + results[ipversion].get(port) is not True for ipversion in ipversions + ): + yield dict( + meta={"port": port}, + data={"service": service, "category": category}, + status="ERROR", + summary="diagnosis_ports_unreachable", + details=[ + "diagnosis_ports_needed_by", + "diagnosis_ports_forwarding_tip", + ], + ) + # If only IPv4 is failed or only IPv6 is failed (if applicable) + else: + passed, failed = (4, 6) if results[4].get(port) is True else (6, 4) + + # Failing in ipv4 is critical. + # If we failed in IPv6 but there's in fact no AAAA record + # It's an acceptable situation and we shall not report an + # error + # If any AAAA record is set, IPv6 is important... + def ipv6_is_important() -> bool: + dnsrecords = Diagnoser.get_cached_report("dnsrecords") or {} + return any( + record["data"].get("AAAA:@") in ["OK", "WRONG"] + for record in dnsrecords.get("items", []) + ) + + if ( + failed == 4 + and settings_get("misc.network.dns_exposure") in ["both", "ipv4"] + ) or (failed == 6 and ipv6_is_important()): + yield dict( + meta={"port": port}, + data={ + "service": service, + "category": category, + "passed": passed, + "failed": failed, + }, + status="ERROR", + summary="diagnosis_ports_partially_unreachable", + details=[ + "diagnosis_ports_needed_by", + "diagnosis_ports_forwarding_tip", + ], + ) + # So otherwise we report a success + # And in addition we report an info about the failure in IPv6 + # *with a different meta* (important to avoid conflicts when + # fetching the other info...) + else: + yield dict( + meta={"port": port}, + data={"service": service, "category": category}, + status="SUCCESS", + summary="diagnosis_ports_ok", + details=["diagnosis_ports_needed_by"], + ) + yield dict( + meta={"test": "ipv6", "port": port}, + data={ + "service": service, + "category": category, + "passed": passed, + "failed": failed, + }, + status="INFO", + summary="diagnosis_ports_partially_unreachable", + details=[ + "diagnosis_ports_needed_by", + "diagnosis_ports_forwarding_tip", + ], + ) diff --git a/src/diagnosers/21-web.py b/src/diagnosers/21-web.py new file mode 100644 index 0000000..c41315f --- /dev/null +++ b/src/diagnosers/21-web.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import random + +import requests + +from ..diagnosis import Diagnoser +from ..domain import domain_list +from ..settings import settings_get +from ..utils.dns import is_special_use_tld +from ..utils.file_utils import mkdir, read_file, rm + +DIAGNOSIS_SERVER = "diagnosis.yunohost.org" + + +class MyDiagnoser(Diagnoser): + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 600 + dependencies: list[str] = ["ip"] + + def run(self): + all_domains = domain_list()["domains"] + domains_to_check = [] + for domain in all_domains: + # If the diagnosis location ain't defined, can't do diagnosis, + # probably because nginx conf manually modified... + nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain + if ".well-known/ynh-diagnosis/" not in read_file(nginx_conf): + yield dict( + meta={"domain": domain}, + status="WARNING", + summary="diagnosis_http_nginx_conf_not_up_to_date", + details=["diagnosis_http_nginx_conf_not_up_to_date_details"], + ) + elif is_special_use_tld(domain): + yield dict( + meta={"domain": domain}, + status="INFO", + summary="diagnosis_http_special_use_tld", + ) + else: + domains_to_check.append(domain) + + self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16)) + 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 + + # To perform hairpinning test, we gotta make sure that port forwarding + # is working and therefore we'll do it only if at least one ipv4 domain + # works. + self.do_hairpinning_test = False + + ipversions = [] + ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} + if ipv4.get("status") == "SUCCESS" and settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv4"]: + ipversions.append(4) + + # To be discussed: we could also make this check dependent on the + # existence of an AAAA record... + ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {} + if ipv6.get("status") == "SUCCESS": + ipversions.append(6) + + for item in self.test_http(domains_to_check, ipversions): + yield item + + # If at least one domain is correctly exposed to the outside, + # attempt to diagnose hairpinning situations. On network with + # hairpinning issues, the server may be correctly exposed on the + # outside, but from the outside, it will be as if the port forwarding + # was not configured... Hence, calling for example + # "curl --head the.global.ip" will simply timeout... + if self.do_hairpinning_test: + global_ipv4 = ipv4.get("data", {}).get("global", None) + if global_ipv4 and settings_get("misc.network.dns_exposure") in [ + "both", + "ipv4", + ]: + try: + requests.head("http://" + global_ipv4, timeout=5) + except requests.exceptions.Timeout: + yield dict( + meta={"test": "hairpinning"}, + status="WARNING", + summary="diagnosis_http_hairpinning_issue", + details=["diagnosis_http_hairpinning_issue_details"], + ) + except Exception: + # Well I dunno what to do if that's another exception + # type... That'll most probably *not* be an hairpinning + # issue but something else super weird ... + pass + + def test_http(self, domains, ipversions): + results = {} + for ipversion in ipversions: + try: + r = Diagnoser.remote_diagnosis( + "check-http", + data={"domains": domains, "nonce": self.nonce}, + ipversion=ipversion, + ) + results[ipversion] = r["http"] + except Exception as e: + yield dict( + meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion}, + data={"error": str(e)}, + status="WARNING", + summary="diagnosis_http_could_not_diagnose", + details=["diagnosis_http_could_not_diagnose_details"], + ) + continue + + ipversions = results.keys() + if not ipversions: + return + + for domain in domains: + # i18n: diagnosis_http_bad_status_code + # i18n: diagnosis_http_connection_error + # i18n: diagnosis_http_timeout + + # If both IPv4 and IPv6 (if applicable) are good + if all( + results[ipversion][domain]["status"] == "ok" for ipversion in ipversions + ): + if 4 in ipversions and settings_get("misc.network.dns_exposure") in [ + "both", + "ipv4", + ]: + self.do_hairpinning_test = True + yield dict( + meta={"domain": domain}, + status="SUCCESS", + summary="diagnosis_http_ok", + ) + # If both IPv4 and IPv6 (if applicable) are failed + elif all( + results[ipversion][domain]["status"] != "ok" for ipversion in ipversions + ): + detail = results[4 if 4 in ipversions else 6][domain]["status"] + yield dict( + meta={"domain": domain}, + status="ERROR", + summary="diagnosis_http_unreachable", + details=[detail.replace("error_http_check", "diagnosis_http")], + ) + # If only IPv4 is failed or only IPv6 is failed (if applicable) + else: + passed, failed = ( + (4, 6) if results[4][domain]["status"] == "ok" else (6, 4) + ) + detail = results[failed][domain]["status"] + + # Failing in ipv4 is critical. + # If we failed in IPv6 but there's in fact no AAAA record + # It's an acceptable situation and we shall not report an + # error + def ipv6_is_important_for_this_domain(): + dnsrecords = ( + Diagnoser.get_cached_report( + "dnsrecords", item={"domain": domain, "category": "basic"} + ) + or {} + ) + AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") + + return AAAA_status in ["OK", "WRONG"] or settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv6"] + + if failed == 4 or ipv6_is_important_for_this_domain(): + yield dict( + meta={"domain": domain}, + data={"passed": passed, "failed": failed}, + status="ERROR", + summary="diagnosis_http_partially_unreachable", + details=[detail.replace("error_http_check", "diagnosis_http")], + ) + # So otherwise we report a success (note that this info is + # later used to know that ACME challenge is doable) + # + # And in addition we report an info about the failure in IPv6 + # *with a different meta* (important to avoid conflicts when + # fetching the other info...) + else: + self.do_hairpinning_test = True + yield dict( + meta={"domain": domain}, + status="SUCCESS", + summary="diagnosis_http_ok", + ) + yield dict( + meta={"test": "ipv6", "domain": domain}, + data={"passed": passed, "failed": failed}, + status="INFO", + summary="diagnosis_http_partially_unreachable", + details=[detail.replace("error_http_check", "diagnosis_http")], + ) diff --git a/src/diagnosers/24-mail.py b/src/diagnosers/24-mail.py new file mode 100644 index 0000000..594c8cd --- /dev/null +++ b/src/diagnosers/24-mail.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import logging +import os +import re +from subprocess import CalledProcessError + +import dns.resolver + +from ..diagnosis import Diagnoser +from ..domain import _get_maindomain, domain_list +from ..settings import settings_get +from ..utils.dns import dig +from ..utils.file_utils import read_yaml +from ..utils.mail import get_pending_mails_nb + +DEFAULT_DNS_BLOCKLIST = "/usr/share/yunohost/dnsbl_list.yml" + +logger = logging.getLogger("yunohost.diagnosis") + + +class MyDiagnoser(Diagnoser): + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 600 + dependencies: list[str] = ["ip"] + + def run(self): + self.ehlo_domain = _get_maindomain().lower() + self.mail_domains = domain_list()["domains"] + self.ipversions, self.ips = self.get_ips_checked() + + # TODO Is a A/AAAA and MX Record ? + # TODO Are outgoing public IPs authorized to send mail by SPF ? + # TODO Validate DKIM and dmarc ? + # TODO check that the recent mail logs are not filled with thousand of email sending (unusual number of mail sent) + # TODO check for unusual failed sending attempt being refused in the logs ? + checks = [ + "check_outgoing_port_25", # i18n: diagnosis_mail_outgoing_port_25_ok + "check_ehlo", # i18n: diagnosis_mail_ehlo_ok + "check_fcrdns", # i18n: diagnosis_mail_fcrdns_ok + "check_blocklist", # i18n: diagnosis_mail_blocklist_ok + "check_queue", # i18n: diagnosis_mail_queue_ok + ] + for check in checks: + logger.debug("Running " + check) + reports = list(getattr(self, check)()) + for report in reports: + yield report + if not reports: + name = check[6:] + yield dict( + meta={"test": "mail_" + name}, + status="SUCCESS", + summary="diagnosis_mail_" + name + "_ok", + ) + + def check_outgoing_port_25(self): + """ + Check outgoing port 25 is open and not blocked by router + This check is ran on IPs we could used to send mail. + """ + + for ipversion in self.ipversions: + cmd = "/bin/nc -{ipversion} -z -w2 yunohost.org 25".format( + ipversion=ipversion + ) + if os.system(cmd) != 0: + yield dict( + meta={"test": "outgoing_port_25", "ipversion": ipversion}, + data={}, + status="ERROR", + summary="diagnosis_mail_outgoing_port_25_blocked", + details=[ + "diagnosis_mail_outgoing_port_25_blocked_details", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn", + ], + ) + + def check_ehlo(self): + """ + Check the server is reachable from outside and it's the good one + This check is ran on IPs we could used to send mail. + """ + + for ipversion in self.ipversions: + try: + r = Diagnoser.remote_diagnosis( + "check-smtp", data={}, ipversion=ipversion + ) + except Exception as e: + yield dict( + meta={ + "test": "mail_ehlo", + "reason": "remote_server_failed", + "ipversion": ipversion, + }, + data={"error": str(e)}, + status="WARNING", + summary="diagnosis_mail_ehlo_could_not_diagnose", + details=["diagnosis_mail_ehlo_could_not_diagnose_details"], + ) + continue + + if r["status"] != "ok": + # i18n: diagnosis_mail_ehlo_bad_answer + # i18n: diagnosis_mail_ehlo_bad_answer_details + # i18n: diagnosis_mail_ehlo_unreachable + # i18n: diagnosis_mail_ehlo_unreachable_details + summary = r["status"].replace("error_smtp_", "diagnosis_mail_ehlo_") + yield dict( + meta={"test": "mail_ehlo", "ipversion": ipversion}, + data={}, + status="ERROR", + summary=summary, + details=[summary + "_details"], + ) + 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}, + status="ERROR", + summary="diagnosis_mail_ehlo_wrong", + details=["diagnosis_mail_ehlo_wrong_details"], + ) + + def check_fcrdns(self): + """ + Check the reverse DNS is well defined by doing a Forward-confirmed + reverse DNS check + This check is ran on IPs we could used to send mail. + """ + + for ip in self.ips: + if ":" in ip: + ipversion = 6 + details = [ + "diagnosis_mail_fcrdns_nok_details", + "diagnosis_mail_fcrdns_nok_alternatives_6", + ] + else: + ipversion = 4 + details = [ + "diagnosis_mail_fcrdns_nok_details", + "diagnosis_mail_fcrdns_nok_alternatives_4", + ] + + rev = dns.reversename.from_address(ip) + subdomain = str(rev.split(3)[0]) + query = subdomain + if ipversion == 4: + query += ".in-addr.arpa" + else: + query += ".ip6.arpa" + + # Do the DNS Query + status, value = dig(query, "PTR", resolvers="force_external") + if status == "nok": + yield dict( + meta={"test": "mail_fcrdns", "ipversion": ipversion}, + data={"ip": ip, "ehlo_domain": self.ehlo_domain}, + status="ERROR", + summary="diagnosis_mail_fcrdns_dns_missing", + details=details, + ) + continue + + rdns_domain = "" + if len(value) > 0: + rdns_domain = value[0][:-1] if value[0].endswith(".") else value[0] + if rdns_domain.lower() != self.ehlo_domain: + details = [ + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details" + ] + details + yield dict( + meta={"test": "mail_fcrdns", "ipversion": ipversion}, + data={ + "ip": ip, + "ehlo_domain": self.ehlo_domain, + "rdns_domain": rdns_domain.lower(), + }, + status="ERROR", + summary="diagnosis_mail_fcrdns_different_from_ehlo_domain", + details=details, + ) + + def check_blocklist(self): + """ + Check with dig onto blocklist DNS server + This check is ran on IPs and domains we could used to send mail. + """ + + dns_blocklists = read_yaml(DEFAULT_DNS_BLOCKLIST) + for item in self.ips + self.mail_domains: + for blocklist in dns_blocklists: + item_type = "domain" + if ":" in item: + item_type = "ipv6" + elif re.match(r"^\d+\.\d+\.\d+\.\d+$", item): + item_type = "ipv4" + + if not blocklist[item_type]: + continue + + # Build the query for DNSBL + subdomain = item + if item_type != "domain": + rev = dns.reversename.from_address(item) + subdomain = str(rev.split(3)[0]) + query = subdomain + "." + blocklist["dns_server"] + + # Do the DNS Query + status, answers = dig(query, "A") + if status != "ok" or ( + answers + and set(answers) <= set(blocklist["non_blocklisted_return_code"]) + ): + continue + + # Try to get the reason + details = [] + status, answers = dig(query, "TXT") + reason = "-" + if status == "ok": + reason = ", ".join(answers) + details.append( + "diagnosis_mail_blocklist_reason" + if "open resolver" not in reason + else "diagnosis_mail_blocklist_reason_openresolver" + ) + + details.append("diagnosis_mail_blocklist_website") + + yield dict( + meta={ + "test": "mail_blocklist", + "item": item, + "blocklist": blocklist["dns_server"], + }, + data={ + "blocklist_name": blocklist["name"], + "blocklist_website": blocklist["website"], + "reason": reason, + }, + status="ERROR", + summary="diagnosis_mail_blocklist_listed_by", + details=details, + ) + + def check_queue(self): + """ + Check mail queue is not filled with hundreds of email pending + """ + + try: + pending_emails = get_pending_mails_nb() + except (ValueError, CalledProcessError) as e: + yield dict( + meta={"test": "mail_queue"}, + data={"error": str(e)}, + status="ERROR", + summary="diagnosis_mail_queue_unavailable", + details=["diagnosis_mail_queue_unavailable_details"], + ) + else: + if pending_emails > 100: + yield dict( + meta={"test": "mail_queue"}, + data={"nb_pending": pending_emails}, + status="WARNING", + summary="diagnosis_mail_queue_too_big", + ) + else: + yield dict( + meta={"test": "mail_queue"}, + data={"nb_pending": pending_emails}, + status="SUCCESS", + summary="diagnosis_mail_queue_ok", + ) + + def get_ips_checked(self): + outgoing_ipversions = [] + outgoing_ips = [] + ipv4 = Diagnoser.get_cached_report("ip", {"test": "ipv4"}) or {} + if ipv4.get("status") == "SUCCESS" and settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv4"]: + outgoing_ipversions.append(4) + global_ipv4 = ipv4.get("data", {}).get("global", {}) + if global_ipv4: + outgoing_ips.append(global_ipv4) + + if settings_get("email.smtp.smtp_allow_ipv6") or settings_get( + "misc.network.dns_exposure" + ) in ["both", "ipv6"]: + ipv6 = Diagnoser.get_cached_report("ip", {"test": "ipv6"}) or {} + if ipv6.get("status") == "SUCCESS": + outgoing_ipversions.append(6) + global_ipv6 = ipv6.get("data", {}).get("global", {}) + if global_ipv6: + outgoing_ips.append(global_ipv6) + return (outgoing_ipversions, outgoing_ips) diff --git a/src/diagnosers/30-services.py b/src/diagnosers/30-services.py new file mode 100644 index 0000000..ca8577e --- /dev/null +++ b/src/diagnosers/30-services.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os + +from ..diagnosis import Diagnoser +from ..service import service_status + + +class MyDiagnoser(Diagnoser): + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 300 + dependencies: list[str] = [] + + def run(self): + all_result = service_status() + + for service, result in sorted(all_result.items()): + item = dict( + meta={"service": service}, + data={ + "status": result["status"], + "configuration": result["configuration"], + }, + ) + + if result["status"] != "running": + item["status"] = "ERROR" if result["status"] != "unknown" else "WARNING" + item["summary"] = "diagnosis_services_bad_status" + item["details"] = ["diagnosis_services_bad_status_tip"] + + elif result["configuration"] == "broken": + item["status"] = "WARNING" + item["summary"] = "diagnosis_services_conf_broken" + item["details"] = result["configuration-details"] + + else: + item["status"] = "SUCCESS" + item["summary"] = "diagnosis_services_running" + + yield item diff --git a/src/diagnosers/50-systemresources.py b/src/diagnosers/50-systemresources.py new file mode 100644 index 0000000..d18df83 --- /dev/null +++ b/src/diagnosers/50-systemresources.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import datetime +import os +import re + +import psutil + +from ..diagnosis import Diagnoser +from ..utils.process import check_output + + +class MyDiagnoser(Diagnoser): + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 300 + dependencies: list[str] = [] + + def run(self): + MB = 1024**2 + GB = MB * 1024 + + # + # RAM + # + + ram = psutil.virtual_memory() + ram_available_percent = 100 * ram.available / ram.total + item = dict( + meta={"test": "ram"}, + data={ + "total": human_size(ram.total), + "available": human_size(ram.available), + "available_percent": round_(ram_available_percent), + }, + ) + + if ram.available < 100 * MB or ram_available_percent < 5: + item["status"] = "ERROR" + item["summary"] = "diagnosis_ram_verylow" + elif ram.available < 200 * MB or ram_available_percent < 10: + item["status"] = "WARNING" + item["summary"] = "diagnosis_ram_low" + else: + item["status"] = "SUCCESS" + item["summary"] = "diagnosis_ram_ok" + yield item + + # + # Swap + # + + swap = psutil.swap_memory() + item = dict( + meta={"test": "swap"}, + data={"total": human_size(swap.total), "recommended": "512 MiB"}, + ) + if swap.total <= 1 * MB: + item["status"] = "INFO" + item["summary"] = "diagnosis_swap_none" + elif swap.total < 450 * MB: + item["status"] = "INFO" + item["summary"] = "diagnosis_swap_notsomuch" + else: + item["status"] = "SUCCESS" + item["summary"] = "diagnosis_swap_ok" + item["details"] = ["diagnosis_swap_tip"] + yield item + + # FIXME : add a check that swapiness is low if swap is on a sdcard... + + # + # Disks usage + # + + disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) + + # Ignore /dev/loop stuff which are ~virtual partitions ? (e.g. mounted to /snap/) + disk_partitions = [ + d + for d in disk_partitions + if d.mountpoint in ["/", "/var"] or not d.device.startswith("/dev/loop") + ] + + for disk_partition in disk_partitions: + device = disk_partition.device + mountpoint = disk_partition.mountpoint + + usage = psutil.disk_usage(mountpoint) + free_percent = 100 - round_(usage.percent) + + item = dict( + meta={"test": "diskusage", "mountpoint": mountpoint}, + data={ + "device": device, + # N.B.: we do not use usage.total because we want + # to take into account the 5% security margin + # correctly (c.f. the doc of psutil ...) + "total": human_size(usage.used + usage.free), + "free": human_size(usage.free), + "free_percent": free_percent, + }, + ) + + # We have an additional absolute constrain on / and /var because + # system partitions are critical, having them full may prevent + # upgrades etc... + if free_percent < 2.5 or ( + mountpoint in ["/", "/var"] and usage.free < 1 * GB + ): + item["status"] = "ERROR" + item["summary"] = "diagnosis_diskusage_verylow" + elif free_percent < 5 or ( + mountpoint in ["/", "/var"] and usage.free < 2 * GB + ): + item["status"] = "WARNING" + item["summary"] = "diagnosis_diskusage_low" + else: + item["status"] = "SUCCESS" + item["summary"] = "diagnosis_diskusage_ok" + + yield item + + # + # Check for minimal space on / + /var + # because some stupid VPS provider only configure a stupidly + # low amount of disk space for the root partition + # which later causes issue when it gets full... + # + + main_disk_partitions = [ + d for d in disk_partitions if d.mountpoint in ["/", "/var"] + ] + main_space = sum( + psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions + ) + if main_space < 10 * GB: + yield dict( + meta={"test": "rootfstotalspace"}, + data={"space": human_size(main_space)}, + status="ERROR", + summary="diagnosis_rootfstotalspace_critical", + ) + elif main_space < 14 * GB: + yield dict( + meta={"test": "rootfstotalspace"}, + data={"space": human_size(main_space)}, + status="WARNING", + summary="diagnosis_rootfstotalspace_warning", + ) + + # + # Recent kills by oom_reaper + # + + kills_count = self.recent_kills_by_oom_reaper() + if kills_count: + kills_summary = "\n".join( + [f"{proc} (x{count})" for proc, count in kills_count] + ) + + yield dict( + meta={"test": "oom_reaper"}, + status="WARNING", + summary="diagnosis_processes_killed_by_oom_reaper", + data={"kills_summary": kills_summary}, + ) + + def recent_kills_by_oom_reaper(self): + if not os.path.exists("/var/log/kern.log"): + return [] + + def analyzed_kern_log(): + cmd = 'tail -n 10000 /var/log/kern.log | grep "oom_reaper: reaped process" || true' + out = check_output(cmd) + lines = out.split("\n") if out else [] + + now = datetime.datetime.now(datetime.timezone.utc) + + for line in reversed(lines): + # Lines look like : + # 2025-10-15T13:58:59.799358+02:00 yolo kernel: [ 9623.613667] oom_reaper: reaped process 11509 (uwsgi), now anon-rss:0kB, file-rss:0kB, shmem-rss:328kB + date_str = line.split()[0] + date = datetime.datetime.fromisoformat(date_str) + diff = now - date + if diff.days >= 1: + break + process_killed = re.search(r"\(.*\)", line).group().strip("()") + yield process_killed + + processes = list(analyzed_kern_log()) + kills_count = [ + (p, len([p_ for p_ in processes if p_ == p])) for p in set(processes) + ] + kills_count = sorted(kills_count, key=lambda p: p[1], reverse=True) + + return kills_count + + +def human_size(bytes_): + # Adapted from https://stackoverflow.com/a/1094933 + for unit in ["", "ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: + if abs(bytes_) < 1024.0: + bytes_ = round_(bytes_) + return f"{bytes_} {unit}B" + bytes_ /= 1024.0 + bytes_ = round_(bytes_) + return f"{bytes_} YiB" + + +def round_(n): + # round_(22.124) -> 22 + # round_(9.45) -> 9.4 + n = round(n, 1) + if n > 10: + n = int(round(n)) + return n diff --git a/src/diagnosers/70-regenconf.py b/src/diagnosers/70-regenconf.py new file mode 100644 index 0000000..a016101 --- /dev/null +++ b/src/diagnosers/70-regenconf.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import re + +from ..diagnosis import Diagnoser +from ..regenconf import _calculate_hash, _get_regenconf_infos +from ..settings import settings_get +from ..utils.file_utils import read_file + + +class MyDiagnoser(Diagnoser): + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 300 + dependencies: list[str] = [] + + def run(self): + regenconf_modified_files = list(self.manually_modified_files()) + + if not regenconf_modified_files: + yield dict( + meta={"test": "regenconf"}, + status="SUCCESS", + summary="diagnosis_regenconf_allgood", + ) + else: + for f in regenconf_modified_files: + yield dict( + meta={ + "test": "regenconf", + "category": f["category"], + "file": f["path"], + }, + status="WARNING", + summary="diagnosis_regenconf_manually_modified", + details=["diagnosis_regenconf_manually_modified_details"], + ) + + if ( + any(f["path"] == "/etc/ssh/sshd_config" for f in regenconf_modified_files) + and os.system( + "grep -q '^ *AllowGroups\\|^ *AllowUsers' /etc/ssh/sshd_config" + ) + != 0 + ): + yield dict( + meta={"test": "sshd_config_insecure"}, + status="ERROR", + summary="diagnosis_sshd_config_insecure", + ) + + # Check consistency between actual ssh port in sshd_config vs. setting + ssh_port_setting = settings_get("security.ssh.ssh_port") + ssh_port_line = re.findall( + r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config") + ) + if len(ssh_port_line) == 1 and int(ssh_port_line[0]) != ssh_port_setting: + yield dict( + meta={"test": "sshd_config_port_inconsistency"}, + status="WARNING", + summary="diagnosis_sshd_config_inconsistent", + details=["diagnosis_sshd_config_inconsistent_details"], + ) + + def manually_modified_files(self): + for category, infos in _get_regenconf_infos().items(): + for path, hash_ in infos["conffiles"].items(): + if hash_ != _calculate_hash(path): + yield {"path": path, "category": category} diff --git a/src/diagnosers/80-apps.py b/src/diagnosers/80-apps.py new file mode 100644 index 0000000..1feee65 --- /dev/null +++ b/src/diagnosers/80-apps.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import logging +import os + +from packaging import version + +from ..app import APPS_SETTING_PATH, AppInfo, app_list +from ..app_catalog import SecurityIssueInfos, _load_security_issues_list +from ..diagnosis import Diagnoser +from ..utils.system import debian_version + +logger = logging.getLogger("yunohost.diagnosis") + + +class MyDiagnoser(Diagnoser): + id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] + cache_duration = 300 + dependencies: list[str] = [] + + def run(self): + self.security_issues_list_per_app = _load_security_issues_list()["apps"] + + apps = app_list(full=True)["apps"] + for app in apps: + app["issues"] = list(self.issues(app)) + + if not any(app["issues"] for app in apps): + yield dict( + meta={"test": "apps"}, + status="SUCCESS", + summary="diagnosis_apps_allgood", + ) + else: + for app in apps: + if not app["issues"]: + continue + + level = ( + "ERROR" + if any(issue[0] == "error" for issue in app["issues"]) + else "WARNING" + ) + + yield dict( + meta={ + "test": "apps", + "app": app["name"], + "installed_version": app["version"], + }, + status=level, + summary="diagnosis_apps_issue", + details=[issue[1] for issue in app["issues"]], + ) + + def issues(self, app: AppInfo): + def app_version_parse(v: str) -> tuple: + if "~" in v: + raw_upstream_version, raw_ynh_version = v.split("~ynh", 1) + else: + raw_upstream_version, raw_ynh_version = (v, "0") + upstream_version = version.parse(raw_upstream_version) + ynh_version = int(raw_ynh_version) + return (upstream_version, ynh_version) + + # Check for security issues reported in the security issue index + + app_base_id = app["manifest"]["id"] + if app_base_id in self.security_issues_list_per_app: + app_version = app_version_parse(app["version"]) + security_issues_for_this_app: list[SecurityIssueInfos] = ( + self.security_issues_list_per_app[app_base_id] + ) + for issue in security_issues_for_this_app: + raw_fixed_in_version = issue["fixed_in_version"] + if isinstance(raw_fixed_in_version, dict): + if debian_version() not in issue["fixed_in_version"]: + logger.warning( + f"Not able to check versions in which security issue is fixed for app '{app_base_id}' (no version specified for Debian {debian_version()})" + ) + continue + fixed_in_version = app_version_parse( + raw_fixed_in_version[debian_version()] + ) + else: + fixed_in_version = app_version_parse(raw_fixed_in_version) + + if app_version >= fixed_in_version: + continue + level = "error" if issue["level"] == "danger" else "warning" + if isinstance(issue["more_infos"], list): + more_infos_list = ", ".join(issue["more_infos"]) + else: + more_infos_list = issue["more_infos"] + + # i18n: diagnosis_apps_security_issue_warning + # i18n: diagnosis_apps_security_issue_error + yield ( + level, + ( + f"diagnosis_apps_security_issue_{level}", + { + **issue, + "more_infos_list": more_infos_list, + "current_version": app["version"], + }, + ), + ) + + # Check quality level in catalog + + if not app.get("from_catalog") or app["from_catalog"].get("state") != "working": + yield ("warning", "diagnosis_apps_not_in_app_catalog") + elif ( + not isinstance(app["from_catalog"].get("level"), int) + or app["from_catalog"]["level"] == 0 + ): + yield ("warning", "diagnosis_apps_broken") + elif app["from_catalog"]["level"] <= 4: + yield ("warning", "diagnosis_apps_bad_quality") + + # Check for super old, deprecated practices + + if app["manifest"].get("packaging_format", 0) < 2: + yield ("error", "diagnosis_apps_outdated_packaging_format") + + yunohost_version_req = ( + app["manifest"].get("requirements", {}).get("yunohost", "").strip(">= ") + ) + if ( + yunohost_version_req.startswith("2.") + or yunohost_version_req.startswith("3.") + or yunohost_version_req.startswith("4.") + ): + yield ("error", "diagnosis_apps_outdated_ynh_requirement") + + app_setting_path = os.path.join(APPS_SETTING_PATH, app["id"]) + deprecated_helpers = [ + "yunohost app setting", + "yunohost app checkurl", + "yunohost app checkport", + "yunohost app initdb", + "yunohost tools port-available", + ] + for deprecated_helper in deprecated_helpers: + if ( + os.system( + f"grep -hr '{deprecated_helper}' {app_setting_path}/scripts/ | grep -v -q '^\\s*#'" + ) + == 0 + ): + yield ("error", "diagnosis_apps_deprecated_practices") + + old_arg_regex = r"^domain=\${?[0-9]" + if ( + os.system(f"grep -q '{old_arg_regex}' {app_setting_path}/scripts/install") + == 0 + ): + yield ("error", "diagnosis_apps_deprecated_practices") diff --git a/src/diagnosers/__init__.py b/src/diagnosers/__init__.py new file mode 100644 index 0000000..48b276e --- /dev/null +++ b/src/diagnosers/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/src/diagnosis.py b/src/diagnosis.py new file mode 100644 index 0000000..f92597c --- /dev/null +++ b/src/diagnosis.py @@ -0,0 +1,719 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import glob +import os +import re +import time +from importlib import import_module +from logging import getLogger + +from moulinette import Moulinette, m18n + +from .log import is_unit_operation +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import ( + read_json, + read_yaml, + write_to_json, + write_to_yaml, +) + +logger = getLogger("yunohost.diagnosis") + +DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/" +DIAGNOSIS_CONFIG_FILE = "/etc/yunohost/diagnosis.yml" +DIAGNOSIS_SERVER = "diagnosis.yunohost.org" + + +def diagnosis_list(): + return {"categories": _list_diagnosis_categories()} + + +def diagnosis_get(category, item): + # Get all the categories + all_categories_names = _list_diagnosis_categories() + + if category not in all_categories_names: + raise YunohostValidationError( + "diagnosis_unknown_categories", categories=category + ) + + if isinstance(item, list): + if any("=" not in criteria for criteria in item): + raise YunohostValidationError( + "Criterias should be of the form key=value (e.g. domain=yolo.test)" + ) + + # Convert the provided criteria into a nice dict + item = {c.split("=")[0]: c.split("=")[1] for c in item} + + return Diagnoser.get_cached_report(category, item=item) + + +def diagnosis_show( + categories=[], issues=False, full=False, share=False, human_readable=False +): + if not os.path.exists(DIAGNOSIS_CACHE): + logger.warning(m18n.n("diagnosis_never_ran_yet")) + return + + # Get all the categories + all_categories_names = _list_diagnosis_categories() + + # Check the requested category makes sense + if categories == []: + categories = all_categories_names + else: + unknown_categories = [c for c in categories if c not in all_categories_names] + if unknown_categories: + raise YunohostValidationError( + "diagnosis_unknown_categories", categories=", ".join(unknown_categories) + ) + + # Fetch all reports + all_reports = [] + for category in categories: + try: + report = Diagnoser.get_cached_report(category) + except Exception as e: + logger.error(m18n.n("diagnosis_failed", category=category, error=str(e))) + continue + + Diagnoser.i18n(report, force_remove_html_tags=share or human_readable) + + add_ignore_flag_to_issues(report) + if not full: + del report["timestamp"] + del report["cached_for"] + report["items"] = [item for item in report["items"] if not item["ignored"]] + for item in report["items"]: + del item["meta"] + del item["ignored"] + if "data" in item: + del item["data"] + if issues: + report["items"] = [ + item + for item in report["items"] + if item["status"] in ["WARNING", "ERROR"] + ] + # Ignore this category if no issue was found + if not report["items"]: + continue + + all_reports.append(report) + + if share: + from .utils.yunopaste import yunopaste + + content = _dump_human_readable_reports(all_reports) + url = yunopaste(content) + + logger.info(m18n.n("log_available_on_yunopaste", url=url)) + if Moulinette.interface.type == "api": + return {"url": url} + else: + return + elif human_readable: + print(_dump_human_readable_reports(all_reports)) + else: + return {"reports": all_reports} + + +def _dump_human_readable_reports(reports): + output = "" + + for report in reports: + output += "=================================\n" + output += "{description} ({id})\n".format(**report) + output += "=================================\n\n" + for item in report["items"]: + output += "[{status}] {summary}\n".format(**item) + for detail in item.get("details", []): + output += " - " + detail.replace("\n", "\n ") + "\n" + output += "\n" + output += "\n\n" + + return output + + +@is_unit_operation(sse_only=True) +def diagnosis_run( + operation_logger, + categories=[], + force=False, + except_if_never_ran_yet=False, + email=False, +): + if (email or except_if_never_ran_yet) and not os.path.exists(DIAGNOSIS_CACHE): + return + + # Get all the categories + all_categories_names = _list_diagnosis_categories() + + # Check the requested category makes sense + if categories == []: + categories = all_categories_names + else: + unknown_categories = [c for c in categories if c not in all_categories_names] + if unknown_categories: + raise YunohostValidationError( + "diagnosis_unknown_categories", categories=", ".join(unknown_categories) + ) + + operation_logger.start() + + issues = [] + # Call the hook ... + diagnosed_categories = [] + for category in categories: + logger.debug(f"Running diagnosis for {category} ...") + + diagnoser = _load_diagnoser(category) + + try: + code, report = diagnoser.diagnose(force=force) + except Exception: + import traceback + + logger.error( + m18n.n( + "diagnosis_failed_for_category", + category=category, + error="\n" + traceback.format_exc(), + ) + ) + else: + diagnosed_categories.append(category) + if report != {}: + issues.extend( + [ + item + for item in report["items"] + if item["status"] in ["WARNING", "ERROR"] + ] + ) + + if email: + _email_diagnosis_issues() + if issues and Moulinette.interface.type == "cli": + logger.warning(m18n.n("diagnosis_display_tip")) + + +@is_unit_operation(flash=True) +def diagnosis_ignore(filter, list=False): + return _diagnosis_ignore(add_filter=filter, list=list) + + +@is_unit_operation(flash=True) +def diagnosis_unignore(filter): + return _diagnosis_ignore(remove_filter=filter) + + +def _diagnosis_ignore(add_filter=None, remove_filter=None, list=False): + """ + This action is meant for the admin to ignore issues reported by the + diagnosis system if they are known and understood by the admin. For + example, the lack of ipv6 on an instance, or badly configured XMPP dns + records if the admin doesn't care so much about XMPP. The point being that + the diagnosis shouldn't keep complaining about those known and "expected" + issues, and instead focus on new unexpected issues that could arise. + + For example, to ignore badly XMPP dnsrecords for domain yolo.test: + + yunohost diagnosis ignore --add-filter dnsrecords domain=yolo.test category=xmpp + ^ ^ ^ + the general additional other + diagnosis criterias criteria + category to to target to target + act on specific specific + reports reports + Or to ignore all dnsrecords issues: + + yunohost diagnosis ignore --add-filter dnsrecords + + The filters are stored in the diagnosis configuration in a data structure like: + + ignore_filters: { + "ip": [ + {"version": 6} # Ignore all issues related to ipv6 + ], + "dnsrecords": [ + {"domain": "yolo.test", "category": "xmpp"}, # Ignore all issues related to DNS xmpp records for yolo.test + {} # Ignore all issues about dnsrecords + ] + } + """ + + # Ignore filters are stored in + configuration = _diagnosis_read_configuration() + + if list: + return {"ignore_filters": configuration.get("ignore_filters", {})} + + def validate_filter_criterias(filter_): + # Get all the categories + all_categories_names = _list_diagnosis_categories() + + # Sanity checks for the provided arguments + if len(filter_) == 0: + 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(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:]} + + return category, criterias + + if add_filter: + category, criterias = validate_filter_criterias(add_filter) + + # Fetch current issues for the requested category + current_issues_for_this_category = diagnosis_show( + categories=[category], issues=True, full=True + ) + current_issues_for_this_category = current_issues_for_this_category["reports"][ + 0 + ].get("items", {}) + + # Accept the given filter only if the criteria effectively match an existing issue + if not any( + issue_matches_criterias(i, criterias) + for i in current_issues_for_this_category + ): + raise YunohostError(m18n.n("diagnosis_ignore_no_issue_found")) + + # Make sure the subdicts/lists exists + if "ignore_filters" not in configuration: + configuration["ignore_filters"] = {} + if category not in configuration["ignore_filters"]: + configuration["ignore_filters"][category] = [] + + if criterias in configuration["ignore_filters"][category]: + logger.warning( + m18n.n("diagnosis_ignore_already_filtered", category=category) + ) + return + + configuration["ignore_filters"][category].append(criterias) + _diagnosis_write_configuration(configuration) + logger.success(m18n.n("diagnosis_ignore_filter_added", category=category)) + return + + if remove_filter: + category, criterias = validate_filter_criterias(remove_filter) + + # Make sure the subdicts/lists exists + if "ignore_filters" not in configuration: + configuration["ignore_filters"] = {} + if category not in configuration["ignore_filters"]: + configuration["ignore_filters"][category] = [] + + if criterias not in configuration["ignore_filters"][category]: + logger.warning( + m18n.n("diagnosis_ignore_no_filter_found", category=category) + ) + return + + configuration["ignore_filters"][category].remove(criterias) + _diagnosis_write_configuration(configuration) + logger.success(m18n.n("diagnosis_ignore_filter_removed", category=category)) + return + + +def _diagnosis_read_configuration(): + if not os.path.exists(DIAGNOSIS_CONFIG_FILE): + return {} + + return read_yaml(DIAGNOSIS_CONFIG_FILE) + + +def _diagnosis_write_configuration(conf): + write_to_yaml(DIAGNOSIS_CONFIG_FILE, conf) + + +def issue_matches_criterias(issue, criterias): + """ + e.g. an issue with: + meta: + domain: yolo.test + category: xmpp + + matches the criterias {"domain": "yolo.test"} + """ + for key, value in criterias.items(): + if key not in issue["meta"]: + return False + if str(issue["meta"][key]) != value: + return False + return True + + +def add_ignore_flag_to_issues(report): + """ + Iterate over issues in a report, and flag them as ignored if they match an + ignored filter from the configuration + + N.B. : for convenience. we want to make sure the "ignored" key is set for + every item in the report + """ + + ignore_filters = ( + _diagnosis_read_configuration().get("ignore_filters", {}).get(report["id"], []) + ) + + for report_item in report["items"]: + report_item["ignored"] = False + if report_item["status"] not in ["WARNING", "ERROR"]: + continue + for criterias in ignore_filters: + if issue_matches_criterias(report_item, criterias): + report_item["ignored"] = True + break + + +############################################################ + + +class Diagnoser: + def __init__(self): + self.cache_file = Diagnoser.cache_file(self.id_) + self.description = Diagnoser.get_description(self.id_) + + def cached_time_ago(self): + if not os.path.exists(self.cache_file): + return 99999999 + return time.time() - os.path.getmtime(self.cache_file) + + def write_cache(self, report): + if not os.path.exists(DIAGNOSIS_CACHE): + os.makedirs(DIAGNOSIS_CACHE) + return write_to_json(self.cache_file, report) + + def diagnose(self, force=False): + if not force and self.cached_time_ago() < self.cache_duration: + logger.debug(f"Cache still valid : {self.cache_file}") + logger.info( + m18n.n("diagnosis_cache_still_valid", category=self.description) + ) + return 0, {} + + for dependency in self.dependencies: + dep_report = Diagnoser.get_cached_report(dependency) + + if dep_report["timestamp"] == -1: # No cache yet for this dep + dep_errors = True + else: + dep_errors = [ + item for item in dep_report["items"] if item["status"] == "ERROR" + ] + + if dep_errors: + logger.error( + m18n.n( + "diagnosis_cant_run_because_of_dep", + category=self.description, + dep=Diagnoser.get_description(dependency), + ) + ) + return 1, {} + + items = list(self.run()) + + for item in items: + if "details" in item and not item["details"]: + del item["details"] + + new_report = {"id": self.id_, "cached_for": self.cache_duration, "items": items} + + logger.debug(f"Updating cache {self.cache_file}") + self.write_cache(new_report) + Diagnoser.i18n(new_report) + add_ignore_flag_to_issues(new_report) + + errors = [ + item + for item in new_report["items"] + if item["status"] == "ERROR" and not item["ignored"] + ] + warnings = [ + item + for item in new_report["items"] + if item["status"] == "WARNING" and not item["ignored"] + ] + errors_ignored = [ + item + for item in new_report["items"] + if item["status"] == "ERROR" and item["ignored"] + ] + warning_ignored = [ + item + for item in new_report["items"] + if item["status"] == "WARNING" and item["ignored"] + ] + ignored_msg = ( + " " + + m18n.n( + "diagnosis_ignored_issues", + nb_ignored=len(errors_ignored + warning_ignored), + ) + if errors_ignored or warning_ignored + else "" + ) + + if errors and warnings: + logger.error( + m18n.n( + "diagnosis_found_errors_and_warnings", + errors=len(errors), + warnings=len(warnings), + category=new_report["description"], + ) + + ignored_msg + ) + elif errors: + logger.error( + m18n.n( + "diagnosis_found_errors", + errors=len(errors), + category=new_report["description"], + ) + + ignored_msg + ) + elif warnings: + logger.warning( + m18n.n( + "diagnosis_found_warnings", + warnings=len(warnings), + category=new_report["description"], + ) + + ignored_msg + ) + else: + logger.success( + m18n.n("diagnosis_everything_ok", category=new_report["description"]) + + ignored_msg + ) + + return 0, new_report + + @staticmethod + def cache_file(id_): + return os.path.join(DIAGNOSIS_CACHE, f"{id_}.json") + + @staticmethod + def get_cached_report(id_, item=None, warn_if_no_cache=True): + cache_file = Diagnoser.cache_file(id_) + if not os.path.exists(cache_file): + if warn_if_no_cache: + logger.warning(m18n.n("diagnosis_no_cache", category=id_)) + report = {"id": id_, "cached_for": -1, "timestamp": -1, "items": []} + else: + report = read_json(cache_file) + report["timestamp"] = int(os.path.getmtime(cache_file)) + + if item: + for report_item in report["items"]: + if report_item.get("meta") == item: + return report_item + return {} + else: + return report + + @staticmethod + def get_description(id_): + key = "diagnosis_description_" + id_ + # If no description available, fallback to id + return m18n.n(key) if m18n.key_exists(key) else id_ + + @staticmethod + def i18n(report, force_remove_html_tags=False): + # "Render" the strings with m18n.n + # N.B. : we do those m18n.n right now instead of saving the already-translated report + # because we can't be sure we'll redisplay the infos with the same locale as it + # was generated ... e.g. if the diagnosing happened inside a cron job with locale EN + # instead of FR used by the actual admin... + + report["description"] = Diagnoser.get_description(report["id"]) + + for item in report["items"]: + # For the summary and each details, we want to call + # m18n() on the string, with the appropriate data for string + # formatting which can come from : + # - infos super-specific to the summary/details (if it's a tuple(key,dict_with_info) and not just a string) + # - 'meta' info = parameters of the test (e.g. which domain/category for DNS conf record) + # - actual 'data' retrieved from the test (e.g. actual global IP, ...) + + meta_data = item.get("meta", {}).copy() + meta_data.update(item.get("data", {})) + + html_tags = re.compile(r"<[^>]+>") + + def m18n_(info): + if not isinstance(info, tuple) and not isinstance(info, list): + info = (info, {}) + info[1].update(meta_data) + s = m18n.n(info[0], **(info[1])) + # In cli, we remove the html tags + if Moulinette.interface.type != "api" or force_remove_html_tags: + s = s.replace("", "'").replace("", "'") + s = html_tags.sub("", s.replace("
", "\n")) + else: + s = s.replace("", "").replace( + "", "" + ) + # Make it so that links open in new tabs + s = s.replace( + "URL: {url}
Status code: {r.status_code}" + ) + if r.status_code == 400: + raise Exception(f"Diagnosis request was refused: {r.content}") + + try: + r = r.json() + except Exception as e: + raise Exception( + f"Failed to parse json from diagnosis server response.\nError: {e}\nOriginal content: {r.content}" + ) + + return r + + +def _list_diagnosis_categories(): + paths = glob.glob(os.path.dirname(__file__) + "/diagnosers/??-*.py") + names = [ + name.split("-")[-1] + for name in sorted([os.path.basename(path)[: -len(".py")] for path in paths]) + ] + + return names + + +def _load_diagnoser(diagnoser_name): + logger.debug(f"Loading diagnoser {diagnoser_name}") + + paths = glob.glob(os.path.dirname(__file__) + f"/diagnosers/??-{diagnoser_name}.py") + + if len(paths) != 1: + raise YunohostError( + f"Uhoh, found several matches (or none?) for diagnoser {diagnoser_name} : {paths}", + raw_msg=True, + ) + + module_id = os.path.basename(paths[0][: -len(".py")]) + + try: + # this is python builtin method to import a module using a name, we + # use that to import the migration as a python object so we'll be + # able to run it in the next loop + module = import_module(f"yunohost.diagnosers.{module_id}") + return module.MyDiagnoser() + except Exception as e: + import traceback + + traceback.print_exc() + + raise YunohostError( + f"Failed to load diagnoser {diagnoser_name} : {e}", raw_msg=True + ) + + +def _email_diagnosis_issues(): + from .domain import _get_maindomain + + maindomain = _get_maindomain() + from_ = f"diagnosis@{maindomain} (Automatic diagnosis on {maindomain})" + to_ = "root" + subject_ = f"Issues found by automatic diagnosis on {maindomain}" + + disclaimer = "The automatic diagnosis on your YunoHost server identified some issues on your server. You will find a description of the issues below. You can manage those issues in the 'Diagnosis' section in your webadmin." + + issues = diagnosis_show(issues=True)["reports"] + if not issues: + return + + content = _dump_human_readable_reports(issues) + + message = f"""\ +From: {from_} +To: {to_} +Subject: {subject_} + +{disclaimer} + +--- + +{content} +""" + + import smtplib + + smtp = smtplib.SMTP("localhost") + smtp.sendmail(from_, [to_], message.encode("utf-8")) + smtp.quit() diff --git a/src/disk.py b/src/disk.py new file mode 100644 index 0000000..7813969 --- /dev/null +++ b/src/disk.py @@ -0,0 +1,111 @@ +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import enum + +from sdbus import sd_bus_open_system + +from .utils.system import binary_to_human +from .utils.udisks2_interfaces import Udisks2Manager + + +class DiskState(enum.StrEnum): + """https://github.com/storaged-project/udisks/issues/1352#issuecomment-2678874537""" + + @staticmethod + def _generate_next_value_(name, start, count, last_values): + return name.upper() + + SANE = enum.auto() + CRITICAL = enum.auto() + UNKNOWN = enum.auto() + + @staticmethod + def parse(drive: dict) -> "DiskState": + match drive: + case {"smart_failing": failing}: + # ATA disk, see https://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Drive.Ata.html#gdbus-property-org-freedesktop-UDisks2-Drive-Ata.SmartFailing + return DiskState.SANE if not failing else DiskState.CRITICAL + case {"smart_critical_warning": failing}: + # NVME; see https://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.NVMe.Controller.html#gdbus-property-org-freedesktop-UDisks2-NVMe-Controller.SmartCriticalWarning + return DiskState.SANE if not failing else DiskState.CRITICAL + case _: + return DiskState.UNKNOWN + + +def _disk_infos(name: str, drive: dict, **kwargs): + human_readable = kwargs.get("human_readable", False) + human_readable_size = kwargs.get("human_readable_size", human_readable) + result = { + "name": name, + "model": drive["model"], + "serial": drive["serial"], + "removable": bool(drive["media_removable"]), + "size": ( + binary_to_human(drive["size"]) if human_readable_size else drive["size"] + ), + "smartStatus": DiskState.parse(drive), + } + + if "connection_bus" in drive: + result["connectionBus"] = drive["connection_bus"] + + if (rotation_rate := drive["rotation_rate"]) == -1: + result.update( + { + "type": "HDD", + "rpm": "Unknown" if human_readable else None, + } + ) + elif rotation_rate == 0: + result["type"] = "SSD" + else: + result.update( + { + "type": "HDD", + "rpm": rotation_rate, + } + ) + + return result + + +def disk_list(**kwargs): + bus = sd_bus_open_system() + disks = Udisks2Manager(bus).get_disks() + + with_info = kwargs.get("with_info", False) + + if not with_info: + return list(disks.keys()) + + result = [_disk_infos(name, disk.props, **kwargs) for name, disk in disks.items()] + + return {"disks": result} + + +def disk_info(name, **kwargs): + bus = sd_bus_open_system() + disk = Udisks2Manager(bus).get_disks().get(name) + + human_readable = kwargs.get("human_readable", False) + + if not disk: + return f"Unknown disk with name {name}" if human_readable else None + + return _disk_infos(name, disk.props, **kwargs) diff --git a/src/dns.py b/src/dns.py new file mode 100644 index 0000000..cb5d6c4 --- /dev/null +++ b/src/dns.py @@ -0,0 +1,1035 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import re +import time +from collections import OrderedDict +from difflib import SequenceMatcher +from logging import getLogger +from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, cast + +from moulinette import Moulinette, m18n + +from .domain import ( + _assert_domain_exists, + _get_domain_settings, + _get_parent_domain_of, + _list_subdomains_of, + _set_domain_settings, + domain_config_get, +) +from .hook import hook_callback +from .log import OperationLogger, is_unit_operation +from .utils.dns import dig, is_special_use_tld, is_yunohost_dyndns_domain +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import mkdir, read_file, read_toml, write_to_file +from .utils.network import get_public_ip + +if TYPE_CHECKING: + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.dns")) +else: + logger = getLogger("yunohost.dns") + +DOMAIN_REGISTRAR_LIST_PATH = "/usr/share/yunohost/registrar_list.toml" + + +def domain_dns_suggest(domain: str) -> str: + """ + Generate DNS configuration for a domain + + Keyword argument: + domain -- Domain name + + """ + + if is_special_use_tld(domain): + return m18n.n("domain_dns_conf_special_use_tld") + + _assert_domain_exists(domain) + + dns_conf = _build_dns_conf(domain) + + result = "" + + if dns_conf["basic"]: + result += "; Basic ipv4/ipv6 records" + for record in dns_conf["basic"]: + result += "\n{name} {ttl} IN {type} {content}".format(**record) + + if dns_conf["mail"]: + result += "\n\n" + result += "; Mail" + for record in dns_conf["mail"]: + result += "\n{name} {ttl} IN {type} {content}".format(**record) + result += "\n\n" + + if dns_conf["extra"]: + result += "\n\n" + result += "; Extra" + for record in dns_conf["extra"]: + result += "\n{name} {ttl} IN {type} {content}".format(**record) + + for name, record_list in dns_conf.items(): + if name not in ("basic", "mail", "extra") and record_list: + result += "\n\n" + result += "; " + name + for record in record_list: + result += "\n{name} {ttl} IN {type} {content}".format(**record) + + if Moulinette.interface.type == "cli": + # FIXME Update this to point to our "dns push" doc + logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) + + return result + + +class DNSRecord(TypedDict): + name: str + type: Literal["A", "AAAA", "MX", "TXT", "SRV", "CAA"] + ttl: int + content: str | None + current: NotRequired[str | list[str] | None] + old_content: NotRequired[str | None] + managed_by_yunohost: NotRequired[bool] + id: NotRequired[Any] + identifier: NotRequired[Any] + action: NotRequired[Literal["delete", "create", "update", "unchanged"]] + + +def _build_dns_conf( + base_domain: str, include_empty_AAAA_if_no_ipv6=False, dkim_split=False +) -> dict[Literal["basic", "mail", "extra"] | str, list[DNSRecord]]: + """ + Internal function that will returns a data structure containing the needed + information to generate/adapt the dns configuration + + Arguments: + domains -- List of a domain and its subdomains + + The returned datastructure will have the following form: + { + "basic": [ + # if ipv4 available + {"type": "A", "name": "@", "content": "123.123.123.123", "ttl": 3600}, + # if ipv6 available + {"type": "AAAA", "name": "@", "content": "valid-ipv6", "ttl": 3600}, + ], + "mail": [ + {"type": "MX", "name": "@", "content": "10 domain.tld.", "ttl": 3600}, + {"type": "TXT", "name": "@", "content": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 }, + {"type": "TXT", "name": "mail._domainkey", "content": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600}, + {"type": "TXT", "name": "_dmarc", "content": "\"v=DMARC1; p=none\"", "ttl": 3600} + ], + "extra": [ + # if ipv4 available + {"type": "A", "name": "*", "content": "123.123.123.123", "ttl": 3600}, + # if ipv6 available + {"type": "AAAA", "name": "*", "content": "valid-ipv6", "ttl": 3600}, + {"type": "CAA", "name": "@", "content": "0 issue \"letsencrypt.org\"", "ttl": 3600}, + ], + "example_of_a_custom_rule": [ + {"type": "SRV", "name": "_matrix", "content": "domain.tld.", "ttl": 3600} + ], + } + """ + + from .settings import settings_get + + basic = [] + mail = [] + extra = [] + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) + + # If this is a ynh_dyndns_domain, we're not gonna include all the subdomains in the conf + # Because dynette only accept a specific list of name/type + # And the wildcard */A already covers the bulk of use cases + if is_yunohost_dyndns_domain(base_domain): + subdomains = [] + else: + subdomains = _list_subdomains_of(base_domain) + + domains_settings = { + domain: domain_config_get(domain, export=True) + for domain in [base_domain] + subdomains + } + + base_dns_zone = _get_dns_zone_for_domain(base_domain) + + for domain, settings in domains_settings.items(): + # Domain # Base DNS zone # Basename # Suffix # + # ------------------ # ----------------- # --------- # -------- # + # domain.tld # domain.tld # @ # # + # sub.domain.tld # domain.tld # sub # .sub # + # foo.sub.domain.tld # domain.tld # foo.sub # .foo.sub # + # sub.domain.tld # sub.domain.tld # @ # # + # foo.sub.domain.tld # sub.domain.tld # foo # .foo # + basename = _get_relative_name_for_dns_zone(domain, base_dns_zone) + suffix = f".{basename}" if basename != "@" else "" + + # ttl = settings["ttl"] + ttl = 3600 + + ########################### + # Basic ipv4/ipv6 records # + ########################### + if ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: + basic.append((basename, ttl, "A", ipv4)) + + if ipv6: + basic.append((basename, ttl, "AAAA", ipv6)) + elif include_empty_AAAA_if_no_ipv6: + basic.append((basename, ttl, "AAAA", None)) # type: ignore[arg-type] + + ######### + # Email # + ######### + if settings["mail_in"]: + mail.append((basename, ttl, "MX", f"10 {domain}.")) + + if settings["mail_out"]: + mail.append((basename, ttl, "TXT", '"v=spf1 a mx -all"')) + + # DKIM/DMARC record + dkim_host, dkim_publickey = _get_DKIM(domain) + + if dkim_host: + if not dkim_split: + dkim_publickey = dkim_publickey.replace('" "', "") + mail += [ + (f"{dkim_host}{suffix}", ttl, "TXT", dkim_publickey), + (f"_dmarc{suffix}", ttl, "TXT", '"v=DMARC1; p=none"'), + ] + + ######### + # Extra # + ######### + + # Only recommend wildcard and CAA for the top level + if domain == base_domain: + if ipv4 and settings_get("misc.network.dns_exposure") in ["both", "ipv4"]: + extra.append((f"*{suffix}", ttl, "A", ipv4)) + + if ipv6: + extra.append((f"*{suffix}", ttl, "AAAA", ipv6)) + elif include_empty_AAAA_if_no_ipv6: + extra.append((f"*{suffix}", ttl, "AAAA", None)) # type: ignore[arg-type] + + extra.append((basename, ttl, "CAA", '0 issue "letsencrypt.org"')) + + #################### + # Standard records # + #################### + + def tuple_to_DNSRecord(t: tuple) -> DNSRecord: + return {"name": t[0], "ttl": t[1], "type": t[2], "content": t[3]} + + records: dict[Literal["basic", "mail", "extra"] | str, list[DNSRecord]] = { + "basic": [tuple_to_DNSRecord(t) for t in basic], + "mail": [tuple_to_DNSRecord(t) for t in mail], + "extra": [tuple_to_DNSRecord(t) for t in extra], + } + + ################## + # Custom records # + ################## + + # 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 + # {'/some/path/to/hook1': + # { 'state': 'succeed', + # 'stdreturn': [{'type': 'SRV', + # 'name': 'stuff.foo.bar.', + # 'value': 'yoloswag', + # 'ttl': 3600}] + # }, + # '/some/path/to/hook2': + # { ... }, + # [...] + # + # Loop over the sub-results + custom_records = [ + v["stdreturn"] for v in results.values() if v and v["stdreturn"] + ] + + records[hook_name] = [] + for record_list in custom_records: + # Check that record_list is indeed a list of dict + # with the required keys + if ( + not isinstance(record_list, list) + or any(not isinstance(record, dict) for record in record_list) + or any( + key not in record + for record in record_list + for key in ["name", "ttl", "type"] + ) + # Legacy : content was 'value' in the past merf ... + or all( + key not in record + for record in record_list + for key in ["value", "content"] + ) + ): + # Display an error, mainly for app packagers trying to implement a hook + logger.warning( + "Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s" + % (hook_name, record_list) + ) + continue + + # Legacy : content was 'value' in the past merf ... + for record in record_list: + if "value" in record: + record["content"] = record["value"] + del record["value"] + + records[hook_name].extend(record_list) + + return records + + +def _get_DKIM(domain): + DKIM_file = f"/etc/dkim/{domain}.mail.txt" + + if not os.path.isfile(DKIM_file): + return (None, None) + + with open(DKIM_file) as f: + dkim_content = f.read() + + # Gotta manage 3 formats : + # + # Legacy + # ----- + # + # mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " + # "p=" ) + # + # New + # ------ + # + # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; " + # "p=" ) + # + # New with 2048 key size + # ------ + # + # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; " + # "p=" + # "" ) + + dkim = re.match( + ( + r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+" + r"[^\(]*\((?P[^\)]+)\)" + ), + dkim_content, + re.M | re.S, + ) + + if not dkim: + return (None, None) + + return ( + dkim.group("host"), + re.sub(r'"[\s\n]+"', '" "', dkim.group("c").strip(), re.M | re.S), + ) + + +def _get_dns_zone_for_domain(domain): + """ + Get the DNS zone of a domain + + Keyword arguments: + domain -- The domain name + + """ + + # First, check if domain is a nohost.me / noho.st / ynh.fr + # This is mainly meant to speed up things for "dyndns update" + # ... otherwise we end up constantly doing a bunch of dig requests + if is_yunohost_dyndns_domain(domain): + # Keep only foo.nohost.me even if we have subsub.sub.foo.nohost.me + return ".".join(domain.rsplit(".", 3)[-3:]) + + # Same thing with .local, .test, ... domains + if is_special_use_tld(domain): + # Keep only foo.local even if we have subsub.sub.foo.local + return ".".join(domain.rsplit(".", 2)[-2:]) + + # Check cache + cache_folder = "/var/cache/yunohost/dns_zones" + cache_file = f"{cache_folder}/{domain}" + cache_duration = 3600 # one hour + if ( + os.path.exists(cache_file) + and abs(os.path.getctime(cache_file) - time.time()) < cache_duration + ): + dns_zone = read_file(cache_file).strip() + if dns_zone: + return dns_zone + + # Check cache for parent domain + # This is another strick to try to prevent this function from being + # a bottleneck on system with 1 main domain + 10ish subdomains + # when building the dns conf for the main domain (which will call domain_config_get, etc...) + parent_domain = _get_parent_domain_of(domain) + if parent_domain: + parent_cache_file = f"{cache_folder}/{parent_domain}" + if ( + os.path.exists(parent_cache_file) + and abs(os.path.getctime(parent_cache_file) - time.time()) < cache_duration + ): + dns_zone = read_file(parent_cache_file).strip() + if dns_zone: + return dns_zone + + # For foo.bar.baz.gni we want to scan all the parent domains + # (including the domain itself) + # foo.bar.baz.gni + # bar.baz.gni + # baz.gni + # gni + # Until we find the first one that has a NS record + parent_list = [domain.split(".", i)[-1] for i, _ in enumerate(domain.split("."))] + + # We don't wan't to do A NS request on the tld + for parent in parent_list[0:-1]: + # Check if there's a NS record for that domain + answer = dig(parent, rdtype="NS", full_answers=True, resolvers="force_external") + + if answer[0] != "ok": + # Some domains have a SOA configured but NO NS record !!! + # See https://github.com/YunoHost/issues/issues/1980 + answer = dig( + parent, rdtype="SOA", full_answers=True, resolvers="force_external" + ) + + if answer[0] == "ok": + mkdir(cache_folder, parents=True, force=True) + write_to_file(cache_file, parent) + return parent + + if len(parent_list) >= 2: + zone = parent_list[-2] + else: + zone = parent_list[-1] + + # Adding this otherwise the CI is flooding about those ... + if domain not in [ + "example.tld", + "sub.example.tld", + "domain.tld", + "sub.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 + + +def _get_relative_name_for_dns_zone(domain, base_dns_zone): + # Strip the base dns zone name from a domain such that it's suitable for DNS manipulation relative to a defined zone + # For example, assuming base_dns_zone is "example.tld": + # example.tld -> @ + # foo.example.tld -> foo + # .foo.example.tld -> foo + # bar.foo.example.tld -> bar.foo + return ( + re.sub(r"\.?" + base_dns_zone.replace(".", r"\.") + "$", "", domain.strip(".")) + or "@" + ) + + +def _get_registrar_config_section(domain: str) -> OrderedDict[str, Any]: + from lexicon.providers.auto import _relevant_provider_for_domain + + 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) + + # If parent domain exists in yunohost + parent_domain = _get_parent_domain_of(domain, topest=True) + if parent_domain: + # Dirty hack to have a link on the webadmin + if Moulinette.interface.type == "api": + parent_domain_link = f"[{parent_domain}](#/domains/{parent_domain}/dns)" + else: + parent_domain_link = 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 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"]["default"] = "yunohost" + registrar_infos["infos"]["style"] = "success" + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_registrar_yunohost") + registrar_infos["recovery_password"] = OrderedDict( + { + "type": "password", + "ask": m18n.n("ask_dyndns_recovery_password"), + "default": "", + } + ) + + return registrar_infos + + elif is_special_use_tld(dns_zone): + registrar_infos["infos"]["ask"] = m18n.n("domain_dns_conf_special_use_tld") + + return registrar_infos + + try: + registrar = _relevant_provider_for_domain(dns_zone)[0] + except ValueError: + 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"]["default"] = registrar + registrar_infos["infos"]["ask"] = m18n.n( + "domain_dns_registrar_supported", registrar=registrar + ) + + TESTED_REGISTRARS = ["ovh", "gandi"] + if registrar not in TESTED_REGISTRARS: + registrar_infos["experimental_disclaimer"] = OrderedDict( + { + "type": "alert", + "style": "danger", + "ask": m18n.n( + "domain_dns_registrar_experimental", registrar=registrar + ), + } + ) + + # TODO : add a help tip with the link to the registar's API doc (c.f. Lexicon's README) + registrar_list: dict[str, dict] = read_toml(DOMAIN_REGISTRAR_LIST_PATH) # type: ignore[assignment] + registrar_credentials = registrar_list.get(registrar) + if registrar_credentials is None: + logger.warning( + f"Registrar {registrar} unknown / Should be added to YunoHost's registrar_list.toml by the development team!" + ) + registrar_credentials = {} + else: + registrar_infos["use_auto_dns"] = { + "type": "boolean", + "ask": m18n.n("domain_dns_registrar_use_auto"), + "default": True, + } + for credential, infos in registrar_credentials.items(): + infos["default"] = infos.get("default", "") + infos["visible"] = "use_auto_dns == true" + registrar_infos.update(registrar_credentials) + + return registrar_infos + + +def _get_registar_settings(domain: str) -> tuple[str, Any]: + _assert_domain_exists(domain) + + settings = domain_config_get(domain, key="dns.registrar", export=True) + + registrar = settings.pop("registrar") + + if "experimental_disclaimer" in settings: + settings.pop("experimental_disclaimer") + + return registrar, settings + + +@is_unit_operation() +def domain_dns_push( + operation_logger: "OperationLogger", + domain: str, + dry_run: bool = False, + force: bool = False, + purge: bool = False, +) -> ( + dict[ + Literal["delete", "create", "update", "unchanged"], list[DNSRecord] | list[str] + ] + | dict[Literal["warnings", "errors"], list[str]] +): + """ + Send DNS records to the previously-configured registrar of the domain. + """ + + from lexicon.client import Client as LexiconClient + from lexicon.config import ConfigResolver as LexiconConfigResolver + + registrar, registrar_credentials = _get_registar_settings(domain) + + _assert_domain_exists(domain) + + if is_special_use_tld(domain): + raise YunohostValidationError("domain_dns_conf_special_use_tld") + + if not registrar or registrar == "None": # yes it's None as a string + raise YunohostValidationError("domain_dns_push_not_applicable", domain=domain) + + # FIXME: in the future, properly unify this with yunohost dyndns update + if registrar == "yunohost": + from .dyndns import dyndns_update + + dyndns_update(domain=domain, force=force) + return {} + + if registrar == "parent_domain": + parent_domain = _get_parent_domain_of(domain, topest=True) + assert parent_domain is not None + registrar, registrar_credentials = _get_registar_settings(parent_domain) + if any(registrar_credentials.values()): + raise YunohostValidationError( + "domain_dns_push_managed_in_parent_domain", + domain=domain, + parent_domain=parent_domain, + ) + else: + 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( + "domain_registrar_is_not_configured", domain=domain + ) + + base_dns_zone = _get_dns_zone_for_domain(domain) + + # Convert the generated conf into a format that matches what we'll fetch using the API + # Makes it easier to compare "wanted records" with "current records on remote" + wanted_records: list[DNSRecord] = [] + for records in _build_dns_conf(domain).values(): + for record in records: + # Make sure the name is a FQDN + name = ( + f"{record['name']}.{base_dns_zone}" + if record["name"] != "@" + else base_dns_zone + ) + type_ = record["type"] + content = record["content"] + + # Make sure the content is also a FQDN (with trailing . ?) + if content == "@" and record["type"] == "CNAME": + content = base_dns_zone + "." + + wanted_records.append( + {"name": name, "type": type_, "ttl": record["ttl"], "content": content} + ) + + # FIXME Lexicon does not support CAA records + # See https://github.com/AnalogJ/lexicon/issues/282 and https://github.com/AnalogJ/lexicon/pull/371 + # They say it's trivial to implement it! + # And yet, it is still not done/merged + # Update by Aleks: it works - at least with Gandi ?! + # wanted_records = [record for record in wanted_records if record["type"] != "CAA"] + + if purge: + wanted_records = [] + force = True + + # Construct the base data structure to use lexicon's API. + + base_config = { + "provider_name": registrar, + "domain": base_dns_zone, + registrar: registrar_credentials, + } + + # Get only records for relevant types: A, AAAA, MX, TXT, CNAME, SRV + relevant_types = ["A", "AAAA", "MX", "TXT", "CNAME", "SRV", "CAA"] + current_records = [] + + for rtype in relevant_types: + query = ( + LexiconConfigResolver() + .with_dict(dict_object=base_config) + .with_dict(dict_object={"action": "list", "type": rtype}) + ) + client = LexiconClient(query) + + try: + current_records.extend(client.execute()) + except Exception as e: + raise YunohostError("domain_dns_push_failed_to_list", error=str(e)) + + managed_dns_records_hashes = _get_managed_dns_records_hashes(domain) + + # Ignore records which are for a higher-level domain + # i.e. we don't care about the records for domain.tld when pushing yuno.domain.tld + current_records = [ + r + for r in current_records + if r["name"].endswith(f".{domain}") or r["name"] == domain + ] + + for record in current_records: + # Try to get rid of weird stuff like ".domain.tld" or "@.domain.tld" + record["name"] = record["name"].strip("@").strip(".") + + # Some API return '@' in content and we shall convert it to absolute/fqdn + record["content"] = ( + record["content"] + .replace("@.", base_dns_zone + ".") + .replace("@", base_dns_zone + ".") + ) + + if record["type"] == "TXT": + if not record["content"].startswith('"'): + record["content"] = '"' + record["content"] + if not record["content"].endswith('"'): + record["content"] = record["content"] + '"' + + # Check if this record was previously set by YunoHost + record["managed_by_yunohost"] = ( + _hash_dns_record(record) in managed_dns_records_hashes + ) + + # Step 0 : Get the list of unique (type, name) + # And compare the current and wanted records + # + # i.e. we want this kind of stuff: + # wanted current + # (A, .domain.tld) 1.2.3.4 1.2.3.4 + # (A, www.domain.tld) 1.2.3.4 5.6.7.8 + # (A, foobar.domain.tld) 1.2.3.4 + # (AAAA, .domain.tld) 2001::abcd + # (MX, .domain.tld) 10 domain.tld [10 mx1.ovh.net, 20 mx2.ovh.net] + # (TXT, .domain.tld) "v=spf1 ..." ["v=spf1", "foobar"] + # (SRV, .domain.tld) 0 5 5269 domain.tld + action: Literal["delete", "create", "update", "unchanged"] + changes: dict[Literal["delete", "create", "update", "unchanged"], list[DNSRecord]] + changes = {"delete": [], "update": [], "create": [], "unchanged": []} + + type_and_names = sorted( + {(r["type"], r["name"]) for r in current_records + wanted_records} + ) + comparison: dict[ + tuple[str, str], dict[Literal["current", "wanted"], list[DNSRecord]] + ] = { + type_and_name: {"current": [], "wanted": []} for type_and_name in type_and_names + } + + for crecord in current_records: + comparison[(crecord["type"], crecord["name"])]["current"].append(crecord) + + for wrecord in wanted_records: + assert isinstance(wrecord["type"], str) + assert isinstance(wrecord["name"], str) + comparison[(wrecord["type"], wrecord["name"])]["wanted"].append(wrecord) + + for type_and_name, cwrecords in comparison.items(): + # + # Step 1 : compute a first "diff" where we remove records which are the same on both sides + # + wanted_contents = [r["content"] for r in cwrecords["wanted"]] + current_contents = [r["content"] for r in cwrecords["current"]] + + current = [ + r for r in cwrecords["current"] if r["content"] not in wanted_contents + ] + wanted = [ + r for r in cwrecords["wanted"] if r["content"] not in current_contents + ] + + # + # Step 2 : simple case: 0 record on one side, 0 on the other + # -> either nothing do (0/0) or creations (0/N) or deletions (N/0) + # + if len(current) == 0 and len(wanted) == 0: + # No diff, nothing to do + changes["unchanged"].extend(cwrecords["current"]) + continue + + elif len(wanted) == 0: + changes["delete"].extend(current) + continue + + elif len(current) == 0: + changes["create"].extend(wanted) + continue + + # + # Step 3 : N record on one side, M on the other + # + # Fuzzy matching strategy: + # For each wanted record, try to find a current record which looks like the wanted one + # -> if found, trigger an update + # -> if no match found, trigger a create + # + for record in wanted: + + def likeliness(r): + # We compute this only on the first 100 chars, to have a high value even for completely different DKIM keys + return SequenceMatcher( + None, r["content"][:100], record["content"][:100] + ).ratio() + + matches = sorted(current, key=lambda r: likeliness(r), reverse=True) + if matches and likeliness(matches[0]) > 0.50: + match = matches[0] + # Remove the match from 'current' so that it's not added to the removed stuff later + current.remove(match) + match["old_content"] = match["content"] + match["content"] = record["content"] + changes["update"].append(match) + else: + changes["create"].append(record) + + # + # For all other remaining current records: + # -> trigger deletions + # + for record in current: + changes["delete"].append(record) + + def human_readable_record(action, record) -> str: + name = record["name"] + name = _get_relative_name_for_dns_zone(record["name"], base_dns_zone) + name = name[:20] + t = record["type"] + + if not force and action in ["update", "delete"]: + ignored = ( + "" + if record["managed_by_yunohost"] + else "(ignored, won't be changed by Yunohost unless forced)" + ) + else: + ignored = "" + + if action == "create": + new_content = record.get("content", "(None)")[:30] + return f"{name:>20} [{t:^5}] {new_content:^30} {ignored}" + elif action == "update": + old_content = record.get("old_content", "(None)")[:30] + new_content = record.get("content", "(None)")[:30] + return ( + f"{name:>20} [{t:^5}] {old_content:^30} -> {new_content:^30} {ignored}" + ) + elif action == "unchanged": + old_content = record.get("content", "(None)")[:30] + return f"{name:>20} [{t:^5}] {old_content:^30}" + else: + old_content = record.get("content", "(None)")[:30] + return f"{name:>20} [{t:^5}] {old_content:^30} {ignored}" + + if dry_run: + if Moulinette.interface.type == "api": + for records in changes.values(): + for record in records: + record["name"] = _get_relative_name_for_dns_zone( + record["name"], base_dns_zone + ) + return changes # type: ignore[return-value] + else: + out: dict[Literal["delete", "create", "update", "unchanged"], list[str]] = { + "delete": [], + "create": [], + "update": [], + "unchanged": [], + } + for action in ["delete", "create", "update", "unchanged"]: # type: ignore[assignment] + for record in changes[action]: + out[action].append(human_readable_record(action, record)) + + return out # type: ignore[return-value] + + # If --force ain't used, we won't delete/update records not managed by yunohost + if not force: + for action in ["delete", "update"]: # type: ignore[assignment] + changes[action] = [r for r in changes[action] if r["managed_by_yunohost"]] + + def progress(info=""): + progress.nb += 1 + width = 20 + bar = int(progress.nb * width / progress.total) + bar = "[" + "#" * bar + "." * (width - bar) + "]" + if info: + bar += " > " + info + if progress.old == bar: + return + progress.old = bar + logger.info(bar) + + progress.nb = 0 # type: ignore[attr-defined] + progress.old = "" # type: ignore[attr-defined] + progress.total = len(changes["delete"] + changes["create"] + changes["update"]) # type: ignore[attr-defined] + + if progress.total == 0: # type: ignore[attr-defined] + logger.success(m18n.n("domain_dns_push_already_up_to_date")) + return {} + + # + # Actually push the records + # + + operation_logger.start() + logger.info(m18n.n("domain_dns_pushing")) + + new_managed_dns_records_hashes = [_hash_dns_record(r) for r in changes["unchanged"]] + results: dict[Literal["warnings", "errors"], list[str]] = { + "warnings": [], + "errors": [], + } + + for action in ["delete", "create", "update"]: # type: ignore[assignment] + for record in changes[action]: + relative_name = _get_relative_name_for_dns_zone( + record["name"], base_dns_zone + ) + progress( + f"{action} {record['type']:^5} / {relative_name}" + ) # FIXME: i18n but meh + + # Apparently Lexicon yields us some 'id' during fetch + # But wants 'identifier' during push ... + if "id" in record: + record["identifier"] = record["id"] + del record["id"] + + if registrar == "godaddy": + if record["name"] == base_dns_zone: + record["name"] = "@." + record["name"] + if record["type"] in ["MX", "SRV", "CAA"]: + logger.warning( + f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy." + ) + results["warnings"].append( + f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy." + ) + continue + elif registrar == "gandi": + if record["name"] == base_dns_zone: + record["name"] = "@." + record["name"] + + record["action"] = action + query = ( + LexiconConfigResolver() + .with_dict(dict_object=base_config) + .with_dict(dict_object=record) + ) + + try: + result = LexiconClient(query).execute() + except Exception as e: + msg = m18n.n( + "domain_dns_push_record_failed", + action=action, + type=record["type"], + name=record["name"], + error=str(e), + ) + logger.error(msg) + results["errors"].append(msg) + else: + if result: + new_managed_dns_records_hashes.append(_hash_dns_record(record)) + else: + msg = m18n.n( + "domain_dns_push_record_failed", + action=action, + type=record["type"], + name=record["name"], + error="unkonwn error?", + ) + logger.error(msg) + results["errors"].append(msg) + + _set_managed_dns_records_hashes(domain, new_managed_dns_records_hashes) + + progress_total = progress.total # type: ignore[attr-defined] + + # Everything succeeded + if len(results["errors"]) + len(results["warnings"]) == 0: + logger.success(m18n.n("domain_dns_push_success")) + return {} + # Everything failed + elif len(results["errors"]) + len(results["warnings"]) == progress_total: + logger.error(m18n.n("domain_dns_push_failed")) + else: + logger.warning(m18n.n("domain_dns_push_partial_failure")) + + return results + + +def _get_managed_dns_records_hashes(domain: str) -> list: + return _get_domain_settings(domain).get("managed_dns_records_hashes", []) + + +def _set_managed_dns_records_hashes(domain: str, hashes: list) -> None: + settings = _get_domain_settings(domain) + settings["managed_dns_records_hashes"] = hashes or [] + _set_domain_settings(domain, settings) + + +def _hash_dns_record(record: DNSRecord) -> int: + fields = ["name", "type", "content"] + record_ = {f: record.get(f) for f in fields} + + return hash(frozenset(record_.items())) diff --git a/src/domain.py b/src/domain.py new file mode 100644 index 0000000..ad081dc --- /dev/null +++ b/src/domain.py @@ -0,0 +1,1052 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import time +from collections import OrderedDict +from logging import getLogger +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Literal, + Mapping, + Optional, + TypedDict, + Union, +) + +from moulinette import Moulinette, m18n +from moulinette.core import MoulinetteError + +from .log import OperationLogger, is_unit_operation +from .regenconf import _force_clear_hashes, _process_regen_conf, regen_conf +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import ( + read_file, + read_json, + read_yaml, + rm, + write_to_file, + write_to_json, + write_to_yaml, +) + +if TYPE_CHECKING: + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny, cast + + from .dns import DNSRecord + from .utils.configpanel import ConfigPanel, ConfigPanelModel, RawConfig, RawSettings + from .utils.form import FormModel + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.domain")) +else: + logger = getLogger("yunohost.domain") + + +DOMAIN_SETTINGS_DIR = "/etc/yunohost/domains" + +# Lazy dev caching to avoid re-query ldap every time we need the domain list +# The cache automatically expire every 15 seconds, to prevent desync between +# yunohost CLI and API which run in different processes +domain_list_cache: list[str] = [] +domain_list_cache_timestamp = 0.0 +main_domain_cache: Optional[str] = None +main_domain_cache_timestamp = 0.0 +DOMAIN_CACHE_DURATION = 15 + + +def _get_maindomain() -> str: + global main_domain_cache + global main_domain_cache_timestamp + if ( + not main_domain_cache + or abs(main_domain_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION + ): + with open("/etc/yunohost/current_host", "r") as f: + main_domain_cache = f.readline().rstrip() + main_domain_cache_timestamp = time.time() + + return main_domain_cache + + +def _get_domains(exclude_subdomains: bool = False) -> list[str]: + global domain_list_cache + global domain_list_cache_timestamp + if ( + not domain_list_cache + or abs(domain_list_cache_timestamp - time.time()) > DOMAIN_CACHE_DURATION + ): + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + result = [ + entry["virtualdomain"][0] + for entry in ldap.search("ou=domains", "virtualdomain=*", ["virtualdomain"]) + ] + + def cmp_domain(domain: str) -> list[str]: + # Keep the main part of the domain and the extension together + # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] + domainlist = domain.split(".") + domainlist[-1] = domainlist[-2] + domainlist.pop() + return list(reversed(domainlist)) + + domain_list_cache = sorted(result, key=cmp_domain) + domain_list_cache_timestamp = time.time() + + if exclude_subdomains: + return [ + domain for domain in domain_list_cache if not _get_parent_domain_of(domain) + ] + + return domain_list_cache + + +def _get_domain_portal_dict() -> dict[str, str]: + domains = _get_domains() + out: OrderedDict[str, str] = 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) + + +DomainDict = OrderedDict[str, "DomainDict"] + + +class DomainList(TypedDict): + domains: list[str] | DomainDict + main: str + + +def domain_list( + exclude_subdomains: bool = False, tree: bool = False, features: list[str] = [] +) -> DomainList: + """ + List domains + + Keyword argument: + exclude_subdomains -- Filter out domains that are subdomains of other declared domains + tree -- Display domains as a hierarchy tree + + """ + + domains = _get_domains(exclude_subdomains) + main = _get_maindomain() + + if features: + domains_filtered = [] + for domain in domains: + config = domain_config_get(domain, key="feature", export=True) + if any(config.get(feature) == 1 for feature in features): + domains_filtered.append(domain) + domains = domains_filtered + + if not tree: + return {"domains": domains, "main": main} + + if tree and exclude_subdomains: + return { + "domains": OrderedDict({domain: {} for domain in domains}), # type: ignore[arg-type,misc] + "main": main, + } + + def get_parent_dict(tree: DomainDict, child: str) -> DomainDict: + # If parent exists it should be the last added (see `_get_domains` ordering) + possible_parent = next(reversed(tree)) if tree else None + if possible_parent and child.endswith(f".{possible_parent}"): + return get_parent_dict(tree[possible_parent], child) + return tree + + result: DomainDict = OrderedDict() + for domain in domains: + parent = get_parent_dict(result, domain) + parent[domain] = OrderedDict() + + return {"domains": result, "main": main} + + +class DomainInfo(TypedDict): + certificate: dict[str, Any] + registrar: str + apps: list[dict[str, str]] + main: bool + topest_parent: str | None + + +def domain_info(domain: str) -> DomainInfo: + """ + Print aggregate data for a specific domain + + Keyword argument: + domain -- Domain to be checked + """ + + from .certificate import certificate_status + from .dns import _get_registar_settings + from .utils.app_utils import _get_app_label, _get_app_settings, _installed_apps + + _assert_domain_exists(domain) + + registrar, _ = _get_registar_settings(domain) + certificate = certificate_status([domain], full=True)["certificates"][domain] + + apps = [] + for app in _installed_apps(): + settings = _get_app_settings(app) + if settings.get("domain") == domain: + apps.append( + { + "id": app, + "name": _get_app_label(app), + "path": settings.get("path", ""), + } + ) + + return { + "certificate": certificate, + "registrar": registrar, + "apps": apps, + "main": _get_maindomain() == domain, + "topest_parent": _get_parent_domain_of(domain, topest=True), + # TODO : add parent / child domains ? + } + + +def _assert_domain_exists(domain: str) -> None: + if domain not in _get_domains(): + raise YunohostValidationError("domain_unknown", domain=domain) + + +def _list_subdomains_of(parent_domain: str) -> list[str]: + _assert_domain_exists(parent_domain) + return [domain for domain in _get_domains() if domain.endswith(f".{parent_domain}")] + + +def _get_parent_domain_of( + domain: str, return_self: bool = False, topest: bool = False +) -> str | None: + domains = _get_domains(exclude_subdomains=topest) + + domain_ = domain + while "." in domain_: + domain_ = domain_.split(".", 1)[1] + if domain_ in domains: + return domain_ + + return domain if return_self else None + + +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_add( + operation_logger: "OperationLogger", + domain: str, + dyndns_recovery_password=None, + ignore_dyndns=False, + install_letsencrypt_cert=False, + skip_tos=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 + install_letsencrypt_cert -- If adding a subdomain of an already added domain, try to install a Let's Encrypt certificate + """ + from .app import app_ssowatconf + from .certificate import ( + _certificate_install_letsencrypt, + _certificate_install_selfsigned, + certificate_status, + ) + from .hook import hook_callback + from .utils.dns import is_yunohost_dyndns_domain + from .utils.ldap import _get_ldap_interface + from .utils.password import assert_password_is_strong_enough + + if dyndns_recovery_password: + operation_logger.data_to_redact.append(dyndns_recovery_password) + + ldap = _get_ldap_interface() + + try: + ldap.validate_uniqueness({"virtualdomain": domain}) + except MoulinetteError: + raise YunohostValidationError("domain_exists") + + # Lower domain to avoid some edge cases issues + # See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765 + domain = domain.lower() + + # Non-latin characters (e.g. café.com => xn--caf-dma.com) + domain = domain.encode("idna").decode("utf-8") + + # 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 .dyndns import is_subscribing_allowed + from .utils.app_utils import _ask_confirmation + + # Do not allow to subscribe to multiple dyndns domains... + if not is_subscribing_allowed(): + raise YunohostValidationError("domain_dyndns_already_subscribed") + + if not skip_tos and Moulinette.interface.type == "cli" and os.isatty(1): + Moulinette.display(m18n.n("tos_dyndns_acknowledgement"), style="warning") + # i18n: confirm_tos_acknowledgement + _ask_confirmation("confirm_tos_acknowledgement", kind="soft") + + if dyndns_recovery_password: + assert_password_is_strong_enough("admin", dyndns_recovery_password) + + operation_logger.start() + + if dyndns: + domain_dyndns_subscribe( + domain=domain, recovery_password=dyndns_recovery_password + ) + + _certificate_install_selfsigned([domain], force=True) + + try: + attr_dict: Mapping[str, str | list[str]] = { + "objectClass": ["mailDomain", "top"], + "virtualdomain": domain, + } + + try: + ldap.add(f"virtualdomain={domain},ou=domains", attr_dict) + except Exception as e: + raise YunohostError("domain_creation_failed", domain=domain, error=e) + finally: + global domain_list_cache + domain_list_cache = [] + + # Don't regen these conf if we're still in postinstall + if os.path.exists("/etc/yunohost/installed"): + # Sometime we have weird issues with the regenconf where some files + # appears as manually modified even though they weren't touched ... + # There are a few ideas why this happens (like backup/restore nginx + # conf ... which we shouldnt do ...). This in turns creates funky + # situation where the regenconf may refuse to re-create the conf + # (when re-creating a domain..) + # So here we force-clear the has out of the regenconf if it exists. + # This is a pretty ad hoc solution and only applied to nginx + # because it's one of the major service, but in the long term we + # should identify the root of this bug... + _force_clear_hashes([f"/etc/nginx/conf.d/{domain}.conf"]) + regen_conf( + names=[ + "nginx", + "dnsmasq", + "postfix", + "mdns", + "dovecot", + "opendkim", + ] + ) + app_ssowatconf() + + except Exception as e: + # Force domain removal silently + try: + domain_remove(domain, force=True) + except Exception: + pass + raise e + + failed_letsencrypt_cert_install = False + if install_letsencrypt_cert: + parent_domain = _get_parent_domain_of(domain) + can_install_letsencrypt = ( + parent_domain + and certificate_status([parent_domain], full=True)["certificates"][ + parent_domain + ]["has_wildcards"] + ) + + if can_install_letsencrypt: + try: + _certificate_install_letsencrypt([domain], force=True, no_checks=True) + except Exception: + failed_letsencrypt_cert_install = True + else: + logger.warning( + "Skipping Let's Encrypt certificate attempt because there's no wildcard configured on the parent domain's DNS records." + ) + failed_letsencrypt_cert_install = True + + hook_callback("post_domain_add", args=[domain]) + + logger.success(m18n.n("domain_created")) + + if failed_letsencrypt_cert_install: + logger.warning(m18n.n("certmanager_cert_install_failed", domains=domain)) + + +@is_unit_operation(exclude=["dyndns_recovery_password"]) +def domain_remove( + operation_logger: "OperationLogger", + domain: str, + remove_apps: bool = False, + force: bool = False, + dyndns_recovery_password: str | None = None, + ignore_dyndns: bool = False, +) -> None: + """ + Delete domains + + Keyword argument: + domain -- Domain to delete + remove_apps -- Remove applications installed on the domain + force -- Force the domain removal and don't not ask confirmation to + remove apps if remove_apps is specified + 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 .app import app_remove, app_ssowatconf + from .hook import hook_callback + from .utils.app_utils import _get_app_label, _get_app_settings, _installed_apps + from .utils.dns import is_yunohost_dyndns_domain + from .utils.ldap import _get_ldap_interface + + 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 + # failed + if not force: + _assert_domain_exists(domain) + + # Check domain is not the main domain + if domain == _get_maindomain(): + other_domains = _get_domains() + other_domains.remove(domain) + + if other_domains: + raise YunohostValidationError( + "domain_cannot_remove_main", + domain=domain, + other_domains="\n * " + ("\n * ".join(other_domains)), + ) + else: + raise YunohostValidationError( + "domain_cannot_remove_main_add_new_one", domain=domain + ) + + # Check if apps are installed on the domain + apps_on_that_domain = [] + + for app in _installed_apps(): + settings = _get_app_settings(app) + label = _get_app_label(app) + if settings.get("domain") == domain: + apps_on_that_domain.append( + ( + app, + ( + f' - {app} "{label}" on https://{domain}{settings["path"]}' + if "path" in settings + else app + ), + ) + ) + + if apps_on_that_domain: + if remove_apps: + if Moulinette.interface.type == "cli" and not force: + answer = Moulinette.prompt( + m18n.n( + "domain_remove_confirm_apps_removal", + apps="\n".join([x[1] for x in apps_on_that_domain]), + answers="y/N", + ), + color="yellow", + ) + if answer.upper() != "Y": + raise YunohostError("aborting") + + for app, _ in apps_on_that_domain: + app_remove(app) + else: + raise YunohostValidationError( + "domain_uninstall_app_first", + apps="\n".join([x[1] for x in apps_on_that_domain]), + ) + + # 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() + try: + ldap.remove("virtualdomain=" + domain + ",ou=domains") + except Exception as e: + raise YunohostError("domain_deletion_failed", domain=domain, error=e) + finally: + global domain_list_cache + domain_list_cache = [] + + # 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)) + + 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 ... + # There are a few ideas why this happens (like backup/restore nginx + # conf ... which we shouldnt do ...). This in turns creates funky + # situation where the regenconf may refuse to re-create the conf + # (when re-creating a domain..) + # + # So here we force-clear the has out of the regenconf if it exists. + # This is a pretty ad hoc solution and only applied to nginx + # because it's one of the major service, but in the long term we + # should identify the root of this bug... + _force_clear_hashes([f"/etc/nginx/conf.d/{domain}.conf"]) + # And in addition we even force-delete the file Otherwise, if the file was + # manually modified, it may not get removed by the regenconf which leads to + # catastrophic consequences of nginx breaking because it can't load the + # cert file which disappeared etc.. + if os.path.exists(f"/etc/nginx/conf.d/{domain}.conf"): + _process_regen_conf( + f"/etc/nginx/conf.d/{domain}.conf", new_conf=None, save=True + ) + + regen_conf(names=["nginx", "dnsmasq", "postfix", "mdns", "opendkim"]) + app_ssowatconf() + + hook_callback("post_domain_remove", args=[domain]) + + logger.success(m18n.n("domain_deleted")) + + +def domain_dyndns_subscribe(*args: Any, **kwargs: Any) -> None: + """ + Subscribe to a DynDNS domain + """ + from .dyndns import dyndns_subscribe + + dyndns_subscribe(*args, **kwargs) + + +def domain_dyndns_unsubscribe(*args: Any, **kwargs: Any) -> None: + """ + Unsubscribe from a DynDNS domain + """ + from .dyndns import dyndns_unsubscribe + + dyndns_unsubscribe(*args, **kwargs) + + +def domain_dyndns_list() -> dict[str, list[str]]: + """ + Returns all currently subscribed DynDNS domains + """ + from .dyndns import dyndns_list + + return dyndns_list() + + +def domain_dyndns_update(*args: Any, **kwargs: Any) -> None: + """ + Update a DynDNS domain + """ + from .dyndns import dyndns_update + + dyndns_update(*args, **kwargs) + + +def domain_dyndns_set_recovery_password(*args: Any, **kwargs: Any) -> None: + """ + Set a recovery password for an already registered dyndns domain + """ + from .dyndns import dyndns_set_recovery_password + + dyndns_set_recovery_password(*args, **kwargs) + + +@is_unit_operation() +def domain_main_domain( + operation_logger: "OperationLogger", new_main_domain: str | None = None +) -> dict[str, str] | None: + """ + Check the current main domain, or change it + + Keyword argument: + new_main_domain -- The new domain to be set as the main domain + + """ + from .tools import _set_hostname + + # If no new domain specified, we return the current main domain + if not new_main_domain: + return {"current_main_domain": _get_maindomain()} + + old_main_domain = _get_maindomain() + + # Check domain exists + _assert_domain_exists(new_main_domain) + + operation_logger.related_to.append(("domain", new_main_domain)) + operation_logger.start() + + # Apply changes to ssl certs + try: + write_to_file("/etc/yunohost/current_host", new_main_domain) + global main_domain_cache + main_domain_cache = new_main_domain + _set_hostname(new_main_domain) + except Exception as e: + logger.warning(str(e), exc_info=1) # type: ignore + raise YunohostError("main_domain_change_failed") + + # Regen configurations + if os.path.exists("/etc/yunohost/installed"): + regen_conf() + + from .user import _update_admins_group_aliases + + _update_admins_group_aliases( + old_main_domain=old_main_domain, new_main_domain=new_main_domain + ) + + logger.success(m18n.n("main_domain_changed")) + return None + + +def domain_url_available(domain: str, path: str) -> bool: + """ + Check availability of a web path + + Keyword argument: + domain -- The domain for the web path (e.g. your.domain.tld) + path -- The path to check (e.g. /coffee) + """ + + from .utils.app_utils import _get_conflicting_apps + + return len(_get_conflicting_apps(domain, path)) == 0 + + +def _get_raw_domain_settings(domain: str) -> dict: + """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) # type: ignore[return-value] + + return {} + + +def domain_config_get( + domain: str, key: str = "", full: bool = False, export: bool = False +) -> Any: + """ + Display a domain configuration + """ + + if full and export: + raise YunohostValidationError( + "You can't use --full and --export together.", raw_msg=True + ) + + mode: Literal["full", "export", "classic"] + if full: + mode = "full" + elif export: + mode = "export" + else: + mode = "classic" + + DomainConfigPanel = _get_DomainConfigPanel() + config = DomainConfigPanel(domain) + return config.get(key, mode) + + +@is_unit_operation() +def domain_config_set( + operation_logger: "OperationLogger", + domain: str, + key: str | None = None, + value: Any | None = None, + args: str | None = None, + args_file: str | None = None, +) -> None: + """ + Apply a new domain configuration + """ + from .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) + + +def _get_DomainConfigPanel() -> type["ConfigPanel"]: + from .dns import _set_managed_dns_records_hashes + from .utils.configpanel import ConfigPanel + + class DomainConfigPanel(ConfigPanel): + entity_type = "domain" + save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml" + save_mode = "diff" + + # i18n: domain_config_cert_renew_help + # i18n: domain_config_default_app_help + + 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() + + panel_id, section_id, option_id = self.filter_key + + # 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 + if panel_id in ["dns", None]: + from .dns import _get_registrar_config_section + + raw_config["dns"]["registrar"] = _get_registrar_config_section( + self.entity + ) + + # Cert stuff + if panel_id in ["cert", None]: + from .certificate import certificate_status + + status = certificate_status([self.entity], full=True)["certificates"][ + self.entity + ] + + raw_config["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 + raw_config["cert"]["cert_"]["cert_summary"]["ask"] = m18n.n( + f"domain_config_cert_summary_{status['summary']}" + ) + + 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 _get_raw_settings(self) -> "RawSettings": + raw_settings = super()._get_raw_settings() + + custom_css = Path( + f"/usr/share/yunohost/portal/customassets/{self.entity}.custom.css" + ) + if custom_css.exists(): + raw_settings["custom_css"] = read_file(str(custom_css)) + + return raw_settings + + def _apply( + self, + form: "FormModel", + config: "ConfigPanelModel", + 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 .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"] + ) + + # NB: this is subtlely different from just checking `next_settings.get("use_auto_dns") since we want to find the exact situation where the admin *disables* the autodns` + remove_auto_dns_feature = ( + "use_auto_dns" in next_settings and not next_settings["use_auto_dns"] + ) + if remove_auto_dns_feature: + # disable auto dns by reseting every registrar form values + registrar_section = config.get_section("registrar") + assert registrar_section is not None + options = [ + option + for option in registrar_section.options + if not option.readonly + and option.id != "use_auto_dns" + and hasattr(form, option.id) + ] + for option in options: + setattr(form, option.id, option.default) + + if "custom_css" in next_settings: + write_to_file( + f"/usr/share/yunohost/portal/customassets/{self.entity}.custom.css", + next_settings.pop("custom_css", "").strip(), + ) + # Make sure the value doesnt get written in the yml + if hasattr(form, "custom_css"): + form.custom_css = "" + + portal_options = [ + "enable_public_apps_page", + "show_other_domains_apps", + "portal_title", + "portal_logo", + "portal_theme", + "portal_tile_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 .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))) # type: ignore[arg-type] + + # 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, # type: ignore[arg-type] + sort_keys=True, + indent=4, + ) + + super()._apply( + form, config, previous_settings, exclude={"recovery_password"} + ) + + # Also remove `managed_dns_records_hashes` in settings which are not handled by the config panel + if remove_auto_dns_feature: + _set_managed_dns_records_hashes(self.entity, []) + + # Reload ssowat if default app changed + if ( + "default_app" in next_settings + or "enable_public_apps_page" in next_settings + ): + from .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", "opendkim", "dnsmasq"} + ) + + if stuff_to_regen_conf: + regen_conf(names=list(stuff_to_regen_conf)) + + return DomainConfigPanel + + +def domain_action_run(domain: str, action: str, args=None) -> None: + import urllib.parse + + action_func: Callable + if action == "cert.cert_.cert_install": + from .certificate import certificate_install as action_func + elif action == "cert.cert_.cert_renew": + from .certificate import certificate_renew as action_func + + args = dict(urllib.parse.parse_qsl(args or "", keep_blank_values=True)) + no_checks = args["cert_no_checks"] in ("y", "yes", "on", "1") + + action_func([domain], force=True, no_checks=no_checks) + + +def _get_domain_settings(domain: str) -> dict: + _assert_domain_exists(domain) + + if os.path.exists(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml"): + return read_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml") or {} # type: ignore[return-value] + else: + return {} + + +def _set_domain_settings(domain: str, settings: dict) -> None: + _assert_domain_exists(domain) + + write_to_yaml(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", settings) + + +# +# +# Stuff managed in other files +# +# + + +def domain_cert_status( + domain_list: list[str], full: bool = False +) -> dict[str, dict[str, Any]]: + from .certificate import certificate_status + + return certificate_status(domain_list, full) + + +def domain_cert_install( + domain_list: list[str], + force: bool = False, + no_checks: bool = False, + self_signed: bool = False, +) -> None: + from .certificate import certificate_install + + return certificate_install(domain_list, force, no_checks, self_signed) + + +def domain_cert_renew( + domain_list: list[str], + force: bool = False, + no_checks: bool = False, + email: bool = False, +) -> None: + from .certificate import certificate_renew + + return certificate_renew(domain_list, force, no_checks, email) + + +def domain_dns_suggest(domain: str) -> str: + from .dns import domain_dns_suggest + + return domain_dns_suggest(domain) + + +def domain_dns_push( + domain: str, dry_run: bool, force: bool, purge: bool +) -> ( + dict[ + Literal["delete", "create", "update", "unchanged"], + list["DNSRecord"] | list[str], + ] + | dict[Literal["warnings", "errors"], list[str]] +): + from .dns import domain_dns_push + + return domain_dns_push(domain, dry_run, force, purge) diff --git a/src/dyndns.py b/src/dyndns.py new file mode 100644 index 0000000..f4207a8 --- /dev/null +++ b/src/dyndns.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import base64 +import glob +import hashlib +import json +import os +import subprocess +from logging import getLogger + +from moulinette import Moulinette, m18n +from moulinette.core import MoulinetteError + +from .domain import _get_maindomain +from .log import is_unit_operation +from .regenconf import regen_conf +from .utils.dns import dig, is_yunohost_dyndns_domain +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import chmod, chown, rm, write_to_file +from .utils.network import get_public_ip + +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: str) -> bool: + """ + Checks if a domain is available on dyndns.yunohost.org + + Keyword arguments: + domain -- The full domain that you'd like.. e.g. "foo.nohost.me" + + 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 = 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 + ) + + 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(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 + recovery_password -- Password that will be used to delete the 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): + # 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 .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() + + # '165' is the convention identifier for hmac-sha512 algorithm + # '1234' is idk? doesnt matter, but the old format contained a number here... + key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key" + + if not os.path.exists("/etc/yunohost/dyndns"): + os.makedirs("/etc/yunohost/dyndns") + + # 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") + + # Idk why but the secret is split in two parts, with the first one + # being 57-long char ... probably some DNS format + secret = f"{secret[:56]} {secret[56:]}" + + key_content = f"{domain}. IN KEY 0 3 165 {secret}" + write_to_file(key_file, key_content) + + chmod("/etc/yunohost/dyndns", 0o600, recursive=True) + chown("/etc/yunohost/dyndns", "root", recursive=True) + + import requests # lazy loading this module for performance reasons + + # Send subscription + try: + # Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever... + b64encoded_key = base64.b64encode(secret.encode()).decode() + 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=data, + timeout=30, + ) + except Exception as e: + rm(key_file, force=True) + 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_subscribe_failed", error=error) + + # Yunohost regen conf will add the dyndns cron job if a key exists + # in /etc/yunohost/dyndns + regen_conf(["yunohost"]) + + # Add some dyndns update in 2 and 4 minutes from now such that user should + # not have to wait 10ish minutes for the conf to propagate + cmd = ( + "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + ) + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... + 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_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")) + + +@is_unit_operation(flash=True) +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 .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() -> dict[str, list[str]]: + """ + Returns all currently subscribed DynDNS domains ( deduced from the key files ) + """ + + from .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() +def dyndns_update( + operation_logger, + domain=None, + force=False, + dry_run=False, +): + """ + Update IP on DynDNS platform + + Keyword argument: + domain -- Full domain to update + """ + + import dns.query + import dns.tsig + import dns.tsigkeyring + import dns.update + + from .dns import _build_dns_conf + + # If domain is not given, update all DynDNS domains + if domain is None: + dyndns_domains = dyndns_list()["domains"] + + 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 + keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key") + + if not keys: + raise YunohostValidationError("dyndns_key_not_found") + + key = keys[0] + + # Get current IPv4 and IPv6 + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) + + if ipv4 is None and ipv6 is None: + logger.debug( + "No ipv4 nor ipv6 ?! Sounds like the server is not connected to the internet, or the ipv4/6.yunohost.org infrastructure is down somehow" + ) + return + + # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' + zone = domain.split(".")[1:] + zone = ".".join(zone) + + logger.debug("Building zone update ...") + + with open(key) as f: + key = f.readline().strip().split(" ", 6)[-1] + + keyring = dns.tsigkeyring.from_text({f"{domain}.": key}) + # Python's dns.update is similar to the old nsupdate cli tool + update = dns.update.Update(zone, keyring=keyring, keyalgorithm=dns.tsig.HMAC_SHA512) + + auth_resolvers = [] + + for dns_auth in DYNDNS_DNS_AUTH: + for type_ in ["A", "AAAA"]: + ok, result = dig(dns_auth, type_) + if ok == "ok" and len(result) and result[0]: + auth_resolvers.append(result[0]) + + if not auth_resolvers: + raise YunohostError( + f"Failed to resolve IPv4/IPv6 for {DYNDNS_DNS_AUTH} ?", raw_msg=True + ) + + def resolve_domain(domain, rdtype): + ok, result = dig(domain, rdtype, resolvers=auth_resolvers) + if ok == "ok": + return result[0] if len(result) else None + elif result[0] == "Timeout": + logger.debug( + f"Timed-out while trying to resolve {rdtype} record for {domain}" + ) + else: + return None + + logger.debug("Falling back to external resolvers") + ok, result = dig(domain, rdtype, resolvers="force_external") + if ok == "ok": + return result[0] if len(result) else None + elif result[0] == "Timeout": + logger.debug( + "Timed-out while trying to resolve %s record for %s using external resolvers : %s" + % (rdtype, domain, result) + ) + else: + return None + + raise YunohostError(f"Failed to resolve {rdtype} for {domain}", raw_msg=True) + + old_ipv4 = resolve_domain(domain, "A") + old_ipv6 = resolve_domain(domain, "AAAA") + + logger.debug(f"Old IPv4/v6 are ({old_ipv4}, {old_ipv6})") + logger.debug(f"Requested IPv4/v6 are ({ipv4}, {ipv6})") + + # no need to update + if (not force and not dry_run) and (old_ipv4 == ipv4 and old_ipv6 == ipv6): + logger.info("No update needed.") + return + else: + operation_logger.related_to.append(("domain", domain)) + operation_logger.start() + logger.info("Update needed, going on...") + + dns_conf = _build_dns_conf(domain, dkim_split=True) + + # Delete custom DNS records, we don't support them (have to explicitly + # authorize them on dynette) + dns_conf = { + cat: v for cat, v in dns_conf.items() if cat in ["basic", "mail", "extra"] + } + + # Delete the old records for all domain/subdomains + + # every dns_conf.values() is a list of : + # [{"name": "...", "ttl": "...", "type": "...", "content": "..."}] + for records in dns_conf.values(): + for record in records: + name = ( + f"{record['name']}.{domain}." if record["name"] != "@" else f"{domain}." + ) + update.delete(name) + + # Add the new records for all domain/subdomains + + for records in dns_conf.values(): + for record in records: + # (For some reason) here we want the format with everytime the + # entire, full domain shown explicitly, not just "muc" or "@", it + # should be muc.the.domain.tld. or the.domain.tld + if record["content"] == "@": + record["content"] = domain + record["content"] = record["content"].replace(";", r"\;") + name = ( + f"{record['name']}.{domain}." if record["name"] != "@" else f"{domain}." + ) + + update.add(name, record["ttl"], record["type"], record["content"]) + + logger.debug("Now pushing new conf to DynDNS host...") + logger.debug(update) + + if not dry_run: + try: + r = dns.query.tcp(update, auth_resolvers[0]) + except Exception as e: + logger.error(e) + raise YunohostError("dyndns_ip_update_failed") + + if "rcode NOERROR" not in str(r): + logger.error(str(r)) + raise YunohostError("dyndns_ip_update_failed") + + logger.success(m18n.n("dyndns_ip_updated")) + else: + print( + "Warning: dry run, this is only the generated config, it won't be applied" + ) diff --git a/src/firewall.py b/src/firewall.py new file mode 100644 index 0000000..06a5132 --- /dev/null +++ b/src/firewall.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import re +import shutil +from logging import getLogger +from pathlib import Path +from typing import Any, Literal, TypedDict + +import miniupnpc +import yaml +from moulinette import m18n + +from .regenconf import regen_conf +from .utils.error import YunohostError, YunohostValidationError + +logger: Any = getLogger("yunohost.firewall") + + +class _YunoFirewallPortSettings(TypedDict): + comment: str + open: bool + upnp: bool + + +class YunoFirewallSettings(TypedDict): + """This is the description of the content of /etc/yunohost/firewall.yml""" + + tcp: dict[int | str, _YunoFirewallPortSettings] # TCP firewall settings + udp: dict[int | str, _YunoFirewallPortSettings] # UDP firewall settings + router_forwarding_upnp: bool # Whether to enable uPNP port forwarding configuration + + +IPProto = Literal["tcp"] | Literal["udp"] + + +class YunoFirewall: + FIREWALL_FILE = Path("/etc/yunohost/firewall.yml") + + def __init__(self) -> None: + self.need_reload = False + self.config: YunoFirewallSettings + + # This is a workaround for when we need to actively close UPnP ports + self.upnp_to_close: list[tuple[str, int | str]] = [] + + self.read() + + def read(self) -> None: + """ + The config is expected to have a structure as below + See also conf/yunohost/firewall.yml + + tcp: + 123: + open: true + upnp: false + comment: "some comment" + 456: + open: true + upnp: true + comment: "some other comment" + udp: + 123: + open: true + upnp: false + comment: "some other other comment" + + router_forwarding_upnp: false + """ + + self.config = yaml.safe_load(self.FIREWALL_FILE.read_text()) or {} # type: ignore + + if "tcp" not in self.config or "udp" not in self.config: + raise Exception( + f"Uhoh, no 'tcp' or 'udp' key found in {self.FIREWALL_FILE} ?!" + ) + + def write(self) -> None: + old_file = self.FIREWALL_FILE.parent / (self.FIREWALL_FILE.name + ".old") + shutil.copyfile(self.FIREWALL_FILE, old_file) + self.FIREWALL_FILE.write_text(yaml.dump(self.config)) + + def list(self, protocol: str, forwarded: bool = False) -> list[int | str]: + protocol, _ = self._validate_port(protocol, 0) + return [ + port + for port, status in self.config[protocol].items() + if (status["upnp"] if forwarded else status["open"]) + ] + + @staticmethod + def _validate_port(protocol: str, port: int | str) -> tuple[IPProto, int | str]: + if isinstance(port, str): + # iptables used ":" and app packages might still do + port = port.replace(":", "-") + # Convert to int if it's not a range + if "-" not in port: + port = int(port) + if protocol not in ["tcp", "udp"]: + raise ValueError(f"protocol should be tcp or udp, not {protocol}") + return protocol, port # type: ignore + + def open_port( + self, protocol: str, port: int | str, comment: str, upnp: bool = False + ) -> None: + protocol, port = self._validate_port(protocol, port) + + if port not in self.config[protocol]: + self.config[protocol][port] = { + "open": False, + "upnp": False, + "comment": comment, + } + + # Keep existing comment if the one passed is empty + if comment: + self.config[protocol][port]["comment"] = comment + + if not self.config[protocol][port]["open"]: + self.config[protocol][port]["open"] = True + self.need_reload = True + + if self.config[protocol][port]["upnp"] != upnp: + self.config[protocol][port]["upnp"] = upnp + self.need_reload = True + self.write() + + def close_port( + self, protocol: str, port: int | str, upnp_only: bool = False + ) -> None: + protocol, port = self._validate_port(protocol, port) + + if port not in self.config[protocol]: + return + + if self.config[protocol][port]["upnp"]: + self.config[protocol][port]["upnp"] = False + # not need_reload, it's only upnp + self.upnp_to_close.append((protocol, port)) + + if upnp_only: + self.write() + return + + if self.config[protocol][port]["open"]: + self.config[protocol][port]["open"] = False + self.need_reload = True + self.write() + + def delete_port(self, protocol: str, port: int | str) -> None: + protocol, port = self._validate_port(protocol, port) + + if port not in self.config[protocol]: + return + + self.close_port(protocol, port, False) + + del self.config[protocol][port] + self.need_reload = True + self.write() + + def apply(self, upnp: bool = True) -> bool: + # FIXME: Ensure SSH is allowed + ssh_port = _get_ssh_port() + if not (conf := self.config["tcp"].get(ssh_port)) or not conf["open"]: + self.open_port("tcp", ssh_port, "SSH port") + + # Just leverage regen_conf that will regen the nftables files, reload nftables + try: + regen_conf(["nftables"], force=True) + except YunohostError: + return False + + self.need_reload = False + + # Refresh port forwarding with UPnP + if self.config.get("router_forwarding_upnp") and upnp: + YunoUPnP(self).refresh(self) + return True + + def clear(self) -> None: + os.system("systemctl stop nftables") + + +class YunoUPnP: + UPNP_CRON_JOB = Path("/etc/cron.d/yunohost-firewall-upnp") + + def __init__(self, firewall: "YunoFirewall") -> None: + self.firewall = firewall + self.description = "Yunohost firewall" + self.upnpc = miniupnpc.UPnP() + self.upnpc.discoverdelay = 3000 + self.device_found = 0 + + def enabled(self, new_status: bool | None = None) -> bool: + if new_status is not None: + self.firewall.config["router_forwarding_upnp"] = new_status + self.firewall.write() + return self.firewall.config.get("router_forwarding_upnp", False) + + def check_status(self) -> bool: + if self.device_found < 1: + return False + + try: + # Connection status, uptime and last connection error + connection_status, _, last_connection_error = self.upnpc.statusinfo() + except Exception: + logger.debug("Unable to check status of UPnP device", exc_info=1) + return False + + return ( + connection_status == "Connected" and last_connection_error == "ERROR_NONE" + ) + + def select_device(self) -> bool: + if self.device_found < 1: + return False + + logger.debug("Select UPnP device...") + try: + # Select UPnP device + self.upnpc.selectigd() + except Exception: + logger.debug("Unable to select UPnP device", exc_info=1) + return False + + return self.check_status() + + def find_gid(self) -> bool: + # Discover UPnP device(s) + logger.debug("Discovering UPnP devices...") + try: + self.device_found = self.upnpc.discover() + except Exception: + logger.warning("Failed to find any UPnP device on the network") + self.device_found = -1 + if self.device_found < 1: + logger.error(m18n.n("upnp_dev_not_found")) + return False + logger.debug("Found %d UPnP device(s)", int(self.device_found)) + return self.select_device() + + def open_port(self, protocol: str, port: int | str, comment: str) -> bool: + if not self.check_status() and not self.find_gid(): + return False + + # FIXME: how should we handle port ranges ? + if not isinstance(port, int): + logger.warning("Can't use UPnP to open '%s'" % port) + return False + + protocol = protocol.upper() + + # Clean the mapping of this port + if self.upnpc.getspecificportmapping(port, protocol): + try: + self.upnpc.deleteportmapping(port, protocol) + except Exception: + return False + + # Add new port mapping + desc = f"{self.description}: port {port} {comment}" + try: + self.upnpc.addportmapping( + port, protocol, self.upnpc.lanaddr, port, desc, "" + ) + except Exception: + logger.debug("Unable to add port %d using UPnP", port, exc_info=1) + return False + return True + + def close_port(self, protocol: str, port: int | str) -> bool: + if not self.check_status() and not self.find_gid(): + return False + + # FIXME: how should we handle port ranges ? + if not isinstance(port, int): + logger.warning("Can't use UPnP to close '%s'" % port) + return False + + protocol = protocol.upper() + + if self.upnpc.getspecificportmapping(port, protocol): + try: + self.upnpc.deleteportmapping(port, protocol) + except Exception: + return False + return True + + def refresh(self, firewall: "YunoFirewall") -> bool: + if not self.check_status() and not self.find_gid(): + return False + + status = True + for protocol, port in firewall.upnp_to_close: + status = status and self.close_port(protocol, port) + + protos: list[IPProto] = ["tcp", "udp"] + for proto in protos: + for port, info in firewall.config[proto].items(): + if self.enabled() and info["open"] and info["upnp"]: + status = status and self.open_port(proto, port, info["comment"]) + else: + status = status and self.close_port(proto, port) + + return status + + def enable(self) -> None: + if not self.check_status() and not self.find_gid(): + logger.error("Not enabling UPnP because no UPnP device was found") + return + if not self.enabled(): + # Add cron job + self.UPNP_CRON_JOB.write_text( + "*/50 * * * * root /usr/bin/yunohost firewall upnp status >>/dev/null\n" + ) + self.enabled(True) + + def close_ports(self) -> None: + if not self.check_status() and not self.find_gid(): + return + + i = 0 + to_remove = [] + # Get all ports from UPNP + while True: + port_mapping = self.upnpc.getgenericportmapping(i) + if port_mapping is None: + break + (port, protocol, (ihost, iport), description, c, d, e) = port_mapping + + # Remove it if IP and description match + if ihost == self.upnpc.lanaddr and description.startswith(self.description): + to_remove.append((port, protocol)) + i = i + 1 + + for port, protocol in to_remove: + self.close_port(protocol, port) + + def disable(self) -> None: + if self.enabled(): + # Remove cron job + self.UPNP_CRON_JOB.unlink(missing_ok=True) + self.close_ports() + self.enabled(False) + + +def firewall_is_open( + port: int | str, + protocol: str, +) -> bool: + """ + Returns whether the specified port is open. + + Keyword arguments: + port -- Port or dash-separated range of ports to open + protocol -- Protocol type to allow (tcp/udp) + + """ + return port in firewall_list(raw=False, protocol=protocol, forwarded=False) + + +def firewall_open( + port: int | str, + protocol: str, + comment: str, + upnp: bool = False, + no_reload: bool = False, + reload_if_changed: bool = False, +) -> None: + """ + Allow connections on a port + + Keyword arguments: + port -- Port or dash-separated range of ports to open + protocol -- Protocol type to allow (tcp/udp) + comment -- A reason for the port to be open + no_upnp -- Do not add forwarding of this port with UPnP + no_reload -- Do not reload firewall rules + """ + firewall = YunoFirewall() + + # Add a readable comment if none was passed but we're handling an app + app_id = os.environ.get("YNH_APP_ID", "") + if not comment: + if app_id: + if port == 53: + comment = f"DNS for {app_id}" + elif port == 67: + comment = f"DHCP for {app_id}" + elif port == 445: + comment = f"SMB for {app_id}" + elif port == 1900: + comment = f"UPnP for {app_id}" + else: + comment = f"For {app_id}" + else: + comment = "Manually set without comment" + + firewall.open_port(protocol, port, comment, upnp) + if not reload_if_changed and not firewall.need_reload: + logger.warning(m18n.n("port_already_opened", port=port)) + + will_reload = (firewall.need_reload and reload_if_changed) or ( + not no_reload and not reload_if_changed + ) + if will_reload: + if firewall.apply(): + logger.success(m18n.n("firewall_reloaded")) + else: + logger.error(m18n.n("firewall_reload_failed")) + + +def firewall_close( + port: int | str, + protocol: str, + upnp_only: bool = False, + no_reload: bool = False, + reload_if_changed: bool = False, +) -> None: + """ + Disallow connections on a port + + Keyword arguments: + port -- Port or dash-separated range of ports to close + protocol -- Protocol type to disallow (tcp/udp) + upnp_only -- Only remove forwarding of this port with UPnP + no_reload -- Do not reload firewall rules + """ + firewall = YunoFirewall() + + firewall.close_port(protocol, port, upnp_only=upnp_only) + if not firewall.need_reload and not reload_if_changed: + logger.warning(m18n.n("port_already_closed", port=port)) + + will_reload = (firewall.need_reload and reload_if_changed) or ( + not no_reload and not reload_if_changed + ) + if will_reload: + if firewall.apply(): + logger.success(m18n.n("firewall_reloaded")) + else: + logger.error(m18n.n("firewall_reload_failed")) + + +# Legacy APIs +def firewall_allow( + protocol: str, + port: int | str, + ipv4_only: bool = False, + ipv6_only: bool = False, + no_upnp: bool = False, + no_reload: bool = False, + reload_only_if_change: bool = False, +) -> None: + if protocol == "Both": + firewall_open(port, "tcp", "", not no_upnp, no_reload, reload_only_if_change) + firewall_open(port, "udp", "", not no_upnp, no_reload, reload_only_if_change) + else: + firewall_open( + port, protocol.lower(), "", not no_upnp, no_reload, reload_only_if_change + ) + + +def firewall_disallow( + protocol: str, + port: int | str, + ipv4_only: bool = False, + ipv6_only: bool = False, + upnp_only: bool = False, + no_reload: bool = False, + reload_only_if_change: bool = False, +) -> None: + if protocol == "Both": + firewall_close(port, "tcp", upnp_only, no_reload, reload_only_if_change) + firewall_close(port, "udp", upnp_only, no_reload, reload_only_if_change) + else: + firewall_close( + port, protocol.lower(), upnp_only, no_reload, reload_only_if_change + ) + + if os.environ.get("YNH_APP_ACTION", "") == "remove": + ports_to_keep = [53, 1900] + if port not in ports_to_keep: + firewall_delete(port, protocol.lower(), no_reload, reload_only_if_change) + + +def firewall_delete( + port: int | str, + protocol: str, + no_reload: bool = False, + reload_if_changed: bool = False, +) -> None: + """ + Delete a port from YunoHost's config + + Keyword arguments: + protocol -- Protocol type to disallow (tcp/udp) + port -- Port or dash-separated range of ports to close + no_reload -- Do not reload firewall rules + """ + firewall = YunoFirewall() + firewall.delete_port(protocol, port) + + if not firewall.need_reload and not reload_if_changed: + logger.warning(m18n.n("port_already_closed", port=port)) + + will_reload = (firewall.need_reload and reload_if_changed) or ( + not no_reload and not reload_if_changed + ) + if will_reload: + if firewall.apply(): + logger.success(m18n.n("firewall_reloaded")) + else: + logger.error(m18n.n("firewall_reload_failed")) + + +def firewall_list( + raw: bool = False, protocol: str = "tcp", forwarded: bool = False +) -> YunoFirewallSettings | dict[str, list[int | str]]: + """ + List all firewall rules + + Keyword arguments: + raw -- Return the complete YAML dict + tcp -- If not raw, list TCP ports + udp -- If not raw, list UDP ports + forwarded -- If not raw, list UPnP forwarded ports instead of open ports + """ + firewall = YunoFirewall() + if raw: + return firewall.config + else: + data = {protocol: firewall.list(protocol, forwarded)} # indirection is for mypy + return data + + +def firewall_reload(skip_upnp: bool = False) -> None: + """ + Reload all firewall rules + + Keyword arguments: + skip_upnp -- Do not refresh port forwarding using UPnP + """ + firewall = YunoFirewall() + if firewall.apply(upnp=not skip_upnp): + logger.success(m18n.n("firewall_reloaded")) + else: + logger.error(m18n.n("firewall_reload_failed")) + + +def firewall_upnp(action: str = "status", no_refresh: bool = False) -> dict[str, bool]: + """ + Manage port forwarding using UPnP + + Available actions are status, enable, disable. + All actions will refresh port forwarding unless 'no_refresh' is False. + + Keyword argument: + action -- Action to perform + no_refresh -- Do not refresh port forwarding + """ + if action not in ["status", "enable", "disable"]: + raise YunohostValidationError("action_invalid", action=action) + + firewall = YunoFirewall() + upnp = YunoUPnP(firewall) + + if action == "enable": + upnp.enable() + if action == "disable": + upnp.disable() + no_refresh = True + if no_refresh: + # Only return current state + return {"enabled": upnp.enabled()} + + if upnp.refresh(firewall): + # Display success message if needed + logger.success( + m18n.n("upnp_enabled") if upnp.enabled() else m18n.n("upnp_disabled") + ) + else: + # FIXME: Do not update the config file to let a refresh handle the failure? + raise YunohostError("upnp_port_open_failed") + + return {"enabled": upnp.enabled()} + + +def firewall_stop() -> None: + """ + Stop nftables + """ + if os.system("nft list ruleset") != 0: + raise YunohostError("nftables_unavailable") + YunoFirewall().clear() + + +def _get_ssh_port(default: int = 22) -> int: + """Return the SSH port to use + + Retrieve the SSH port from the sshd_config file or used the default + one if it's not defined. + """ + try: + with open("/etc/ssh/sshd_config") as f: + matches = re.findall(r"^Port[ \t]+([0-9]+)$", f.read(), re.MULTILINE) + if not matches: + raise Exception("No match found for the Port statement in sshd_config ?") + return int(matches[0]) + except Exception as e: + logger.debug( + f"Uhoh, failed to parse the current SSH port ? (returning {default} as default) Error: {e}" + ) + return default diff --git a/src/hook.py b/src/hook.py new file mode 100644 index 0000000..27f39fe --- /dev/null +++ b/src/hook.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import mimetypes +import os +import re +import sys +import tempfile +from glob import iglob +from importlib import import_module +from logging import getLogger + +from moulinette import Moulinette, m18n + +from .utils import jinja_filters +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import cp, read_yaml + +HOOK_FOLDER = "/usr/share/yunohost/hooks/" +CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/" + +logger = getLogger("yunohost.hook") + + +def hook_add(app, file): + """ + Store hook script to filesystem + + Keyword argument: + app -- App to link with + file -- Script to add (/path/priority-file) + + """ + path, filename = os.path.split(file) + priority, action = _extract_filename_parts(filename) + + try: + os.listdir(CUSTOM_HOOK_FOLDER + action) + except OSError: + os.makedirs(CUSTOM_HOOK_FOLDER + action) + + finalpath = CUSTOM_HOOK_FOLDER + action + "/" + priority + "-" + app + cp(file, finalpath) + + return {"hook": finalpath} + + +def hook_remove(app): + """ + Remove hooks linked to a specific app + + Keyword argument: + app -- Scripts related to app will be removed + + """ + try: + for action in os.listdir(CUSTOM_HOOK_FOLDER): + for script in os.listdir(CUSTOM_HOOK_FOLDER + action): + if script.endswith(app): + os.remove(CUSTOM_HOOK_FOLDER + action + "/" + script) + except OSError: + pass + + +def hook_info(action, name): + """ + Get information about a given hook + + Keyword argument: + action -- Action name + name -- Hook name + + """ + hooks = [] + priorities = set() + + # Search in custom folder first + for h in iglob(f"{CUSTOM_HOOK_FOLDER}{action}/*-{name}"): + priority, _ = _extract_filename_parts(os.path.basename(h)) + priorities.add(priority) + hooks.append( + { + "priority": priority, + "path": h, + } + ) + # Append non-overwritten system hooks + for h in iglob(f"{HOOK_FOLDER}{action}/*-{name}"): + priority, _ = _extract_filename_parts(os.path.basename(h)) + if priority not in priorities: + hooks.append( + { + "priority": priority, + "path": h, + } + ) + + if not hooks: + raise YunohostValidationError("hook_name_unknown", name=name) + return { + "action": action, + "name": name, + "hooks": hooks, + } + + +def hook_list(action, list_by="name", show_info=False): + """ + List available hooks for an action + + Keyword argument: + action -- Action name + list_by -- Property to list hook by + show_info -- Show hook information + + """ + result = {} + + # Process the property to list hook by + if list_by == "priority": + if show_info: + + def _append_hook(d, priority, name, path): + # Use the priority as key and a dict of hooks names + # with their info as value + value = {"path": path} + try: + d[priority][name] = value + except KeyError: + d[priority] = {name: value} + + else: + + def _append_hook(d, priority, name, path): + # Use the priority as key and the name as value + try: + d[priority].add(name) + except KeyError: + d[priority] = {name} + + elif list_by == "name" or list_by == "folder": + if show_info: + + def _append_hook(d, priority, name, path): + # Use the name as key and a list of hooks info - the + # executed ones with this name - as value + name_list = d.get(name, list()) + for h in name_list: + # Only one priority for the hook is accepted + if h["priority"] == priority: + # Custom hooks overwrite system ones and they + # are appended at the end - so overwite it + if h["path"] != path: + h["path"] = path + return + name_list.append({"priority": priority, "path": path}) + d[name] = name_list + + else: + if list_by == "name": + result = set() + + def _append_hook(d, priority, name, path): + # Add only the name + d.add(name) + + else: + raise YunohostValidationError("hook_list_by_invalid") + + def _append_folder(d, folder): + # Iterate over and add hook from a folder + for f in os.listdir(folder + action): + if ( + f[0] == "." + or f[-1] == "~" + or f.endswith(".pyc") + or (f.startswith("__") and f.endswith("__")) + ): + continue + path = f"{folder}{action}/{f}" + priority, name = _extract_filename_parts(f) + _append_hook(d, priority, name, path) + + try: + # Append system hooks first + if list_by == "folder": + result["system"] = dict() if show_info else set() + _append_folder(result["system"], HOOK_FOLDER) + else: + _append_folder(result, HOOK_FOLDER) + except OSError: + pass + + try: + # Append custom hooks + if list_by == "folder": + result["custom"] = dict() if show_info else set() + _append_folder(result["custom"], CUSTOM_HOOK_FOLDER) + else: + _append_folder(result, CUSTOM_HOOK_FOLDER) + except OSError: + pass + + return {"hooks": result} + + +def hook_callback( + action: str, + hooks: list[str] = [], + args: list[str | bool] | None = None, + chdir=None, + env=None, + pre_callback=None, + post_callback=None, +): + """ + Execute all scripts binded to an action + + Keyword argument: + action -- Action name + hooks -- List of hooks names to execute + args -- Ordered list of arguments to pass to the scripts + chdir -- The directory from where the scripts will be executed + env -- Dictionnary of environment variables to export + pre_callback -- An object to call before each script execution with + (name, priority, path, args) as arguments and which must return + the arguments to pass to the script + post_callback -- An object to call after each script execution with + (name, priority, path, succeed) as arguments + + """ + result: dict[str, dict] = {} + hooks_dict = {} + + # Retrieve hooks + if not hooks: + hooks_dict = hook_list(action, list_by="priority", show_info=True)["hooks"] + else: + hooks_names = hook_list(action, list_by="name", show_info=True)["hooks"] + + # Add similar hooks to the list + # For example: Having a 16-postfix hook in the list will execute a + # xx-postfix_dkim as well + all_hooks = [] + for n in hooks: + for key in hooks_names.keys(): + if key == n or key.startswith("%s_" % n) and key not in all_hooks: + all_hooks.append(key) + + # Iterate over given hooks names list + for n in all_hooks: + try: + hl = hooks_names[n] + except KeyError: + raise YunohostValidationError("hook_name_unknown", n) + # Iterate over hooks with this name + for h in hl: + # Update hooks dict + d = hooks_dict.get(h["priority"], dict()) + d.update({n: {"path": h["path"]}}) + hooks_dict[h["priority"]] = d + if not hooks_dict: + return result + + # Validate callbacks + if not callable(pre_callback): + + def pre_callback(name, priority, path, args): + return args + + if not callable(post_callback): + + def post_callback(name, priority, path, succeed): + return None + + # Iterate over hooks and execute them + for priority in sorted(hooks_dict): + for name, info in iter(hooks_dict[priority].items()): + state = "succeed" + path = info["path"] + try: + hook_args = pre_callback( + name=name, priority=priority, path=path, args=args + ) + hook_return = hook_exec( + path, args=hook_args, chdir=chdir, env=env, raise_on_error=True + )[1] + except YunohostError as e: + state = "failed" + hook_return = {} + logger.error(e.strerror, exc_info=True) + post_callback(name=name, priority=priority, path=path, succeed=False) + else: + post_callback(name=name, priority=priority, path=path, succeed=True) + if name not in result: + result[name] = {} + result[name][path] = {"state": state, "stdreturn": hook_return} + return result + + +def hook_exec( + path, + args=None, + raise_on_error=False, + chdir=None, + env=None, + user="root", + return_format="yaml", +): + """ + Execute hook from a file with arguments + + Keyword argument: + path -- Path of the script to execute + args -- Ordered list of arguments to pass to the script + raise_on_error -- Raise if the script returns a non-zero exit code + chdir -- The directory from where the script will be executed + env -- Dictionnary of environment variables to export + user -- User with which to run the command + """ + + # Validate hook path + if path[0] != "/": + path = os.path.realpath(path) + if not os.path.isfile(path): + raise YunohostError("file_does_not_exist", path=path) + + def is_relevant_warning(msg): + # Ignore empty warning messages... + if not msg: + return False + + # Some of these are shit sent from apt and we don't give a shit about + # them because they ain't actual warnings >_> + irrelevant_warnings = [ + r"invalid value for trace file descriptor", + r"Creating config file .* with new version", + r"Created symlink '?/etc/systemd", + r"dpkg: warning: while removing .* not empty so not removed", + r"apt-key output should not be parsed", + r"update-rc.d: ", + r"update-alternatives: ", + # Postgresql boring messages -_- + r"Adding user postgres to group ssl-cert", + r"Building PostgreSQL dictionaries from .*", + r"Removing obsolete dictionary files", + r"Creating new PostgreSQL cluster", + r"/usr/lib/postgresql/13/bin/initdb", + r"/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", + r"The default database encoding has accordingly been set to", + r"The default text search configuration will be set to", + r"Data page checksums are disabled.", + r"fixing permissions on existing directory /var/lib/postgresql/13/main ... ok", + r"fixing permissions on existing directory /var/lib/postgresql/15/main ... ok", + r"creating subdirectories \.\.\. ok", + r"selecting dynamic .* \.\.\. ", + r"selecting default .* \.\.\. ", + r"creating configuration files \.\.\. ok", + r"running bootstrap script \.\.\. ok", + r"performing post-bootstrap initialization \.\.\. ok", + r"syncing data to disk \.\.\. ok", + r"Success. You can now start the database server using:", + r"pg_ctlcluster \d\d main start", + r"Ver\s*Cluster\s*Port\s*Status\s*Owner\s*Data\s*directory", + r"/var/lib/postgresql/\d\d/main /var/log/postgresql/postgresql-\d\d-main.log", + # Java boring messages + r"cannot open '/etc/ssl/certs/java/cacerts'", + # Misc + r"update-binfmts: warning:", + r"Not building database", + r"Reloading AppArmor profiles", + r"aspell-autobuildhash: processing:", + r"Setcap failed on /usr/sbin/mysqld", + r"Invalid file '/usr/sbin/mysqld'", + r"is a disabled or a static unit, not starting it.", + ] + 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.info(l.rstrip()), + ) + + # Check the type of the hook (bash by default) + # For now we support only python and bash hooks. + hook_type = mimetypes.MimeTypes().guess_type(path)[0] + if hook_type == "text/x-python": + returncode, returndata = _hook_exec_python(path, args, env, loggers) + else: + returncode, returndata = _hook_exec_bash( + path, args, chdir, env, user, return_format, loggers + ) + + # Check and return process' return code + if returncode is None: + if raise_on_error: + raise YunohostError("hook_exec_not_terminated", path=path) + else: + logger.error(m18n.n("hook_exec_not_terminated", path=path)) + return 1, {} + elif raise_on_error and returncode != 0: + raise YunohostError("hook_exec_failed", path=path) + + return returncode, returndata + + +def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers): + from .utils.process import call_async_output + + # Construct command variables + cmd_args = "" + if args and isinstance(args, list): + # Concatenate escaped arguments + cmd_args = " ".join(shell_quote(s) for s in args) + if not chdir: + # use the script directory as current one + chdir, cmd_script = os.path.split(path) + cmd_script = f"./{cmd_script}" + else: + cmd_script = path + + # Add Execution dir to environment var + if env is None: + env = {} + env["YNH_CWD"] = chdir + + env["YNH_INTERFACE"] = Moulinette.interface.type + + stdreturn = os.path.join(tempfile.mkdtemp(), "stdreturn") + with open(stdreturn, "w") as f: + f.write("") + env["YNH_STDRETURN"] = stdreturn + + # Construct command to execute + if user == "root": + command = ["sh", "-c"] + else: + command = ["sudo", "-n", "-u", user, "-H", "sh", "-c"] + + # use xtrace on fd 7 which is redirected to stdout + env["BASH_XTRACEFD"] = "7" + command.append(f'/bin/bash -x "{cmd_script}" {cmd_args} 7>&1') + + logger.debug("Executing command '%s'" % command) + + _env = os.environ.copy() + if "YNH_CONTEXT" in _env: + del _env["YNH_CONTEXT"] + _env.update(env) + + # Remove the 'HOME' var which is causing some inconsistencies between + # cli and webapi (HOME ain't defined in yunohost-api because ran from systemd) + # Apps that need the HOME var should define it in the app scripts + if "HOME" in _env: + del _env["HOME"] + + # Pass jinja filters path for template helper + _env["YNH_J2_FILTERS_FILE_PATH"] = jinja_filters.__file__ + + returncode = call_async_output(command, loggers, shell=False, cwd=chdir, env=_env) + + raw_content = None + try: + with open(stdreturn, "r") as f: + raw_content = f.read() + returncontent = {} + + if return_format == "yaml": + if raw_content != "": + try: + returncontent = read_yaml(stdreturn) + except Exception as e: + raise YunohostError( + "hook_json_return_error", + path=path, + msg=str(e), + raw_content=raw_content, + ) + + elif return_format == "plain_dict": + for line in raw_content.split("\n"): + if "=" in line: + key, value = line.strip().split("=", 1) + returncontent[key] = value + + else: + raise YunohostError( + "Expected value for return_format is either 'json' or 'plain_dict', got '%s'" + % return_format + ) + finally: + stdreturndir = os.path.split(stdreturn)[0] + os.remove(stdreturn) + os.rmdir(stdreturndir) + + return returncode, returncontent + + +def _hook_exec_python(path, args, env, loggers): + dir_ = os.path.dirname(path) + name = os.path.splitext(os.path.basename(path))[0] + + if dir_ not in sys.path: + sys.path = [dir_] + sys.path + module = import_module(name) + + ret = module.main(args, env, loggers) + # # Assert that the return is a (int, dict) tuple + assert ( + isinstance(ret, tuple) + and len(ret) == 2 + and isinstance(ret[0], int) + and isinstance(ret[1], dict) + ), "Module %s did not return a (int, dict) tuple !" % module + return ret + + +def hook_exec_with_script_debug_if_failure(*args, **kwargs): + operation_logger = kwargs.pop("operation_logger") + error_message_if_failed = kwargs.pop("error_message_if_failed") + error_message_if_script_failed = kwargs.pop("error_message_if_script_failed") + + failed = True + failure_message_with_debug_instructions = None + try: + retcode, retpayload = hook_exec(*args, **kwargs) + failed = True if retcode != 0 else False + if failed: + error = error_message_if_script_failed + # 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": + operation_logger.dump_script_log_extract_for_debugging() + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(error_message_if_failed(error)) + failure_message_with_debug_instructions = operation_logger.error(error) + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(error_message_if_failed(error)) + failure_message_with_debug_instructions = operation_logger.error(error) + + return failed, failure_message_with_debug_instructions + + +def _extract_filename_parts(filename): + """Extract hook parts from filename""" + if "-" in filename: + priority, action = filename.split("-", 1) + else: + priority = "50" + action = filename + + # Remove extension if there's one + action = os.path.splitext(action)[0] + return priority, action + + +# Taken from Python 3 shlex module -------------------------------------------- + +_find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.UNICODE).search + + +def shell_quote(s): + """Return a shell-escaped version of the string *s*.""" + s = str(s) + if not s: + return "''" + if _find_unsafe(s) is None: + return s + + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return "'" + s.replace("'", "'\"'\"'") + "'" diff --git a/src/log.py b/src/log.py new file mode 100644 index 0000000..c7cb65a --- /dev/null +++ b/src/log.py @@ -0,0 +1,1029 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import copy +import glob +import os +import re +import time +from datetime import datetime, timedelta +from io import IOBase +from logging import INFO, FileHandler, Formatter, getLogger +from typing import Any + +import psutil +import yaml +from moulinette import Moulinette, m18n +from moulinette.core import MoulinetteError + +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import read_file, read_yaml +from .utils.logging import SUCCESS +from .utils.system import get_ynh_package_version + +logger = getLogger("yunohost.log") + +OPERATIONS_PATH = "/var/log/yunohost/operations/" + +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", + r".*Helper used in legacy mode.*", + r"ynh_handle_getopts_args", + r"ynh_script_progression", + r"sleep 0.5", + r"'\[' (1|0) -eq (1|0) '\]'$", + r"\[?\['? -n '' '?\]\]?$", + r"rm -rf /var/cache/yunohost/download/$", + r"type -t ynh_clean_setup$", + r"DEBUG - \+ 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_cache_symlinks(since_days_ago=365): + cutoff_time = time.time() - since_days_ago * 24 * 3600 + + logs = glob.iglob(OPERATIONS_PATH + "*.yml") + for log_md in logs: + if os.path.getmtime(log_md) < cutoff_time: + # Let's ignore old files because hmpf reading a shitload of yml is not free + continue + + name = log_md.split("/")[-1][: -len(".yml")] + parent_symlink = os.path.join(OPERATIONS_PATH, f".{name}.parent.yml") + success_symlink = os.path.join(OPERATIONS_PATH, f".{name}.success") + if os.path.islink(parent_symlink) and ( + os.path.islink(success_symlink) + and os.path.getmtime(success_symlink) < os.path.getmtime(log_md) + ): + continue + + try: + metadata = ( + read_yaml(log_md) 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 + + if not os.path.islink(success_symlink) or os.path.getmtime( + success_symlink + ) < os.path.getmtime(log_md): + success = metadata.get("success", "?") + if success is True: + success_target = "/usr/bin/true" + elif success is False: + success_target = "/usr/bin/false" + else: + success_target = "/dev/null" + try: + os.symlink(success_target, success_symlink) + except Exception as e: + logger.warning(f"Failed to create symlink {parent_symlink} ? {e}") + + if not os.path.islink(parent_symlink): + parent = metadata.get("parent") + parent = parent + ".yml" 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}") + + +log_list_cache: dict[str, dict[str, Any]] = {} + + +def log_list( + limit=None, with_details=False, with_suboperations=False, since_days_ago=30 +): + """ + List available logs + + Keyword argument: + limit -- Maximum number of logs + with_details -- Include details (e.g. if the operation was a success). + Likely to increase the command time as it needs to open and parse the + metadata file for each log... + with_suboperations -- Include operations that are not the "main" + operation but are sub-operations triggered by another ongoing operation + ... (e.g. initializing groups/permissions when installing an app) + """ + + operations = {} + + _update_log_cache_symlinks(since_days_ago) + + since = time.time() - since_days_ago * 24 * 3600 + logs = [ + x.split("/")[-1] + for x in glob.iglob(OPERATIONS_PATH + "*.yml") + if os.path.getmtime(x) > since + ] + logs = list(reversed(sorted(logs))) + + if not with_suboperations: + + def parent_symlink_points_to_dev_null(log): + name = log[: -len(".yml")] + 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: + logs = logs[:limit] + + for log in logs: + name = log[: -len(".yml")] + md_path = os.path.join(OPERATIONS_PATH, log) + + entry = { + "name": name, + "path": md_path, + "description": _get_description_from_name(name), + } + + success_symlink = os.path.join(OPERATIONS_PATH, f".{name}.success") + entry["success"] = "?" + if os.path.islink(success_symlink): + success_target = os.path.realpath(success_symlink) + if success_target == "/usr/bin/false": + entry["success"] = False + elif success_target == "/usr/bin/true": + entry["success"] = True + + try: + entry["started_at"] = _get_datetime_from_name(name) + except ValueError: + pass + + if with_details or with_suboperations: + if ( + name in log_list_cache + and os.path.getmtime(md_path) == log_list_cache[name]["time"] + ): + metadata = log_list_cache[name]["metadata"] + else: + try: + metadata = ( + read_yaml(md_path) 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=md_path, error=e) + ) + continue + else: + log_list_cache[name] = { + "time": os.path.getmtime(md_path), + "metadata": metadata, + } + + if with_details: + entry["success"] = metadata.get("success", "?") + entry["parent"] = metadata.get("parent") + entry["started_by"] = metadata.get("started_by") + + if with_suboperations: + entry["parent"] = metadata.get("parent") + entry["suboperations"] = [] + + 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 + # "suboperations" suboperations etc... + if with_suboperations: + suboperations = [o for o in operations.values() if o["parent"] is not None] + for suboperation in suboperations: + parent = operations.get(suboperation["parent"]) + if not parent: + continue + parent["suboperations"].append(suboperation) + operations = [o for o in operations.values() if o["parent"] is None] + else: + operations = [o for o in operations.values()] + + if limit: + operations = operations[:limit] + + operations = list(reversed(sorted(operations, key=lambda o: o["name"]))) + # Reverse the order of log when in cli, more comfortable to read (avoid + # unecessary scrolling) + is_api = Moulinette.interface.type == "api" + if not is_api: + operations = list(reversed(operations)) + + return {"operation": operations} + + +def log_show( + path, number=None, share=False, filter_irrelevant=False, with_suboperations=False +): + """ + Display a log file enriched with metadata if any. + + If the file_name is not an absolute path, it will try to search the file in + the unit operations log path (see OPERATIONS_PATH). + + Argument: + file_name + number + 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 YunohostError("There isn't that many logs", raw_msg=True) + + path = logs[-position]["path"] + + if share: + filter_irrelevant = True + + if filter_irrelevant: + + def _filter(lines): + filters = [re.compile(f) for f in BORING_LOG_LINES] + return [ + line + for line in lines + if not any(f.search(line.strip()) for f in filters) + ] + + else: + + def _filter(lines): + return lines + + # Normalize log/metadata paths and filenames + abs_path = path + log_path = None + if not path.startswith("/"): + abs_path = os.path.join(OPERATIONS_PATH, path) + + if os.path.exists(abs_path) and not path.endswith(".yml"): + log_path = abs_path + + if abs_path.endswith(".yml") or abs_path.endswith(".log"): + base_path = "".join(os.path.splitext(abs_path)[:-1]) + else: + base_path = abs_path + base_filename = os.path.basename(base_path) + md_path = base_path + ".yml" + if log_path is None: + log_path = base_path + ".log" + + if not os.path.exists(md_path) and not os.path.exists(log_path): + raise YunohostError("log_does_exists", log=path) + + infos = {} + + # If it's a unit operation, display the name and the description + if base_path.startswith(OPERATIONS_PATH): + infos["description"] = _get_description_from_name(base_filename) + infos["name"] = base_filename + + if share: + from .utils.yunopaste import yunopaste + + content = "" + if os.path.exists(md_path): + content += read_file(md_path) + content += "\n============\n\n" + if os.path.exists(log_path): + actual_log = read_file(log_path) + content += "\n".join(_filter(actual_log.split("\n"))) + + url = yunopaste(content) + + logger.success(m18n.n("log_available_on_yunopaste", url=url)) + if Moulinette.interface.type == "api": + return {"url": url} + else: + return + + # Display metadata if exist + if os.path.exists(md_path): + try: + metadata = read_yaml(md_path) or {} + except MoulinetteError as e: + error = m18n.n("log_corrupted_md_file", md_file=md_path, error=e) + if os.path.exists(log_path): + logger.warning(error) + else: + raise YunohostError(error) + else: + infos["metadata_path"] = md_path + infos["metadata"] = metadata + + if "log_path" in metadata: + log_path = metadata["log_path"] + + if with_suboperations: + + def suboperations(): + try: + log_start = _get_datetime_from_name(base_filename) + except ValueError: + return + + for filename in os.listdir(OPERATIONS_PATH): + if not filename.endswith(".yml"): + continue + + # We first retrict search to a ~48h time window to limit the number + # of .yml we look into + try: + date = _get_datetime_from_name(filename) + except ValueError: + continue + if (date < log_start) or ( + date > log_start + timedelta(hours=48) + ): + continue + + try: + submetadata = read_yaml( + os.path.join(OPERATIONS_PATH, filename) + ) + except Exception: + continue + + if submetadata and submetadata.get("parent") == base_filename: + name = filename[: -len(".yml")] + yield { + "name": name, + "description": _get_description_from_name(name), + "success": submetadata.get("success", "?"), + } + + metadata["suboperations"] = list(suboperations()) + + # Display logs if exist + if os.path.exists(log_path): + from .service import _tail + + if number and filter_irrelevant: + logs = _tail(log_path, int(number * 6)) + elif number: + logs = _tail(log_path, int(number)) + else: + logs = read_file(log_path) + logs = list(_filter(logs)) + if number: + logs = logs[-number:] + infos["log_path"] = log_path + infos["logs"] = logs + + return infos + + +from typing import Callable, Concatenate, ParamSpec, TypeVar + +# FuncT = TypeVar("FuncT", bound=Callable[..., Any]) +Param = ParamSpec("Param") +RetType = TypeVar("RetType") + + +def is_unit_operation( + entities=["app", "domain", "group", "service", "user"], + exclude=["password"], + sse_only=False, + flash=False, +) -> Callable[ + [Callable[Concatenate["OperationLogger", Param], RetType]], Callable[Param, RetType] +]: + """ + Configure quickly a unit operation + + This decorator help you to configure the record of a unit operations. + + Argument: + entities A list of entity types related to the unit operation. The entity + type is searched inside argument's names of the decorated function. If + something match, the argument value is added as related entity. If the + argument name is different you can specify it with a tuple + (argname, entity_type) instead of just put the entity type. + + exclude Remove some arguments from the context. By default, arguments + called 'password' are removed. If an argument is an object, you need to + exclude it or create manually the unit operation without this decorator. + + """ + + def decorate( + func: Callable[Concatenate["OperationLogger", Param], RetType], + ) -> Callable[Param, RetType]: + def func_wrapper(*args, **kwargs): + # If the function is called directly from an other part of the code + # and not by the moulinette framework, we need to complete kwargs + # dictionnary with the args list. + # Indeed, we use convention naming in this decorator and we need to + # know name of each args (so we need to use kwargs instead of args) + if len(args) > 0: + from inspect import signature + + keys = list(signature(func).parameters.keys()) + if "operation_logger" in keys: + keys.remove("operation_logger") + for k, arg in enumerate(args): + kwargs[keys[k]] = arg + args = () + + # Search related entity in arguments of the decorated function + related_to = [] + for entity in entities: + if isinstance(entity, tuple): + entity_type = entity[1] + entity = entity[0] + else: + entity_type = entity + + if entity in kwargs and kwargs[entity] is not None: + if isinstance(kwargs[entity], str): + related_to.append((entity_type, kwargs[entity])) + else: + for x in kwargs[entity]: + related_to.append((entity_type, x)) + + context = kwargs.copy() + + # Exclude unappropriate data from the context + for field in exclude: + if field in context: + context.pop(field, None) + + # Context is made from args given to main function by argparse + # This context will be added in extra parameters in yml file, so this context should + # be serializable and short enough (it will be displayed in webadmin) + # Argparse can provide some File or Stream, so here we display the filename or + # the IOBase, if we have no name. + for field, value in context.items(): + if isinstance(value, IOBase): + try: + context[field] = value.name + except Exception: + context[field] = "IOBase" + operation_logger = OperationLogger( + func.__name__, related_to, sse_only, flash, args=context + ) + + try: + # Start the actual function, and give the unit operation + # in argument to let the developper start the record itself + if not flash: + args = (operation_logger,) + args + result = func(*args, **kwargs) + except Exception as e: + operation_logger.error(e) + raise + else: + operation_logger.success() + return result + + return func_wrapper + + return decorate + + +# This is just a wrapper to is_unit_operation for proper typing purposes +def is_flash_unit_operation( + entities=["app", "domain", "group", "service", "user"], + exclude=["password"], + sse_only=False, +) -> Callable[[Callable[Param, RetType]], Callable[Param, RetType]]: + return is_unit_operation(entities, exclude, sse_only, True) # type: ignore + + +class RedactingFormatter(Formatter): + def __init__(self, format_string, data_to_redact): + super(RedactingFormatter, self).__init__(format_string) + self.data_to_redact = data_to_redact + + def format(self, record): + msg = super(RedactingFormatter, self).format(record) + self.identify_data_to_redact(msg) + for data in self.data_to_redact: + # we check that data is not empty string, + # otherwise this may lead to super epic stuff + # (try to run "foo".replace("", "bar")) + if data: + msg = msg.replace(data, "**********") + # bash set -x display comparison like this: [[ ohno != \o\h\n\o ]] + msg = msg.replace("\\" + "\\".join(data), "**********") + return msg + + def identify_data_to_redact(self, record): + # Wrapping this in a try/except because we don't want this to + # break everything in case it fails miserably for some reason :s + try: + # This matches stuff like db_pwd=the_secret or admin_password=other_secret + # (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=") + # Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest + match = re.search( + r"(pwd|pass|passwd|password|passphrase|secret\w*|\w+key|token|PASSPHRASE)=(\S{3,})$", + record.strip(), + ) + if ( + match + and match.group(2) not in self.data_to_redact + and match.group(1) not in ["key", "manifest_key"] + ): + self.data_to_redact.append(match.group(2)) + except Exception as e: + logger.warning( + "Failed to parse line to try to identify data to redact ... : %s" % e + ) + + +class OperationLogger: + """ + Instances of this class represents unit operation done on the ynh instance. + + Each time an action of the yunohost cli/api change the system, one or + several unit operations should be registered. + + This class record logs and metadata like context or time/end time. + """ + + _instances: list["OperationLogger"] = [] + + def __init__( + self, operation, related_to=None, sse_only=False, flash=False, **kwargs + ): + # TODO add a way to not save password on app installation + self.operation = operation + self.related_to = related_to + self.extra = kwargs + self.started_at = None + self.ended_at = None + self.logger = None + self.file_handler = None + self.sse_handler = None + self._name = None + self.sse_only = sse_only + self.flash = flash + self.data_to_redact = [] + self.parent = self.parent_logger() + self._instances.append(self) + + for filename in ["/etc/yunohost/mysql", "/etc/yunohost/psql"]: + if os.path.exists(filename): + self.data_to_redact.append(read_file(filename).strip()) + + self.started_by = None + if not self.parent: + if Moulinette.interface.type == "api": + try: + from .authenticators.ldap_admin import Authenticator as Auth + + auth = Auth().get_session_cookie() + self.started_by = auth["user"] + except Exception: + # During postinstall, we're not actually authenticated so eeeh what happens exactly? + self.started_by = "root" + else: + self.started_by = _guess_who_started_process(psutil.Process()) + + if not os.path.exists(OPERATIONS_PATH): + os.makedirs(OPERATIONS_PATH) + + # Autostart the logger for flash operations ? + if self.flash: + self.start() + + def parent_logger(self): + # If there are other operation logger instances + for instance in reversed(self._instances): + # Is one of these operation logger started but not yet done ? + if instance.started_at is not None and instance.ended_at is None: + # We are a child of the first one we found + return instance.name + + # If no lock exists, we are probably in tests or yunohost is used as a + # lib ... let's not really care about that case and assume we're the + # root logger then. + if not os.path.exists("/var/run/moulinette_yunohost.lock"): + return None + + locks = read_file("/var/run/moulinette_yunohost.lock").strip().split("\n") + # If we're the process with the lock, we're the root logger + if locks == [] or str(os.getpid()) in locks: + return None + + # If we get here, we are in a yunohost command called by a yunohost + # (maybe indirectly from an app script for example...) + # + # The strategy is : + # 1. list 20 most recent log files + # 2. iterate over the PID of parent processes + # 3. see if parent process has some log file open (being actively + # written in) + # 4. if among those file, there's an operation log file, we use the id + # of the most recent file + + recent_operation_logs = sorted( + glob.iglob(OPERATIONS_PATH + "*.log"), key=os.path.getmtime, reverse=True + )[:20] + + proc = psutil.Process().parent() + while proc is not None: + # We use proc.open_files() to list files opened / actively used by this proc + # We only keep files matching a recent yunohost operation log + active_logs = sorted( + (f.path for f in proc.open_files() if f.path in recent_operation_logs), + key=os.path.getmtime, + reverse=True, + ) + if active_logs != []: + # extra the log if from the full path + return os.path.basename(active_logs[0])[:-4] + else: + proc = proc.parent() + continue + + # If nothing found, assume we're the root operation logger + return None + + def start(self) -> None: + """ + Start to record logs that change the system + Until this start method is run, no unit operation will be registered. + """ + + if self.started_at is None: + self.started_at = datetime.utcnow() + self.flush() + self._register_log() + if self.sse_handler is not None and not self.flash: + self.sse_handler.emit_operation_start( + self.started_at, + _get_description_from_name(self.name), + self.started_by, + ) + + @property + def md_path(self): + """ + Metadata path file + """ + return f"{OPERATIONS_PATH}/{self.name}.yml" + + @property + def log_path(self): + """ + Log path file + """ + return f"{OPERATIONS_PATH}/{self.name}.log" + + def _register_log(self): + """ + Register log with a handler connected on log system + """ + + if not self.sse_only and not self.flash: + self.file_handler = FileHandler(self.log_path) + # We use a custom formatter that's able to redact all stuff in self.data_to_redact + # N.B. : the subtle thing here is that the class will remember a pointer to the list, + # so we can directly append stuff to self.data_to_redact and that'll be automatically + # propagated to the RedactingFormatter + self.file_handler.formatter = RedactingFormatter( + "%(asctime)s: %(levelname)s - %(message)s", self.data_to_redact + ) + + # Only do this one for the main parent operation + if not self.parent: + from .utils.sse import SSELogStreamingHandler + + self.sse_handler = SSELogStreamingHandler(self.name, flash=self.flash) + self.sse_handler.level = INFO if not self.flash else SUCCESS + self.sse_handler.formatter = RedactingFormatter( + "%(message)s", self.data_to_redact + ) + + # Listen to the root logger + self.logger = getLogger("yunohost") + if self.file_handler is not None: + self.logger.addHandler(self.file_handler) + + if self.sse_handler is not None: + self.logger.addHandler(self.sse_handler) + + def flush(self): + """ + Write or rewrite the metadata file with all metadata known + """ + if self.sse_only or self.flash: + return + + metadata = copy.copy(self.metadata) + + # Remove lower-case keys ... this is because with the new v2 app packaging, + # all settings are included in the env but we probably don't want to dump all of these + # which may contain various secret/private data ... + if "env" in metadata: + metadata["env"] = { + k: v for k, v in metadata["env"].items() if k == k.upper() + } + + dump = yaml.safe_dump(metadata, default_flow_style=False) + for data in self.data_to_redact: + # N.B. : we need quotes here, otherwise yaml isn't happy about loading the yml later + dump = dump.replace(data, "'**********'") + with open(self.md_path, "w") as outfile: + outfile.write(dump) + + @property + def name(self): + """ + Name of the operation + This name is used as filename, so don't use space + """ + if self._name is not None: + return self._name + + name = [self.started_at.strftime("%Y%m%d-%H%M%S")] + name += [self.operation] + + if hasattr(self, "name_parameter_override"): + # This is for special cases where the operation is not really + # unitary. For instance, the regen conf cannot be logged "per + # service" because of the way it's built + name.append(self.name_parameter_override) + elif self.related_to: + # We use the name of the first related thing + name.append(self.related_to[0][1]) + + self._name = "-".join(name) + return self._name + + @property + def metadata(self): + """ + Dictionnary of all metadata collected + """ + + data = { + "started_at": self.started_at, + "operation": self.operation, + "parent": self.parent, + "yunohost_version": get_ynh_package_version("yunohost")["version"], + "interface": Moulinette.interface.type, + } + if self.related_to is not None: + data["related_to"] = self.related_to + if self.ended_at is not None: + data["ended_at"] = self.ended_at + data["success"] = self._success + if self.error is not None: + data["error"] = self._error + if self.started_by is not None: + data["started_by"] = self.started_by + # TODO: detect if 'extra' erase some key of 'data' + data.update(self.extra) + # Remove the 'args' arg from args (yodawg). It corresponds to url-encoded args for app install, config panel set, etc + # Because the data are url encoded, it's hell to properly redact secrets inside it, + # and the useful info is usually already available in `env` too + if "args" in data and isinstance(data["args"], dict) and "args" in data["args"]: + data["args"].pop("args") + return data + + def success(self): + """ + Declare the success end of the unit operation + """ + self.close() + + def error(self, error): + """ + Declare the failure of the unit operation + """ + return self.close(error) + + def close(self, error=None): + """ + Close properly the unit operation + """ + + # When the error happen's in the is_unit_operation try/except, + # we want to inject the log ref in the exception, such that it may be + # transmitted to the webadmin which can then redirect to the appropriate + # log page + if ( + self.started_at + and isinstance(error, Exception) + and not isinstance(error, YunohostValidationError) + and not self.flash + and not self.sse_only + ): + error.log_ref = self.name + + if self.ended_at is not None or self.started_at is None: + return + if error is not None and not isinstance(error, str): + error = str(error) + + self.ended_at = datetime.utcnow() + self._error = error + self._success = error is None + + if self.sse_handler is not None: + if not self.flash: + self.sse_handler.emit_operation_end( + self.ended_at, self._success, self._error + ) + elif self._error: + self.sse_handler.emit_error_toast(self._error) + + if self.file_handler is not None: + self.logger.removeHandler(self.file_handler) + self.file_handler.close() + if self.sse_handler is not None: + self.logger.removeHandler(self.sse_handler) + self.sse_handler.close() + + if not self.flash: + is_api = Moulinette.interface.type == "api" + desc = _get_description_from_name(self.name) + if error is None: + if is_api: + msg = m18n.n("log_link_to_log", name=self.name, desc=desc) + else: + msg = m18n.n("log_help_to_get_log", name=self.name, desc=desc) + logger.debug(msg) + else: + if is_api: + msg = ( + "" + + m18n.n("log_link_to_failed_log", name=self.name, desc=desc) + + "" + ) + else: + msg = m18n.n( + "log_help_to_get_failed_log", name=self.name, desc=desc + ) + logger.info(msg) + else: + msg = None + self.flush() + return msg + + def __del__(self): + """ + Try to close the unit operation, if it's missing. + The missing of the message below could help to see an electrical + shortage. + """ + if self.ended_at is not None or self.started_at is None: + return + else: + self.error(m18n.n("log_operation_unit_unclosed_properly")) + + def dump_script_log_extract_for_debugging(self): + with open(self.log_path, "r") as f: + lines = f.readlines() + + # A line typically looks like + # 2019-10-19 16:10:27,611: DEBUG - + mysql -u piwigo --password=********** -B piwigo + # And we just want the part starting by "DEBUG - " + lines = [line for line in lines if ":" in line.strip()] + lines = [line.strip().split(": ", 1)[-1] for line in lines] + # And we ignore boring/irrelevant lines + # Annnnnnd we also ignore lines matching [number] + such as + # 72971 DEBUG 29739 + ynh_exit_properly + # which are lines from backup-before-upgrade or restore-after-failed-upgrade ... + filters = [re.compile(f_) for f_ in BORING_LOG_LINES] + filters.append(re.compile(r"\d+ \+ ")) + lines = [ + line + for line in lines + if not any(filter_.search(line) for filter_ in filters) + ] + + lines_to_display = [] + + # Get the 20 lines before the last 'ynh_exit_properly' + rev_lines = list(reversed(lines)) + for i, line in enumerate(rev_lines[:50]): + if line.endswith("+ ynh_exit_properly"): + lines_to_display = reversed(rev_lines[i : i + 20]) + break + + # If didnt find anything, just get the last 20 lines + if not lines_to_display: + lines_to_display = lines[-20:] + + logger.warning( + "Here's an extract of the logs before the crash. It might help debugging the error:" + ) + for line in lines_to_display: + logger.info(line) + + +def _get_datetime_from_name(name): + # Filenames are expected to follow the format: + # 20200831-170740-short_description-and-stuff + + raw_datetime = " ".join(name.split("-")[:2]) + return datetime.strptime(raw_datetime, "%Y%m%d %H%M%S") + + +def _get_description_from_name(name): + """ + Return the translated description from the filename + """ + + parts = name.split("-", 3) + try: + try: + datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") + except ValueError: + key = "log_" + parts[0] + args = parts[1:] + else: + key = "log_" + parts[2] + args = parts[3:] + return m18n.n(key, *args) + except IndexError: + return name + + +@is_unit_operation(flash=True) +def log_share(path): + return log_show(path, share=True) + + +def _guess_who_started_process(process: psutil.Process) -> str: + if "SUDO_USER" in process.environ(): + return process.environ()["SUDO_USER"] + + parents = process.parents() + cmdlines = [parent.cmdline() for parent in parents] + + if any("/usr/sbin/CRON" in cli for cli in cmdlines): + return m18n.n("automatic_task") + + elif any("/usr/bin/yunohost-api" in cli for cli in cmdlines): + return m18n.n("yunohost_api") + + elif process.terminal() is None: + return m18n.n("noninteractive_task") + + else: + return "root" diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py new file mode 100644 index 0000000..9243e9e --- /dev/null +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -0,0 +1,536 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Explicitly import packages to prevent an issue that may arise later because of python3.9 being replaced by 3.11 in the middle of the upgrade etc +import _strptime # noqa: F401 +import glob +import os +import subprocess +from datetime import date +from time import sleep + +import _ldap # noqa: F401 +from moulinette import Moulinette, m18n + +from ..app import app_list +from ..regenconf import manually_modified_files, regen_conf +from ..tools import Migration, _write_migration_state, tools_update +from ..utils.error import YunohostError +from ..utils.file_utils import read_file, write_to_file +from ..utils.process import call_async_output, check_output +from ..utils.system import ( + _list_upgradable_apt_packages, + aptitude_with_progress_bar, + free_space_in_directory, + get_ynh_package_version, +) + +# 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 = 11 +N_CURRENT_YUNOHOST = 11 + +VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bookworm_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 + # 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 | grep -E -v 'pkg(-|_)resources==' > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null" + ) + + +def unstable_apps() -> list[str]: + output = [] + deprecated_apps = ["mailman", "ffsync"] + + for infos in app_list(full=True)["apps"]: + if ( + not infos.get("from_catalog") + or infos.get("from_catalog", {}).get("state") + in [ + "inprogress", + "notworking", + ] + or infos["id"] in deprecated_apps + ): + output.append(infos["id"]) + + return output + + +class MyMigration(Migration): + "Upgrade the system to Debian Bookworm and Yunohost 12.x" + + mode = "manual" + + def run(self): + self.check_assertions() + + logger.info(m18n.n("migration_0027_start")) + + # + # Add new apt .deb signing key + # + + new_apt_key = "https://forge.yunohost.org/yunohost_bookworm.asc" + 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 + # 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"' + ) + + # + # 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 + # + + _backup_pip_freeze_for_python_app_venvs() + + # + # Run apt update + # + + aptitude_with_progress_bar("update") + + # 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" + ) + + # Stupid stuff because resolvconf later wants to edit /etc/resolv.conf and will miserably crash if it's immutable + os.system("chattr -i /etc/resolv.conf") + + # 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" ]] && 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_0027_patch_yunohost_conflicts")) + + self.patch_yunohost_conflicts() + + # + # Critical fix for RPI otherwise network is down after rebooting + # https://forum.yunohost.org/t/20652 + # + # 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_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() + 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( + "full-upgrade cron rspamd- luajit- libluajit-5.1-2- --show-why -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'" + ) + + # For some reason aptitude is derping about python3 / python3-venv so try to explicitly tell to install python3.11 to replace 3.9... + # Note the '+M' prefix which is here to mark the packages as automatically installed + python_upgrade_list = "python3 python3.11+M python3.9- " + if os.system('dpkg --list | grep -q "^ii python3.9-venv "') == 0: + python_upgrade_list += "python3-venv+M python3.11-venv+M python3.9-venv-" + aptitude_with_progress_bar( + f"full-upgrade {python_upgrade_list} --show-why -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'" + ) + + # Full upgrade of "every" packages except the yunohost ones which are held + 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_0027_still_on_bullseye_after_main_upgrade") + + # Clean the mess + logger.info(m18n.n("migration_0027_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 + # + # 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_0027_yunohost_upgrade")) + aptitude_with_progress_bar("unhold yunohost moulinette ssowat yunohost-admin") + + full_upgrade_cmd = ( + "full-upgrade --show-why -o Dpkg::Options::='--force-confold' " + ) + full_upgrade_cmd += "yunohost yunohost-admin yunohost-portal moulinette ssowat " + # This one is needed to solve aptitude derping with nginx dependencies + full_upgrade_cmd += "libluajit2-5.1-2 " + + try: + aptitude_with_progress_bar(full_upgrade_cmd) + 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_cmd) + 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)}") + + # Mark this migration as completed before triggering the "new" migrations + _write_migration_state(self.id, "done") + + 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) + + # 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]) + + if self.yunohost_major_version() != N_CURRENT_YUNOHOST + 1: + raise YunohostError( + "Still on YunoHost 11.x at the end of the migration, eh? Sounds like the migration didn't really complete!?", + raw_msg=True, + ) + + 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 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 > 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 + ): + 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_0027_not_bullseye") + + # Have > 1 Go free space on /var/ ? + if free_space_in_directory("/var/") / (1024**3) < 1.0: + raise YunohostError("migration_0027_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 " bookworm " 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_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 11.x + # NB : we do both check to cover situations where the upgrade crashed + # 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 + ): + 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_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 😉!" + if date.today() < date(2025, 3, 30) + else "" + ) + + "\n\n" + + "You can read the full release note, remaining known issues and feedback from the community here: . In particular, we encourage you to pay attention to the fact that:\n" + + "- Packages `metronome` (xmpp server) and `rspamd` (mail antispam) are now independent applications available in the catalog. Make sure to explicitly install these applications after the migration if you care about those!\n" + + "- The user portal / SSO system was totally reworked. You may lose custom theming if you have any. However, the new system also has plenty of customization capabilities (more details in the release note).\n" + + "\n" + + message + ) + + if problematic_apps: + message += "\n\n" + m18n.n( + "migration_0027_problematic_apps_warning", + problematic_apps=problematic_apps, + ) + + if modified_files: + message += "\n\n" + m18n.n( + "migration_0027_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 '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 '/non-free-firmware/!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" + " | grep -v deinstall" + " | awk '{print $1}'" + " | { grep 'ynh-deps$' || true; }" + ) + + output = check_output(command) + + return output.split("\n") if output else [] + + def get_php_packages(self): + command = ( + "dpkg --get-selections" + " | grep -v deinstall" + " | awk '{print $1}'" + " | { grep '^php' || true; }" + ) + + output = check_output(command) + + return output.split("\n") if output else [] + + 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 11.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/0028_delete_legacy_xmpp_permission.py b/src/migrations/0028_delete_legacy_xmpp_permission.py new file mode 100644 index 0000000..bc06c6d --- /dev/null +++ b/src/migrations/0028_delete_legacy_xmpp_permission.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from logging import getLogger + +from ..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 = [] + + @Migration.ldap_migration + def run(self, *args): + # Superseded by migration 0033 / permission rework to move infos out of ldap + pass diff --git a/src/migrations/0029_postgresql_13_to_15.py b/src/migrations/0029_postgresql_13_to_15.py new file mode 100644 index 0000000..a3c04d8 --- /dev/null +++ b/src/migrations/0029_postgresql_13_to_15.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from .postgresql import PostgreSQLMigration + + +class MyMigration(PostgreSQLMigration): + "Migrate DBs from Postgresql 13 to 15 after migrating to Bookworm" + + previous_version = "13" + target_version = "15" + + dependencies = ["migrate_to_bookworm"] diff --git a/src/migrations/0030_rebuild_python_venv_in_bookworm.py b/src/migrations/0030_rebuild_python_venv_in_bookworm.py new file mode 100644 index 0000000..7c68937 --- /dev/null +++ b/src/migrations/0030_rebuild_python_venv_in_bookworm.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from .python import PythonMigration + + +class MyMigration(PythonMigration): + dependencies = ["migrate_to_bookworm"] + migration_id = "0030_rebuild_python_venv_in_bookworm" diff --git a/src/migrations/0031_terms_of_services.py b/src/migrations/0031_terms_of_services.py new file mode 100644 index 0000000..afcba75 --- /dev/null +++ b/src/migrations/0031_terms_of_services.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import logging + +from moulinette import m18n + +from ..tools import Migration + +logger = logging.getLogger("yunohost.migration") + + +class MyMigration(Migration): + "Display new terms of services to admins" + + mode = "manual" + + def run(self): + pass + + @property + def disclaimer(self): + return ( + m18n.n("migration_0031_terms_of_services") + + "\n\n" + + m18n.n("tos_postinstall_acknowledgement") + ) diff --git a/src/migrations/0032_firewall_config.py b/src/migrations/0032_firewall_config.py new file mode 100644 index 0000000..2c22aaa --- /dev/null +++ b/src/migrations/0032_firewall_config.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import logging +from typing import Any + +import yaml + +from ..app import app_list +from ..firewall import YunoFirewall +from ..service import service_enable +from ..tools import Migration + +logger = logging.getLogger("yunohost.migration") + + +class MyMigration(Migration): + "Rework the firewall configuration" + + introduced_in_version = "12.1" + mode = "auto" + + apps = app_list(full=True) + + def _app_comment_of_port(self, port: int) -> str: + for app in self.apps["apps"]: + settings = app["settings"] + port_keys: list[str] = [ + key + for key in settings.keys() + if key.startswith("port_") or key == "port" + ] + for key in port_keys: + if settings[key] not in [port, str(port)]: + continue + port_name = "main" if key == "port" else key.removeprefix("port_") + return f"App {app['id']}: {port_name} port" + + return "" + + def firewall_file_migrate(self) -> None: + old_data = yaml.safe_load( + YunoFirewall.FIREWALL_FILE.open("r", encoding="utf-8") + ) + + new_data: dict[str, Any] = { + "router_forwarding_upnp": old_data["uPnP"]["enabled"], + "tcp": {}, + "udp": {}, + } + for proto in ["TCP", "UDP"]: + new_data[proto.lower()] = { + port if isinstance(port, int) else port.replace(":", "-"): { + "open": True, + "upnp": port in old_data["uPnP"][proto], + "comment": self._app_comment_of_port(port), + } + for port in set(old_data["ipv4"][proto] + old_data["ipv6"][proto]) + } + yaml.dump(new_data, YunoFirewall.FIREWALL_FILE.open("w", encoding="utf-8")) + + def run(self): + self.firewall_file_migrate() + service_enable("nftables") + + def run_after_system_restore(self): + self.run() diff --git a/src/migrations/0033_rework_permission_infos.py b/src/migrations/0033_rework_permission_infos.py new file mode 100644 index 0000000..f090b5e --- /dev/null +++ b/src/migrations/0033_rework_permission_infos.py @@ -0,0 +1,198 @@ +import os +from logging import getLogger + +from ..app import app_setting, app_ssowatconf +from ..permission import ( + _set_system_perms, + _sync_permissions_with_ldap, + permission_create, +) +from ..regenconf import regen_conf +from ..tools import Migration +from ..user import user_group_list +from ..utils.app_utils import _is_installed +from ..utils.file_utils import read_yaml +from ..utils.ldap import _get_ldap_interface, _ldap_path_extract + +logger = getLogger("yunohost.migration") + +SYSTEM_PERMS = ["mail", "sftp", "ssh"] + + +class MyMigration(Migration): + introduced_in_version = "12.1" + dependencies = [] + + @Migration.ldap_migration + def run(self, backup_folder: str) -> None: + regen_conf(["slapd"], force=True) + + self.ldap_migration_started = True + + permissions_per_app, permission_system = self.read_legacy_permissions() + for app, permissions in permissions_per_app.items(): + if not _is_installed(app): + logger.warning( + f"Found permissions for app {app}, but this app is not installed. It may just be a permission that was not properly cleaned up in the past. Details: {permissions}" + ) + continue + + app_setting(app, "_permissions", permissions) + + _set_system_perms(permission_system) + + self.delete_legacy_permissions() + _sync_permissions_with_ldap() + app_ssowatconf() + + def run_after_system_restore(self): + regen_conf(["slapd"], force=True) + + _, permission_system = self.read_legacy_permissions() + _set_system_perms(permission_system) + + self.delete_legacy_permissions() + _sync_permissions_with_ldap() + app_ssowatconf() + + def run_before_app_restore(self, app_id, app_backup_in_archive): + # Prior to 12.1, the truth source for app permission was the LDAP db rather than app settings. + # The LDAP db corresponding to the app was dump into a separate yaml + permfile = f"/etc/yunohost/apps/{app_id}/permissions.yml" + if not os.path.isfile(permfile): + logger.warning( + "Uhoh, this app backup is from yunohost <= 12.0, but there is no 'permissions.yml' ? Skipping perm restoration … You might have to reconfigure permissions yourself." + ) + return + + legacy_permissions_yml = read_yaml(permfile) + + existing_groups = user_group_list()["groups"] + + for permission_name, permission_infos in legacy_permissions_yml.items(): + if "allowed" not in permission_infos: + logger.warning( + f"'allowed' key corresponding to allowed groups for permission {permission_name} not found when restoring app {app_id} … You might have to reconfigure permissions yourself." + ) + should_be_allowed = ["all_users"] + else: + should_be_allowed = [ + g for g in permission_infos["allowed"] if g in existing_groups + ] + + permission_create( + permission_name, + allowed=should_be_allowed, + url=permission_infos.get("url"), + additional_urls=permission_infos.get("additional_urls"), + auth_header=permission_infos.get("auth_header"), + show_tile=permission_infos.get("show_tile", True), + protected=permission_infos.get("protected", False), + sync_perm=False, + ) + + os.remove(permfile) + + _sync_permissions_with_ldap() + app_ssowatconf() + + def read_legacy_permissions(self): + ldap = _get_ldap_interface() + permissions_infos = ldap.search( + "ou=permission", + "(objectclass=permissionYnh)", + [ + "cn", + "URL", + "additionalUrls", + "authHeader", + "label", + "showTile", + "isProtected", + "groupPermission", + ], + ) + + permissions_per_app = {} + permissions_system = {p: {"allowed": []} for p in SYSTEM_PERMS} + + for infos in permissions_infos: + app, name = infos["cn"][0].split(".") + + if app in SYSTEM_PERMS: + if name == "main": + permissions_system[app]["allowed"] = [ + _ldap_path_extract(p, "cn") + for p in infos.get("groupPermission", []) + ] + continue + + if app not in permissions_per_app: + permissions_per_app[app] = {} + + permissions_per_app[app][name] = { + "label": infos.get("label", [None])[0], + "show_tile": infos.get("showTile", [False])[0] == "TRUE", + "auth_header": infos.get("authHeader", [False])[0] == "TRUE", + "protected": infos.get("isProtected", [False])[0] == "TRUE", + "url": infos.get("URL", [None])[0], + "additional_urls": infos.get("additionalUrls", []), + "allowed": [ + _ldap_path_extract(p, "cn") + for p in infos.get("groupPermission", []) + ], + } + + return permissions_per_app, permissions_system + + def delete_legacy_permissions(self): + try: + ldap = _get_ldap_interface() + permissions_infos = ldap.search( + "ou=permission", + "(objectclass=permissionYnh)", + ["cn"], + ) + # LDAP is fucking stupid, therefore we have to un-mark the attributes as obsolete + # to be able to empty them ... + # (and yeah why is this all so fucking complex why can't we just drop the column like a real DB or something...) + os.system("sed -i 's@ OBSOLETE$@@g' /etc/ldap/schema/permission.ldif") + os.system( + "/usr/share/yunohost/hooks/conf_regen/06-slapd _regenerate_slapd_conf" + ) + os.system("systemctl restart slapd") + for infos in permissions_infos: + try: + ldap.update( + f"cn={infos['cn'][0]},ou=permission", + { + "label": [], + "authHeader": [], + "showTile": [], + "isProtected": [], + "URL": [], + "additionalUrls": [], + "groupPermission": [], + }, + ) + except Exception as e: + logger.warning("Failed to delete the legacy permission ? " + str(e)) + logger.warning("Retrying without the label idk") + try: + ldap.update( + f"cn={infos['cn'][0]},ou=permission", + { + "authHeader": [], + "showTile": [], + "isProtected": [], + "URL": [], + "additionalUrls": [], + "groupPermission": [], + }, + ) + except Exception as e: + logger.warning( + "Failed to delete the legacy permission ? " + str(e) + ) + finally: + regen_conf(["slapd"], force=True) diff --git a/src/migrations/0034_fix_missing_admins_aliases.py b/src/migrations/0034_fix_missing_admins_aliases.py new file mode 100644 index 0000000..85b72a0 --- /dev/null +++ b/src/migrations/0034_fix_missing_admins_aliases.py @@ -0,0 +1,13 @@ +from ..domain import _get_maindomain +from ..tools import Migration +from ..user import _update_admins_group_aliases + + +class MyMigration(Migration): + introduced_in_version = "12.1" + dependencies = [] + + def run(self, *args): + _update_admins_group_aliases( + old_main_domain=None, new_main_domain=_get_maindomain() + ) diff --git a/src/migrations/0035_fix_apps_nodejs_version.py b/src/migrations/0035_fix_apps_nodejs_version.py new file mode 100644 index 0000000..610c1bc --- /dev/null +++ b/src/migrations/0035_fix_apps_nodejs_version.py @@ -0,0 +1,95 @@ +import os +from logging import getLogger + +from ..app import app_setting +from ..tools import Migration +from ..utils.app_utils import _get_app_settings, _installed_apps +from ..utils.process import check_output + +logger = getLogger("yunohost.migration") + + +def get_installed_nodejs_versions(): + n = "/usr/share/yunohost/helpers.v2.1.d/vendor/n/n" + N_INSTALL_DIR = "/opt/node_n" + installed_versions_raw = check_output(f"{n} ls", env={"N_PREFIX": N_INSTALL_DIR}) + installed_versions = [ + version.split("/")[-1] for version in installed_versions_raw.strip().split("\n") + ] + return installed_versions + + +def patch_app(app, base_dir=""): + settings = _get_app_settings(app) + nodejs_version = settings.get("nodejs_version") + if nodejs_version is None or "." in str(nodejs_version): + return + + nodejs_version = str(nodejs_version) + + installed_versions = get_installed_nodejs_versions() + matching_versions = [ + v + for v in installed_versions + if v == nodejs_version or v.startswith(nodejs_version + ".") + ] + if not matching_versions: + logger.warning( + f"Uhoh, no matching version found among {installed_versions} for nodejs {nodejs_version} for app {app} ?" + ) + return + + sorted_versions = sorted( + matching_versions, key=lambda s: list(map(int, s.split("."))) + ) + actual_version = sorted_versions[-1] + + logger.debug( + f"Updating nodejs version setting for {app} from {nodejs_version} to {actual_version}" + ) + app_setting(app, "nodejs_version", actual_version) + + service_files_for_this_app_raw = check_output( + f'grep -lr "^User={app}$" "{base_dir}/etc/systemd/system" || true' + ).strip() + if not service_files_for_this_app_raw: + logger.debug(f"No service file to be patched for {app}") + return + + service_files_for_this_app = service_files_for_this_app_raw.split("\n") + + service_files_manually_modified = [] + for file in service_files_for_this_app: + cleaned_file = file.replace(base_dir, "") if base_dir else file + setting_name = f"checksum_{cleaned_file.replace('/', '_')}" + md5 = check_output(f"md5sum '{file}'").strip().split()[0] + if md5 != settings.get(setting_name): + service_files_manually_modified.append(cleaned_file) + + logger.debug( + f"Patching nodejs version for app {app} in {', '.join(service_files_for_this_app)} ..." + ) + old_node_path = f"/opt/node_n/n/versions/node/{nodejs_version}/bin" + new_node_path = f"/opt/node_n/n/versions/node/{actual_version}/bin" + os.system( + f"sed -i 's@{old_node_path}@{new_node_path}@g' {' '.join(service_files_for_this_app)}" + ) + for file in service_files_for_this_app: + cleaned_file = file.replace(base_dir, "") if base_dir else file + if cleaned_file in service_files_manually_modified: + continue + setting_name = f"checksum_{cleaned_file.replace('/', '_')}" + md5 = check_output(f"md5sum '{file}'").strip().split()[0] + app_setting(app, setting_name, md5) + + +class MyMigration(Migration): + introduced_in_version = "12.1" + dependencies = [] + + def run(self, *args): + for app in _installed_apps(): + patch_app(app) + + def run_before_app_restore(self, app, app_backup_in_archive): + return patch_app(app, base_dir=app_backup_in_archive) diff --git a/src/migrations/0036_migrate_to_trixie.py.disabled b/src/migrations/0036_migrate_to_trixie.py.disabled new file mode 100644 index 0000000..3197764 --- /dev/null +++ b/src/migrations/0036_migrate_to_trixie.py.disabled @@ -0,0 +1,657 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Devlopement notes to test the migration: +incus image copy yunohost:yunohost/bookworm-stable/core-tests local: --copy-aliases --auto-update +incus delete --force migration-to-trixie +incus launch yunohost/bookworm-stable/core-tests migration-to-trixie +incus exec migration-to-trixie -- apt update +incus exec migration-to-trixie -- apt full-upgrade -y +incus exec migration-to-trixie -- apt install -y python3-debian nano htop +incus snapshot create migration-to-trixie postinstall +--- +incus snapshot restore migration-to-trixie postinstall +incus file push src/migrations/0036_migrate_to_trixie.py.disabled migration-to-trixie/usr/lib/python3/dist-packages/yunohost/migrations/0036_trixie.py +incus exec migration-to-trixie -- yunohost tools migrations list +incus exec migration-to-trixie -- yunohost tools migrations run 0036 --accept-disclaimer + +""" + +# See https://www.debian.org/releases/trixie/release-notes/upgrading.html + +# Explicitly import packages to prevent an issue that may arise later because of python3.9 being replaced by 3.11 in the middle of the upgrade etc +# TRIXIE? import _strptime # noqa: F401 +import jinja2 +import logging +import re +import subprocess +import textwrap +from datetime import date +from pathlib import Path +from time import sleep + +import requests +from debian.deb822 import Deb822 + +# TRIXIE? import _ldap # noqa: F401 +from moulinette import Moulinette, m18n + +from ..app import app_list +from ..regenconf import manually_modified_files +from ..tools import Migration, _write_migration_state, tools_update +from ..utils.error import YunohostError +from ..utils.process import call_async_output, check_output +from ..utils.system import ( + _list_upgradable_apt_packages, + aptitude_with_progress_bar, + free_space_in_directory, + get_ynh_package_version, +) + +logger = logging.getLogger("yunohost.migration") + + +N_CURRENT_DEBIAN = 12 +N_CURRENT_YUNOHOST = 12 + +VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_trixie_upgrade.txt" + + +def _get_all_venvs(dir: Path, level: int = 0, maxlevel: int = 3) -> list[Path]: + """ + 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 dir.exists(): + return [] + + result = [] + # Using os functions instead of glob, because glob doesn't support hidden folders, and we need recursion with a fixed depth + for path in dir.iterdir(): + if path.is_dir(): + activatepath = path / "bin" / "activate" + if activatepath.is_file(): + content = activatepath.read_text() + 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(Path("/opt/")) + _get_all_venvs(Path("/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 + pip = venv / "bin" / "pip" + if not pip.is_file(): + logger.warning(f"Skipping venv {venv} because no 'pip' bin found") + continue + pip_freeze = subprocess.check_output([pip, "freeze"]).decode(encoding="utf-8") + pip_freeze = re.sub(r"^pkg(-|_)resources==.*$", "", pip_freeze) + (venv / VENV_REQUIREMENTS_SUFFIX).write_text(pip_freeze) + + +def unstable_apps() -> list[str]: + output = [] + # FIXME: update this at some point with apps that we know are not going to make it past the bookworm era + # in https://apps.yunohost.org/dash?filter=regressions_trixie + deprecated_apps = ["mailman", "ffsync"] + + for infos in app_list(full=True)["apps"]: + if ( + not infos.get("from_catalog") + or infos.get("from_catalog", {}).get("state") + in [ + "inprogress", + "notworking", + ] + or infos["id"] in deprecated_apps + ): + output.append(infos["id"]) + + return output + + +def download_gpg_key(url: str, file: Path) -> None: + armored = requests.get(url, timeout=900).content + dearmored = subprocess.check_output(["gpg", "--dearmor"], input=armored) + file.write_bytes(dearmored) + + +def package_is_installed(package: str) -> bool: + result = subprocess.run( + f"dpkg --list | grep '^ii ' | grep -q -w {package}", + check=False, + shell=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + return result.returncode == 0 + + +class MyMigration(Migration): + "Upgrade the system to Debian Trixie and Yunohost 13.x" + + mode = "manual" + + def run(self) -> None: + self.check_assertions() + + logger.info(m18n.n("migration_0036_start")) + + # Stupid stuff because resolvconf later wants to edit /etc/resolv.conf and will miserably crash if it's immutable + subprocess.check_call(["chattr", "-i", Path("/etc/resolv.conf").resolve()]) + + # Get requirements of the different venvs from python apps + _backup_pip_freeze_for_python_app_venvs() + + # Tell libc6 it's okay to restart system stuff during the upgrade + subprocess.run( + ["debconf-set-selections"], + input="libc6 libraries/restart-without-asking boolean true".encode("utf-8"), + check=True, + ) + + # Don't send an email to root about the postgresql migration. It should be handled automatically after. + if package_is_installed("postgresql-common"): + subprocess.run( + ["debconf-set-selections"], + input="postgresql-common postgresql-common/obsolete-major seen true".encode("utf-8"), + check=True, + ) + + # Do not restart services during apt upgrade, that we know for sure will be broken before a conf-regen + # FIXME : we should find a ~robust way to auto-delete the policy-rc.d file after the migration went through, + # ideally wrapping everything in a context manager somehow + self.prevent_services_restart_during_upgrade([ + "nginx", + "dovecot" + ]) + + # Hold import yunohost packages + apps_packages = self.get_apps_equivs_packages() + ynh_packages = [ + "yunohost", + "moulinette", + "ssowat", + "yunohost-admin", + "yunohost-portal", + ] + aptitude_with_progress_bar(" ".join(("hold", *ynh_packages, *apps_packages))) + + # + # Patch yunohost conflicts + # + logger.info(m18n.n("migration_0036_patch_yunohost_dpkg")) + + self.patch_yunohost_dpkg() + + # Patch sources.list + logger.info(m18n.n("migration_0036_patching_sources_list")) + + # Add new apt .deb signing key + download_gpg_key( + "https://repo.yunohost.org/keys/yunohost_trixie.asc", + Path("/usr/share/keyrings/yunohost-trixie.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 Trixie + # and that'll block the migration + download_gpg_key( + "https://packages.sury.org/php/apt.gpg", + Path("/usr/share/keyrings/sury_php.gpg"), + ) + + self.patch_apt_sources_list() + + # + # Main upgrade + # + logger.info(m18n.n("migration_0036_main_upgrade")) + + # + # Run apt update + # + + aptitude_with_progress_bar("update") + + # Specific trick because libluajit changed name **again** + # And also the t64 apocalypse that makes aptitude derp + # For some unknown reason, we need to run the upgrade in two separate steps + # First upgrade the luajit packages, then the t64 packages + # Running them together in a single full-upgrade doesn't work properly + + # Upgrade problematic libluajit packages + libluajit_pkgs = "libluajit2-5.1-2- libluajit2-5.1-common- libluajit-5.1-2+M libluajit-5.1-common+M" + aptitude_with_progress_bar( + f"full-upgrade {libluajit_pkgs} --show-why -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'" + ) + + # Upgrade the t64-related packages + aptitude_with_progress_bar( + f"full-upgrade {' '.join(self.apt_t64_packages_list())} --show-why -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'" + ) + + # For some reason aptitude is derping about python3 / python3-venv so try to explicitly tell to install python3.11 to replace 3.9... + # Note the '+M' prefix which is here to mark the packages as automatically installed + # FIXME: This is from bookworm migration, might not be needed for Trixie ... not sure ... keeping this code commented for now + # python_upgrade_list = "python3 python3.11+M python3.9- " + # if os.system('dpkg --list | grep -q "^ii python3.9-venv "') == 0: + # python_upgrade_list += "python3-venv+M python3.11-venv+M python3.9-venv-" + # aptitude_with_progress_bar( + # f"full-upgrade {python_upgrade_list} --show-why -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'" + # ) + + # Full upgrade of "every" packages except the yunohost ones which are held + 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" >_> ... + # FIXME: This is from bookworm migration, ... might be needed still required? + # regen_conf(names=["nsswitch"], force=True) + + if self.debian_major_version() == N_CURRENT_DEBIAN: + raise YunohostError("migration_0036_still_on_bookworm_after_main_upgrade") + + # Clean the mess + logger.info(m18n.n("migration_0036_cleaning_up")) + subprocess.run( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes", + shell=True, + check=True, + ) + subprocess.run("apt clean --assume-yes", shell=True, check=True) + + for filename in ["extra_php_version.list", "yarn.list"]: + legacy_file: Path = Path("/etc/apt/sources.list.d") / filename + if legacy_file.exists(): + logger.warning(m18n.n("migration_0036_apt_lists_file_still_exists", file=str(legacy_file))) + legacy_file.rename(legacy_file.with_name(legacy_file.name + ".legacy_bookworm")) + + # + # Stupid hack for stupid dnsmasq not picking up its new init.d script then breaking everything ... + # https://forum.yunohost.org/t/20676 + # + # 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_0036_yunohost_upgrade")) + aptitude_with_progress_bar(" ".join(("unhold", *ynh_packages))) + + full_upgrade_cmd = ( + "full-upgrade --show-why -o Dpkg::Options::='--force-confold' " + ) + full_upgrade_cmd += "yunohost yunohost-admin yunohost-portal moulinette ssowat " + + try: + aptitude_with_progress_bar(full_upgrade_cmd) + except Exception: + # Retry after unholding the app packages, maybe it can unlock the situation idk + if apps_packages: + aptitude_with_progress_bar("unhold " + ' '.join(apps_packages)) + aptitude_with_progress_bar(full_upgrade_cmd) + else: + # If the upgrade was sucessful, we want to unhold the apps packages + if apps_packages: + aptitude_with_progress_bar("unhold " + ' '.join(apps_packages)) + + # Mark this migration as completed before triggering the "new" migrations + _write_migration_state(self.id, "done") + + 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) + + # If running from the webadmin, restart the API after a delay + if Moulinette.interface.type == "api": + logger.warning(m18n.n("migration_0036_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]) + + if self.yunohost_major_version() != N_CURRENT_YUNOHOST + 1: + raise YunohostError( + "Still on YunoHost 12.x at the end of the migration, eh? Sounds like the migration didn't really complete!?", + raw_msg=True, + ) + + 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 bookworm (12.x) and yunohost 12.x + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be > 13.x but yunohost package + # would still be in 12.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) + migration_logs = sorted( + [ + file + for file in Path("/var/log/yunohost/operations").glob( + "*migrate*.log" + ) + # if file.stat().st_size > 10 * 1024 + ], + key=lambda file: file.stat().st_mtime, + ) + if migration_logs: + logger.info( + f"NB: the previous migration log id seems to be {migration_logs[-1]}. " + f"You can share it with the support team with : " + f"sudo yunohost log share {migration_logs[-1]}" + ) + except Exception: + # Yeah it's not that important ... it's to simplify support ... + pass + + raise YunohostError("migration_0036_not_bullseye") + + # Have > 1 Go free space on /var/ ? + if free_space_in_directory("/var/") / (1024**3) < 1.0: + raise YunohostError("migration_0036_not_enough_free_space") + + # Have > 70 MB free space on /boot/ ? + 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 'trixie' is already in the sources.list ... + # which means maybe a previous upgrade crashed and we're re-running it) + sources_list = Path("/etc/apt/sources.list") + if sources_list.exists() and " trixie " not in sources_list.read_text(): + 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 + # TODO: + 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_0036_system_not_fully_up_to_date") + + @property + def disclaimer(self): + # Avoid having a super long disclaimer + uncessary check if we ain't + # on bookworm / yunohost 12.x + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be 13.x but yunohost package + # would still be in 21.x... + if ( + not self.debian_major_version() == N_CURRENT_DEBIAN + and not self.yunohost_major_version() == N_CURRENT_YUNOHOST + ): + return None + + # TODO: + + message_jinja = textwrap.dedent("""\ + {%- if migration_recent and not beta -%} + 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 😉! + + {% endif -%} + You can read the full **BETA** release note, remaining known issues and \ +feedback from the community here: . \ +In particular, we encourage you to pay attention to the fact that: + - the LDAP integration has been reworked and may cause issue if \ +configurations are not properly regenerated during the upgrade. It is recommended \ +to double-check that you have a way to login **directly as root** in case things go \ +sideway (typically by SSHing from the local network, or through rescue access on a VPS). \ +*Or* at least keep a terminal open on the side, in which you are root (e.g. via `sudo -i`). + + {{ migration_0036_general_warning }} + {%- if problematic_apps %} + + {{ problematic_apps_warning }} + {%- for app in problematic_apps %} + - {{app}} + {%- endfor %} + {%- endif -%} + {%- if modified_files %} + + {{ modified_files_warning }} + {%- for file in modified_files %} + - {{file}} + {%- endfor %} + {% endif %} + """) + + return jinja2.Template(message_jinja).render( + beta=True, + migration_recent=date.today() < date(2026, 6, 1), # FIXME: maybe retweak this later for the stable trixie release + migration_0036_general_warning=m18n.n("migration_0036_general_warning"), + # Get list of problematic apps ? I.e. not official or community+working + problematic_apps=unstable_apps(), + problematic_apps_warning=m18n.n("migration_0036_problematic_apps_warning"), + # Manually modified files ? (c.f. yunohost service regen-conf) + modified_files=manually_modified_files(), + modified_files_warning=m18n.n("migration_0036_modified_files"), + ) + + def patch_apt_sources_list(self): + sources_list_d = Path("/etc/apt/sources.list.d") + dot_list: list[Path] = [*sources_list_d.glob("*.list")] + if (main_sources := Path("/etc/apt/sources.list")).exists(): + dot_list.append(main_sources) + + deb822s: list[Path] = [*sources_list_d.glob("*.sources")] + + # TODO: migrate to deb822? See apt modernize-sources after upgrade + + # This : + # - replace single 'bookworm' occurence by 'trixie' + # - comments lines containing "backports" + # - replace 'bookworm/updates' by 'trixie/updates' (or same with -) + # Special note about the security suite: + # https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html#security-archive + def patch_list_line(line: str) -> str: + line = ( + line.replace( + "/usr/share/keyrings/yunohost-bookworm.gpg", + "/usr/share/keyrings/yunohost-trixie.gpg", + ) + .replace( + "http://forge.yunohost.org/debian/ bookworm stable", + # FIXME: REPLACE WITH STABLE + "https://repo.yunohost.org/debian/ trixie unstable", + ) + .replace(" bookworm ", " trixie ") + .replace(" bookworm-", " trixie-") + ) + + if "backports" in line: + line = f"# {line}" + return line + + for file in dot_list: + lines = file.read_text().splitlines(keepends=True) + file.write_text("".join(patch_list_line(line) for line in lines)) + + for file in deb822s: + new_file_pars = [] + paragraphs = Deb822.iter_paragraphs(file.read_text()) + for paragraph in paragraphs: + paragraph["Suites"] = "trixie" + + if re.match( + r"^https?://(repo|forge)\.yunohost\.org.*", paragraph["URIs"] + ): + paragraph["URIs"] = "https://repo.yunohost.org/debian/" + paragraph["Signed-By"] = "/usr/share/keyrings/yunohost-trixie.gpg" + + new_file_pars.append(str(paragraph)) + + file.write_text("\n".join(new_file_pars)) + + # Stupid OVH has some repo configured which dont work with next debian and break apt... + for file in sources_list_d.glob("ovh-*.list"): + file.unlink() + + def prevent_services_restart_during_upgrade(self, services: list[str]) -> None: + # 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 ... + + shell_test = "true" + for service in services: + shell_test += f' || [[ "$1" =~ "{service}" ]]' + + policy_rc = Path("/usr/sbin/policy-rc.d") + policy_rc.write_text( + textwrap.dedent(f"""\ + #!/usr/bin/env bash + if {shell_test}; then + exit 101 + fi + """) + ) + policy_rc.chmod(755) + + def apt_t64_packages_list(self) -> list[str]: + # https://media1.tenor.com/m/vzLCBSJxywUAAAAd/quelle-indignit%C3%A9-sarkozy.gif + + installed_packages = check_output("apt list --installed 2>/dev/null | grep -v -F 'Listing...' | awk -F/ '{print $1}'", shell=True).strip().split("\n") + t64_packages = check_output("apt search '.*t64' 2>/dev/null | grep 't64/' | awk -F/ '{print $1}'", shell=True).strip().split("\n") + + t64_upgrades = [] + for t64_package in t64_packages: + package = t64_package.replace("t64", "") + if package in installed_packages: + t64_upgrades += [package + "-", t64_package + "+M"] + + return t64_upgrades + + 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 patch_yunohost_dpkg(self) -> None: + # + # 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 12.x + if self.yunohost_major_version() != N_CURRENT_YUNOHOST: + return + + dpkg_status = Path("/var/lib/dpkg/status") + + conflicts = check_output("dpkg-query -s yunohost | grep '^Conflicts:'").strip() + # We want to keep conflicting with apache/bind9 tho + new_conflicts = "Conflicts: apache2, bind9" + + depends = check_output("dpkg-query -s yunohost | grep '^Depends:'").strip() + depends_to_remove = [ + "dovecot-antispam", + "udisks2-zram", + "udisks2-bcache", + "sudo-ldap", + ] + new_depends = depends + for to_remove in depends_to_remove: + new_depends = new_depends.replace(f"{to_remove}, ", "") + + dpkg_status.write_text( + dpkg_status.read_text() + .replace(conflicts, new_conflicts) + .replace(depends, new_depends) + ) diff --git a/src/migrations/0037_upgrade_dkim_keys.py b/src/migrations/0037_upgrade_dkim_keys.py new file mode 100644 index 0000000..e48e840 --- /dev/null +++ b/src/migrations/0037_upgrade_dkim_keys.py @@ -0,0 +1,149 @@ +import hashlib +import os +import subprocess +from logging import getLogger + +from moulinette import m18n +from yunohost.domain import domain_list +from yunohost.dyndns import dyndns_list, dyndns_update +from yunohost.service import service_restart +from yunohost.tools import Migration +from yunohost.utils.file_utils import chmod, chown, cp, rm +from yunohost.utils.mail import get_pending_mails_nb + +from ..utils.error import YunohostError + +logger = getLogger("yunohost.migration") + + +def get_upgradable_domains(): + """Find domains with a 1024 bits DKIM to upgrade""" + # Avoid to filter by mail_in and mail_out features cause + # 1024 bits keys could already exists, and features could + # be reactivated + mail_domains = domain_list()["domains"] + for domain in mail_domains: + domain_key = f"/etc/dkim/{domain}.mail.key" + # Do not recreate the key if it does not exist + if not os.path.isfile(domain_key): + continue + + # Do not recreate keys bigger than 1024 bits (about 16 lines) + with open(domain_key, "r") as f: + # Here we used an aproximative way to check key size + # In order to avoid loading a crypto lib + if len(f.readlines()) > 20: + continue + yield domain + + +class MyMigration(Migration): + """Replace 1024 bits DKIM keys by 2048 bits""" + + introduced_in_version = "12.1" + dependencies: list[str] = [] + upgradable_domains = set(get_upgradable_domains()) + dyndns_domains = set(dyndns_list()["domains"]) + + @property + def manual_domains(self): + domains = self.upgradable_domains - self.dyndns_domains + domains = [domain for domain in domains if not domain.endswith(".local")] + return domains + + @property + def mode(self): + if not self.upgradable_domains: + return "auto" + + return "manual" + + @property + def disclaimer(self): + if self.upgradable_domains: + domains = "\n - " + "\n - ".join(self.upgradable_domains) + else: + domains = "no domains seems concerned" + + return m18n.n("migration_0037_upgrade_dkim_keys_disclaimer", domains=domains) + + def check_assertions(self): + try: + pending_mails = get_pending_mails_nb() + except (ValueError, subprocess.CalledProcessError): + return + if pending_mails > 0: + raise YunohostError( + "migration_0037_upgrade_dkim_keys_pending_mails", + pending_mails=pending_mails, + ) + + def run(self, *args): + self.check_assertions() + # Find 1024 bits keys to upgrade + # Deal with potential admin customization with + # domains sharing the same 1048 bits dkim keys + dkim_keys = {} + for domain in self.upgradable_domains: + domain_key = f"/etc/dkim/{domain}.mail.key" + + with open(domain_key, "rb", buffering=0) as f: + dkim_key_hash = hashlib.file_digest(f, "sha256").hexdigest() + + if dkim_key_hash not in dkim_keys: + dkim_keys[dkim_key_hash] = [] + + dkim_keys[dkim_key_hash].append(domain) + + # Generate 2048 bits keys for each 1024 bits dkim keys + for dkim_key_hash, domains in dkim_keys.items(): + try: + subprocess.check_call( + [ + "opendkim-genkey", + "-d", + domains[0], + "-b", + "2048", + "--selector=mail", + "--directory=/etc/dkim", + ] + ) + except subprocess.CalledProcessError: + logger.error( + m18n.n( + "migration_0037_upgrade_dkim_keys_failed", + domains=", ".join(domains), + ) + ) + continue + + with open("/etc/dkim/mail.txt", "r") as f: + data = f.read() + + for domain in domains: + cp("/etc/dkim/mail.private", f"/etc/dkim/{domain}.mail.key") + with open(f"/etc/dkim/{domain}.mail.txt", "w") as file: + file.write(data.replace(domains[0], domain)) + rm("/etc/dkim/mail.private") + rm("/etc/dkim/mail.txt") + + # Reapply permissions just in case + chmod("/etc/dkim/", mode=0o700, recursive=True) + chown("/etc/dkim/", uid="opendkim", gid="root") + + # Restart opendkim + service_restart("opendkim") + + # If an upgradable domain is a dyndns domain, update dyndns + if self.upgradable_domains & self.dyndns_domains: + dyndns_update(force=True) + + # If an upgradable domain is a dyndns domain, update dyndns + if self.manual_domains: + domains = "\n - " + "\n - ".join(self.manual_domains) + logger.warning( + m18n.n( + "migration_0037_upgrade_dkim_keys_manual_action", domains=domains + ) + ) diff --git a/src/migrations/__init__.py b/src/migrations/__init__.py new file mode 100644 index 0000000..3b3672e --- /dev/null +++ b/src/migrations/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/src/migrations/postgresql.py b/src/migrations/postgresql.py new file mode 100644 index 0000000..7bbf8bf --- /dev/null +++ b/src/migrations/postgresql.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import subprocess +import time +from logging import getLogger + +from moulinette import m18n + +from ..tools import Migration +from ..utils.error import YunohostError, YunohostValidationError +from ..utils.system import free_space_in_directory, space_used_by_directory + +logger = getLogger("yunohost.migration") + + +class PostgreSQLMigration(Migration): + "Migrate DBs between Postgresql versions after migrating to a new Debian version" + + # Provided by calling class + previous_version: str + target_version: str + + def run(self): + if ( + os.system( + 'grep -A10 "ynh-deps" /var/lib/dpkg/status | grep -E "Package:|Depends:" | grep -B1 postgresql' + ) + != 0 + ): + logger.info("No YunoHost app seem to require postgresql... Skipping!") + return + + if not self.package_is_installed(f"postgresql-{self.previous_version}"): + logger.warning(m18n.n("migration_postgresql_previous_not_installed")) + return + + if not self.package_is_installed(f"postgresql-{self.target_version}"): + raise YunohostValidationError( + "migration_postgresql_target_not_installed", + previous=self.previous_version, + target=self.target_version, + ) + + # Make sure there's a 15 cluster + try: + self.runcmd(f"pg_lsclusters | grep -q '^{self.previous_version} '") + except Exception: + logger.warning( + f"It looks like there's not active {self.previous_version} cluster, so probably don't need to run this migration" + ) + return + + if not space_used_by_directory( + f"/var/lib/postgresql/{self.previous_version}" + ) > free_space_in_directory("/var/lib/postgresql"): + raise YunohostValidationError( + "migration_not_enough_space", path="/var/lib/postgresql/" + ) + + self.runcmd("systemctl stop postgresql") + time.sleep(3) + self.runcmd( + f"LC_ALL=C pg_dropcluster --stop {self.target_version} main || true" + ) # We do not trigger an exception if the command fails because that probably means cluster self.target_version doesn't exists, which is fine because it's created during the pg_upgradecluster) + time.sleep(3) + self.runcmd( + f"LC_ALL=C pg_upgradecluster -m upgrade {self.previous_version} main -v {self.target_version}" + ) + self.runcmd(f"LC_ALL=C pg_dropcluster --stop {self.previous_version} main") + self.runcmd("systemctl start postgresql") + + def package_is_installed(self, package_name): + (returncode, out, err) = self.runcmd( + "dpkg --list | grep '^ii ' | grep -q -w {}".format(package_name), + raise_on_errors=False, + ) + return returncode == 0 + + def runcmd(self, cmd, raise_on_errors=True): + logger.debug("Running command: " + cmd) + + p = subprocess.Popen( + cmd, + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + out, err = p.communicate() + returncode = p.returncode + if raise_on_errors and returncode != 0: + raise YunohostError( + "Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format( + cmd, returncode, out, err + ) + ) + + out = out.strip().split(b"\n") + return (returncode, out, err) diff --git a/src/migrations/python.py b/src/migrations/python.py new file mode 100644 index 0000000..6754617 --- /dev/null +++ b/src/migrations/python.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +from logging import getLogger + +from moulinette import m18n + +from ..tools import Migration, tools_migrations_state +from ..utils.file_utils import rm +from ..utils.process import call_async_output +from ..utils.system import debian_version + +logger = getLogger("yunohost.migration") + + +class PythonMigration(Migration): + """ + After the update, recreate a python virtual env based on the previously + generated requirements file + """ + + ignored_python_apps = [ + "diacamma", # Does an ugly sed in the sites-packages/django_auth_ldap3_ad + "homeassistant", # uses a custom version of Python + "immich", # uses a custom version of Python + "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 + ] + + migration_id: str + state = None + + def venv_requirements_suffix(self) -> str: + return f".requirements_backup_for_{debian_version()}_upgrade.txt" + + def extract_app_from_venv_path(self, venv_path: str) -> str: + venv_path = venv_path.replace("/var/www/", "") + venv_path = venv_path.replace("/opt/yunohost/", "") + venv_path = venv_path.replace("/opt/", "") + return venv_path.split("/")[0] + + def _get_all_venvs(self, dir: str, level: int = 0, maxlevel: int = 3) -> list[str]: + """ + 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 [] + + # Using os functions instead of glob, because glob doesn't support hidden + # folders, and we need recursion with a fixed depth + result: list[str] = [] + 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) and os.path.isfile( + path + self.venv_requirements_suffix() + ): + result.append(path) + continue + if level < maxlevel: + result += self._get_all_venvs(path, level=level + 1) + return result + + def is_pending(self): + if not self.state: + self.state = tools_migrations_state()["migrations"].get( + self.migration_id, "pending" + ) + return self.state == "pending" + + @property + def mode(self): + if not self.is_pending(): + return "auto" + + if self._get_all_venvs("/opt/") + self._get_all_venvs("/var/www/"): + return "manual" + else: + return "auto" + + @property + def disclaimer(self): + # Avoid having a super long disclaimer to generate if migrations has + # been done + if not self.is_pending(): + return None + + # Disclaimer should be empty if in auto, otherwise it excepts the --accept-disclaimer option during debian postinst + if self.mode == "auto": + return None + + ignored_apps = [] + rebuild_apps = [] + + venvs = self._get_all_venvs("/opt/") + self._get_all_venvs("/var/www/") + for venv in venvs: + if not os.path.isfile(venv + self.venv_requirements_suffix()): + continue + + app_corresponding_to_venv = self.extract_app_from_venv_path(venv) + + # Search for ignore apps + if any( + app_corresponding_to_venv.startswith(app) + for app in self.ignored_python_apps + ): + ignored_apps.append(app_corresponding_to_venv) + else: + rebuild_apps.append(app_corresponding_to_venv) + + msg = m18n.n( + "migration_python_venv_rebuild_disclaimer_base", + debian_pretty=debian_version().title(), + ) + if rebuild_apps: + msg += "\n\n" + m18n.n( + "migration_python_venv_rebuild_disclaimer_rebuild", + rebuild_apps="\n - " + "\n - ".join(rebuild_apps), + ) + if ignored_apps: + msg += "\n\n" + m18n.n( + "migration_python_venv_rebuild_disclaimer_ignored", + ignored_apps="\n - " + "\n - ".join(ignored_apps), + ) + + return msg + + def run(self): + if self.mode == "auto": + return + + venvs = self._get_all_venvs("/opt/") + self._get_all_venvs("/var/www/") + for venv in venvs: + app_corresponding_to_venv = self.extract_app_from_venv_path(venv) + + # Search for ignore apps + if any( + app_corresponding_to_venv.startswith(app) + for app in self.ignored_python_apps + ): + rm(venv + self.venv_requirements_suffix()) + logger.info( + m18n.n( + "migration_python_venv_rebuild_broken_app", + app=app_corresponding_to_venv, + ) + ) + continue + + logger.info( + m18n.n( + "migration_python_venv_rebuild_in_progress", + app=app_corresponding_to_venv, + ) + ) + + # Recreate the venv + rm(venv, recursive=True) + callbacks = ( + lambda l: logger.debug("+ " + l.rstrip() + "\r"), + lambda l: logger.warning(l.rstrip()), + ) + call_async_output(["python", "-m", "venv", venv], callbacks) + status = call_async_output( + [ + f"{venv}/bin/pip", + "install", + "-r", + venv + self.venv_requirements_suffix(), + ], + callbacks, + ) + if status != 0: + logger.error( + m18n.n( + "migration_python_venv_rebuild_failed", + app=app_corresponding_to_venv, + ) + ) + else: + rm(venv + self.venv_requirements_suffix()) diff --git a/src/permission.py b/src/permission.py new file mode 100644 index 0000000..05454da --- /dev/null +++ b/src/permission.py @@ -0,0 +1,1052 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import copy +import grp +import os +import random +import re +from logging import getLogger +from typing import ( + TYPE_CHECKING, + BinaryIO, + Literal, + Mapping, + NotRequired, + TypedDict, + cast, +) + +from moulinette import m18n + +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import read_yaml, write_to_yaml + +if TYPE_CHECKING: + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.permission")) +else: + logger = getLogger("yunohost.permission") + +SYSTEM_PERMS: dict[str, dict] = { + "mail": {"label": "Email", "gid": 5001}, + "sftp": {"label": "SFTP", "gid": 5004}, + "ssh": {"label": "SSH", "gid": 5003}, +} +SYSTEM_PERM_CONF = "/etc/yunohost/permissions.yml" + + +class SystemPermInfos(TypedDict): + label: NotRequired[str] + allowed: list[str] + corresponding_users: NotRequired[list[str] | set[str]] + protected: NotRequired[bool] + + +class AppPermInfos(SystemPermInfos): + url: str | None + additional_urls: list[str] + auth_header: bool + show_tile: bool | None + hide_from_public: NotRequired[bool] + logo_hash: NotRequired[str] + description: NotRequired[str] + order: NotRequired[int] + + +PermInfos = AppPermInfos | SystemPermInfos + +# +# +# The followings are the methods exposed through the "yunohost user permission" interface +# +# + + +def user_permission_list( + full: bool = False, + ignore_system_perms: bool = False, + absolute_urls: bool = False, + apps: list[str] = [], +) -> dict[Literal["permissions"], dict[str, PermInfos]]: + """ + List permissions and corresponding accesses + """ + + # Fetch relevant informations + from .user import user_group_list + from .utils.app_utils import _get_app_settings, _installed_apps + + # Parse / organize information to be outputed + filter_ = apps + if filter_: + apps = sorted(a for a in filter_ if a not in SYSTEM_PERMS) + else: + apps = sorted(_installed_apps()) + + permissions: dict[str, PermInfos] = {} + for app in apps: + settings = _get_app_settings(app) + + subperms = settings.get("_permissions", {}) + if "main" not in subperms: + subperms["main"] = {} + + app_label = ( + subperms["main"].get("label") or settings.get("label") or app.title() + ) + + for subperm, infos in subperms.items(): + name = f"{app}.{subperm}" + perm: AppPermInfos = { + "label": "", + "url": None, + "additional_urls": [], + "auth_header": True, + "show_tile": None, # Automagically set to True by default if an url is defined and show_tile not provided + "protected": False, + "allowed": [], + } + perm.update(infos) + if subperm != "main": + # Redefine the subperm label to : () + subperm_label = perm["label"] or subperm + perm["label"] = f"{app_label} ({subperm_label})" + elif not perm["label"]: + perm["label"] = app_label + + if perm["show_tile"] is None and perm["url"] is not None: + perm["show_tile"] = True + + if absolute_urls: + if "domain" in settings and "path" in settings: + app_base_path = settings["domain"] + settings["path"] + else: + # Meh in some situation where the app is currently installed/removed, + # this function may be called and we still need to act as if the corresponding + # permission indeed exists ... dunno if that's really the right way to proceed but okay. + app_base_path = "" + + perm["url"] = ( + _get_absolute_url(perm["url"], app_base_path) + if perm["url"] is not None + else None + ) + perm["additional_urls"] = [ + _get_absolute_url(url, app_base_path) + for url in perm["additional_urls"] + ] + permissions[name] = perm + + if not ignore_system_perms and ( + not filter_ or any(p in filter_ for p in SYSTEM_PERMS.keys()) + ): + system_perm_conf = _get_system_perms() + for name, infos in SYSTEM_PERMS.items(): + if filter_ and name not in filter_: + continue + permissions[f"{name}.main"] = system_perm_conf[name] + + if full: + map_group_to_users = { + g: infos["members"] for g, infos in user_group_list()["groups"].items() + } + for infos in permissions.values(): + corresponding_users: set[str] = set() + for group in infos["allowed"]: + # FIXME: somewhere we may want to have some sort of garbage collection + # to automatically remove user/groups from the "allowed" info when they + # somehow disappeared from the system (for example this may happen when + # restoring an app on which not all the user/group exist) + users_in_group = set(map_group_to_users.get(group, [])) + corresponding_users |= users_in_group + infos["corresponding_users"] = list(sorted(corresponding_users)) + else: + # Keep the output concise when used without --full, meant to not bloat CLI + for infos in permissions.values(): + for key in [ + "additional_urls", + "auth_header", + "logo_hash", + "order", + "protected", + "show_tile", + ]: + if key in infos: + del infos[key] # type: ignore + + return {"permissions": permissions} + + +def user_permission_update( + permission: str, + add: str | list[str] | None = None, + remove: str | list[str] | None = None, + label: str | None = None, + show_tile: bool | None = None, + protected: bool | None = None, + force: bool = False, + sync_perm: bool = True, + log_success_as_debug: bool = False, +) -> PermInfos: + """ + Allow or Disallow a user or group to a permission for a specific application + + Keyword argument: + permission -- Name of the permission (e.g. mail or or wordpress or wordpress.editors) + add -- (optional) List of groups or usernames to add to this permission + remove -- (optional) List of groups or usernames to remove from to this permission + label -- (optional) Define a name for the permission. This label will be shown on the SSO and in the admin + show_tile -- (optional) Define if a tile will be shown in the SSO + protected -- (optional) Define if the permission can be added/removed to the visitor group + force -- (optional) Give the possibility to add/remove access from the visitor group to a protected permission + """ + from .app import app_ssowatconf + from .user import user_group_list + + # By default, manipulate main permission + if "." not in permission: + permission = permission + ".main" + + app = permission.split(".")[0] + existing_permission = user_permission_info(permission) + + if app in SYSTEM_PERMS: + # Refuse to add "visitors" to mail/ssh/sftp ... they require an account to make sense. + if add and "visitors" in add: + raise YunohostValidationError( + "permission_require_account", permission=permission + ) + # Refuse to add "all_users" to ssh/sftp permissions + if app in ["ssh", "sftp"] and (add and "all_users" in add) and not force: + raise YunohostValidationError( + "permission_cant_add_to_all_users", permission=permission + ) + # Label, show_tile and protected only make sense for actual apps + if any(v is not None for v in [label, show_tile, protected]): + raise YunohostValidationError( + f"Cannot change label, show_tile or protected for system permission {app}", + raw_msg=True, + ) + else: + # Refuse to add "visitors" to protected permission + if ( + ((add and "visitors" in add) or (remove and "visitors" in remove)) + and existing_permission.get("protected") + and not force + ): + raise YunohostValidationError("permission_protected", permission=permission) + + # Fetch currently allowed groups for this permission + current_allowed_groups = existing_permission["allowed"] + + # Compute new allowed group list (and make sure what we're doing make sense) + new_allowed_groups = copy.copy(current_allowed_groups) + all_existing_groups = user_group_list()["groups"].keys() + + if add: + groups_to_add = [add] if not isinstance(add, list) else add + for group in groups_to_add: + if group not in all_existing_groups: + raise YunohostValidationError("group_unknown", group=group) + if group in current_allowed_groups: + logger.warning( + m18n.n( + "permission_already_allowed", permission=permission, group=group + ) + ) + else: + new_allowed_groups += [group] + + if remove: + groups_to_remove = [remove] if not isinstance(remove, list) else remove + for group in groups_to_remove: + if group not in current_allowed_groups: + logger.warning( + m18n.n( + "permission_already_disallowed", + permission=permission, + group=group, + ) + ) + + new_allowed_groups = [ + g for g in new_allowed_groups if g not in groups_to_remove + ] + + # If we end up with something like allowed groups is ["all_users", "volunteers"] + # we shall warn the users that they should probably choose between one or + # the other, because the current situation is probably not what they expect + # / is temporary ? Note that it's fine to have ["all_users", "visitors"] + # though, but it's not fine to have ["all_users", "visitors", "volunteers"] + if "all_users" in new_allowed_groups and len(new_allowed_groups) >= 2: + if "visitors" not in new_allowed_groups or len(new_allowed_groups) >= 3: + logger.warning(m18n.n("permission_currently_allowed_for_all_users")) + + if ( + existing_permission.get("url") + and existing_permission["url"].startswith("re:") # type: ignore + and show_tile + ): + logger.warning( + m18n.n( + "regex_incompatible_with_tile", + regex=existing_permission["url"], # type: ignore + permission=permission, + ) + ) + + # Commit the new allowed group list + if app not in SYSTEM_PERMS: + _update_app_permission_setting( + permission=permission, + label=label, + show_tile=show_tile, + protected=protected, + allowed=new_allowed_groups, + ) + else: + system_perms = _get_system_perms() + system_perms[app]["allowed"] = list(sorted(new_allowed_groups)) + _set_system_perms(system_perms) + + if sync_perm: + _sync_permissions_with_ldap() + if app not in SYSTEM_PERMS: + app_ssowatconf() + + # This is meant to reduce noise during resource update/provisioning + # but display a "success" flash message when admins trigger this operation manually ? + if log_success_as_debug: + logger.debug(m18n.n("permission_updated", permission=permission)) + else: + logger.success(m18n.n("permission_updated", permission=permission)) + + return user_permission_info(permission) + + +def user_permission_info(permission: str) -> PermInfos: + """ + Return informations about a specific permission + + Keyword argument: + permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) + """ + + if "." in permission: + app = permission.split(".")[0] + else: + # By default, manipulate main permission if only an app name is provided + app = permission + permission = permission + ".main" + + # Fetch existing permission + + perms = user_permission_list(full=True, apps=[app])["permissions"] + perm = perms.get(permission) + if perm is None: + raise YunohostValidationError("permission_not_found", permission=permission) + + return perm + + +# +# +# The followings methods are *not* directly exposed. +# They are used to create/delete the permissions (e.g. during app install/remove) +# and by some app helpers to possibly add additional permissions +# +# + + +def permission_create( + permission: str, + allowed: str | list[str] | None = None, + url: str | None = None, + additional_urls: list[str] | None = None, + auth_header: bool = True, + show_tile: bool = False, + protected: bool = False, + sync_perm: bool = True, +) -> PermInfos: + """ + Create a new permission for a specific application + + Keyword argument: + permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) + allowed -- (optional) List of group/user to allow for the permission + url -- (optional) URL for which access will be allowed/forbidden + additional_urls -- (optional) List of additional URL for which access will be allowed/forbidden + auth_header -- (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application + show_tile -- (optional) Define if a tile will be shown in the SSO + protected -- (optional) Define if the permission can be added/removed to the visitor group + + If provided, 'url' 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' can be later treated as a 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]*$ + """ + + from .app import _is_installed, app_ssowatconf + from .user import user_group_list + + # By default, manipulate main permission + if "." not in permission: + permission = permission + ".main" + + app, subperm = permission.split(".") + + if allowed is not None: + if not isinstance(allowed, list): + allowed = [allowed] + + # Validate that the groups to add actually exist + all_existing_groups = user_group_list()["groups"].keys() + for group in allowed or []: + if group not in all_existing_groups: + raise YunohostValidationError("group_unknown", group=group) + + assert _is_installed(app), ( + f"'{app}' is not a currently installed app, can not create perm {permission}" + ) + + permission_url( + permission, + url=url, + add_url=additional_urls, + auth_header=auth_header, + sync_perm=False, + ) + + _update_app_permission_setting( + permission=permission, + show_tile=show_tile, + protected=protected, + allowed=allowed or [], + ) + + if sync_perm: + _sync_permissions_with_ldap() + app_ssowatconf() + + logger.debug(m18n.n("permission_created", permission=permission)) + return user_permission_info(permission) + + +def permission_url( + permission: str, + url: str | None = None, + add_url: list[str] | None = None, + remove_url: list[str] | None = None, + set_url: list[str] | None = None, + auth_header: bool | None = None, + clear_urls: bool = False, + sync_perm: bool = True, +) -> PermInfos: + """ + Update urls related to a permission for a specific application + + Keyword argument: + permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) + url -- (optional) URL for which access will be allowed/forbidden. + add_url -- (optional) List of additional url to add for which access will be allowed/forbidden + remove_url -- (optional) List of additional url to remove for which access will be allowed/forbidden + set_url -- (optional) List of additional url to set/replace for which access will be allowed/forbidden + auth_header -- (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application + clear_urls -- (optional) Clean all urls (url and additional_urls) + """ + from .app import app_setting, app_ssowatconf + + # By default, manipulate main permission + if "." not in permission: + permission = permission + ".main" + + app, sub_permission = permission.split(".") + + if app in SYSTEM_PERMS: + logger.warning(f"Cannot change urls / auth_header for system perm {permission}") + + if url or add_url: + domain = app_setting(app, "domain") + path = app_setting(app, "path") + if domain is None or path is None: + raise YunohostError("unknown_main_domain_path", app=app) + else: + assert isinstance(domain, str) + assert isinstance(path, str) + app_main_path = domain + path + + # Fetch existing permission + update_settings: AppPermInfos = {} # type: ignore + existing_permission = app_setting(app, "_permissions") or {} + assert isinstance(existing_permission, dict) + if sub_permission not in existing_permission: + existing_permission[sub_permission] = {} + existing_permission = existing_permission[sub_permission] + + if url is not None: + url = _validate_and_sanitize_permission_url(url, app_main_path, app) + update_settings["url"] = url + assert url + if url.startswith("re:") and existing_permission.get("show_tile"): + logger.warning( + m18n.n("regex_incompatible_with_tile", regex=url, permission=permission) + ) + update_settings["show_tile"] = False + + current_additional_urls = existing_permission.get("additional_urls", []) + new_additional_urls: list[str] = copy.copy(current_additional_urls) + + if add_url: + for ur in add_url: + if ur in current_additional_urls: + logger.warning( + m18n.n( + "additional_urls_already_added", permission=permission, url=ur + ) + ) + else: + ur = _validate_and_sanitize_permission_url(ur, app_main_path, app) + new_additional_urls += [ur] + + if remove_url: + for ur in remove_url: + if ur not in current_additional_urls: + logger.warning( + m18n.n( + "additional_urls_already_removed", permission=permission, url=ur + ) + ) + + new_additional_urls = [u for u in new_additional_urls if u not in remove_url] + + if set_url: + new_additional_urls = set_url + + # Guarantee uniqueness of all values, which would otherwise make ldap.update angry. + update_settings["additional_urls"] = list(set(new_additional_urls)) + + if auth_header is not None: + update_settings["auth_header"] = auth_header + + if clear_urls: + update_settings["url"] = None + update_settings["additional_urls"] = [] + update_settings["show_tile"] = False + + # Actually commit the change + try: + perm_settings = app_setting(app, "_permissions") or {} + assert isinstance(perm_settings, dict) + if sub_permission not in perm_settings: + perm_settings[sub_permission] = {} + + perm_settings[sub_permission].update(update_settings) + app_setting(app, "_permissions", perm_settings) + except Exception as e: + raise YunohostError("permission_update_failed", permission=permission, error=e) + + if sync_perm: + # In the past, this was a call to _sync_permissions_with_ldap but nowadays these changes dont impact ldap, only the ssowat conf + app_ssowatconf() + + logger.debug(m18n.n("permission_updated", permission=permission)) + return user_permission_info(permission) + + +def permission_delete( + permission: str, force: bool = False, sync_perm: bool = True +) -> None: + from .app import _assert_is_installed, app_setting, app_ssowatconf + + # By default, manipulate main permission + if "." not in permission: + permission = permission + ".main" + + if permission.endswith(".main") and not force: + raise YunohostValidationError("permission_cannot_remove_main") + + app, subperm = permission.split(".") + + if app in SYSTEM_PERMS: + raise YunohostValidationError( + f"Cannot delete system permission {permission}", raw_msg=True + ) + + _assert_is_installed(app) + + # Actually delete the permission + perm_settings = app_setting(app, "_permissions") or {} + assert isinstance(perm_settings, dict) + if subperm in perm_settings: + del perm_settings[subperm] + app_setting(app, "_permissions", perm_settings) + + if sync_perm: + _sync_permissions_with_ldap() + app_ssowatconf() + + logger.debug(m18n.n("permission_deleted", permission=permission)) + + +def _sync_permissions_with_ldap() -> None: + """ + Sychronize the 'memberUid' / 'inheritPermission' attributes in the ldap permission object + according to the group members and permission "allowed" info from app settings (from user_permission_list) + """ + + _garbarge_collect_permissions_for_nonexistent_users() + + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + + permissions_wanted = { + perm: set(infos["corresponding_users"]) + for perm, infos in user_permission_list(full=True)["permissions"].items() + } + permissions_current = { + entry["cn"][0]: set(entry.get("memberUid", [])) + for entry in ldap.search( + "ou=permission", "(objectclass=permissionYnh)", ["cn", "memberUid"] + ) + } + + # Compute the todolist by comparing the current state vs. the wanted state for each perm + todos_create: dict[str, set[str]] = {} + todos_delete: list[str] = [] + todos_update: dict[str, set[str]] = {} + + for perm in permissions_current.keys(): + if perm not in permissions_wanted: + todos_delete.append(perm) + for perm, members_wanted in permissions_wanted.items(): + if perm not in permissions_current: + todos_create[perm] = members_wanted + elif members_wanted != permissions_current[perm]: + todos_update[perm] = members_wanted + + # Actually perform the delete / create / update operations + + for perm in todos_delete: + logger.debug(f"Removing LDAP perm {perm}") + try: + ldap.remove(f"cn={perm},ou=permission") + except Exception as e: + raise YunohostError("permission_deletion_failed", permission=perm, error=e) + + all_gids = {str(x.gr_gid) for x in grp.getgrall()} + for perm in todos_create: + logger.debug(f"Creating LDAP perm {perm}") + app = perm.split(".")[0] + if app in SYSTEM_PERMS: + gid = str(SYSTEM_PERMS[app]["gid"]) + else: + while True: + gid = str(random.randint(200, 99999)) + if gid not in all_gids: + break + + # Save the gid to the list of existing gid, to avoid picking the same gid twice in the unlikely case where we would be creating several perm at the same time + all_gids.add(gid) + + attr_dict: Mapping[str, str | list[str]] = { + "objectClass": ["top", "permissionYnh", "posixGroup"], + "cn": perm, + "gidNumber": gid, + # NB: the "inheritPermission" and "memberUid" info is redundant + # but is needed because "memberUid" corresponds to the posixGroup object + # whereas inheritPermission automatically creates the symetric link + # from user to perm (cf the "permission" key on users) + # (cf the olcOverlay={2}memberof ) + "inheritPermission": list( + sorted( + f"uid={u},ou=users,dc=yunohost,dc=org" + for u in permissions_wanted[perm] + ) + ), + "memberUid": list(sorted(permissions_wanted[perm])), + } + try: + ldap.add(f"cn={perm},ou=permission", attr_dict) + except Exception as e: + raise YunohostError("permission_creation_failed", permission=perm, error=e) + for perm in todos_update: + logger.debug(f"Updating LDAP perm {perm}") + try: + # Same note about redundant memberUid vs inheritPermission as before + ldap.update( + f"cn={perm},ou=permission", + { + "inheritPermission": list( + sorted( + f"uid={u},ou=users,dc=yunohost,dc=org" + for u in permissions_wanted[perm] + ) + ), + "memberUid": list(sorted(permissions_wanted[perm])), + }, + ) + except Exception as e: + raise YunohostError("permission_update_failed", permission=perm, error=e) + + logger.debug("Permissions were resynchronized to LDAP") + + # Reload/invalidate unscd cache to full propagate the changes + os.system("nscd --invalidate=passwd") + os.system("nscd --invalidate=group") + + +def _update_app_permission_setting( + permission: str, + label: str | None = None, + show_tile: bool | None = None, + protected: bool | None = None, + allowed: str | list[str] | None = None, + logo: BinaryIO | Literal[""] | None = None, + description: str | None = None, + hide_from_public: bool | None = None, + order: int | None = None, +) -> None: + from .app import app_setting + + app, sub_permission = permission.split(".") + update_settings: AppPermInfos = {} # type: ignore + perm_settings = app_setting(app, "_permissions") or {} + assert isinstance(perm_settings, dict) + if sub_permission not in perm_settings: + perm_settings[sub_permission] = {} + + if label is not None: + update_settings["label"] = str(label) + + if description is not None: + update_settings["description"] = description + + if hide_from_public is not None: + update_settings["hide_from_public"] = hide_from_public + + if order is not None: + update_settings["order"] = order + + # Delete the logo hash info if the provided logo is literally empty string + if logo == "": + if "logo_hash" in perm_settings[sub_permission]: + del perm_settings[sub_permission]["logo_hash"] + + elif logo is not None: + import hashlib + + from .app_catalog import APPS_CATALOG_LOGOS + + logo_content = logo.read() + if not logo_content.startswith(b"\x89PNG\r\n\x1a\n"): + raise YunohostValidationError( + "The provided logo file doesn't seem to be a PNG file. Only PNG logos are supported.", + raw_msg=True, + ) + + logo_hash = hashlib.sha256(logo_content).hexdigest() + with open(f"{APPS_CATALOG_LOGOS}/{logo_hash}.png", "wb") as f: + f.write(logo_content) + + update_settings["logo_hash"] = logo_hash + + if protected is not None: + update_settings["protected"] = protected + + if show_tile is not None: + update_settings["show_tile"] = show_tile + existing_permission_url = perm_settings[sub_permission].get("url") + if show_tile is True: + if not existing_permission_url: + logger.warning( + m18n.n( + "show_tile_cant_be_enabled_for_url_not_defined", + permission=permission, + ) + ) + update_settings["show_tile"] = False + elif existing_permission_url.startswith("re:"): + logger.warning( + m18n.n("show_tile_cant_be_enabled_for_regex", permission=permission) + ) + update_settings["show_tile"] = False + + if "label" in update_settings and sub_permission == "main": + label = update_settings["label"] + app_setting(app, "label", label) + + if allowed is not None: + old_permission = user_permission_info(permission) + assert isinstance(allowed, list) or isinstance(allowed, str) + allowed = [allowed] if not isinstance(allowed, list) else allowed + # Guarantee uniqueness of values in allowed, which would otherwise make ldap.update angry. + allowed = list(set(allowed)) + update_settings["allowed"] = allowed + + # Actually update the settings + perm_settings[sub_permission].update(update_settings) + app_setting(app, "_permissions", perm_settings) + + # If we updated the allowed users + if allowed is not None: + # Trigger app callbacks + new_permission = user_permission_info(permission) + + old_corresponding_users = set(old_permission["corresponding_users"]) + new_corresponding_users = set(new_permission["corresponding_users"]) + + old_allowed_users = set(old_permission["allowed"]) + new_allowed_users = set(new_permission["allowed"]) + + effectively_added_users = new_corresponding_users - old_corresponding_users + effectively_removed_users = old_corresponding_users - new_corresponding_users + + effectively_added_group = ( + new_allowed_users - old_allowed_users - effectively_added_users + ) + effectively_removed_group = ( + old_allowed_users - new_allowed_users - effectively_removed_users + ) + + from .hook import hook_callback + + if effectively_added_users or effectively_added_group: + hook_callback( + "post_app_addaccess", + args=[ + app, + ",".join(effectively_added_users), + sub_permission, + ",".join(effectively_added_group), + ], + ) + if effectively_removed_users or effectively_removed_group: + hook_callback( + "post_app_removeaccess", + args=[ + app, + ",".join(effectively_removed_users), + sub_permission, + ",".join(effectively_removed_group), + ], + ) + + +def _get_system_perms() -> dict[str, SystemPermInfos]: + raw_system_perm_conf: dict[str, SystemPermInfos] + try: + raw_system_perm_conf = read_yaml(SYSTEM_PERM_CONF) or {} # type: ignore[assignment] + assert isinstance(raw_system_perm_conf, dict), ( + "Uhoh, the system perm conf read is not a dict ?!" + ) + except Exception as e: + logger.warning(f"Failed to read system perm configuration ? : {e}") + raw_system_perm_conf = {} + + system_perm_conf: dict[str, SystemPermInfos] = {} + for p, infos in raw_system_perm_conf.items(): + if p not in SYSTEM_PERMS.keys(): + logger.warning( + f"Ignoring unexpected key '{p}' in system perm conf {SYSTEM_PERM_CONF}" + ) + continue + if "allowed" not in infos: + infos["allowed"] = [] + system_perm_conf[p] = infos + + # Try to have a failsafe to keep admins allowed for ssh access and mail + # when the conf is broken for some reason... + if "ssh" not in system_perm_conf: + system_perm_conf["ssh"] = {"allowed": ["admins"]} + if "mail" not in system_perm_conf: + system_perm_conf["mail"] = {"allowed": ["admins"]} + if "sftp" not in system_perm_conf: + system_perm_conf["sftp"] = {"allowed": []} + + for p, infos in system_perm_conf.items(): + infos["label"] = SYSTEM_PERMS[p]["label"] + infos["protected"] = True + + return system_perm_conf + + +def _set_system_perms(system_perm_conf: dict[str, SystemPermInfos]) -> None: + # We actually only write the 'allowed' groups info + conf_to_write = { + p: {"allowed": infos["allowed"]} for p, infos in system_perm_conf.items() + } + + try: + write_to_yaml(SYSTEM_PERM_CONF, conf_to_write) # type: ignore[arg-type] + except Exception as e: + raise YunohostError( + f"Failed to write system perm configuration ? : {e}", raw_msg=True + ) + + +def _get_absolute_url(url: str, base_path: str) -> str: + """ + For example transform: + (/, domain.tld/) into domain.tld (no trailing /) + (/api, domain.tld/nextcloud) into domain.tld/nextcloud/api + (/api, domain.tld/nextcloud/) into domain.tld/nextcloud/api + (re:/foo.*, domain.tld/app) into re:domain\\.tld/app/foo.* + (domain.tld/bar, domain.tld/app) into domain.tld/bar + (some.other.domain/, domain.tld/app) into some.other.domain (no trailing /) + """ + base_path = base_path.rstrip("/") + if url.startswith("/"): + return base_path + url.rstrip("/") + if url.startswith("re:/"): + return "re:" + base_path.replace(".", "\\.") + url[3:] + else: + return url.rstrip("/") + + +def _validate_and_sanitize_permission_url( + url: str, app_base_path: str, app: str +) -> str: + """ + Check and normalize the urls passed for all permissions + Also check that the Regex is valid + + As documented in the 'ynh_permission_create' helper: + + If provided, 'url' 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 + domain.tld -> domain.tld + + 'url' can be later treated as a 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]*$ + + We can also have less-trivial regexes like: + re:^/api/.*|/scripts/api.js$ + """ + + from .domain import _assert_domain_exists + from .utils.app_utils import _assert_no_conflicting_apps + + # + # Regexes + # + + def validate_regex(regex): + if "%" in regex: + logger.warning( + "/!\\ Packagers! You are probably using a lua regex. You should use a PCRE regex instead." + ) + return + + try: + re.compile(regex) + except Exception: + raise YunohostValidationError("invalid_regex", regex=regex) + + if url.startswith("re:"): + # regex without domain + # we check for the first char after 're:' + if url[3] in ["/", "^", "\\"]: + validate_regex(url[3:]) + return url + + # regex with domain + + if "/" not in url: + raise YunohostValidationError("regex_with_only_domain") + domain, path = url[3:].split("/", 1) + path = "/" + path + + domain_with_no_regex = domain.replace("%", "").replace("\\", "") + _assert_domain_exists(domain_with_no_regex) + + validate_regex(path) + + return "re:" + domain + path + + # + # "Regular" URIs + # + + def split_domain_path(url: str) -> tuple[str, str]: + url = url.strip("/") + (domain, path) = url.split("/", 1) if "/" in url else (url, "/") + if path != "/": + path = "/" + path + return (domain, path) + + # uris without domain + if url.startswith("/"): + # if url is for example /admin/ + # we want sanitized_url to be: /admin + # and (domain, path) to be : (domain.tld, /app/admin) + sanitized_url = "/" + url.strip("/") + domain, path = split_domain_path(app_base_path) + path = "/" + path.strip("/") + sanitized_url + + # uris with domain + else: + # if url is for example domain.tld/wat/ + # we want sanitized_url to be: domain.tld/wat + # and (domain, path) to be : (domain.tld, /wat) + domain, path = split_domain_path(url) + sanitized_url = domain + path + + _assert_domain_exists(domain) + + _assert_no_conflicting_apps(domain, path, ignore_app=app) + + return sanitized_url + + +def _garbarge_collect_permissions_for_nonexistent_users() -> None: + # automatically remove user/groups from the "allowed" info when they + # somehow disappeared from the system (for example this may happen when + # restoring an app on which not all the user/group exist) + + from .app import app_setting + from .user import user_group_list + from .utils.app_utils import _installed_apps + + all_existing_groups = user_group_list()["groups"].keys() + for app in _installed_apps(): + perm_settings = app_setting(app, "_permissions") or {} + assert isinstance(perm_settings, dict) + for subperm, infos in perm_settings.items(): + for group in infos.get("allowed") or []: + if group not in all_existing_groups: + logger.debug( + f"Removing {group} from {app}.{subperm} permission because this user or group doesn't exist (anymore?)" + ) + infos["allowed"].remove(group) + app_setting(app, "_permissions", perm_settings) diff --git a/src/portal.py b/src/portal.py new file mode 100644 index 0000000..044a81d --- /dev/null +++ b/src/portal.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import logging +from pathlib import Path +from typing import Any, Union + +import ldap + +from .authenticators.ldap_ynhuser import Authenticator as Auth +from .authenticators.ldap_ynhuser import user_is_allowed_on_domain +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import read_json +from .utils.ldap import LDAPInterface, _get_ldap_interface, _ldap_path_extract +from .utils.password import ( + _hash_user_password, + assert_password_is_compatible, + assert_password_is_strong_enough, +) + +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 +) -> dict[str, Any]: + """ + 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_tile_theme": "simple", + "portal_title": "YunoHost", + "show_other_domains_apps": True, + "domain": domain, + "portal_allow_edit_email": False, + "portal_allow_edit_email_alias": False, + "portal_allow_edit_email_forward": False, + } + + portal_settings_path = Path(f"{PORTAL_SETTINGS_DIR}/{domain}.json") + + if portal_settings_path.exists(): + settings.update(read_json(str(portal_settings_path))) # type: ignore[arg-type] + # Portal may be public (no login required) + settings["public"] = bool(settings.pop("enable_public_apps_page", False)) + + # 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): + path_dict: dict[str, dict] = read_json(path) # type: ignore[assignment] + apps.update(path_dict["apps"]) + + if username: + # Add user allowed or public apps + settings["apps"] = { + app: infos + for app, infos in apps.items() + if username in infos["users"] or infos["public"] + } + elif settings["public"]: + # Add public apps (e.g. with "visitors" in group permission) + settings["apps"] = { + app: infos + for app, infos in apps.items() + if infos["public"] and not infos.get("hide_from_public") + } + + # Sort dictionnary according to the "order" info + settings["apps"] = dict( + sorted( + [(app, infos) for app, infos in settings["apps"].items()], + key=lambda v: (v[1].get("order", 100), v[0]), + ) + ) + + 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() + + try: + Auth().get_session_cookie() + except Exception: + 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, + mail: 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 .domain import domain_list + + domains = domain_list()["domains"] + username, domain, current_user = _get_user_infos( + ["givenName", "sn", "cn", "mail", "maildrop", "memberOf"] + ) + new_attr_dict: dict[str, Any] = {} + portal_settings = _get_portal_settings(domain, username) + + 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() + + new_mails = current_user["mail"] + + if mail is not None: + is_allowed_to_edit_main_email = portal_settings["portal_allow_edit_email"] + if not is_allowed_to_edit_main_email: + raise YunohostValidationError("mail_edit_operation_unauthorized") + + if mail not in new_mails: + local_part, domain = mail.split("@") + if local_part in ADMIN_ALIASES: + raise YunohostValidationError("mail_unavailable") + + try: + _get_ldap_interface().validate_uniqueness({"mail": mail}) + except YunohostError: + raise YunohostValidationError("mail_already_exists", mail=mail) + + if domain not in domains or not user_is_allowed_on_domain(username, domain): + raise YunohostValidationError("mail_alias_unauthorized", domain=domain) + new_mails[0] = mail + else: + # email already exist in the list we just move it on the first place + new_mails.remove(mail) + new_mails = [mail] + new_mails[1:] + + new_attr_dict["mail"] = new_mails + + if mailalias is not None: + is_allowed_to_edit_mail_alias = portal_settings["portal_allow_edit_email_alias"] + if not is_allowed_to_edit_mail_alias: + raise YunohostValidationError("mail_edit_operation_unauthorized") + + mailalias = [mail.strip() for mail in mailalias if mail and mail.strip()] + # keep first current mail unaltered + mails = [new_mails[0]] + + for index, mail in enumerate(mailalias): + if mail in new_mails: + if mail != new_mails[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: + is_allowed_to_edit_mail_forward = portal_settings[ + "portal_allow_edit_email_forward" + ] + if not is_allowed_to_edit_mail_forward: + raise YunohostValidationError("mail_edit_operation_unauthorized") + + 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"], + "mail": new_attr_dict["mail"][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 new file mode 100644 index 0000000..f1c3f6f --- /dev/null +++ b/src/regenconf.py @@ -0,0 +1,756 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import hashlib +import json +import os +import shutil +from datetime import datetime +from difflib import unified_diff +from logging import getLogger +from typing import TYPE_CHECKING, Any, cast + +import yaml +from moulinette import m18n + +from .hook import hook_callback, hook_list +from .log import is_unit_operation +from .utils.error import YunohostError +from .utils.file_utils import mkdir +from .utils.process import check_output + +BASE_CONF_PATH = "/var/cache/yunohost/regenconf" +BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, "backup") +PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, "pending") +REGEN_CONF_FILE = "/etc/yunohost/regenconf.yml" + +if TYPE_CHECKING: + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.regenconf")) +else: + logger = getLogger("yunohost.regenconf") + + +# FIXME : those ain't just services anymore ... what are we supposed to do with this ... +# FIXME : check for all reference of 'service' close to operation_logger stuff +@is_unit_operation([("names", "configuration")]) +def regen_conf( + operation_logger, + names=None, + with_diff=False, + force=False, + dry_run=False, + list_pending=False, +) -> dict[str, dict[str, Any]]: + """ + Regenerate the configuration file(s) + + Keyword argument: + names -- Categories to regenerate configuration of + with_diff -- Show differences in case of configuration changes + force -- Override all manual modifications in configuration files + dry_run -- Show what would have been regenerated + list_pending -- List pending configuration files and exit + + """ + + from .settings import settings_get + + all_available_conf_regen_categories = hook_list( + "conf_regen", list_by="name", show_info=False + )["hooks"] + + if names is None: + names = [] + elif names and isinstance(names, list): + unknowns = [ + name for name in names if name not in all_available_conf_regen_categories + ] + names = [name for name in names if name in all_available_conf_regen_categories] + if not names: + raise YunohostError( + f"No regen-conf categories named '{', '.join(unknowns)}'", raw_msg=True + ) + else: + for name in unknowns: + logger.warning(f"No regen-conf category named '{name}'") + + result = {} + + # Return the list of pending conf + if list_pending: + pending_conf = _get_pending_conf(names) + + if not with_diff: + return pending_conf + + for category, conf_files in pending_conf.items(): + for system_path, pending_path in conf_files.items(): + pending_conf[category][system_path] = { + "pending_conf": pending_path, + "diff": _get_files_diff(system_path, pending_path, True), + } + + return pending_conf + + if not dry_run: + operation_logger.related_to = [("configuration", x) for x in names] + if not names: + operation_logger.name_parameter_override = "all" + elif len(names) != 1: + operation_logger.name_parameter_override = ( + str(len(operation_logger.related_to)) + "_categories" + ) + operation_logger.start() + + # Clean pending conf directory + if os.path.isdir(PENDING_CONF_DIR): + if not names: + shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True) + else: + for name in names: + shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True) + else: + mkdir(PENDING_CONF_DIR, 0o755, True) + + # Execute hooks for pre-regen + # element 2 and 3 with empty string is because of legacy... + pre_args = ["pre", "", ""] + + def _pre_call(name, priority, path, args): + # create the pending conf directory for the category + category_pending_path = os.path.join(PENDING_CONF_DIR, name) + mkdir(category_pending_path, 0o755, True, uid="root") + + # return the arguments to pass to the script + return pre_args + [ + category_pending_path, + ] + + ssh_explicitly_specified = isinstance(names, list) and "ssh" in names + + # By default, we regen everything + if not names: + names = all_available_conf_regen_categories + + # [Optimization] We compute and feed the domain list to the conf regen + # hooks to avoid having to call "yunohost domain list" so many times which + # ends up in wasted time (about 3~5 seconds per call on a RPi2) + from .domain import domain_list + + env = {} + # Well we can only do domain_list() if postinstall is done ... + # ... but hooks that effectively need the domain list are only + # called only after the 'installed' flag is set so that's all good, + # though kinda tight-coupled to the postinstall logic :s + if os.path.exists("/etc/yunohost/installed"): + env["YNH_DOMAINS"] = " ".join(domain_list()["domains"]) + env["YNH_MAIN_DOMAINS"] = " ".join( + domain_list(exclude_subdomains=True)["domains"] + ) + env["YNH_DOMAINS_WITH_MAIL_IN"] = " ".join( + domain_list(features=["mail_in"])["domains"] + ) + env["YNH_DOMAINS_WITH_MAIL_IN_AND_OUT"] = " ".join( + domain_list(features=["mail_in", "mail_out"])["domains"] + ) + + env["YNH_CONTEXT"] = "regenconf" + env["YNH_HELPERS_VERSION"] = "2" + # 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)) + env["FORCE"] = "true" if force else "false" + + pre_result = hook_callback("conf_regen", names, pre_callback=_pre_call, env=env) + + # Keep only the hook names with at least one success + names = [ + hook + for hook, infos in pre_result.items() + if any(result["state"] == "succeed" for result in infos.values()) + ] + + # FIXME : what do in case of partial success/failure ... + if not names: + ret_failed = [ + hook + for hook, infos in pre_result.items() + if any(result["state"] == "failed" for result in infos.values()) + ] + raise YunohostError("regenconf_failed", categories=", ".join(ret_failed)) + + # Set the processing method + _regen = _process_regen_conf if not dry_run else lambda *a, **k: True + + operation_logger.related_to = [] + + # Iterate over categories and process pending conf + for category, conf_files in _get_pending_conf(names).items(): + if not dry_run: + operation_logger.related_to.append(("configuration", category)) + + if dry_run: + logger.debug(m18n.n("regenconf_pending_applying", category=category)) + else: + logger.debug(m18n.n("regenconf_dry_pending_applying", category=category)) + + conf_hashes = _get_conf_hashes(category) + succeed_regen = {} + failed_regen = {} + + # Here we are doing some weird legacy shit + # The thing is, on some very old or specific setup, the sshd_config file + # was absolutely not managed by the regenconf ... + # But we now want to make sure that this file is managed. + # However, we don't want to overwrite a specific custom sshd_config + # which may make the admin unhappy ... + # So : if the hash for this file does not exists, we set the hash as the + # hash of the pending configuration ... + # That way, the file will later appear as manually modified. + sshd_config = "/etc/ssh/sshd_config" + if ( + category == "ssh" + and sshd_config not in conf_hashes + and sshd_config in conf_files + ): + conf_hashes[sshd_config] = _calculate_hash(conf_files[sshd_config]) + _update_conf_hashes(category, conf_hashes) + + # Consider the following scenario: + # - you add a domain foo.bar + # - the regen-conf creates file /etc/dnsmasq.d/foo.bar + # - the admin manually *deletes* /etc/dnsmasq.d/foo.bar + # - the file is now understood as manually deleted because there's the old file hash in regenconf.yml + # + # ... so far so good, that's the expected behavior. + # + # But then: + # - the admin remove domain foo.bar entirely + # - but now the hash for /etc/dnsmasq.d/foo.bar is *still* in + # regenconf.yml and and the file is still flagged as manually + # modified/deleted... And the user cannot even do anything about it + # except removing the hash in regenconf.yml... + # + # Expected behavior: it should forget about that + # hash because dnsmasq's regen-conf doesn't say anything about what's + # the state of that file so it should assume that it should be deleted. + # + # - then the admin tries to *re-add* foo.bar ! + # - ... but because the file is still flagged as manually modified + # the regen-conf refuses to re-create the file. + # + # Excepted behavior : the regen-conf should have forgot about the hash + # from earlier and this wouldnt happen. + # ------ + # conf_files contain files explicitly set by the current regen conf run + # conf_hashes contain all files known from the past runs + # we compare these to get the list of stale hashes and flag the file as + # "should be removed" + stale_files = set(conf_hashes.keys()) - set(conf_files.keys()) + stale_files_with_non_empty_hash = [f for f in stale_files if conf_hashes.get(f)] + for f in stale_files_with_non_empty_hash: + conf_files[f] = None + # End discussion about stale file hashes + + force_update_hashes_for_this_category = False + + for system_path, pending_path in conf_files.items(): + logger.debug( + "processing pending conf '%s' to system conf '%s'", + pending_path, + system_path, + ) + conf_status = None + regenerated = False + + # Get the diff between files + conf_diff = ( + _get_files_diff(system_path, pending_path, True) if with_diff else None + ) + + # Check if the conf must be removed + to_remove = ( + True if pending_path and os.path.getsize(pending_path) == 0 else False + ) + + # Retrieve and calculate hashes + system_hash = _calculate_hash(system_path) + saved_hash = conf_hashes.get(system_path, None) + new_hash = None if to_remove else _calculate_hash(pending_path) + + # -> configuration was previously managed by yunohost but should now + # be removed / unmanaged + if system_path in stale_files_with_non_empty_hash: + # File is already deleted, so let's just silently forget about this hash entirely + if not system_hash: + logger.debug("> forgetting about stale file/hash") + conf_hashes[system_path] = None + conf_status = "forget-about-it" + regenerated = True + # Otherwise there's still a file on the system but it's not managed by + # Yunohost anymore... But if user requested --force we shall + # force-erase it + elif force: + logger.debug("> force-remove stale file") + regenerated = _regen(system_path) + conf_status = "force-removed" + # Otherwise, flag the file as manually modified + else: + logger.warning( + m18n.n("regenconf_file_manually_modified", conf=system_path) + ) + conf_status = "modified" + + # -> system conf does not exists + elif not system_hash: + if to_remove: + logger.debug("> system conf is already removed") + os.remove(pending_path) + conf_hashes[system_path] = None + conf_status = "forget-about-it" + force_update_hashes_for_this_category = True + continue + elif not saved_hash or force: + if force: + logger.debug("> system conf has been manually removed") + conf_status = "force-created" + else: + logger.debug("> system conf does not exist yet") + conf_status = "created" + regenerated = _regen(system_path, pending_path, save=False) + else: + logger.info( + m18n.n("regenconf_file_manually_removed", conf=system_path) + ) + conf_status = "removed" + + # -> system conf is not managed yet + elif not saved_hash: + logger.debug("> system conf is not managed yet") + if system_hash == new_hash: + logger.debug("> no changes to system conf has been made") + conf_status = "managed" + regenerated = True + elif not to_remove: + # If the conf exist but is not managed yet, and is not to be removed, + # we assume that it is safe to regen it, since the file is backuped + # anyway (by default in _regen), as long as we warn the user + # appropriately. + logger.info( + m18n.n( + "regenconf_now_managed_by_yunohost", + conf=system_path, + category=category, + ) + ) + regenerated = _regen(system_path, pending_path) + conf_status = "new" + elif force: + regenerated = _regen(system_path) + conf_status = "force-removed" + else: + logger.info( + m18n.n( + "regenconf_file_kept_back", + conf=system_path, + category=category, + ) + ) + conf_status = "unmanaged" + + # -> system conf has not been manually modified + elif system_hash == saved_hash: + if to_remove: + regenerated = _regen(system_path) + conf_status = "removed" + elif system_hash != new_hash: + regenerated = _regen(system_path, pending_path) + conf_status = "updated" + else: + logger.debug("> system conf is already up-to-date") + os.remove(pending_path) + continue + + else: + logger.debug("> system conf has been manually modified") + if system_hash == new_hash: + logger.debug("> new conf is as current system conf") + conf_status = "managed" + regenerated = True + elif ( + force + and system_path == sshd_config + and not ssh_explicitly_specified + ): + logger.warning(m18n.n("regenconf_need_to_explicitly_specify_ssh")) + conf_status = "modified" + elif force: + regenerated = _regen(system_path, pending_path) + conf_status = "force-updated" + else: + logger.warning( + m18n.n("regenconf_file_manually_modified", conf=system_path) + ) + conf_status = "modified" + + # Store the result + conf_result = {"status": conf_status} + if conf_diff is not None: + conf_result["diff"] = conf_diff + if regenerated: + succeed_regen[system_path] = conf_result + conf_hashes[system_path] = new_hash + if pending_path and os.path.isfile(pending_path): + os.remove(pending_path) + else: + failed_regen[system_path] = conf_result + + # Check for category conf changes + if not succeed_regen and not failed_regen: + logger.debug(m18n.n("regenconf_up_to_date", category=category)) + continue + elif not failed_regen: + if not dry_run: + logger.success(m18n.n("regenconf_updated", category=category)) + else: + logger.success(m18n.n("regenconf_would_be_updated", category=category)) + + if (succeed_regen or force_update_hashes_for_this_category) and not dry_run: + _update_conf_hashes(category, conf_hashes) + + # Append the category results + result[category] = {"applied": succeed_regen, "pending": failed_regen} + + # Return in case of dry run + if dry_run: + return result + + # Execute hooks for post-regen + # element 2 and 3 with empty string is because of legacy... + post_args = ["post", "", ""] + + def _pre_call2(name, priority, path, args): + # append coma-separated applied changes for the category + if name in result and result[name]["applied"]: + regen_conf_files = ",".join(result[name]["applied"].keys()) + else: + regen_conf_files = "" + return post_args + [ + regen_conf_files, + ] + + hook_callback("conf_regen", names, pre_callback=_pre_call2, env=env) + + operation_logger.success() + + return result + + +def _get_regenconf_infos(): + """ + Get a dict of regen conf informations + """ + try: + with open(REGEN_CONF_FILE, "r") as f: + data = yaml.safe_load(f) + # Cleanup legacy + if "metronome" in data: + del data["metronome"] + if "rspamd" in data: + del data["rspamd"] + return data + except Exception: + return {} + + +def _save_regenconf_infos(infos): + """ + Save the regen conf informations + Keyword argument: + categories -- A dict containing the regenconf infos + """ + + try: + with open(REGEN_CONF_FILE, "w") as f: + yaml.safe_dump(infos, f, default_flow_style=False) + except Exception as e: + logger.warning( + f"Error while saving regenconf infos, exception: {e}", exc_info=1 + ) + raise + + +def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): + """Compare two files and return the differences + + Read and compare two files. The differences are returned either as a delta + in unified diff format or a formatted string if as_string is True. The + header can also be removed if skip_header is True. + + """ + + if orig_file and os.path.exists(orig_file): + with open(orig_file, "r") as orig_file: + orig_file = orig_file.readlines() + else: + orig_file = [] + + if new_file and os.path.exists(new_file): + with open(new_file, "r") as new_file: + new_file = new_file.readlines() + else: + new_file = [] + + # Compare files and format output + diff = unified_diff(orig_file, new_file) + + if skip_header: + try: + next(diff) + next(diff) + except Exception: + pass + + if as_string: + return "".join(diff).rstrip() + + return diff + + +def _calculate_hash(path): + """Calculate the MD5 hash of a file""" + + if not path or not os.path.exists(path): + return None + + hasher = hashlib.md5() + + try: + with open(path, "rb") as f: + hasher.update(f.read()) + return hasher.hexdigest() + + except IOError as e: + logger.warning(f"Error while calculating file '{path}' hash: {e}", exc_info=1) + return None + + +def _get_pending_conf(categories=[]): + """Get pending configuration for categories + + Iterate over the pending configuration directory for given categories - or + all if empty - and look for files inside. Each file is considered as a + pending configuration file and therefore must be in the same directory + tree than the system file that it replaces. + The result is returned as a dict of categories with pending configuration as + key and a dict of `system_conf_path` => `pending_conf_path` as value. + + """ + result = {} + + if not os.path.isdir(PENDING_CONF_DIR): + return result + + if not categories: + categories = os.listdir(PENDING_CONF_DIR) + + for name in categories: + category_pending_path = os.path.join(PENDING_CONF_DIR, name) + + if not os.path.isdir(category_pending_path): + continue + + path_index = len(category_pending_path) + category_conf = {} + + for root, dirs, files in os.walk(category_pending_path): + for filename in files: + pending_path = os.path.join(root, filename) + category_conf[pending_path[path_index:]] = pending_path + + if category_conf: + result[name] = category_conf + else: + # remove empty directory + shutil.rmtree(category_pending_path, ignore_errors=True) + + return result + + +def _get_conf_hashes(category): + """Get the registered conf hashes for a category""" + + categories = _get_regenconf_infos() + + if category not in categories: + logger.debug(f"category {category} is not in categories.yml yet.") + return {} + + elif categories[category] is None or "conffiles" not in categories[category]: + logger.debug(f"No configuration files for category {category}.") + return {} + + else: + return categories[category]["conffiles"] + + +def _update_conf_hashes(category, hashes): + """Update the registered conf hashes for a category""" + logger.debug(f"updating conf hashes for '{category}' with: {hashes}") + + categories = _get_regenconf_infos() + category_conf = categories.get(category, {}) + + # Handle the case where categories[category] is set to null in the yaml + if category_conf is None: + category_conf = {} + + # If a file shall be removed and is indeed removed, forget entirely about + # that path. + # It avoid keeping weird old entries like + # /etc/nginx/conf.d/some.domain.that.got.removed.conf + hashes = { + path: hash_ + for path, hash_ in hashes.items() + if hash_ is not None or os.path.exists(path) + } + + category_conf["conffiles"] = hashes + categories[category] = category_conf + _save_regenconf_infos(categories) + + +def _force_clear_hashes(paths: list[str]) -> None: + categories = _get_regenconf_infos() + for path in paths: + for category in categories.keys(): + if path in categories[category]["conffiles"]: + logger.debug( + f"force-clearing old conf hash for {path} in category {category}" + ) + del categories[category]["conffiles"][path] + + _save_regenconf_infos(categories) + + +def _process_regen_conf( + system_conf: str, new_conf: str | None = None, save: bool = True +) -> bool: + """Regenerate a given system configuration file + + Replace a given system configuration file by a new one or delete it if + new_conf is None. A backup of the file - keeping its directory tree - will + be done in the backup conf directory before any operation if save is True. + + """ + if save: + system_conf_ = system_conf.lstrip("/") + now_ = datetime.utcnow().strftime("%Y%m%d.%H%M%S") + backup_path = os.path.join(BACKUP_CONF_DIR, f"{system_conf_}-{now_}") + backup_dir = os.path.dirname(backup_path) + + if not os.path.isdir(backup_dir): + mkdir(backup_dir, 0o755, True) + + shutil.copy2(system_conf, backup_path) + logger.debug( + m18n.n("regenconf_file_backed_up", conf=system_conf, backup=backup_path) + ) + + try: + if not new_conf: + os.remove(system_conf) + logger.debug(m18n.n("regenconf_file_removed", conf=system_conf)) + else: + system_dir = os.path.dirname(system_conf) + + if not os.path.isdir(system_dir): + mkdir(system_dir, 0o755, True) + + shutil.copyfile(new_conf, system_conf) + logger.debug(m18n.n("regenconf_file_updated", conf=system_conf)) + except Exception as e: + logger.warning( + f"Exception while trying to regenerate conf '{system_conf}': {e}", + exc_info=True, + ) + if not new_conf and os.path.exists(system_conf): + logger.warning( + m18n.n("regenconf_file_remove_failed", conf=system_conf), exc_info=True + ) + return False + + elif new_conf: + try: + # From documentation: + # Raise an exception if an os.stat() call on either pathname fails. + # (os.stats returns a series of information from a file like type, size...) + copy_succeed = os.path.samefile(system_conf, new_conf) + except Exception: + copy_succeed = False + finally: + if not copy_succeed: + logger.warning( + m18n.n( + "regenconf_file_copy_failed", conf=system_conf, new=new_conf + ), + exc_info=True, + ) + return False + + return True + + +def manually_modified_files(): + output = [] + regenconf_categories = _get_regenconf_infos() + for category, infos in regenconf_categories.items(): + conffiles = infos["conffiles"] + for path, hash_ in conffiles.items(): + if hash_ != _calculate_hash(path): + output.append(path) + + return output + + +def manually_modified_files_compared_to_debian_default( + ignore_handled_by_regenconf=False, +): + # from https://serverfault.com/a/90401 + files = check_output( + "dpkg-query -W -f='${Conffiles}\n' '*' \ + | awk 'OFS=\" \"{print $2,$1}' \ + | md5sum -c 2>/dev/null \ + | awk -F': ' '$2 !~ /OK/{print $1}'" + ) + files = files.strip().split("\n") + + if ignore_handled_by_regenconf: + regenconf_categories = _get_regenconf_infos() + regenconf_files = [] + for infos in regenconf_categories.values(): + regenconf_files.extend(infos["conffiles"].keys()) + + files = [f for f in files if f not in regenconf_files] + + return files diff --git a/src/service.py b/src/service.py new file mode 100644 index 0000000..83a723d --- /dev/null +++ b/src/service.py @@ -0,0 +1,874 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import re +import subprocess +import time +from datetime import datetime +from glob import glob +from logging import getLogger + +import yaml +from moulinette import Moulinette, m18n + +from .diagnosis import diagnosis_ignore, diagnosis_unignore +from .log import is_unit_operation +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import ( + append_to_file, + read_file, + read_yaml, + write_to_file, + write_to_yaml, +) +from .utils.process import check_output + +MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" + +SERVICES_CONF = "/etc/yunohost/services.yml" +SERVICES_CONF_BASE = "/usr/share/yunohost/conf/yunohost/services.yml" + +logger = getLogger("yunohost.service") + + +def service_add( + name, + description=None, + log=None, + test_status=None, + test_conf=None, + needs_exposed_ports=None, + need_lock=False, +): + """ + Add a custom service + + Keyword argument: + name -- Service name to add + description -- description of the service + log -- Absolute path to log file to display + test_status -- Specify a custom bash command to check the status of the service. N.B. : it only makes sense to specify this if the corresponding systemd service does not return the proper information. + test_conf -- Specify a custom bash command to check if the configuration of the service is valid or broken, similar to nginx -t. + needs_exposed_ports -- A list of ports that needs to be publicly exposed for the service to work as intended. + need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands. + """ + services = _get_services() + + services[name] = service = {} + + if log is not None: + if not isinstance(log, list): + log = [log] + + service["log"] = log + + if not description: + # Try to get the description from systemd service + unit, _ = _get_service_information_from_systemd(name) + description = str(unit.get("Description", "")) if unit is not None else "" + # If the service does not yet exists or if the description is empty, + # systemd will anyway return foo.service as default value, so we wanna + # make sure there's actually something here. + if description == name + ".service": + description = "" + + if description: + service["description"] = description + else: + logger.warning( + "/!\\ Packagers! You added a custom service without specifying a description. Please add a proper Description in the systemd configuration, or use --description to explain what the service does in a similar fashion to existing services." + ) + + if need_lock: + service["need_lock"] = True + + if test_status: + service["test_status"] = test_status + else: + # Try to get the description from systemd service + _, systemd_info = _get_service_information_from_systemd(name) + type_ = systemd_info.get("Type") if systemd_info is not None else "" + if type_ == "oneshot": + logger.warning( + "/!\\ Packagers! Please provide a --test_status when adding oneshot-type services in Yunohost, such that it has a reliable way to check if the service is running or not." + ) + + if test_conf: + service["test_conf"] = test_conf + + if needs_exposed_ports: + service["needs_exposed_ports"] = needs_exposed_ports + + try: + _save_services(services) + except Exception as e: + logger.warning(e) + # we'll get a logger.warning with more details in _save_services + raise YunohostError("service_add_failed", service=name) + + logger.success(m18n.n("service_added", service=name)) + + +def service_remove(name): + """ + Remove a custom service + + Keyword argument: + name -- Service name to remove + + """ + services = _get_services() + + if name not in services: + raise YunohostValidationError("service_unknown", service=name) + + del services[name] + try: + _save_services(services) + except Exception: + # we'll get a logger.warning with more details in _save_services + raise YunohostError("service_remove_failed", service=name) + + logger.success(m18n.n("service_removed", service=name)) + + +@is_unit_operation(flash=True) +def service_start(names): + """ + Start one or more services + + Keyword argument: + names -- Services name to start + + """ + if isinstance(names, str): + names = [names] + + for name in names: + if _run_service_command("start", name): + logger.success(m18n.n("service_started", service=name)) + else: + if service_status(name)["status"] != "running": + logs = _get_journalctl_logs(name, number=25) + if Moulinette.interface.type != "api": + logger.error(logs) + raise YunohostError( + "service_start_failed", + service=name, + error_details=logs, + ) + logger.debug(m18n.n("service_already_started", service=name)) + + +@is_unit_operation(flash=True) +def service_stop(names): + """ + Stop one or more services + + Keyword argument: + name -- Services name to stop + + """ + if isinstance(names, str): + names = [names] + for name in names: + if _run_service_command("stop", name): + logger.success(m18n.n("service_stopped", service=name)) + else: + if service_status(name)["status"] != "inactive": + logs = _get_journalctl_logs(name, number=25) + if Moulinette.interface.type != "api": + logger.error(logs) + raise YunohostError( + "service_stop_failed", + service=name, + error_details=logs, + ) + logger.debug(m18n.n("service_already_stopped", service=name)) + + +def service_reload(names): + """ + Reload one or more services + + Keyword argument: + name -- Services name to reload + + """ + if isinstance(names, str): + names = [names] + for name in names: + if _run_service_command("reload", name): + logger.success(m18n.n("service_reloaded", service=name)) + else: + if service_status(name)["status"] != "running": + logs = _get_journalctl_logs(name, number=25) + if Moulinette.interface.type != "api": + logger.error(logs) + raise YunohostError( + "service_reload_failed", + service=name, + error_details=logs, + ) + + +@is_unit_operation(flash=True) +def service_restart(names): + """ + Restart one or more services. If the services are not running yet, they will be started. + + Keyword argument: + name -- Services name to restart + + """ + if isinstance(names, str): + names = [names] + for name in names: + if _run_service_command("restart", name): + logger.success(m18n.n("service_restarted", service=name)) + else: + if service_status(name)["status"] != "running": + logs = _get_journalctl_logs(name, number=25) + if Moulinette.interface.type != "api": + logger.error(logs) + raise YunohostError( + "service_restart_failed", + service=name, + error_details=logs, + ) + + +def service_reload_or_restart(names, test_conf=True): + """ + Reload one or more services if they support it. If not, restart them instead. If the services are not running yet, they will be started. + + Keyword argument: + name -- Services name to reload or restart + + """ + if isinstance(names, str): + names = [names] + + services = _get_services() + + for name in names: + logger.debug(f"Reloading service {name}") + + test_conf_cmd = services.get(name, {}).get("test_conf") + if test_conf and test_conf_cmd: + p = subprocess.Popen( + test_conf_cmd, + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + out, _ = p.communicate() + if p.returncode != 0: + errors = out.decode().strip().split("\n") + logger.error( + m18n.n( + "service_not_reloading_because_conf_broken", + name=name, + errors=errors, + ) + ) + continue + + if _run_service_command("reload-or-restart", name): + logger.success(m18n.n("service_reloaded_or_restarted", service=name)) + else: + if service_status(name)["status"] != "running": + logs = _get_journalctl_logs(name, number=25) + if Moulinette.interface.type != "api": + logger.error(logs) + raise YunohostError( + "service_reload_or_restart_failed", + service=name, + error_details=logs, + ) + + +@is_unit_operation(flash=True) +def service_enable(names): + """ + Enable one or more services + + Keyword argument: + names -- Services name to enable + + """ + if isinstance(names, str): + 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("service_enable_failed", service=name) + + +@is_unit_operation(flash=True) +def service_disable(names): + """ + Disable one or more services + + Keyword argument: + names -- Services name to disable + + """ + if isinstance(names, str): + 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("service_disable_failed", service=name) + + +def service_status(names=[]): + """ + Show status information about one or more services (all by default) + + Keyword argument: + names -- Services name to show + + """ + services = _get_services() + + # If function was called with a specific list of service + if names != []: + # If user wanna check the status of a single service + if isinstance(names, str): + names = [names] + + # Validate service names requested + for name in names: + if name not in services.keys(): + raise YunohostValidationError("service_unknown", service=name) + + # Filter only requested servivces + services = {k: v for k, v in services.items() if k in names} + + # Remove services that aren't "real" services + # + # the historical reason is because regenconf has been hacked into the + # service part of YunoHost will in some situation we need to regenconf + # for things that aren't services + # the hack was to add fake services... + services = {k: v for k, v in services.items() if v.get("status", "") is not None} + + output = { + s: _get_and_format_service_status(s, infos) for s, infos in services.items() + } + + if len(names) == 1: + return output[names[0]] + return output + + +def _get_service_information_from_systemd(service): + "this is the equivalent of 'systemctl status $service'" + import dbus + + d = dbus.SystemBus() + + systemd = d.get_object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") + manager = dbus.Interface(systemd, "org.freedesktop.systemd1.Manager") + + # c.f. https://zignar.net/2014/09/08/getting-started-with-dbus-python-systemd/ + # Very interface, much intuitive, wow + service_unit = manager.LoadUnit(service + ".service") + service_proxy = d.get_object("org.freedesktop.systemd1", str(service_unit)) + properties_interface = dbus.Interface( + service_proxy, "org.freedesktop.DBus.Properties" + ) + + unit = properties_interface.GetAll("org.freedesktop.systemd1.Unit") + service = properties_interface.GetAll("org.freedesktop.systemd1.Service") + + if unit.get("LoadState", "not-found") == "not-found": + # Service doesn't really exist + return (None, None) + else: + return (unit, service) + + +def _get_and_format_service_status(service, infos): + systemd_service = infos.get("actual_systemd_service", service) + raw_status, raw_service = _get_service_information_from_systemd(systemd_service) + + if raw_status is None: + logger.error( + f"Failed to get status information via dbus for service {systemd_service}, systemctl didn't recognize this service ('NoSuchUnit')." + ) + return { + "status": "unknown", + "start_on_boot": "unknown", + "last_state_change": "unknown", + "description": "Error: failed to get information for this service, it doesn't exists for systemd", + "configuration": "unknown", + } + + # Try to get description directly from services.yml + description = infos.get("description") + + # If no description was there, try to get it from the .json locales + if not description: + translation_key = f"service_description_{service}" + if m18n.key_exists(translation_key): + description = m18n.n(translation_key) + else: + description = str(raw_status.get("Description", "")) + + output = { + "status": str(raw_status.get("SubState", "unknown")), + "start_on_boot": str(raw_status.get("UnitFileState", "unknown")), + "last_state_change": "unknown", + "description": description, + "configuration": "unknown", + } + + # Fun stuff™ : to obtain the enabled/disabled status for sysv services, + # gotta do this ... cf code of /lib/systemd/systemd-sysv-install + if output["start_on_boot"] == "generated": + output["start_on_boot"] = ( + "enabled" if glob("/etc/rc[S5].d/S??" + service) else "disabled" + ) + elif os.path.exists( + f"/etc/systemd/system/multi-user.target.wants/{service}.service" + ): + output["start_on_boot"] = "enabled" + + if "StateChangeTimestamp" in raw_status: + output["last_state_change"] = ( + datetime.utcfromtimestamp(raw_status["StateChangeTimestamp"] / 1000000) + if raw_status["StateChangeTimestamp"] != 0 + else "unknown" + ) + + # 'test_status' is an optional field to test the status of the service using a custom command + if "test_status" in infos: + p = subprocess.Popen( + infos["test_status"], + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + p.communicate() + + output["status"] = "running" if p.returncode == 0 else "failed" + elif ( + raw_service.get("Type", "").lower() == "oneshot" + and output["status"] == "exited" + ): + # These are services like hotspot, vpnclient... + # they will be "exited" why doesn't provide any info about + # the real state of the service (unless they did provide a + # test_status, c.f. previous condition) + output["status"] = "unknown" + + # 'test_status' is an optional field to test the status of the service using a custom command + if "test_conf" in infos: + p = subprocess.Popen( + infos["test_conf"], + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + out, _ = p.communicate() + if p.returncode == 0: + output["configuration"] = "valid" + else: + out = out.decode() + output["configuration"] = "broken" + output["configuration-details"] = out.strip().split("\n") + + return output + + +def service_log(name, number=50): + """ + Log every log files of a service + + Keyword argument: + name -- Service name to log + number -- Number of lines to display + + """ + services = _get_services() + number = int(number) + + if name not in services.keys(): + raise YunohostValidationError("service_unknown", service=name) + + log_list = services[name].get("log", []) + + if not isinstance(log_list, list): + log_list = [log_list] + + # Legacy stuff related to --log_type where we'll typically have the service + # name in the log list but it's not an actual logfile. Nowadays journalctl + # is automatically fetch as well as regular log files. + if name in log_list: + log_list.remove(name) + + result = {} + + # First we always add the logs from journalctl / systemd + result["journalctl"] = _get_journalctl_logs(name, number).splitlines() + + for log_path in log_list: + if not os.path.exists(log_path): + continue + + # Make sure to resolve symlinks + log_path = os.path.realpath(log_path) + + # log is a file, read it + if os.path.isfile(log_path): + result[log_path] = _tail(log_path, number) + continue + elif not os.path.isdir(log_path): + result[log_path] = [] + continue + + for log_file in os.listdir(log_path): + log_file_path = os.path.join(log_path, log_file) + # not a file : skip + if not os.path.isfile(log_file_path): + continue + + if not log_file.endswith(".log"): + continue + + result[log_file_path] = ( + _tail(log_file_path, number) if os.path.exists(log_file_path) else [] + ) + + return result + + +def _run_service_command(action: str, service: str) -> bool: + """ + Run services management command (start, stop, enable, disable, restart, reload) + + Keyword argument: + action -- Action to perform + service -- Service name + + """ + services = _get_services() + if service not in services.keys(): + raise YunohostValidationError("service_unknown", service=service) + + possible_actions = [ + "start", + "stop", + "restart", + "reload", + "reload-or-restart", + "enable", + "disable", + ] + if action not in possible_actions: + raise ValueError( + f"Unknown action '{action}', available actions are: {', '.join(possible_actions)}" + ) + + cmd = f"systemctl {action} {service}" + + need_lock = services[service].get("need_lock", False) and action in [ + "start", + "stop", + "restart", + "reload", + "reload-or-restart", + ] + + if action in ["enable", "disable"]: + cmd += " --quiet" + + try: + # Launch the command + logger.debug(f"Running '{cmd}'") + p = subprocess.Popen(cmd.split(), stderr=subprocess.STDOUT) + # If this command needs a lock (because the service uses yunohost + # commands inside), find the PID and add a lock for it + if need_lock: + PID = _give_lock(action, service, p) + # Wait for the command to complete + p.communicate() + + if p.returncode != 0: + logger.warning(m18n.n("service_cmd_exec_failed", command=cmd)) + return False + + except Exception as e: + logger.warning(m18n.n("unexpected_error", error=str(e))) + return False + + finally: + # Remove the lock if one was given + if need_lock and PID != 0: + _remove_lock(PID) + + return True + + +def _give_lock(action, service, p): + # Depending of the action, systemctl calls the PID differently :/ + if action == "start" or action == "restart": + systemctl_PID_name = "MainPID" + else: + systemctl_PID_name = "ControlPID" + + cmd_get_son_PID = f"systemctl show {service} -p {systemctl_PID_name}" + son_PID = 0 + # As long as we did not found the PID and that the command is still running + while son_PID == 0 and p.poll() is None: + # Call systemctl to get the PID + # Output of the command is e.g. ControlPID=1234 + son_PID = check_output(cmd_get_son_PID).split("=")[1] + son_PID = int(son_PID) + time.sleep(1) + + # If we found a PID + if son_PID != 0: + # Append the PID to the lock file + logger.debug(f"Giving a lock to PID {son_PID} for service {service} !") + append_to_file(MOULINETTE_LOCK, f"\n{son_PID}") + + return son_PID + + +def _remove_lock(PID_to_remove): + # FIXME ironically not concurrency safe because it's not atomic... + + PIDs = read_file(MOULINETTE_LOCK).split("\n") + PIDs_to_keep = [PID for PID in PIDs if int(PID) != PID_to_remove] + write_to_file(MOULINETTE_LOCK, "\n".join(PIDs_to_keep)) + + +def _get_services(): + """ + Get a dict of managed services with their parameters + + """ + try: + services = read_yaml(SERVICES_CONF_BASE) or {} + + # These are keys flagged 'null' in the base conf + legacy_keys_to_delete = [k for k, v in services.items() if v is None] + + services.update(read_yaml(SERVICES_CONF) or {}) + + services = { + name: infos + for name, infos in services.items() + if name not in legacy_keys_to_delete + } + except Exception: + return {} + + # Dirty hack to automatically find custom SSH port ... + ssh_port_line = re.findall( + r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config") + ) + if len(ssh_port_line) == 1: + services["ssh"]["needs_exposed_ports"] = [int(ssh_port_line[0])] + + # Dirty hack to check the status of ynh-vpnclient + if "ynh-vpnclient" in services: + if "log" not in services["ynh-vpnclient"]: + services["ynh-vpnclient"]["log"] = ["/var/log/ynh-vpnclient.log"] + + services_with_package_condition = [ + name + for name, infos in services.items() + if infos.get("ignore_if_package_is_not_installed") + ] + for name in services_with_package_condition: + package = services[name]["ignore_if_package_is_not_installed"] + if ( + 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\s*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 + if version == "7.3": + continue + services[f"php{version}-fpm"] = { + "log": f"/var/log/php{version}-fpm.log", + "test_conf": f"php-fpm{version} --test", # ofc the service is phpx.y-fpm but the program is php-fpmx.y because why not ... + "category": "web", + } + + # Remove legacy /var/log/daemon.log and /var/log/syslog from log entries + # because they are too general. Instead, now the journalctl log is + # returned by default which is more relevant. + for infos in services.values(): + if infos.get("log") in ["/var/log/syslog", "/var/log/daemon.log"]: + del infos["log"] + + return services + + +def _save_services(services): + """ + Save managed services to files + + Keyword argument: + services -- A dict of managed services with their parameters + + """ + + # Compute the diff with the base file + # such that /etc/yunohost/services.yml contains the minimal + # changes with respect to the base conf + + conf_base = yaml.safe_load(open(SERVICES_CONF_BASE)) or {} + + diff = {} + + for service_name, service_infos in services.items(): + # Ignore php-fpm services, they are to be added dynamically by the core, + # but not actually saved + if service_name.startswith("php") and service_name.endswith("-fpm"): + continue + + service_conf_base = conf_base.get(service_name, {}) or {} + diff[service_name] = {} + + for key, value in service_infos.items(): + if service_conf_base.get(key) != value: + diff[service_name][key] = value + + diff = { + name: infos for name, infos in diff.items() if infos or name not in conf_base + } + + write_to_yaml(SERVICES_CONF, diff) + + +def _tail(file, n): + """ + Reads a n lines from f with an offset of offset lines. The return + value is a tuple in the form ``(lines, has_more)`` where `has_more` is + an indicator that is `True` if there are more lines in the file. + + This function works even with splitted logs (gz compression, log rotate...) + """ + avg_line_length = 74 + to_read = n + + try: + if file.endswith(".gz"): + import gzip + + f = gzip.open(file) + lines = f.read().splitlines() + else: + f = open(file, errors="replace") + pos = 1 + lines = [] + while len(lines) < to_read and pos > 0: + try: + f.seek(-(avg_line_length * to_read), 2) + except IOError: + # woops. apparently file is smaller than what we want + # to step back, go to the beginning instead + f.seek(0) + + pos = f.tell() + lines = f.read().splitlines() + + if len(lines) >= to_read: + return lines[-to_read:] + + avg_line_length *= 1.3 + f.close() + + except IOError as e: + logger.warning("Error while tailing file '%s': %s", file, e, exc_info=1) + return [] + + if len(lines) < to_read: + previous_log_file = _find_previous_log_file(file) + if previous_log_file is not None: + lines = _tail(previous_log_file, to_read - len(lines)) + lines + + return lines + + +def _find_previous_log_file(file): + """ + Find the previous log file + """ + splitext = os.path.splitext(file) + if splitext[1] == ".gz": + file = splitext[0] + splitext = os.path.splitext(file) + ext = splitext[1] + i = re.findall(r"\.(\d+)", ext) + i = int(i[0]) + 1 if len(i) > 0 else 1 + + previous_file = file if i == 1 else splitext[0] + previous_file = previous_file + f".{i}" + if os.path.exists(previous_file): + return previous_file + + previous_file = previous_file + ".gz" + if os.path.exists(previous_file): + return previous_file + + return None + + +def _get_journalctl_logs(service, number="all"): + services = _get_services() + systemd_service = services.get(service, {}).get("actual_systemd_service", service) + try: + return check_output( + f"journalctl --no-hostname --no-pager -u {systemd_service} -n{number}" + ) + except Exception: + import traceback + + trace_ = traceback.format_exc() + return f"error while get services logs from journalctl:\n{trace_}" diff --git a/src/settings.py b/src/settings.py new file mode 100644 index 0000000..18c7c7d --- /dev/null +++ b/src/settings.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import subprocess +from logging import getLogger +from typing import TYPE_CHECKING, Any, Callable, Union + +from moulinette import m18n + +from .firewall import firewall_reload +from .log import is_unit_operation +from .regenconf import regen_conf +from .utils.configpanel import ConfigPanel, parse_filter_key +from .utils.error import YunohostError, YunohostValidationError +from .utils.form import BaseOption + +if TYPE_CHECKING: + from typing import cast + + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + + from .log import OperationLogger + from .utils.configpanel import ( + ConfigPanelGetMode, + ConfigPanelModel, + RawSettings, + ) + from .utils.form import FormModel + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.settings")) +else: + logger = getLogger("yunohost.settings") + +SETTINGS_PATH = "/etc/yunohost/settings.yml" + + +def settings_get(key="", full=False, export=False): + """ + Get an entry value in the settings + + Keyword argument: + key -- Settings key + + """ + if full and export: + raise YunohostValidationError( + "You can't use --full and --export together.", raw_msg=True + ) + + if full: + mode = "full" + elif export: + mode = "export" + else: + mode = "classic" + + settings = SettingsConfigPanel() + return settings.get(key, mode) + + +def settings_list(full=False): + settings = settings_get(full=full) + + if full: + return settings + else: + return { + k: v + for k, v in settings.items() + if not k.startswith("security.root_access") + } + + +@is_unit_operation() +def settings_set(operation_logger, key=None, value=None, args=None, args_file=None): + """ + Set an entry value in the settings + + Keyword argument: + key -- Settings key + value -- New value + + """ + BaseOption.operation_logger = operation_logger + settings = SettingsConfigPanel() + return settings.set(key, value, args, args_file, operation_logger=operation_logger) + + +@is_unit_operation() +def settings_reset(operation_logger, key): + """ + Set an entry value to its default one + + Keyword argument: + key -- Settings key + + """ + + settings = SettingsConfigPanel() + return settings.reset(key, operation_logger=operation_logger) + + +@is_unit_operation() +def settings_reset_all(operation_logger): + """ + Reset all settings to their default value + + Keyword argument: + yes -- Yes I'm sure I want to do that + + """ + settings = SettingsConfigPanel() + return settings.reset(operation_logger=operation_logger) + + +class SettingsConfigPanel(ConfigPanel): + entity_type = "global" + save_path_tpl = SETTINGS_PATH + save_mode = "diff" + virtual_settings = {"root_password", "root_password_confirm", "passwordless_sudo"} + + def __init__(self, config_path=None, save_path=None, creation=False) -> None: + super().__init__("settings") + + def get( + self, key: str | None = None, mode: "ConfigPanelGetMode" = "classic" + ) -> Any: + result = super().get(key=key, mode=mode) + + # 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: str | None = None, + operation_logger: Union["OperationLogger", None] = None, + ) -> None: + self.filter_key = parse_filter_key(key) + + # Read config panel toml + self.config, self.form = self._get_config_panel(prevalidate=True) + + # FIXME find a better way to exclude previous settings + previous_settings = self.form.dict() + + 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 + + # 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.form, self.config, previous_settings) + except YunohostError: + raise + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(m18n.n("config_apply_failed", error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_apply_failed", error=error)) + raise + + logger.success(m18n.n("global_settings_reset_success")) + + if operation_logger: + operation_logger.success() + + 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 + raw_settings["root_password"] = "" + raw_settings["root_password_confirm"] = "" + + # Specific logic for virtual setting "passwordless_sudo" + try: + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + raw_settings["passwordless_sudo"] = "!authenticate" in ldap.search( + "ou=sudo", "cn=admins", ["sudoOption"] + )[0].get("sudoOption", []) + except Exception: + raw_settings["passwordless_sudo"] = False + + return raw_settings + + def _apply( + self, + form: "FormModel", + config: "ConfigPanelModel", + 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: + raise YunohostValidationError("password_confirmation_not_the_same") + + from .tools import tools_rootpw + + tools_rootpw(root_password, check_strength=True) + + if passwordless_sudo is not None: + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + ldap.update( + "cn=admins,ou=sudo", + {"sudoOption": "!authenticate" if passwordless_sudo else []}, + ) + + # First save settings except virtual + default ones + super()._apply(form, config, 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 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, previous_settings.get(setting_name), value + ) + except Exception as e: + logger.error(f"Post-change hook for setting failed : {e}") + raise + + +# Meant to be a dict of setting_name -> function to call +post_change_hooks: dict[str, Callable] = {} + + +def post_change_hook(setting_name): + # TODO: Check that setting_name exists + def decorator(func): + post_change_hooks[setting_name] = func + return func + + return decorator + + +def trigger_post_change_hook(setting_name, old_value, new_value): + if setting_name not in post_change_hooks: + logger.debug(f"Nothing to do after changing setting {setting_name}") + return + + f = post_change_hooks[setting_name] + f(setting_name, old_value, new_value) + + +# =========================================== +# +# Actions to trigger when changing a setting +# You can define such an action with : +# +# @post_change_hook("your.setting.name") +# def some_function_name(setting_name, old_value, new_value): +# # Do some stuff +# +# =========================================== + + +@post_change_hook("portal_theme") +@post_change_hook("portal_allow_edit_email") +@post_change_hook("portal_allow_edit_email_alias") +@post_change_hook("portal_allow_edit_email_forward") +def regen_ssowatconf(setting_name, old_value, new_value): + if old_value != new_value: + from .app import app_ssowatconf + + app_ssowatconf() + + +@post_change_hook("dns_custom_resolvers_enabled") +@post_change_hook("dns_custom_resolvers_list") +def reconfigure_dnsmasq(setting_name, old_value, new_value): + if old_value != new_value: + regen_conf(names=["dnsmasq"]) + + +@post_change_hook("tls_passthrough_enabled") +@post_change_hook("tls_passthrough_list") +@post_change_hook("nginx_redirect_to_https") +@post_change_hook("nginx_compatibility") +@post_change_hook("webadmin_allowlist_enabled") +@post_change_hook("webadmin_allowlist") +def reconfigure_nginx(setting_name, old_value, new_value): + if old_value != new_value: + regen_conf(names=["nginx"]) + + +@post_change_hook("security_experimental_enabled") +def reconfigure_nginx_and_yunohost(setting_name, old_value, new_value): + if old_value != new_value: + regen_conf(names=["nginx", "yunohost"]) + + +@post_change_hook("ssh_compatibility") +@post_change_hook("ssh_password_authentication") +def reconfigure_ssh(setting_name, old_value, new_value): + if old_value != new_value: + regen_conf(names=["ssh"]) + + +@post_change_hook("ssh_port") +def reconfigure_ssh_and_fail2ban(setting_name, old_value, new_value): + if old_value != new_value: + regen_conf(names=["ssh", "fail2ban"]) + firewall_reload() + + +@post_change_hook("smtp_allow_ipv6") +@post_change_hook("smtp_relay_host") +@post_change_hook("smtp_relay_port") +@post_change_hook("smtp_relay_user") +@post_change_hook("smtp_relay_password") +@post_change_hook("smtp_backup_mx_domains") +@post_change_hook("smtp_backup_mx_emails_whitelisted") +@post_change_hook("postfix_compatibility") +def reconfigure_postfix(setting_name, old_value, new_value): + if old_value != new_value: + regen_conf(names=["postfix"]) + + +@post_change_hook("pop3_enabled") +def reconfigure_dovecot(setting_name, old_value, new_value): + environment = os.environ.copy() + environment.update({"DEBIAN_FRONTEND": "noninteractive"}) + + # Depending on how consistent the config panel is, it may spit 1 or True or ..? ... + if new_value: + command = [ + "apt-get", + "-y", + "--no-remove", + "-o Dpkg::Options::=--force-confdef", + "-o Dpkg::Options::=--force-confold", + "install", + "dovecot-pop3d", + ] + subprocess.call(command, env=environment) + if old_value != new_value: + regen_conf(names=["dovecot"]) + else: + if old_value != new_value: + regen_conf(names=["dovecot"]) + command = ["apt-get", "-y", "remove", "dovecot-pop3d"] + subprocess.call(command, env=environment) diff --git a/src/ssh.py b/src/ssh.py new file mode 100644 index 0000000..1004461 --- /dev/null +++ b/src/ssh.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import pwd +import re +from typing import Literal, NotRequired, TypedDict + +from .utils.error import YunohostValidationError +from .utils.file_utils import chmod, chown, mkdir, read_file, write_to_file + +SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" + + +class UserSshInfo(TypedDict): + username: str + fullname: str + uid: NotRequired[str] + mail: str + homeDirectory: str + + +def user_ssh_list_keys(username: str) -> dict[Literal["keys"], list[dict[str, str]]]: + user = _get_user_for_ssh(username, ["homeDirectory"]) + if not user: + raise YunohostValidationError("user_unknown", user=username) + + authorized_keys_file = os.path.join( + user["homeDirectory"][0], ".ssh", "authorized_keys" + ) + + if not os.path.exists(authorized_keys_file): + return {"keys": []} + + keys = [] + last_comment = "" + for line in read_file(authorized_keys_file).split("\n"): + # empty line + if not line.strip(): + continue + + if line.lstrip().startswith("#"): + last_comment = line.lstrip().lstrip("#").strip() + continue + + # assuming a key per non empty line + key = line.strip() + keys.append( + { + "key": key, + "name": last_comment, + } + ) + + last_comment = "" + + return {"keys": keys} + + +def user_ssh_add_key(username: str, key: str, comment: str | None) -> None: + user = _get_user_for_ssh(username, ["homeDirectory", "uid"]) + if not user: + raise YunohostValidationError("user_unknown", user=username) + + authorized_keys_file = os.path.join( + user["homeDirectory"][0], ".ssh", "authorized_keys" + ) + + if not os.path.exists(authorized_keys_file): + # ensure ".ssh" exists + mkdir( + os.path.join(user["homeDirectory"][0], ".ssh"), + force=True, + parents=True, + uid=user["uid"][0], + ) + chmod(os.path.join(user["homeDirectory"][0], ".ssh"), 0o700) + + # create empty file to set good permissions + write_to_file(authorized_keys_file, "") + chown(authorized_keys_file, uid=user["uid"][0]) + chmod(authorized_keys_file, 0o600) + + authorized_keys_content = read_file(authorized_keys_file) + + authorized_keys_content += "\n" + authorized_keys_content += "\n" + + if comment and comment.strip(): + if not comment.lstrip().startswith("#"): + comment = "# " + comment + authorized_keys_content += comment.replace("\n", " ").strip() + authorized_keys_content += "\n" + + authorized_keys_content += key.strip() + authorized_keys_content += "\n" + + write_to_file(authorized_keys_file, authorized_keys_content) + + +def user_ssh_remove_key(username: str, key: str) -> None: + user = _get_user_for_ssh(username, ["homeDirectory", "uid"]) + if not user: + raise YunohostValidationError("user_unknown", user=username) + + authorized_keys_file = os.path.join( + user["homeDirectory"][0], ".ssh", "authorized_keys" + ) + + if not os.path.exists(authorized_keys_file): + raise YunohostValidationError( + f"this key doesn't exists ({authorized_keys_file} dosesn't exists)", + raw_msg=True, + ) + + authorized_keys_content = read_file(authorized_keys_file) + + if key not in authorized_keys_content: + raise YunohostValidationError( + f"Key '{key}' is not present in authorized_keys", raw_msg=True + ) + + # don't delete the previous comment because we can't verify if it's legit + + # this regex approach failed for some reasons and I don't know why :( + # authorized_keys_content = re.sub("{} *\n?".format(key), + # "", + # authorized_keys_content, + # flags=re.MULTILINE) + + authorized_keys_content = authorized_keys_content.replace(key, "") + + write_to_file(authorized_keys_file, authorized_keys_content) + + +# +# Helpers +# + + +def _get_user_for_ssh( + username: str, attrs: list[str] | None = None +) -> UserSshInfo | None: + def ssh_root_login_status() -> dict[Literal["PermitRootLogin"], bool]: + # XXX temporary placed here for when the ssh_root commands are integrated + # extracted from https://github.com/YunoHost/yunohost/pull/345 + # XXX should we support all the options? + # this is the content of "man sshd_config" + # PermitRootLogin + # Specifies whether root can log in using ssh(1). The argument must be + # “yes”, “without-password”, “forced-commands-only”, or “no”. The + # default is “yes”. + sshd_config_content = read_file(SSHD_CONFIG_PATH) + + if re.search( + "^ *PermitRootLogin +(no|forced-commands-only) *$", + sshd_config_content, + re.MULTILINE, + ): + return {"PermitRootLogin": False} + + return {"PermitRootLogin": True} + + if username == "root": + root_unix = pwd.getpwnam("root") + return { + "username": "root", + "fullname": "", + "mail": "", + "homeDirectory": root_unix.pw_dir, + } + + # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + user: list[UserSshInfo] = ldap.search( # type: ignore + "ou=users", + "(&(objectclass=person)(uid=%s))" % username, + attrs, + ) + + assert len(user) in (0, 1) + + if not user: + return None + + return user[0] diff --git a/src/storage.py b/src/storage.py new file mode 100644 index 0000000..3b293f2 --- /dev/null +++ b/src/storage.py @@ -0,0 +1,28 @@ +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +def storage_disk_list(**kargs): + from .disk import disk_list + + return disk_list(**kargs) + + +def storage_disk_info(name, **kargs): + from .disk import disk_info + + return disk_info(name, **kargs) diff --git a/src/tools.py b/src/tools.py new file mode 100644 index 0000000..67ac5e0 --- /dev/null +++ b/src/tools.py @@ -0,0 +1,1188 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import os +import pwd +import re +import subprocess +import time +from importlib import import_module +from logging import getLogger +from typing import TYPE_CHECKING, Any, Callable, Literal, cast + +from moulinette import Moulinette, m18n +from packaging import version +from typing_extensions import TypedDict + +from .log import OperationLogger, is_unit_operation +from .utils.error import YunohostError, YunohostValidationError +from .utils.file_utils import chown, cp, mkdir, read_yaml, rm, write_to_yaml +from .utils.process import call_async_output +from .utils.system import ( + _apt_log_line_is_relevant, + _dump_sources_list, + _group_packages_per_categories, + _list_upgradable_apt_packages, + dpkg_is_broken, + dpkg_lock_available, + ynh_packages_version, +) + +if TYPE_CHECKING: + from .app import AppInfo + +MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations.yaml" + +if TYPE_CHECKING: + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.tools")) +else: + logger = getLogger("yunohost.tools") + + +def tools_versions() -> dict[str, dict[str, str]]: + return ynh_packages_version() + + +def tools_rootpw(new_password: str, check_strength: bool = True) -> None: + from .utils.password import ( + assert_password_is_compatible, + assert_password_is_strong_enough, + ) + + assert_password_is_compatible(new_password) + if check_strength: + assert_password_is_strong_enough("admin", new_password) + + proc = subprocess.run( + ["passwd"], + input=f"{new_password}\n{new_password}\n".encode("utf-8"), + capture_output=True, + ) + + if proc.returncode == 0: + logger.info(m18n.n("root_password_changed")) + else: + logger.warning(proc.stdout) + logger.warning(proc.stderr) + logger.warning(m18n.n("root_password_desynchronized")) + + +def tools_maindomain(new_main_domain: str | None = None) -> dict[str, str] | None: + from .domain import domain_main_domain + + logger.warning( + m18n.g( + "deprecated_command_alias", + prog="yunohost", + old="tools maindomain", + new="domain main-domain", + ) + ) + return domain_main_domain(new_main_domain=new_main_domain) + + +def _set_hostname(hostname: str, pretty_hostname: str | None = None) -> None: + """ + Change the machine hostname using hostnamectl + """ + + if not pretty_hostname: + pretty_hostname = f"(YunoHost/{hostname})" + + # First clear nsswitch cache for hosts to make sure hostname is resolved... + subprocess.call(["nscd", "-i", "hosts"]) + + # Then call hostnamectl + commands = [ + "hostnamectl --static set-hostname".split() + [hostname], + "hostnamectl --transient set-hostname".split() + [hostname], + "hostnamectl --pretty set-hostname".split() + [pretty_hostname], + ] + + for command in commands: + p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + out, _ = p.communicate() + + if p.returncode != 0: + logger.warning(command) + logger.warning(out) + logger.error(m18n.n("domain_hostname_failed")) + else: + logger.debug(out) + + +@is_unit_operation(exclude=["dyndns_recovery_password", "password"]) +def tools_postinstall( + operation_logger: OperationLogger, + domain: str, + username: str, + fullname: str, + password: str, + dyndns_recovery_password: str | None = None, + ignore_dyndns: bool = False, + force_diskspace: bool = False, + overwrite_root_password: bool = True, + i_have_read_terms_of_services: bool = False, +) -> None: + import psutil + + from .app_catalog import _update_apps_catalog + from .domain import domain_add, domain_main_domain + from .dyndns import _dyndns_available, dyndns_unsubscribe + from .permission import _set_system_perms + from .service import _run_service_command + from .user import ADMIN_ALIASES, user_create + from .utils.app_utils import _ask_confirmation + from .utils.dns import is_yunohost_dyndns_domain + from .utils.password import ( + assert_password_is_compatible, + assert_password_is_strong_enough, + ) + + # Do some checks at first + if os.path.isfile("/etc/yunohost/installed"): + raise YunohostValidationError("yunohost_already_installed") + + if os.path.isdir("/etc/yunohost/apps") and os.listdir("/etc/yunohost/apps") != []: + raise YunohostValidationError( + "It looks like you're trying to re-postinstall a system that was already working previously ... If you recently had some bug or issues with your installation, please first discuss with the team on how to fix the situation instead of savagely re-running the postinstall ...", + raw_msg=True, + ) + + if Moulinette.interface.type == "cli" and os.isatty(1): + Moulinette.display(m18n.n("tos_postinstall_acknowledgement"), style="warning") + if not i_have_read_terms_of_services: + # i18n: confirm_tos_acknowledgement + _ask_confirmation("confirm_tos_acknowledgement", kind="soft") + + # Crash early if the username is already a system user, which is + # a common confusion. We don't want to crash later and end up in an half-configured state. + all_existing_usernames = {x.pw_name for x in pwd.getpwall()} + if username in all_existing_usernames: + raise YunohostValidationError("system_username_exists") + + if username in ADMIN_ALIASES: + raise YunohostValidationError( + f"Unfortunately, {username} cannot be used as a username", raw_msg=True + ) + + # Check there's at least 10 GB on the rootfs... + disk_partitions = sorted( + psutil.disk_partitions(all=True), key=lambda k: k.mountpoint + ) + main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ["/", "/var"]] + main_space = sum( + psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions + ) + GB = 1024**3 + if not force_diskspace and main_space < 10 * GB: + raise YunohostValidationError("postinstall_low_rootfsspace") + + # Check password + assert_password_is_compatible(password) + assert_password_is_strong_enough("admin", password) + + # If this is a nohost.me/noho.st, actually check for availability + dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) + if dyndns: + # Check if the domain is available... + try: + available = _dyndns_available(domain) + # If an exception is thrown, most likely we don't have internet + # connectivity or something. Assume that this domain isn't manageable + # and inform the user that we could not contact the dyndns host server. + except Exception: + raise YunohostValidationError( + "dyndns_provider_unreachable", provider="dyndns.yunohost.org" + ) + else: + 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("nft -V >/dev/null 2>/dev/null") != 0: + raise YunohostValidationError( + "nftables does not seems to be working on your setup. You may be in a container or your kernel does have the proper modules loaded. Sometimes, rebooting the machine may solve the issue.", + raw_msg=True, + ) + + internet_ok = False + if os.system("ping -c1 -w3 yunohost.org >/dev/null") == 0: + internet_ok = True + elif os.system("ping -c1 -w3 8.8.8.8 >/dev/null") == 0: + if os.system("timeout 3 dig +short yunohost.org") == 0: + # yunohost.org does resolves, and 8.8.8.8 pings ... most likely yunohost.org is down? + logger.warning( + "This machine can ping the Internet and resolve DNS, but not yunohost.org? Maybe there's currently an outage on yunohost.org infrastructure which may or may not impact the postinstall process..." + ) + else: + # yunohost.org doesnt ping, but 8.8.8.8 pings ... most likely DNS resolution is broken? + logger.warning( + "It looks like DNS resolution is broken on your server, which may impact the postinstall process..." + ) + else: + logger.warning( + "It looks like internet connectivity is not available, which may or may not be what you're expecting ..." + ) + + operation_logger.start() + logger.info(m18n.n("yunohost_installing")) + + _set_system_perms( + { + "ssh": {"allowed": ["admins"]}, + "sftp": {"allowed": []}, + "mail": {"allowed": ["all_users"]}, + } + ) + + # New domain config + domain_add( + domain, + dyndns_recovery_password=dyndns_recovery_password, + ignore_dyndns=ignore_dyndns, + skip_tos=True, # skip_tos is here to prevent re-asking about the ToS when adding a dyndns service, because the ToS are already displayed right before in postinstall + ) + domain_main_domain(domain) + + # First user + user_create(username, domain, password, admin=True, fullname=fullname) + + if overwrite_root_password: + tools_rootpw(password) + + # Try to fetch the apps catalog ... + # we don't fail miserably if this fails, + # because that could be for example an offline installation... + if internet_ok is True: + try: + _update_apps_catalog() + except Exception as e: + logger.warning(str(e)) + else: + logger.warning( + "Skipping catalog initialization due to lack of Internet connectivity?" + ) + + # Init migrations (skip them, no need to run them on a fresh system) + _skip_all_migrations() + + os.system("touch /etc/yunohost/installed") + + # Enable and start YunoHost firewall at boot time + _run_service_command("enable", "nftables") + + tools_regen_conf(names=["ssh"], force=True) + + # Restore original ssh conf, as chosen by the + # admin during the initial install + # + # c.f. the install script and in particular + # https://github.com/YunoHost/install_script/pull/50 + # The user can now choose during the install to keep + # the initial, existing sshd configuration + # instead of YunoHost's recommended conf + # + original_sshd_conf = "/etc/ssh/sshd_config.before_yunohost" + if os.path.exists(original_sshd_conf): + os.rename(original_sshd_conf, "/etc/ssh/sshd_config") + + tools_regen_conf(force=True) + + logger.success(m18n.n("yunohost_configured")) + + logger.warning(m18n.n("yunohost_postinstall_end_tip")) + + +def tools_regen_conf( + names: list[str] = [], + with_diff: bool = False, + force: bool = False, + dry_run: bool = False, + list_pending: bool = False, +) -> dict[str, dict[str, Any]]: + from .regenconf import regen_conf + + if (names == [] or "nftables" in names) and tools_migrations_state()[ + "migrations" + ].get("0032_firewall_config") not in ["skipped", "done"]: + # Make sure the firewall conf is migrated before running the regenconf, + # otherwise the nftable regenconf wont work + try: + tools_migrations_run(["0032_firewall_config"]) + except Exception as e: + logger.error(e) + + return regen_conf(names, with_diff, force, dry_run, list_pending) + + +class AvailableUpdatesInfos(TypedDict): + system: dict[str, list[dict[str, str]]] + apps: list["AppInfo"] + important_yunohost_upgrade: bool + pending_migrations: list[dict[str, Any]] + last_apt_update: int + last_apps_catalog_update: int + + +def tools_update_norefresh() -> AvailableUpdatesInfos: + return tools_update(no_refresh=True) + + +@is_unit_operation(sse_only=True) +def tools_update( + operation_logger, target=None, no_refresh=False +) -> AvailableUpdatesInfos: + """ + Update apps & system package cache + """ + from .app_catalog import _update_apps_catalog + + refresh = not no_refresh + + if not target: + target = "all" + + if target not in ["system", "apps", "all"]: + raise YunohostError( + f"Unknown target {target}, should be 'system', 'apps' or 'all'", + raw_msg=True, + ) + + if refresh: + operation_logger.start() + + upgradable_system_packages = [] + if target in ["system", "all"]: + # Update APT cache + # LC_ALL=C is here to make sure the results are in english + command = "LC_ALL=C apt-get update --error-on=any -o Acquire::Retries=3 --allow-releaseinfo-change --error-on=any" + + # Filter boring message about "apt not having a stable CLI interface" + # Also keep track of whether or not we encountered a warning... + warnings = [] + + def is_legit_warning(m: str) -> bool: + legit_warning = ( + bool(m.rstrip()) + and "apt does not have a stable CLI interface" not in m.rstrip() + ) + if legit_warning: + warnings.append(m) + return legit_warning + + callbacks = ( + # 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()) + ), + ) + + if refresh: + logger.info(m18n.n("updating_apt_cache")) + + returncode = call_async_output(command, callbacks, shell=True) + + if returncode != 0: + raise YunohostError( + "update_apt_cache_failed", + sourceslist="\n".join(_dump_sources_list()), + ) + elif warnings: + logger.error( + m18n.n( + "update_apt_cache_warning", + sourceslist="\n".join(_dump_sources_list()), + ) + ) + + logger.debug(m18n.n("done")) + + upgradable_system_packages = list(_list_upgradable_apt_packages()) + + apps = [] + upgradable_apps = [] + if target in ["apps", "all"]: + if refresh: + try: + _update_apps_catalog() + except YunohostError as e: + logger.error(str(e)) + + apps = _list_apps_with_upgrade_infos() + upgradable_apps = [ + app + for app in apps + if app["upgrade"]["status"] in ["upgradable", "fail_requirements"] + ] + + if len(upgradable_apps) == 0 and len(upgradable_system_packages) == 0: + logger.info(m18n.n("already_up_to_date")) + + important_yunohost_upgrade = False + if upgradable_system_packages and any( + p["name"] == "yunohost" for p in upgradable_system_packages + ): + yunohost = [p for p in upgradable_system_packages if p["name"] == "yunohost"][0] + current_version = yunohost["current_version"].split(".")[:2] + new_version = yunohost["new_version"].split(".")[:2] + important_yunohost_upgrade = current_version != new_version + + upgradable_system_packages_per_categories = _group_packages_per_categories( + upgradable_system_packages + ) + + # Wrapping this in a try/except just in case for some reason we can't load + # the migrations, which would result in the update/upgrade process being blocked... + try: + pending_migrations = tools_migrations_list(pending=True)["migrations"] + except Exception as e: + logger.error(e) + pending_migrations = [] + + try: + last_apt_update_in_seconds = int( + time.time() - os.stat("/var/cache/apt/pkgcache.bin").st_mtime + ) + except Exception as e: + logger.warning(f"Failed to compute last apt update time ? {e}") + last_apt_update_in_seconds = 99999 * 3600 + + try: + last_apps_catalog_update_in_seconds = int( + time.time() - os.stat("/var/cache/yunohost/repo/default.json").st_mtime + ) + except Exception as e: + logger.warning(f"Failed to compute last apps catalog update time ? {e}") + last_apps_catalog_update_in_seconds = 99999 * 3600 + + return { + "system": upgradable_system_packages_per_categories, + "apps": apps, + "important_yunohost_upgrade": important_yunohost_upgrade, + "pending_migrations": pending_migrations, + "last_apt_update": last_apt_update_in_seconds, + "last_apps_catalog_update": last_apps_catalog_update_in_seconds, + } + + +def _list_apps_with_upgrade_infos( + with_pre_upgrade_notifications: bool = True, +) -> list["AppInfo"]: + from .app import _installed_apps, app_info + + apps = [] + for app_id in sorted(_installed_apps()): + try: + app_info_dict = app_info( + app_id, + with_upgrade_infos=True, + with_pre_upgrade_notifications=with_pre_upgrade_notifications, + ) + except Exception as e: + logger.error(f"Failed to read info for {app_id} : {e}", exc_info=True) + continue + if app_info_dict["upgrade"]["status"] == "up_to_date": + continue + if app_info_dict["upgrade"]["requirements"]: + app_info_dict["upgrade"]["requirements"] = { + k: r + for k, r in app_info_dict["upgrade"]["requirements"].items() + if not r["passed"] + } + if "settings" in app_info_dict: + del app_info_dict["settings"] + + apps.append(app_info_dict) + + if not with_pre_upgrade_notifications: + return apps + + return apps + + +@is_unit_operation() +def tools_upgrade(operation_logger: OperationLogger, target: str | None = None) -> None: + """ + Update apps & package cache, then display changelog + + Keyword arguments: + apps -- List of apps to upgrade (or [] to update all apps) + system -- True to upgrade system + """ + + from .app import app_upgrade + + if dpkg_is_broken(): + raise YunohostValidationError("dpkg_is_broken") + + # Check for obvious conflict with other dpkg/apt commands already running in parallel + if not dpkg_lock_available(): + raise YunohostValidationError("dpkg_lock_not_available") + + if target not in ["apps", "system"]: + raise YunohostValidationError( + "Uhoh ?! tools_upgrade should have 'apps' or 'system' value for argument target", + raw_msg=True, + ) + + # + # Apps + # This is basically just an alias to yunohost app upgrade ... + # + + if target == "apps": + # Make sure there's actually something to upgrade + + apps = _list_apps_with_upgrade_infos(with_pre_upgrade_notifications=False) + upgradable_apps = [ + app["id"] + for app in apps + if app["upgrade"]["status"] in ["upgradable", "fail_requirements"] + ] + + if not upgradable_apps: + logger.info(m18n.n("apps_already_up_to_date")) + return + + # Actually start the upgrades + + try: + app_upgrade(app=upgradable_apps) + except Exception as e: + logger.warning(f"unable to upgrade apps: {e}") + logger.error(m18n.n("app_upgrade_some_app_failed")) + + return + + # + # System + # + + if target == "system": + # Check that there's indeed some packages to upgrade + upgradables = list(_list_upgradable_apt_packages()) + if not upgradables: + logger.info(m18n.n("already_up_to_date")) + + logger.info(m18n.n("upgrading_packages")) + operation_logger.start() + + # Prepare dist-upgrade command + dist_upgrade = "DEBIAN_FRONTEND=noninteractive" + if Moulinette.interface.type == "api": + dist_upgrade += " YUNOHOST_API_RESTART_WILL_BE_HANDLED_BY_YUNOHOST=yes" + dist_upgrade += " APT_LISTCHANGES_FRONTEND=none" + dist_upgrade += " apt-get" + dist_upgrade += ( + " --fix-broken --show-upgraded --assume-yes --quiet -o=Dpkg::Use-Pty=0" + ) + for conf_flag in ["old", "miss", "def"]: + dist_upgrade += ' -o Dpkg::Options::="--force-conf{}"'.format(conf_flag) + dist_upgrade += " dist-upgrade" + + logger.info(m18n.n("tools_upgrade")) + + 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()) + ), + ) + returncode = call_async_output(dist_upgrade, callbacks, shell=True) + + # If yunohost is being upgraded from the webadmin + if ( + any(p["name"] == "yunohost" for p in upgradables) + and Moulinette.interface.type == "api" + ): + # Restart the API after 10 sec (at now doesn't support sub-minute times...) + # We do this so that the API / webadmin still gets the proper HTTP response + # It's then up to the webadmin to implement a proper UX process to wait 10 sec and then auto-fresh the webadmin + cmd = 'at -M now >/dev/null 2>&1 <<< "sleep 10; systemctl restart yunohost-api"' + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicitly... + subprocess.check_call(["bash", "-c", cmd]) + + if returncode != 0: + upgradables = list(_list_upgradable_apt_packages()) + logger.warning( + m18n.n( + "tools_upgrade_failed", + packages_list=", ".join([p["name"] for p in upgradables]), + ) + ) + + logger.success(m18n.n("system_upgraded")) + operation_logger.success() + + +@is_unit_operation() +def tools_shutdown(operation_logger: OperationLogger, force: bool = False) -> None: + shutdown = force + if not shutdown: + try: + # Ask confirmation for server shutdown + i = Moulinette.prompt(m18n.n("server_shutdown_confirm", answers="y/N")) + except NotImplementedError: + pass + else: + if i.lower() == "y" or i.lower() == "yes": + shutdown = True + + if shutdown: + operation_logger.start() + logger.warning(m18n.n("server_shutdown")) + subprocess.check_call(["systemctl", "poweroff"]) + + +@is_unit_operation() +def tools_reboot(operation_logger: OperationLogger, force: bool = False) -> None: + reboot = force + if not reboot: + try: + # Ask confirmation for restoring + i = Moulinette.prompt(m18n.n("server_reboot_confirm", answers="y/N")) + except NotImplementedError: + pass + else: + if i.lower() == "y" or i.lower() == "yes": + reboot = True + if reboot: + operation_logger.start() + logger.warning(m18n.n("server_reboot")) + subprocess.check_call(["systemctl", "reboot"]) + + +def tools_shell(command: str | None = None) -> None: + """ + Launch an (i)python shell in the YunoHost context. + + This is entirely aim for development. + """ + + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + + if command: + exec(command) + return + + logger.warning("The \033[1;34mldap\033[0m interface is available in this context") + try: + from IPython import embed + + embed() + except (ImportError, ModuleNotFoundError): + logger.warning( + "You don't have IPython installed, consider installing it as it is way better than the standard shell." + ) + logger.warning("Falling back on the standard shell.") + + import readline # will allow Up/Down/History in the console + + readline # to please pyflakes + import code + + vars = globals().copy() + vars.update(locals()) + shell = code.InteractiveConsole(vars) + shell.interact() + + +def tools_basic_space_cleanup() -> None: + """ + Basic space cleanup. + + apt autoremove + apt autoclean + journalctl vacuum (leaves 50M of logs) + archived logs removal + yunohost 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) + subprocess.run( + "find /var/log/yunohost/operations/ -type f,l -mtime +90 -execdir rm {} +", + shell=True, + ) + + +# ############################################ # +# # +# Migrations management # +# # +# ############################################ # + + +def tools_migrations_list( + pending: bool = False, done: bool = False +) -> dict[str, list[dict[str, Any]]]: + """ + List existing migrations + """ + + # Check for option conflict + if pending and done: + raise YunohostValidationError("migrations_list_conflict_pending_done") + + # Get all migrations + _migrations = _get_migrations_list() + + # Reduce to dictionaries + migrations = [ + { + "id": migration.id, + "number": migration.number, + "name": migration.name, + "mode": migration.mode, + "state": migration.state, + "description": migration.description, + "disclaimer": migration.disclaimer, + } + for migration in _migrations + ] + + # If asked, filter pending or done migrations + if pending or done: + if done: + migrations = [m for m in migrations if m["state"] != "pending"] + if pending: + migrations = [m for m in migrations if m["state"] == "pending"] + + return {"migrations": migrations} + + +def tools_migrations_run( + targets: list[str] = [], + skip: bool = False, + auto: bool = False, + force_rerun: bool = False, + accept_disclaimer: bool = False, +) -> None: + """ + Perform migrations + + targets A list migrations to run (all pendings by default) + --skip Skip specified migrations (to be used only if you know what you are doing) (must explicit which migrations) + --auto Automatic mode, won't run manual migrations (to be used only if you know what you are doing) + --force-rerun Re-run already-ran migrations (to be used only if you know what you are doing)(must explicit which migrations) + --accept-disclaimer Accept disclaimers of migrations (please read them before using this option) (only valid for one migration) + """ + + all_migrations = _get_migrations_list() + + # Small utility that allows up to get a migration given a name, id or number later + def get_matching_migration(target): + for m in all_migrations: + if m.id == target or m.name == target or m.id.split("_")[0] == target: + return m + + raise YunohostValidationError("migrations_no_such_migration", id=target) + + # Dirty hack to mark the bullseye->bookworm as done ... + # it may still be marked as 'pending' if for some reason the migration crashed, + # but the admins ran 'apt full-upgrade' to manually finish the migration + # ... in which case it won't be magically flagged as 'done' until here + migrate_to_bookworm = get_matching_migration("migrate_to_bookworm") + if migrate_to_bookworm.state == "pending": + migrate_to_bookworm.state = "done" + _write_migration_state(migrate_to_bookworm.id, "done") + + # auto, skip and force are exclusive options + if auto + skip + force_rerun > 1: + raise YunohostValidationError("migrations_exclusive_options") + + # If no target specified + if not targets: + # skip, revert or force require explicit targets + if skip or force_rerun: + raise YunohostValidationError("migrations_must_provide_explicit_targets") + + # Otherwise, targets are all pending migrations + migrationtargets = [m for m in all_migrations if m.state == "pending"] + + # If explicit targets are provided, we shall validate them + else: + migrationtargets = [get_matching_migration(t) for t in targets] + done = [t.id for t in migrationtargets if t.state != "pending"] + pending = [t.id for t in migrationtargets if t.state == "pending"] + + if skip and done: + raise YunohostValidationError( + "migrations_not_pending_cant_skip", ids=", ".join(done) + ) + if force_rerun and pending: + raise YunohostValidationError( + "migrations_pending_cant_rerun", ids=", ".join(pending) + ) + if not (skip or force_rerun) and done: + raise YunohostValidationError("migrations_already_ran", ids=", ".join(done)) + + # So, is there actually something to do ? + if not migrationtargets: + logger.info(m18n.n("migrations_no_migrations_to_run")) + return + + # Actually run selected migrations + for migration in migrationtargets: + # If we are migrating in "automatic mode" (i.e. from debian configure + # during an upgrade of the package) but we are asked for running + # migrations to be ran manually by the user, stop there and ask the + # user to run the migration manually. + if auto and migration.mode == "manual": + logger.warning(m18n.n("migrations_to_be_ran_manually", id=migration.id)) + + # We go to the next migration + continue + + # Check for migration dependencies + if not skip: + dependencies = [ + get_matching_migration(dep) for dep in migration.dependencies + ] + pending_dependencies = [ + dep.id for dep in dependencies if dep.state == "pending" + ] + if pending_dependencies: + logger.error( + m18n.n( + "migrations_dependencies_not_satisfied", + id=migration.id, + dependencies_id=", ".join(pending_dependencies), + ) + ) + continue + + # If some migrations have disclaimers (and we're not trying to skip them) + if migration.disclaimer and not skip: + # require the --accept-disclaimer option. + # Otherwise, go to the next migration + if not accept_disclaimer: + logger.warning( + m18n.n( + "migrations_need_to_accept_disclaimer", + id=migration.id, + disclaimer=migration.disclaimer, + ) + ) + continue + # --accept-disclaimer will only work for the first migration + else: + accept_disclaimer = False + + # Start register change on system + operation_logger = OperationLogger("tools_migrations_migrate_forward") + operation_logger.start() + + if skip: + logger.warning(m18n.n("migrations_skip_migration", id=migration.id)) + migration.state = "skipped" + _write_migration_state(migration.id, "skipped") + operation_logger.success() + else: + try: + logger.info(m18n.n("migrations_running_forward", id=migration.id)) + migration.run() + except Exception as e: + # migration failed, let's stop here but still update state because + # we managed to run the previous ones + msg = m18n.n( + "migrations_migration_has_failed", exception=e, id=migration.id + ) + logger.error(msg, exc_info=True) + operation_logger.error(msg) + else: + logger.success(m18n.n("migrations_success_forward", id=migration.id)) + migration.state = "done" + _write_migration_state(migration.id, "done") + + operation_logger.success() + + +def tools_migrations_state() -> dict[str, dict[Any, Any]]: + """ + Show current migration state + """ + if not os.path.exists(MIGRATIONS_STATE_PATH): + return {"migrations": {}} + + return read_yaml(MIGRATIONS_STATE_PATH) # type: ignore[return-value] + + +def _write_migration_state(migration_id, state): + current_states = tools_migrations_state() + current_states["migrations"][migration_id] = state + write_to_yaml(MIGRATIONS_STATE_PATH, current_states) + + +def _get_migrations_list() -> list["Migration"]: + # states is a datastructure that represents the last run migration + # it has this form: + # { + # "0001_foo": "skipped", + # "0004_baz": "done", + # "0002_bar": "skipped", + # "0005_zblerg": "done", + # } + # (in particular, pending migrations / not already ran are not listed + states = tools_migrations_state()["migrations"] + + migrations = [] + migrations_folder = os.path.dirname(__file__) + "/migrations/" + for migration_file in [ + x + for x in os.listdir(migrations_folder) + if re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x) + ]: + m = _load_migration(migration_file) + m.state = states.get(m.id, "pending") + migrations.append(m) + + return sorted(migrations, key=lambda m: m.id) + + +def _get_migration_by_name(migration_name): + """ + Low-level / "private" function to find a migration by its name + """ + + try: + from . import migrations + except ImportError: + raise AssertionError(f"Unable to find migration with name {migration_name}") + + migrations_path = migrations.__path__[0] + migrations_found = [ + x + for x in os.listdir(migrations_path) + if re.match(r"^\d+_%s\.py$" % migration_name, x) + ] + + assert len(migrations_found) == 1, ( + f"Unable to find migration with name {migration_name}" + ) + + return _load_migration(migrations_found[0]) + + +def _load_migration(migration_file: str) -> "Migration": + migration_id = migration_file[: -len(".py")] + + logger.debug(m18n.n("migrations_loading_migration", id=migration_id)) + + try: + # this is python builtin method to import a module using a name, we + # use that to import the migration as a python object so we'll be + # able to run it in the next loop + module = import_module("yunohost.migrations.{}".format(migration_id)) + return module.MyMigration(migration_id) + except Exception as e: + import traceback + + traceback.print_exc() + + raise YunohostError( + "migrations_failed_to_load_migration", id=migration_id, error=e + ) + + +def _skip_all_migrations() -> None: + """ + Skip all pending migrations. + This is meant to be used during postinstall to + initialize the migration system. + """ + all_migrations = _get_migrations_list() + new_states: dict[Literal["migrations"], dict[str, str]] = {"migrations": {}} + for migration in all_migrations: + new_states["migrations"][migration.id] = "skipped" + write_to_yaml(MIGRATIONS_STATE_PATH, new_states) # type: ignore[arg-type] + + +def _tools_migrations_run_after_system_restore(backup_version: str) -> None: + all_migrations = _get_migrations_list() + + current_version = version.parse(ynh_packages_version()["yunohost"]["version"]) + backup_version_v = version.parse(backup_version) + + if backup_version_v == current_version: + return + + for migration in all_migrations: + migration_version = getattr(migration, "introduced_in_version", None) + migration_method = getattr(migration, "run_after_system_restore", None) + + if ( + migration_version is not None + and version.parse(migration_version) > backup_version_v + and migration_method is not None + ): + try: + logger.info(m18n.n("migrations_running_forward", id=migration.id)) + migration_method() + except Exception as e: + msg = m18n.n( + "migrations_migration_has_failed", exception=e, id=migration.id + ) + logger.error(msg, exc_info=True) + raise + + +def _tools_migrations_run_before_app_restore( + backup_version, app_id, app_backup_in_archive +): + all_migrations = _get_migrations_list() + + current_version = version.parse(ynh_packages_version()["yunohost"]["version"]) + backup_version = version.parse(backup_version) + + if backup_version == current_version: + return + + for migration in all_migrations: + migration_version = getattr(migration, "introduced_in_version", None) + migration_method = getattr(migration, "run_before_app_restore", None) + + if ( + migration_version is not None + and version.parse(migration_version) > backup_version + and migration_method is not None + ): + try: + logger.info(m18n.n("migrations_running_forward", id=migration.id)) + migration_method(app_id, app_backup_in_archive) + except Exception as e: + msg = m18n.n( + "migrations_migration_has_failed", exception=e, id=migration.id + ) + logger.error(msg, exc_info=1) + raise + + +class Migration: + # Those are to be implemented by daughter classes + + state: Literal["pending", "done", "skipped"] | None = None + mode: Literal["auto", "manual"] = "auto" + # List of migration ids required before running this migration + dependencies: list[str] = [] + + # For migrations that have @ldap_migration + ldap_migration_started = False + + @property + def disclaimer(self) -> str | None: + return None + + def run(self) -> None: + raise NotImplementedError() + + # The followings shouldn't be overridden + + def __init__(self, id_: str) -> None: + self.id = id_ + self.number = int(self.id.split("_", 1)[0]) + self.name = self.id.split("_", 1)[1] + + @property + def description(self) -> str: + return m18n.n(f"migration_description_{self.id}") # type: ignore + + @staticmethod + def ldap_migration(run: Callable[[Any, str], None]) -> Callable[[Any], None]: + def func(self: "Migration") -> None: + # Backup LDAP before the migration + logger.info(m18n.n("migration_ldap_backup_before_migration")) + try: + backup_folder = "/home/yunohost.backup/premigration/" + time.strftime( + "%Y%m%d-%H%M%S", time.gmtime() + ) + mkdir(backup_folder, 0o750, parents=True) + os.system("systemctl stop slapd") + cp("/etc/ldap", f"{backup_folder}/ldap_config", recursive=True) + cp("/var/lib/ldap", f"{backup_folder}/ldap_db", recursive=True) + cp( + "/etc/yunohost/apps", + f"{backup_folder}/apps_settings", + recursive=True, + ) + except Exception as e: + raise YunohostError( + "migration_ldap_can_not_backup_before_migration", error=str(e) + ) + finally: + os.system("systemctl start slapd") + + try: + run(self, backup_folder) + except Exception: + if self.ldap_migration_started: + logger.warning( + m18n.n("migration_ldap_migration_failed_trying_to_rollback") + ) + os.system("systemctl stop slapd") + # To be sure that we don't keep some part of the old config + rm("/etc/ldap", force=True, recursive=True) + cp(f"{backup_folder}/ldap_config", "/etc/ldap", recursive=True) + chown("/etc/ldap/schema/", "openldap", "openldap", recursive=True) + chown("/etc/ldap/slapd.d/", "openldap", "openldap", recursive=True) + rm("/var/lib/ldap", force=True, recursive=True) + cp(f"{backup_folder}/ldap_db", "/var/lib/ldap", recursive=True) + rm("/etc/yunohost/apps", force=True, recursive=True) + chown("/var/lib/ldap/", "openldap", recursive=True) + cp( + f"{backup_folder}/apps_settings", + "/etc/yunohost/apps", + recursive=True, + ) + os.system("systemctl start slapd") + rm(backup_folder, force=True, recursive=True) + logger.info(m18n.n("migration_ldap_rollback_success")) + raise + else: + rm(backup_folder, force=True, recursive=True) + + return func diff --git a/src/user.py b/src/user.py new file mode 100644 index 0000000..f9519a8 --- /dev/null +++ b/src/user.py @@ -0,0 +1,1679 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import copy +import grp +import os +import pwd +import random +import re +import subprocess +from logging import getLogger +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + BinaryIO, + Callable, + Literal, + Mapping, + NotRequired, + TextIO, + TypedDict, + Union, + cast, +) + +from moulinette import Moulinette, m18n + +from .log import is_flash_unit_operation, is_unit_operation +from .service import service_status +from .utils.error import YunohostError, YunohostValidationError +from .utils.process import check_output +from .utils.system import binary_to_human + +if TYPE_CHECKING: + from bottle import HTTPResponse as HTTPResponseType + + from .log import OperationLogger + from .permission import PermInfos + from .utils.logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.user")) +else: + logger = getLogger("yunohost.user") + + +FIELDS_FOR_IMPORT = { + "username": r"^[a-z0-9][-a-z0-9_.]*$", + "firstname": r"^([^\W\d_]{1,30}[ ,.\'-]{0,3})+$", + "lastname": r"^([^\W\d_]{1,30}[ ,.\'-]{0,3})+$", + "password": r"^|(.{3,})$", + "mail": r"^([\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}))$", + "mail-alias": r"^|([\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}),?)+$", + "mail-forward": r"^|([\w\+.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}),?)+$", + "mailbox-quota": r"^(\d+[bkMGT])|0|$", + "groups": r"^|([a-z0-9][-a-z0-9_.]*(,?[a-z0-9][-a-z0-9_.]*)*)$", +} + +ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"] + + +def user_list(fields: list[str] | None = None) -> dict[str, dict[str, Any]]: + from .utils.ldap import _get_ldap_interface + + ldap_attrs = { + "username": "uid", + "password": "", # We can't request password in ldap + "fullname": "cn", + "firstname": "givenName", + "lastname": "sn", + "mail": "mail", + "mail-alias": "mail", + "mail-forward": "maildrop", + "mailbox-quota": "mailuserquota", + "groups": "memberOf", + "shell": "loginShell", + "home-path": "homeDirectory", + } + + def display_default(values: list[str], _: dict[str, list[str]]) -> str | list[str]: + return values[0] if len(values) == 1 else values + + display: dict[str, Callable[[list[str], dict[str, list[str]]], Any]] = { + "password": lambda values, user: "", + "mail": lambda values, user: display_default(values[:1], user), + "mail-alias": lambda values, _: values[1:], + "mail-forward": lambda values, user: [ + forward for forward in values if forward != user["uid"][0] + ], + "groups": lambda values, user: [ + group[3:].split(",")[0] + for group in values + if not group.startswith("cn=all_users,") + and not group.startswith("cn=" + user["uid"][0] + ",") + ], + "shell": lambda values, _: ( + len(values) > 0 and values[0].strip() == "/bin/false" + ), + } + + attrs = {"uid"} + users = {} + + if not fields: + fields = ["username", "fullname", "mail", "mailbox-quota"] + + for field in fields: + if field in ldap_attrs: + attrs.add(ldap_attrs[field]) + else: + raise YunohostError("field_invalid", field=field) + + ldap = _get_ldap_interface() + result = ldap.search( + "ou=users", + "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))", + attrs, + ) + + for user in result: + entry: dict[str, str] = {} + for field in fields: + values = [] + if ldap_attrs[field] in user: + values = user[ldap_attrs[field]] + entry[field] = display.get(field, display_default)(values, user) + + username: str = user["uid"][0] + users[username] = entry + + # Dict entry 0 has incompatible type "str": "dict[Any, dict[str, Any]]"; + # expected "str": "dict[str, str]" [dict-item] + return {"users": users} + + +def list_shells() -> list[str]: + return [ + line.strip() + for line in Path("/etc/shells").open("r").readlines() + if line.startswith("/") + ] + + +def shellexists(shell: str) -> bool: + """Check if the provided shell exists and is executable.""" + return os.path.isfile(shell) and os.access(shell, os.X_OK) + + +@is_unit_operation([("username", "user")]) +def user_create( + operation_logger: "OperationLogger", + username: str, + domain: str, + password: str, + fullname: str, + mailbox_quota: str | None = "0", + admin: bool = False, + from_import: bool = False, + loginShell: str | None = None, +) -> dict[str, str]: + if not fullname.strip(): + 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 .app import app_ssowatconf + from .domain import _assert_domain_exists, _get_maindomain, domain_list + from .hook import hook_callback + from .utils.ldap import _get_ldap_interface + from .utils.password import ( + _hash_user_password, + assert_password_is_compatible, + assert_password_is_strong_enough, + ) + + # Ensure compatibility and sufficiently complex password + assert_password_is_compatible(password) + assert_password_is_strong_enough("admin" if admin else "user", password) + + # Validate domain used for email address account + if domain is None: + if Moulinette.interface.type == "api": + raise YunohostValidationError( + "Invalid usage, you should specify a domain argument" + ) + else: + # On affiche les differents domaines possibles + Moulinette.display(m18n.n("domains_available")) + for domain in domain_list()["domains"]: + Moulinette.display(f"- {domain}") + + maindomain = _get_maindomain() + domain = Moulinette.prompt( + m18n.n("ask_user_domain") + f" (default: {maindomain})" + ) + if not domain: + domain = maindomain + + # Check that the domain exists + _assert_domain_exists(domain) + + mail = f"{username}@{domain}" + ldap = _get_ldap_interface() + + if username in user_list()["users"]: + raise YunohostValidationError("user_already_exists", user=username) + + # Validate uniqueness of username and mail in LDAP + try: + ldap.validate_uniqueness({"uid": username, "mail": mail, "cn": username}) + except Exception as e: + raise YunohostValidationError("user_creation_failed", user=username, error=e) + + # Validate uniqueness of username in system users + all_existing_usernames = {x.pw_name for x in pwd.getpwall()} + if username in all_existing_usernames: + raise YunohostValidationError("system_username_exists") + + if mail.split("@")[0] in ADMIN_ALIASES: + raise YunohostValidationError("mail_unavailable") + + if not from_import: + operation_logger.start() + + # Get random UID/GID + all_uid = {str(x.pw_uid) for x in pwd.getpwall()} + all_gid = {str(x.gr_gid) for x in grp.getgrall()} + + # Prevent users from obtaining uid 1007 which is the uid of the legacy admin, + # and there could be a edge case where a new user becomes owner of an old, removed admin user + all_uid.add("1007") + all_gid.add("1007") + + uid_guid_found = False + while not uid_guid_found: + # LXC uid number is limited to 65536 by default + uid: str = str(random.randint(1001, 65000)) + uid_guid_found = uid not in all_uid and uid not in all_gid + + if not loginShell: + loginShell = "/bin/bash" + else: + if not shellexists(loginShell) or loginShell not in list_shells(): + raise YunohostValidationError("invalid_shell", shell=loginShell) + + attr_dict: Mapping[str, str | list[str]] = { + "objectClass": [ + "mailAccount", + "inetOrgPerson", + "posixAccount", + "userPermissionYnh", + ], + "givenName": [firstname], + "sn": [lastname], + "displayName": [fullname], + "cn": [fullname], + "uid": [username], + "mail": mail, # NOTE: this one seems to be already a list + "maildrop": [username], + "mailuserquota": [mailbox_quota or "0"], + "userPassword": [_hash_user_password(password)], + "gidNumber": [uid], + "uidNumber": [uid], + "homeDirectory": ["/home/" + username], + "loginShell": [loginShell], + } + + try: + ldap.add(f"uid={username},ou=users", attr_dict) + except Exception as e: + raise YunohostError("user_creation_failed", user=username, error=e) + + # Invalidate passwd and group to take user and group creation into account + subprocess.call(["nscd", "-i", "passwd"]) + subprocess.call(["nscd", "-i", "group"]) + + try: + # Attempt to create user home folder + subprocess.check_call(["mkhomedir_helper", username]) + except subprocess.CalledProcessError: + home = f"/home/{username}" + if not os.path.isdir(home): + logger.warning( + m18n.n("user_home_creation_failed", home=home), exc_info=True + ) + + try: + subprocess.check_call(["setfacl", "-m", "g:all_users:---", f"/home/{username}"]) + except subprocess.CalledProcessError: + logger.warning(f"Failed to protect /home/{username}", exc_info=True) + + # Create group for user and add to group 'all_users' + user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) + if admin: + user_group_update(groupname="admins", add=username, sync_perm=False) + user_group_update(groupname="all_users", add=username, force=True, sync_perm=True) + app_ssowatconf() + + # Trigger post_user_create hooks + env_dict = { + "YNH_USER_USERNAME": username, + "YNH_USER_MAIL": mail, + "YNH_USER_PASSWORD": password, + "YNH_USER_FIRSTNAME": firstname, + "YNH_USER_LASTNAME": lastname, + } + + hook_callback("post_user_create", args=[username, mail], env=env_dict) + + # TODO: Send a welcome mail to user + if not from_import: + logger.success(m18n.n("user_created")) + + return {"fullname": fullname, "username": username, "mail": mail} + + +@is_unit_operation([("username", "user")]) +def user_delete( + operation_logger: "OperationLogger", + username: str, + purge: bool = False, + from_import: bool = False, + force: bool = False, +) -> None: + from .app import app_ssowatconf + from .authenticators.ldap_admin import Authenticator as AdminAuth + from .authenticators.ldap_ynhuser import Authenticator as PortalAuth + from .hook import hook_callback + from .permission import _sync_permissions_with_ldap + from .utils.ldap import _get_ldap_interface + + groups = user_group_list()["groups"] + + if username not in user_list()["users"]: + raise YunohostValidationError("user_unknown", user=username) + elif force and username in groups["admins"] and len(groups["admins"]) <= 1: + raise YunohostValidationError("user_cannot_delete_last_admin") + + if not from_import: + operation_logger.start() + + user_group_update( + "all_users", + remove=username, + force=True, + from_import=from_import, + sync_perm=False, + ) + for group, infos in groups.items(): + if group == "all_users": + continue + # If the user is in this group (and it's not the primary group), + # remove the member from the group + if username != group and username in infos["members"]: + user_group_update( + group, + remove=username, + sync_perm=False, + from_import=from_import, + force=force, + ) + + # Delete primary group if it exists (why wouldnt it exists ? because some + # epic bug happened somewhere else and only a partial removal was + # performed...) + if username in user_group_list()["groups"].keys(): + user_group_delete(username, force=True, sync_perm=True) + + ldap = _get_ldap_interface() + try: + ldap.remove(f"uid={username},ou=users") + except Exception as e: + raise YunohostError("user_deletion_failed", user=username, error=e) + + _sync_permissions_with_ldap() + app_ssowatconf() + + 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"]) + + if purge: + subprocess.call(["rm", "-rf", f"/home/{username}"]) + subprocess.call(["rm", "-rf", f"/var/mail/{username}"]) + + hook_callback("post_user_delete", args=[username, purge]) + + if not from_import: + logger.success(m18n.n("user_deleted")) + + +@is_unit_operation([("username", "user")], exclude=["change_password"]) +def user_update( + operation_logger: "OperationLogger", + username: str, + mail: str | None = None, + change_password: str | None = None, + add_mailforward: None | str | list[str] = None, + remove_mailforward: None | str | list[str] = None, + add_mailalias: None | str | list[str] = None, + remove_mailalias: None | str | list[str] = None, + mailbox_quota: str | None = None, + from_import: bool = False, + fullname: str | None = None, + loginShell: str | None = None, +): + 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 .app import app_ssowatconf + from .domain import domain_list + from .hook import hook_callback + from .utils.ldap import _get_ldap_interface + from .utils.password import ( + _hash_user_password, + assert_password_is_compatible, + assert_password_is_strong_enough, + ) + + domains = domain_list()["domains"] + + # Populate user informations + ldap = _get_ldap_interface() + attrs_to_fetch = ["givenName", "sn", "mail", "maildrop", "memberOf"] + result = ldap.search( + base="ou=users", + filter="uid=" + username, + attrs=attrs_to_fetch, + ) + if not result: + raise YunohostValidationError("user_unknown", user=username) + user = result[0] + env_dict: dict[str, str] = {"YNH_USER_USERNAME": username} + + # Get modifications from arguments + new_attr_dict: dict[str, Any] = {} + if firstname: + new_attr_dict["givenName"] = firstname # TODO: Validate + new_attr_dict["cn"] = new_attr_dict["displayName"] = ( + firstname + " " + user["sn"][0] + ).strip() + env_dict["YNH_USER_FIRSTNAME"] = firstname + + if lastname: + new_attr_dict["sn"] = lastname # TODO: Validate + new_attr_dict["cn"] = new_attr_dict["displayName"] = ( + user["givenName"][0] + " " + lastname + ).strip() + env_dict["YNH_USER_LASTNAME"] = lastname + + if lastname and firstname: + new_attr_dict["cn"] = new_attr_dict["displayName"] = ( + firstname + " " + lastname + ).strip() + + # change_password is None if user_update is not called to change the password + if change_password is not None and change_password != "": + # when in the cli interface if the option to change the password is called + # without a specified value, change_password will be set to the const 0. + # In this case we prompt for the new password. + if Moulinette.interface.type == "cli" and not change_password: + change_password = cast( + str, + Moulinette.prompt( + m18n.n("ask_password"), is_password=True, confirm=True + ), + ) + + # Ensure compatibility and sufficiently complex password + assert_password_is_compatible(change_password) + is_admin = "cn=admins,ou=groups,dc=yunohost,dc=org" in user["memberOf"] + assert_password_is_strong_enough( + "admin" if is_admin else "user", change_password + ) + + new_attr_dict["userPassword"] = _hash_user_password(change_password) + env_dict["YNH_USER_PASSWORD"] = change_password + + if mail: + # If the requested mail address is already as main address or as an alias by this user + if mail in user["mail"]: + if mail != user["mail"][0]: + user["mail"].remove(mail) + # Othewise, check that this mail address is not already used by this user + else: + try: + ldap.validate_uniqueness({"mail": mail}) + except Exception as e: + raise YunohostError("user_update_failed", user=username, error=e) + if mail[mail.find("@") + 1 :] not in domains: + raise YunohostError( + "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] + ) + + if mail.split("@")[0] in ADMIN_ALIASES: + raise YunohostValidationError("mail_unavailable") + + user["mail"] = [mail] + user["mail"][1:] + new_attr_dict["mail"] = user["mail"] + + if add_mailalias is not None: + if not isinstance(add_mailalias, list): + add_mailalias = [add_mailalias] + for mail in add_mailalias: + if mail.split("@")[0] in ADMIN_ALIASES: + raise YunohostValidationError("mail_unavailable") + + # (c.f. similar stuff as before) + if mail in user["mail"]: + continue + else: + try: + ldap.validate_uniqueness({"mail": mail}) + except Exception as e: + raise YunohostError("user_update_failed", user=username, error=e) + if mail[mail.find("@") + 1 :] not in domains: + raise YunohostError( + "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] + ) + user["mail"].append(mail) + new_attr_dict["mail"] = user["mail"] + + if remove_mailalias: + if not isinstance(remove_mailalias, list): + remove_mailalias = [remove_mailalias] + for mail in remove_mailalias: + if len(user["mail"]) > 1 and mail in user["mail"][1:]: + user["mail"].remove(mail) + else: + raise YunohostValidationError("mail_alias_remove_failed", mail=mail) + new_attr_dict["mail"] = user["mail"] + + if "mail" in new_attr_dict: + env_dict["YNH_USER_MAILS"] = ",".join(new_attr_dict["mail"]) + + if add_mailforward: + if not isinstance(add_mailforward, list): + add_mailforward = [add_mailforward] + new_attr_dict["maildrop"] = set(user["maildrop"]) + new_attr_dict["maildrop"].update(set(add_mailforward)) + + if remove_mailforward: + if not isinstance(remove_mailforward, list): + remove_mailforward = [remove_mailforward] + new_attr_dict["maildrop"] = set(user["maildrop"]) - set(remove_mailforward) + + if len(user["maildrop"]) - len(remove_mailforward) != len( + new_attr_dict["maildrop"] + ): + raise YunohostValidationError("mail_forward_remove_failed", mail=mail) + + if "maildrop" in new_attr_dict: + env_dict["YNH_USER_MAILFORWARDS"] = ",".join(new_attr_dict["maildrop"]) + + if mailbox_quota is not None: + new_attr_dict["mailuserquota"] = mailbox_quota + env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota + + if loginShell is not None: + if not shellexists(loginShell) or loginShell not in list_shells(): + raise YunohostValidationError("invalid_shell", shell=loginShell) + new_attr_dict["loginShell"] = loginShell + env_dict["YNH_USER_LOGINSHELL"] = loginShell + + if not from_import: + operation_logger.start() + + try: + ldap.update(f"uid={username},ou=users", new_attr_dict) + except Exception as e: + raise YunohostError("user_update_failed", user=username, error=e) + + if "userPassword" in new_attr_dict: + logger.info("Invalidating sessions") + from .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"]) + + # Trigger post_user_update hooks + hook_callback("post_user_update", env=env_dict) + + if not from_import: + app_ssowatconf() + logger.success(m18n.n("user_updated")) + return user_info(username) + + +# Gotta use this syntax because some of the keys contain dashes (-) which are not valid varnames T_T +UserInfos = TypedDict( + "UserInfos", + { + "username": str, + "fullname": str, + "mail": str, + "loginShell": str, + "mail-aliases": list[str], + "mail-forward": list[str], + "mailbox-quota": NotRequired[dict[Literal["limit", "use"], Any]], + }, +) + + +def user_info(username: str) -> UserInfos: + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + + user_attrs = ["cn", "mail", "uid", "maildrop", "mailuserquota", "loginShell"] + + if len(username.split("@")) == 2: + filter = "mail=" + username + else: + filter = "uid=" + username + + result = ldap.search("ou=users", filter, user_attrs) + + if result: + user = result[0] + else: + raise YunohostValidationError("user_unknown", user=username) + + result_dict: UserInfos = { + "username": user["uid"][0], + "fullname": user["cn"][0], + "mail": user["mail"][0], + "loginShell": user["loginShell"][0], + "mail-aliases": [], + "mail-forward": [], + } + + if len(user["mail"]) > 1: + result_dict["mail-aliases"] = user["mail"][1:] + + if len(user["maildrop"]) > 1: + user["maildrop"].remove(username) + result_dict["mail-forward"] = user["maildrop"] + + if "mailuserquota" in user: + userquota = user["mailuserquota"][0] + + if isinstance(userquota, int): + userquota = str(userquota) + + # Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ ) + is_limited = not re.match("0[bkMGT]?", userquota) + storage_use = "?" + + 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.debug(m18n.n("mailbox_disabled", user=username)) + else: + try: + uid_ = user["uid"][0] + cmd_result = check_output(f"doveadm -f flow quota get -u {uid_}") + except Exception as e: + cmd_result = "" + logger.warning(f"Failed to fetch quota info ... : {e}") + + # Exemple of return value for cmd: + # """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0 + # Quota name=User quota Type=MESSAGE Value=0 Limit=- %=0""" + has_value = re.search(r"Value=(\d+)", cmd_result) + + if has_value: + storage_use_int = int(has_value.group(1)) * 1000 + storage_use = binary_to_human(storage_use_int) + + if is_limited: + has_percent = re.search(r"%=(\d+)", cmd_result) + + if has_percent: + percentage = int(has_percent.group(1)) + storage_use += " (%s%%)" % percentage + + result_dict["mailbox-quota"] = { + "limit": userquota if is_limited else m18n.n("unlimit"), + "use": storage_use, + } + + return result_dict + + +def user_export() -> Union[str, "HTTPResponseType"]: + """ + Export users into CSV + """ + import csv # CSV are needed only in this function + from io import StringIO + + with StringIO() as csv_io: + writer = csv.DictWriter( + csv_io, list(FIELDS_FOR_IMPORT.keys()), delimiter=";", quotechar='"' + ) + writer.writeheader() + users = user_list(list(FIELDS_FOR_IMPORT.keys()))["users"] + for username, user in users.items(): + user["mail-alias"] = ",".join(user["mail-alias"]) + user["mail-forward"] = ",".join(user["mail-forward"]) + user["groups"] = ",".join(user["groups"]) + writer.writerow(user) + + body = csv_io.getvalue().rstrip() + if Moulinette.interface.type == "api": + # We return a raw bottle HTTPresponse (instead of serializable data like + # list/dict, ...), which is gonna be picked and used directly by moulinette + from bottle import HTTPResponse + + response = HTTPResponse( + body=body, + headers={ + "Content-Disposition": "attachment; filename=users.csv", + "Content-Type": "text/csv", + }, + ) + return response + else: + return body + + +@is_unit_operation() +def user_import( + operation_logger: "OperationLogger", + csvfile: TextIO, + update: bool = False, + delete: bool = False, +) -> dict[str, int]: + """ + Import users from CSV + + Keyword argument: + csvfile -- CSV file with columns username;firstname;lastname;password;mailbox_quota;mail;alias;forward;groups + + """ + + import csv # CSV are needed only in this function + + from .app import app_ssowatconf + from .domain import domain_list + from .permission import _sync_permissions_with_ldap + from .utils.misc import random_ascii + + # Pre-validate data and prepare what should be done + actions: dict[str, list[dict[str, Any]]] = { + "created": [], + "updated": [], + "deleted": [], + } + is_well_formatted = True + + def to_list(str_list): + L = str_list.split(",") if str_list else [] + L = [element.strip() for element in L] + return L + + existing_users = user_list()["users"] + existing_groups = user_group_list()["groups"] + existing_domains = domain_list()["domains"] + + reader = csv.DictReader(csvfile, delimiter=";", quotechar='"') + reader_fields = cast(list[str], reader.fieldnames) + users_in_csv = [] + + missing_columns: list[str] = [ + key for key in FIELDS_FOR_IMPORT.keys() if key not in reader_fields + ] + if missing_columns: + raise YunohostValidationError( + "user_import_missing_columns", columns=", ".join(missing_columns) + ) + + for user in reader: + # Validate column values against regexes + format_errors = [ + f"{key}: '{user[key]}' doesn't match the expected format" + for key, validator in FIELDS_FOR_IMPORT.items() + if user[key] is None or not re.match(validator, user[key]) + ] + + # Check for duplicated username lines + if user["username"] in users_in_csv: + format_errors.append(f"username '{user['username']}' duplicated") + users_in_csv.append(user["username"]) + + # Validate that groups exist + user["groups"] = to_list(user["groups"]) + unknown_groups = [g for g in user["groups"] if g not in existing_groups] + if unknown_groups: + format_errors.append( + f"username '{user['username']}': unknown groups {', '.join(unknown_groups)}" + ) + + # Validate that domains exist + user["mail-alias"] = to_list(user["mail-alias"]) + user["mail-forward"] = to_list(user["mail-forward"]) + user["domain"] = user["mail"].split("@")[1] + + unknown_domains = [] + if user["domain"] not in existing_domains: + unknown_domains.append(user["domain"]) + + unknown_domains += [ + mail.split("@", 1)[1] + for mail in user["mail-alias"] + if mail.split("@", 1)[1] not in existing_domains + ] + unknown_domains = list(set(unknown_domains)) + + if unknown_domains: + format_errors.append( + f"username '{user['username']}': unknown domains {', '.join(unknown_domains)}" + ) + + if format_errors: + logger.error( + m18n.n( + "user_import_bad_line", + line=reader.line_num, + details=", ".join(format_errors), + ) + ) + is_well_formatted = False + continue + + # Choose what to do with this line and prepare data + user["mailbox-quota"] = user["mailbox-quota"] or "0" + + # User creation + if user["username"] not in existing_users: + # Generate password if not exists + # This could be used when reset password will be merged + if not user["password"]: + user["password"] = random_ascii(70) + actions["created"].append(user) + # User update + elif update: + actions["updated"].append(user) + + if delete: + actions["deleted"] = [ + {"username": user} for user in existing_users if user not in users_in_csv + ] + + if delete and not users_in_csv: + logger.error( + "You used the delete option with an empty csv file ... You probably did not really mean to do that, did you !?" + ) + is_well_formatted = False + + if not is_well_formatted: + raise YunohostValidationError("user_import_bad_file") + + total = len(actions["created"] + actions["updated"] + actions["deleted"]) + + if total == 0: + logger.info(m18n.n("user_import_nothing_to_do")) + return {} + + # Apply creation, update and deletion operation + result = {"created": 0, "updated": 0, "deleted": 0, "errors": 0} + + def progress(info=""): + progress.nb += 1 + width = 20 + bar = int(progress.nb * width / total) + bar = "[" + "#" * bar + "." * (width - bar) + "]" + if info: + bar += " > " + info + if progress.old == bar: + return + progress.old = bar + logger.info(bar) + + progress.nb = 0 # type: ignore[attr-defined] + progress.old = "" # type: ignore[attr-defined] + + def _on_failure(user, exception): + if exception.key == "group_cannot_remove_last_admin": + logger.warning( + user + + ": " + + m18n.n("user_import_cannot_edit_or_delete_admins", user=user) + ) + else: + result["errors"] += 1 + logger.error(user + ": " + str(exception)) + + def _import_update(new_infos, old_infos=False): + remove_alias = None + remove_forward = None + remove_groups = [] + add_groups = new_infos["groups"] + if old_infos: + new_infos["mail"] = ( + None if old_infos["mail"] == new_infos["mail"] else new_infos["mail"] + ) + remove_alias = list( + set(old_infos["mail-alias"]) - set(new_infos["mail-alias"]) + ) + remove_forward = list( + set(old_infos["mail-forward"]) - set(new_infos["mail-forward"]) + ) + new_infos["mail-alias"] = list( + set(new_infos["mail-alias"]) - set(old_infos["mail-alias"]) + ) + new_infos["mail-forward"] = list( + set(new_infos["mail-forward"]) - set(old_infos["mail-forward"]) + ) + + remove_groups = list(set(old_infos["groups"]) - set(new_infos["groups"])) + add_groups = list(set(new_infos["groups"]) - set(old_infos["groups"])) + + for group, infos in existing_groups.items(): + # Loop only on groups in 'remove_groups' + # Ignore 'all_users' and primary group + if ( + group in ["all_users", new_infos["username"]] + or group not in remove_groups + ): + continue + # If the user is in this group (and it's not the primary group), + # remove the member from the group + if new_infos["username"] in infos["members"]: + user_group_update( + group, + remove=new_infos["username"], + sync_perm=False, + from_import=True, + ) + + user_update( + new_infos["username"], + fullname=(new_infos["firstname"] + " " + new_infos["lastname"]).strip(), + change_password=new_infos["password"], + mailbox_quota=new_infos["mailbox-quota"], + mail=new_infos["mail"], + add_mailalias=new_infos["mail-alias"], + remove_mailalias=remove_alias, + remove_mailforward=remove_forward, + add_mailforward=new_infos["mail-forward"], + from_import=True, + ) + + for group in add_groups: + if group in ["all_users", new_infos["username"]]: + continue + user_group_update( + group, add=new_infos["username"], sync_perm=False, from_import=True + ) + + users = user_list(list(FIELDS_FOR_IMPORT.keys()))["users"] + operation_logger.start() + # We do delete and update before to avoid mail uniqueness issues + for user in actions["deleted"]: + progress(f"Deleting {user['username']}") + try: + user_delete(user["username"], purge=True, from_import=True) + result["deleted"] += 1 + except YunohostError as e: + _on_failure(user, e) + + for user in actions["updated"]: + progress(f"Updating {user['username']}") + try: + _import_update(user, users[user["username"]]) + result["updated"] += 1 + except YunohostError as e: + _on_failure(user["username"], e) + + for user in actions["created"]: + progress(f"Creating {user['username']}") + try: + user_create( + user["username"], + user["domain"], + user["password"], + mailbox_quota=user["mailbox-quota"], + from_import=True, + fullname=(user["firstname"] + " " + user["lastname"]).strip(), + ) + _import_update(user) + result["created"] += 1 + except YunohostError as e: + _on_failure(user["username"], e) + + _sync_permissions_with_ldap() + app_ssowatconf() + + if result["errors"]: + msg = m18n.n("user_import_partial_failed") + if result["created"] + result["updated"] + result["deleted"] == 0: + msg = m18n.n("user_import_failed") + logger.error(msg) + operation_logger.error(msg) + else: + logger.success(m18n.n("user_import_success")) + operation_logger.success() + return result + + +# +# Group subcategory +# +def user_group_list( + full: bool = False, include_primary_groups: bool = True +) -> dict[str, dict[str, dict]]: + """ + List groups + + Keyword argument: + full -- List all the info available for each groups + include_primary_groups -- Include groups corresponding to users (which should always only contains this user) + This option is set to false by default in the action map because we don't want to have + these displayed when the user runs `yunohost user group list`, but internally we do want + to list them when called from other functions + """ + + # Fetch relevant informations + + from .utils.ldap import _get_ldap_interface, _ldap_path_extract + + ldap = _get_ldap_interface() + groups_infos = ldap.search( + "ou=groups", + "(objectclass=groupOfNamesYnh)", + ["cn", "member"], + ) + + # Parse / organize information to be outputed + + users = user_list()["users"] + groups: dict[str, dict[str, Any]] = {} + for ginfos in groups_infos: + name = ginfos["cn"][0] + + if not include_primary_groups and name in users: + continue + + groups[name] = {} + + groups[name]["members"] = [ + _ldap_path_extract(p, "uid") for p in ginfos.get("member", []) + ] + + if full: + for group in groups: + groups[group]["permissions"] = [] + + from .permission import user_permission_list + + perms = user_permission_list(full=False)["permissions"] + for perm, pinfos in perms.items(): + for group in pinfos["allowed"]: + if group in groups: + groups[group]["permissions"].append(perm) + + return {"groups": groups} + + +@is_unit_operation([("groupname", "group")]) +def user_group_create( + operation_logger: "OperationLogger", + groupname: str, + gid: str | None = None, + primary_group: bool = False, + sync_perm: bool = True, +) -> dict[str, str]: + """ + Create group + + Keyword argument: + groupname -- Must be unique + + """ + from .permission import _sync_permissions_with_ldap + from .utils.ldap import _get_ldap_interface + + ldap = _get_ldap_interface() + + # Validate uniqueness of groupname in LDAP + conflict = ldap.get_conflict({"cn": groupname}, base_dn="ou=groups") + if conflict: + raise YunohostValidationError("group_already_exist", group=groupname) + + # Validate uniqueness of groupname in system group + all_existing_groupnames = {x.gr_name for x in grp.getgrall()} + if groupname in all_existing_groupnames: + if primary_group: + logger.warning( + m18n.n("group_already_exist_on_system_but_removing_it", group=groupname) + ) + subprocess.check_call( + ["sed", "--in-place", f"/^{groupname}:/d", "/etc/group"] + ) + else: + raise YunohostValidationError( + "group_already_exist_on_system", group=groupname + ) + + if not gid: + # Get random GID + all_gid = {str(x.gr_gid) for x in grp.getgrall()} + + uid_guid_found = False + while not uid_guid_found: + gid = str(random.randint(200, 99999)) + uid_guid_found = gid not in all_gid + + assert gid + + attr_dict: dict[str, str | list[str]] = { + "objectClass": ["top", "groupOfNamesYnh", "posixGroup"], + "cn": groupname, + "gidNumber": [gid], + } + + # Here we handle the creation of a primary group + # We want to initialize this group to contain the corresponding user + # (then we won't be able to add/remove any user in this group) + if primary_group: + attr_dict["member"] = ["uid=" + groupname + ",ou=users,dc=yunohost,dc=org"] + + operation_logger.start() + try: + ldap.add(f"cn={groupname},ou=groups", attr_dict) + except Exception as e: + raise YunohostError("group_creation_failed", group=groupname, error=e) + + if sync_perm: + _sync_permissions_with_ldap() + + if not primary_group: + logger.success(m18n.n("group_created", group=groupname)) + else: + logger.debug(m18n.n("group_created", group=groupname)) + + return {"name": groupname} + + +@is_unit_operation([("groupname", "group")]) +def user_group_delete( + operation_logger: "OperationLogger", + groupname: str, + force: bool = False, + sync_perm: bool = True, +) -> None: + """ + Delete user + + Keyword argument: + groupname -- Groupname to delete + + """ + from .permission import _sync_permissions_with_ldap + from .utils.ldap import _get_ldap_interface + + existing_groups = list(user_group_list()["groups"].keys()) + if groupname not in existing_groups: + raise YunohostValidationError("group_unknown", group=groupname) + + # Refuse to delete primary groups of a user (e.g. group 'sam' related to user 'sam') + # without the force option... + # + # We also can't delete "all_users" because that's a special group... + existing_users = list(user_list()["users"].keys()) + undeletable_groups = existing_users + ["all_users", "visitors", "admins"] + if groupname in undeletable_groups and not force: + raise YunohostValidationError("group_cannot_be_deleted", group=groupname) + + operation_logger.start() + ldap = _get_ldap_interface() + try: + ldap.remove(f"cn={groupname},ou=groups") + except Exception as e: + raise YunohostError("group_deletion_failed", group=groupname, error=e) + + if sync_perm: + _sync_permissions_with_ldap() + + if groupname not in existing_users: + logger.success(m18n.n("group_deleted", group=groupname)) + else: + logger.debug(m18n.n("group_deleted", group=groupname)) + + +@is_unit_operation([("groupname", "group")]) +def user_group_update( + operation_logger: "OperationLogger", + groupname: str, + add: None | str | list[str] = None, + remove: None | str | list[str] = None, + add_mailalias: None | str | list[str] = None, + remove_mailalias: None | str | list[str] = None, + force: bool = False, + sync_perm: bool = True, + from_import: bool = False, +) -> None | dict[str, Any]: + from .hook import hook_callback + from .permission import _sync_permissions_with_ldap + from .utils.ldap import _get_ldap_interface, _ldap_path_extract + + existing_users = list(user_list()["users"].keys()) + + # Refuse to edit a primary group of a user (e.g. group 'sam' related to user 'sam') + # Those kind of group should only ever contain the user (e.g. sam) and only this one. + # We also can't edit "all_users" without the force option because that's a special group... + # Also prevent to remove the last admin + if not force: + if groupname == "all_users": + raise YunohostValidationError("group_cannot_edit_all_users") + elif groupname == "visitors": + raise YunohostValidationError("group_cannot_edit_visitors") + elif groupname in existing_users: + raise YunohostValidationError( + "group_cannot_edit_primary_group", group=groupname + ) + elif groupname == "admins" and remove: + admins = user_group_info("admins")["members"] + if isinstance(remove, str): + remove = [remove] + if admins and not set(admins) - set(remove): + raise YunohostValidationError( + "group_cannot_remove_last_admin", user=remove[0] + ) + + ldap = _get_ldap_interface() + + # Fetch info for this group + result = ldap.search( + "ou=groups", + "cn=" + groupname, + ["cn", "member", "permission", "mail", "objectClass"], + ) + + if not result: + raise YunohostValidationError("group_unknown", group=groupname) + + group = result[0] + + # We extract the uid for each member of the group to keep a simple flat list of members + current_group_mail = group.get("mail", []) + new_group_mail = copy.copy(current_group_mail) + current_group_members = [ + _ldap_path_extract(p, "uid") for p in group.get("member", []) + ] + new_group_members = copy.copy(current_group_members) + new_attr_dict: dict[str, Any] = {} + + # Group permissions + current_group_permissions = [ + _ldap_path_extract(p, "cn") for p in group.get("permission", []) + ] + + if add: + users_to_add = [add] if not isinstance(add, list) else add + + for user in users_to_add: + if user not in existing_users: + raise YunohostValidationError("user_unknown", user=user) + + if user in current_group_members: + logger.warning( + m18n.n("group_user_already_in_group", user=user, group=groupname) + ) + else: + operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_add", group=groupname, user=user)) + + new_group_members += users_to_add + + if remove: + users_to_remove = [remove] if not isinstance(remove, list) else remove + + for user in users_to_remove: + if user not in current_group_members: + logger.warning( + m18n.n("group_user_not_in_group", user=user, group=groupname) + ) + else: + operation_logger.related_to.append(("user", user)) + logger.info(m18n.n("group_user_remove", group=groupname, user=user)) + + # Remove users_to_remove from new_group_members + # Kinda like a new_group_members -= users_to_remove + new_group_members = [u for u in new_group_members if u not in users_to_remove] + + # If something changed, we add this to the stuff to commit later in the code + if set(new_group_members) != set(current_group_members): + new_group_members_dns = [ + "uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group_members + ] + new_attr_dict["member"] = list(set(new_group_members_dns)) + new_attr_dict["memberUid"] = list(set(new_group_members)) + + # Check the whole alias situation + if add_mailalias: + from .domain import domain_list + + domains = domain_list()["domains"] + + if not isinstance(add_mailalias, list): + add_mailalias = [add_mailalias] + for mail in add_mailalias: + if mail.split("@")[0] in ADMIN_ALIASES and groupname != "admins": + raise YunohostValidationError("mail_unavailable") + if mail in current_group_mail: + continue + try: + ldap.validate_uniqueness({"mail": mail}) + except Exception as e: + raise YunohostError("group_update_failed", group=groupname, error=e) + if mail[mail.find("@") + 1 :] not in domains: + raise YunohostError( + "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] + ) + new_group_mail.append(mail) + logger.info(m18n.n("group_mailalias_add", group=groupname, mail=mail)) + + if remove_mailalias: + from .domain import _get_maindomain + + if not isinstance(remove_mailalias, list): + remove_mailalias = [remove_mailalias] + for mail in remove_mailalias: + if ( + "@" in mail + and mail.split("@")[0] in ADMIN_ALIASES + and groupname == "admins" + and mail.split("@")[1] == _get_maindomain() + ): + raise YunohostValidationError( + f"The alias {mail} can not be removed from the 'admins' group", + raw_msg=True, + ) + if mail in new_group_mail: + new_group_mail.remove(mail) + logger.info( + m18n.n("group_mailalias_remove", group=groupname, mail=mail) + ) + else: + raise YunohostValidationError("mail_alias_remove_failed", mail=mail) + + if set(new_group_mail) != set(current_group_mail): + logger.info(m18n.n("group_update_aliases", group=groupname)) + new_attr_dict["mail"] = list(set(new_group_mail)) + + if new_attr_dict["mail"]: + new_attr_dict["objectClass"] = set(group["objectClass"]) + new_attr_dict["objectClass"].add("mailGroup") + else: + new_attr_dict["objectClass"] = set(group["objectClass"]) - { + "mailGroup", + "mailAccount", + } + + if new_attr_dict: + if not from_import: + operation_logger.start() + try: + ldap.update(f"cn={groupname},ou=groups", new_attr_dict) + except Exception as e: + raise YunohostError("group_update_failed", group=groupname, error=e) + + if groupname == "admins" and remove: + from .authenticators.ldap_admin import Authenticator as AdminAuth + + for user in users_to_remove: + AdminAuth.invalidate_all_sessions_for_user(user) + + if sync_perm: + _sync_permissions_with_ldap() + + if add and users_to_add: + for permission in current_group_permissions: + app = permission.split(".")[0] + sub_permission = permission.split(".")[1] + + hook_callback( + "post_app_addaccess", + args=[app, ",".join(users_to_add), sub_permission, ""], + ) + + if remove and users_to_remove: + for permission in current_group_permissions: + app = permission.split(".")[0] + sub_permission = permission.split(".")[1] + + hook_callback( + "post_app_removeaccess", + args=[app, ",".join(users_to_remove), sub_permission, ""], + ) + + if not from_import: + if groupname != "all_users": + if not new_attr_dict: + logger.info(m18n.n("group_no_change", group=groupname)) + else: + logger.success(m18n.n("group_updated", group=groupname)) + else: + logger.debug(m18n.n("group_updated", group=groupname)) + + return user_group_info(groupname) + + return None + + +def user_group_info(groupname: str) -> dict[str, Any]: + """ + Get user informations + + Keyword argument: + groupname -- Groupname to get informations + + """ + + from .utils.ldap import _get_ldap_interface, _ldap_path_extract + + ldap = _get_ldap_interface() + + # Fetch info for this group + result = ldap.search( + "ou=groups", + "cn=" + groupname, + ["cn", "member", "mail"], + ) + + if not result: + raise YunohostValidationError("group_unknown", group=groupname) + + infos = result[0] + + # Format data + + return { + "members": [_ldap_path_extract(p, "uid") for p in infos.get("member", [])], + "permissions": [ + _ldap_path_extract(p, "cn") for p in infos.get("permission", []) + ], + "mail-aliases": [m for m in infos.get("mail", [])], + } + + +def user_group_add( + groupname: str, usernames: list[str], force: bool = False, sync_perm: bool = True +) -> dict[str, Any] | None: + """ + Add user(s) to a group + + Keyword argument: + groupname -- Groupname to update + usernames -- User(s) to add in the group + + """ + return user_group_update(groupname, add=usernames, force=force, sync_perm=sync_perm) + + +def user_group_remove( + groupname: str, usernames: list[str], force: bool = False, sync_perm: bool = True +) -> dict[str, Any] | None: + """ + Remove user(s) from a group + + Keyword argument: + groupname -- Groupname to update + usernames -- User(s) to remove from the group + + """ + return user_group_update( + groupname, remove=usernames, force=force, sync_perm=sync_perm + ) + + +def user_group_add_mailalias( + groupname: str, aliases: list[str], force: bool = False +) -> dict[str, Any] | None: + return user_group_update( + groupname, add_mailalias=aliases, force=force, sync_perm=False + ) + + +def user_group_remove_mailalias( + groupname: str, aliases: list[str], force: bool = False +) -> dict[str, Any] | None: + return user_group_update( + groupname, remove_mailalias=aliases, force=force, sync_perm=False + ) + + +# +# Permission subcategory +# + + +def user_permission_list( + full: bool = False, apps: list[str] = [] +) -> dict[Literal["permissions"], dict[str, "PermInfos"]]: + from .permission import user_permission_list + + return user_permission_list(full=full, absolute_urls=True, apps=apps) + + +@is_flash_unit_operation() +def user_permission_update( + permission: str, + label: str | None = None, + show_tile: bool | None = None, + logo: BinaryIO | Literal[""] | None = None, + description: str | None = None, + hide_from_public: bool | None = None, + order: int | None = None, +) -> dict[str, Any]: + from .app import _assert_is_installed, app_setting, app_ssowatconf + from .permission import _update_app_permission_setting + + # By default, manipulate main permission + if "." not in permission: + permission = permission + ".main" + + app, permname = permission.split(".", 1) + _assert_is_installed(app) + + app_permissions = app_setting(app, "_permissions") or {} + assert isinstance(app_permissions, dict) + if permname not in app_permissions: + raise YunohostValidationError( + f"Unknown permission {permname} for app {app}", raw_msg=True + ) + + # We get these from CLI as string (because we want to be able to differentiate between True, False and "unspecified" = "do not change the value" + if isinstance(show_tile, str): + show_tile = True if show_tile.lower() == "true" else False + if isinstance(hide_from_public, str): + hide_from_public = True if hide_from_public.lower() == "true" else False + + _update_app_permission_setting( + permission=permission, + label=label, + show_tile=show_tile, + logo=logo, + description=description, + hide_from_public=hide_from_public, + order=order, + ) + + app_ssowatconf() + + logger.success(m18n.n("permission_updated", permission=permission)) + + app_permissions = app_setting(app, "_permissions") or {} + assert isinstance(app_permissions, dict) + return app_permissions.get(permname, "") + + +@is_flash_unit_operation() +def user_permission_add( + permission: str, + names: list[str], + protected: bool | None = None, + force: bool = False, + sync_perm: bool = True, +) -> "PermInfos": + from .permission import user_permission_update + + return user_permission_update( + permission, add=names, protected=protected, force=force, sync_perm=sync_perm + ) + + +@is_flash_unit_operation() +def user_permission_remove( + permission: str, + names: list[str], + protected: bool | None = None, + force: bool = False, + sync_perm: bool = True, +) -> "PermInfos": + from .permission import user_permission_update + + return user_permission_update( + permission, remove=names, protected=protected, force=force, sync_perm=sync_perm + ) + + +def user_permission_info(permission: str) -> "PermInfos": + from .permission import user_permission_info + + return user_permission_info(permission) + + +def user_permission_ldapsync() -> None: + from .permission import _sync_permissions_with_ldap + + _sync_permissions_with_ldap() + + +# +# SSH subcategory +# + + +def user_ssh_list_keys(username: str) -> dict[Literal["keys"], list[dict[str, str]]]: + from .ssh import user_ssh_list_keys + + return user_ssh_list_keys(username) + + +def user_ssh_add_key(username: str, key: str, comment: str | None = None) -> None: + from .ssh import user_ssh_add_key + + return user_ssh_add_key(username, key, comment) + + +def user_ssh_remove_key(username: str, key: str) -> None: + from .ssh import user_ssh_remove_key + + return user_ssh_remove_key(username, key) + + +# +# End SSH subcategory +# + + +def _update_admins_group_aliases( + old_main_domain: str | None, new_main_domain: str +) -> None: + current_admin_aliases = user_group_info("admins")["mail-aliases"] + + if old_main_domain is None: + aliases_to_remove = [] + else: + aliases_to_remove = [ + a + for a in current_admin_aliases + if "@" in a + and a.split("@")[1] == old_main_domain + and a.split("@")[0] in ADMIN_ALIASES + ] + aliases_to_add = [f"{a}@{new_main_domain}" for a in ADMIN_ALIASES] + aliases_to_add = [a for a in aliases_to_add if a not in current_admin_aliases] + + user_group_update( + "admins", add_mailalias=aliases_to_add, remove_mailalias=aliases_to_remove + ) diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..48b276e --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/src/utils/app_utils.py b/src/utils/app_utils.py new file mode 100644 index 0000000..14d2fff --- /dev/null +++ b/src/utils/app_utils.py @@ -0,0 +1,1509 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import copy +import os +import re +import shutil +import subprocess +import time +from logging import getLogger +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Iterator, + Literal, + TypedDict, + cast, +) + +import yaml +from moulinette import Moulinette, m18n +from packaging import version + +from .error import YunohostError, YunohostValidationError +from .file_utils import ( + chmod, + chown, + cp, + read_file, + read_json, + read_toml, +) +from .i18n import _value_for_locale +from .process import check_output +from .system import ( + binary_to_human, + debian_version, + dpkg_is_broken, + free_space_in_directory, + get_ynh_package_version, + human_to_binary, + ram_available, + system_arch, +) + +if TYPE_CHECKING: + from .logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.app")) +else: + logger = getLogger("yunohost.app") + +APPS_SETTING_PATH = "/etc/yunohost/apps/" +APPS_TMP_WORKDIRS = "/var/cache/yunohost/app_tmp_work_dirs" +GIT_CLONE_CACHE = "/var/cache/yunohost/gitclones" + +re_app_instance_name = re.compile( + r"^(?P[\w-]+?)(__(?P[1-9][0-9]*))?$" +) + +APP_REPO_URL = re.compile( + r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./~]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?(tree|src/(branch|tag|commit))/[a-zA-Z0-9-_.]+)?(\.git)?/?$" +) + +# TODO: lol +# Ideally this should be a readonly / frozen dict ? +AppManifest = dict[str, Any] + + +def _confirm_app_install(app: str, force: bool = False) -> None: + # Ignore if there's nothing for confirm (good quality app), if --force is used + # or if request on the API (confirm already implemented on the API side) + if force or Moulinette.interface.type == "api": + return + + quality = _app_quality(app) + if quality == "success": + return + + # i18n: confirm_app_install_warning + # i18n: confirm_app_install_danger + # i18n: confirm_app_install_thirdparty + + if quality in ["danger", "thirdparty"]: + _ask_confirmation("confirm_app_install_" + quality, kind="hard") + else: + _ask_confirmation("confirm_app_install_" + quality, kind="soft") + + +app_settings_cache: dict[str, dict[str, Any]] = {} +app_settings_cache_timestamp: dict[str, float] = {} + + +def _get_app_settings(app: str) -> dict[str, Any]: + """ + Get settings of an installed app + + Keyword arguments: + app -- The app id (like nextcloud__2) + + """ + _assert_is_installed(app) + + app_setting_path = os.path.join(APPS_SETTING_PATH, app, "settings.yml") + app_setting_timestamp = os.path.getmtime(app_setting_path) + + # perf: app settings are cached using the settings.yml's modification date, + # such that we don't have to worry too much about calling this function + # too many times (because ultimately parsing yml is not free) + if app_settings_cache_timestamp.get(app) == app_setting_timestamp: + return app_settings_cache[app].copy() + + try: + with open(app_setting_path) as f: + settings = yaml.safe_load(f) or {} + # If label contains unicode char, this may later trigger issues when building strings... + # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... + settings = {k: v for k, v in settings.items()} + + # App settings should never be empty, there should always be at least some standard, internal keys like id, install_time etc. + # Otherwise, this probably means that the app settings disappeared somehow... + if not settings: + logger.error( + f"It looks like settings.yml for {app} is empty ... This should not happen ..." + ) + logger.error(m18n.n("app_not_correctly_installed", app=app)) + return {} + + # Make the app id available as $app too + settings["app"] = app + + # FIXME: it's not clear why this code exists... Shouldn't we hard-define 'id' as $app ...? + if app != settings["id"]: + return {} + + # Cache the settings + app_settings_cache[app] = settings.copy() + app_settings_cache_timestamp[app] = app_setting_timestamp + + return settings + except (IOError, TypeError, KeyError): + logger.error(m18n.n("app_not_correctly_installed", app=app)) + return {} + + +def _set_app_settings(app: str, settings: dict[str, Any]) -> None: + """ + Set settings of an app + + Keyword arguments: + app_id -- The app id (like nextcloud__2) + settings -- Dict with app settings + + """ + with open(os.path.join(APPS_SETTING_PATH, app, "settings.yml"), "w") as f: + yaml.safe_dump(settings, f, default_flow_style=False) + + if app in app_settings_cache_timestamp: + del app_settings_cache_timestamp[app] + if app in app_settings_cache: + del app_settings_cache[app] + + +def _get_app_label(app: str, manifest: AppManifest | None = None) -> str: + _assert_is_installed(app) + settings = _get_app_settings(app) + main_perm = settings.get("_permissions", {}).get("main", {}) + + if main_perm.get("label"): + return main_perm["label"] + elif settings.get("label"): + return settings["label"] + elif manifest: + # This case is just to provide a manifest to avoid re-fetching + # the manifest when the upper scope already has it + return manifest["name"] + else: + return _get_manifest_of_app(app)["name"] + + +def _parse_app_version(v: str) -> tuple[version.Version, int]: + if v in ["?", "-"]: + return (version.parse("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) + + +app_manifests_cache: dict[str, AppManifest] = {} +app_manifests_cache_timestamp: dict[str, float] = {} + + +def _get_manifest_of_app(path_or_app_id: str) -> AppManifest: + # sample data to get an idea of what is going on + # this toml extract: + # + # license = "free" + # url = "https://example.com" + # multi_instance = true + # version = "1.0~ynh1" + # packaging_format = 1 + # services = ["nginx", "php7.0-fpm", "mysql"] + # id = "ynhexample" + # name = "YunoHost example app" + # + # [requirements] + # yunohost = ">= 3.5" + # + # [maintainer] + # url = "http://example.com" + # name = "John doe" + # email = "john.doe@example.com" + # + # [description] + # fr = "Exemple de package d'application pour YunoHost." + # en = "Example package for YunoHost application." + # + # [arguments] + # [arguments.install.domain] + # type = "domain" + # example = "example.com" + # [arguments.install.domain.ask] + # fr = "Choisissez un nom de domaine pour ynhexample" + # en = "Choose a domain name for ynhexample" + # + # will be parsed into this: + # + # OrderedDict([(u'license', u'free'), + # (u'url', u'https://example.com'), + # (u'multi_instance', True), + # (u'version', u'1.0~ynh1'), + # (u'packaging_format', 1), + # (u'services', [u'nginx', u'php7.0-fpm', u'mysql']), + # (u'id', u'ynhexample'), + # (u'name', u'YunoHost example app'), + # (u'requirements', OrderedDict([(u'yunohost', u'>= 3.5')])), + # (u'maintainer', + # OrderedDict([(u'url', u'http://example.com'), + # (u'name', u'John doe'), + # (u'email', u'john.doe@example.com')])), + # (u'description', + # OrderedDict([(u'fr', + # u"Exemple de package d'application pour YunoHost."), + # (u'en', + # u'Example package for YunoHost application.')])), + # (u'arguments', + # OrderedDict([(u'install', + # OrderedDict([(u'domain', + # OrderedDict([(u'type', u'domain'), + # (u'example', + # u'example.com'), + # (u'ask', + # OrderedDict([(u'fr', + # u'Choisissez un nom de domaine pour ynhexample'), + # (u'en', + # u'Choose a domain name for ynhexample')]))])), + # + # and needs to be converted into this: + # + # { + # "name": "YunoHost example app", + # "id": "ynhexample", + # "packaging_format": 1, + # "description": { + # ¦ "en": "Example package for YunoHost application.", + # ¦ "fr": "Exemple de package d’application pour YunoHost." + # }, + # "version": "1.0~ynh1", + # "url": "https://example.com", + # "license": "free", + # "maintainer": { + # ¦ "name": "John doe", + # ¦ "email": "john.doe@example.com", + # ¦ "url": "http://example.com" + # }, + # "requirements": { + # ¦ "yunohost": ">= 3.5" + # }, + # "multi_instance": true, + # "services": [ + # ¦ "nginx", + # ¦ "php7.0-fpm", + # ¦ "mysql" + # ], + # "arguments": { + # ¦ "install" : [ + # ¦ ¦ { + # ¦ ¦ ¦ "name": "domain", + # ¦ ¦ ¦ "type": "domain", + # ¦ ¦ ¦ "ask": { + # ¦ ¦ ¦ ¦ "en": "Choose a domain name for ynhexample", + # ¦ ¦ ¦ ¦ "fr": "Choisissez un nom de domaine pour ynhexample" + # ¦ ¦ ¦ }, + # ¦ ¦ ¦ "example": "example.com" + # ¦ ¦ }, + + if "/" in path_or_app_id: + path = Path(path_or_app_id) + else: + path = Path(APPS_SETTING_PATH) / path_or_app_id + + if (path / "manifest.toml").exists(): + manifest_path = path / "manifest.toml" + read_manifest = read_toml + elif (path / "manifest.json").exists(): + manifest_path = path / "manifest.json" + read_manifest = read_json + else: + raise YunohostError( + f"There doesn't seem to be any manifest file in {path} ... It looks like an app was not correctly installed/removed.", + raw_msg=True, + ) + + # Check cache + if path_or_app_id in app_manifests_cache: + cache_timestamp = app_manifests_cache_timestamp[path_or_app_id] + # Hmpf I'm confused between mtime and ctime so let's use both ... + manifest_and_doc_timestamps = [manifest_path.stat().st_mtime] + manifest_and_doc_timestamps += [manifest_path.stat().st_ctime] + manifest_and_doc_timestamps += [ + p.stat().st_mtime for p in (path / "doc").rglob("*") + ] + manifest_and_doc_timestamps += [ + p.stat().st_ctime for p in (path / "doc").rglob("*") + ] + if cache_timestamp > max(manifest_and_doc_timestamps): + return copy.deepcopy(app_manifests_cache[path_or_app_id]) + + manifest: AppManifest = read_manifest(str(manifest_path)) # type: ignore[assignment] + + manifest["packaging_format"] = float( + str(manifest.get("packaging_format", "")).strip() or "0" + ) + + if manifest["packaging_format"] < 2: + manifest = _convert_v1_manifest_to_v2(manifest) + + manifest["install"] = _set_default_ask_questions(manifest.get("install", {})) + manifest["doc"], manifest["notifications"] = _parse_app_doc_and_notifications(path) + + # Cache the result ... but only for "raw" app names, not paths, + # which are likely just temporary and would fill the cache with stuff that's not likely to be useful? + if "/" not in path_or_app_id: + app_manifests_cache[path_or_app_id] = manifest + app_manifests_cache_timestamp[path_or_app_id] = time.time() + + return copy.deepcopy(manifest) + + +AppDocDict = dict[str, dict[str, str]] +AppNotificationsDict = dict[str, dict[str, dict[str, str]]] + + +def _parse_app_doc_and_notifications( + path: Path, +) -> tuple[AppDocDict, AppNotificationsDict]: + doc: AppDocDict = {} + notification_names = ["PRE_INSTALL", "POST_INSTALL", "PRE_UPGRADE", "POST_UPGRADE"] + + for filepath in (path / "doc").glob("*.md"): + # to be improved : [a-z]{2,3} is a clumsy way of parsing the + # lang code ... some lang code are more complex that this é_è + m = re.match("([A-Z]*)(_[a-z]{2,3})?.md", str(filepath).split("/")[-1]) + + if not m: + # FIXME: shall we display a warning ? idk + continue + + pagename, lang = m.groups() + + if pagename in notification_names: + continue + + lang = lang.strip("_") if lang else "en" + + if pagename not in doc: + doc[pagename] = {} + + try: + doc[pagename][lang] = read_file(str(filepath)).strip() + except Exception as e: + logger.error(e) + continue + + notifications: AppNotificationsDict = {} + + for step in notification_names: + notifications[step] = {} + for filepath in (path / "doc").glob(f"{step}*.md"): + m = re.match(step + "(_[a-z]{2,3})?.md", str(filepath).split("/")[-1]) + if not m: + continue + pagename = "main" + lang = m.groups()[0].strip("_") if m.groups()[0] else "en" + if pagename not in notifications[step]: + notifications[step][pagename] = {} + try: + notifications[step][pagename][lang] = read_file(str(filepath)).strip() + except Exception as e: + logger.error(e) + continue + + for filepath in (path / "doc" / f"{step}.d").glob("*.md"): + m = re.match( + r"([A-Za-z0-9\.\~]*)(_[a-z]{2,3})?.md", str(filepath).split("/")[-1] + ) + if not m: + continue + pagename, lang = m.groups() + lang = lang.strip("_") if lang else "en" + if pagename not in notifications[step]: + notifications[step][pagename] = {} + + try: + notifications[step][pagename][lang] = read_file(str(filepath)).strip() + except Exception as e: + logger.error(e) + continue + + return doc, notifications + + +def _hydrate_app_template(template: str, data: dict[str, Any]): + # 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: + varname = stuff.strip("_").lower() + + if varname in data: + template = template.replace(stuff, str(data[varname])) + + return template.strip() + + +def _convert_v1_manifest_to_v2(manifest: dict[str, Any]) -> AppManifest: + manifest = copy.deepcopy(manifest) + + if "upstream" not in manifest: + manifest["upstream"] = {} + + if "license" in manifest and "license" not in manifest["upstream"]: + manifest["upstream"]["license"] = manifest["license"] + + if "url" in manifest and "website" not in manifest["upstream"]: + manifest["upstream"]["website"] = manifest["url"] + + manifest["integration"] = { + "yunohost": manifest.get("requirements", {}) + .get("yunohost", "") + .replace(">", "") + .replace("=", "") + .replace(" ", ""), + "architectures": "?", + "multi_instance": manifest.get("multi_instance", False), + "ldap": "?", + "sso": "?", + "disk": "?", + "ram": {"build": "?", "runtime": "?"}, + } + + maintainers = manifest.get("maintainer", {}) + if isinstance(maintainers, list): + maintainers = [m["name"] for m in maintainers] + else: + maintainers = [maintainers["name"]] if maintainers.get("name") else [] + + manifest["maintainers"] = maintainers + + install_questions = manifest["arguments"]["install"] + + manifest["install"] = {} + for question in install_questions: + name = question.pop("name") + if "ask" in question and name in [ + "domain", + "path", + "admin", + "is_public", + "password", + ]: + question.pop("ask") + if question.get("example") and question.get("type") in [ + "domain", + "path", + "user", + "boolean", + "password", + ]: + question.pop("example") + + manifest["install"][name] = question + + manifest["resources"] = {"system_user": {}, "install_dir": {"alias": "final_path"}} + + keys_to_keep = [ + "packaging_format", + "id", + "name", + "description", + "version", + "maintainers", + "upstream", + "integration", + "install", + "resources", + ] + + keys_to_del = [key for key in manifest.keys() if key not in keys_to_keep] + for key in keys_to_del: + del manifest[key] + + return manifest + + +def _set_default_ask_questions(questions: dict[str, Any], script_name: str = "install"): + # arguments is something like + # { "domain": + # { + # "type": "domain", + # .... + # }, + # "path": { + # "type": "path", + # ... + # }, + # ... + # } + + # We set a default for any question with these matching (type, name) + # type namei + # N.B. : this is only for install script ... should be reworked for other + # scripts if we supports args for other scripts in the future... + questions_with_default = [ + ("domain", "domain"), # i18n: app_manifest_install_ask_domain + ("path", "path"), # i18n: app_manifest_install_ask_path + ("password", "password"), # i18n: app_manifest_install_ask_password + ("user", "admin"), # i18n: app_manifest_install_ask_admin + ("boolean", "is_public"), # i18n: app_manifest_install_ask_is_public + ( + "group", + "init_main_permission", + ), # i18n: app_manifest_install_ask_init_main_permission + ( + "group", + "init_admin_permission", + ), # i18n: app_manifest_install_ask_init_admin_permission + ] + + for 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["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['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"]: + if "example" in question: + del question["example"] + if "default" in question: + del question["default"] + + return questions + + +def _is_app_repo_url(string: str) -> bool: + string = string.strip() + + # Dummy test for ssh-based stuff ... should probably be improved somehow + if "@" in string: + return True + + return bool(APP_REPO_URL.match(string)) + + +def _app_quality(src: str) -> Literal["success", "warning", "danger", "thirdparty"]: + """ + app may in fact be an app name, an url, or a path + """ + + from ..app_catalog import _load_apps_catalog + + raw_app_catalog = _load_apps_catalog()["apps"] + if src in raw_app_catalog or _is_app_repo_url(src): + # If we got an app name directly (e.g. just "wordpress"), we gonna test this name + if src in raw_app_catalog: + app_name_to_test = src + # If we got an url like "https://github.com/foo/bar_ynh, we want to + # extract "bar" and test if we know this app + elif ("http://" in src) or ("https://" in src): + app_name_to_test = src.strip("/").split("/")[-1].replace("_ynh", "") + else: + # FIXME : watdo if '@' in app ? + return "thirdparty" + + if app_name_to_test in raw_app_catalog: + state = raw_app_catalog[app_name_to_test].get("state", "notworking") + level = raw_app_catalog[app_name_to_test].get("level", None) + if state in ["working", "validated"]: + if isinstance(level, int) and level >= 5: + return "success" + elif isinstance(level, int) and level > 0: + return "warning" + return "danger" + else: + return "thirdparty" + + elif os.path.exists(src): + return "thirdparty" + else: + if "http://" in src or "https://" in src: + logger.error( + f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh" + ) + raise YunohostValidationError("app_unknown") + + +def _extract_app(src: str) -> tuple[AppManifest, str]: + """ + src may be an app name, an url, or a path + """ + + from ..app_catalog import _load_apps_catalog + + raw_app_catalog = _load_apps_catalog()["apps"] + + # App is an appname in the catalog + if src in raw_app_catalog: + if "git" not in raw_app_catalog[src]: + raise YunohostValidationError("app_unsupported_remote_type") + + app_info = raw_app_catalog[src] + url = app_info["git"]["url"] + branch = app_info["git"]["branch"] + revision = str(app_info["git"]["revision"]) + return _extract_app_from_gitrepo( + url, branch=branch, revision=revision, app_info=app_info + ) + # App is a git repo url + elif _is_app_repo_url(src): + url = src.strip().strip("/") + # gitlab urls may look like 'https://domain/org/group/repo/-/tree/testing' + # compated to github urls looking like 'https://domain/org/repo/tree/testing' + if "/-/" in url: + url = url.replace("/-/", "/") + if "/tree/" in url: + url, branch = url.split("/tree/", 1) + else: + branch = None + return _extract_app_from_gitrepo(url, branch=branch) + # App is a local folder + elif os.path.exists(src): + return _extract_app_from_folder(src) + else: + if "http://" in src or "https://" in src: + logger.error( + f"{src} is not a valid app url: app url are expected to look like https://domain.tld/path/to/repo_ynh" + ) + raise YunohostValidationError("app_unknown") + + +def _extract_app_from_folder(path: str) -> tuple[AppManifest, str]: + """ + Unzip / untar / copy application tarball or directory to a tmp work directory + + Keyword arguments: + path -- Path of the tarball or directory + """ + logger.debug(m18n.n("extracting")) + + path = os.path.abspath(path) + + extracted_app_folder = _make_tmp_workdir_for_app() + + if os.path.isdir(path): + shutil.rmtree(extracted_app_folder) + if path[-1] != "/": + path = path + "/" + cp(path, extracted_app_folder, recursive=True) + # Change the last edit time which is used in _make_tmp_workdir_for_app + # to cleanup old dir ... otherwise it may end up being incorrectly removed + # at the end of the safety-backup-before-upgrade :/ + os.system(f"touch {extracted_app_folder}") + else: + try: + shutil.unpack_archive(path, extracted_app_folder) + except Exception: + raise YunohostError("app_extraction_failed") + + try: + if len(os.listdir(extracted_app_folder)) == 1: + for folder in os.listdir(extracted_app_folder): + extracted_app_folder = extracted_app_folder + "/" + folder + except IOError: + raise YunohostError("app_install_files_invalid") + + manifest = _get_manifest_of_app(extracted_app_folder) + manifest["lastUpdate"] = int(time.time()) + + logger.debug(m18n.n("done")) + + manifest["remote"] = {"type": "file", "path": path} + manifest["quality"] = {"level": -1, "state": "thirdparty"} + manifest["antifeatures"] = [] + manifest["potential_alternative_to"] = [] + + return manifest, extracted_app_folder + + +def _git_clone_light( + dest_dir: str, url: str, branch: str | None = None, revision: str = "HEAD" +) -> str: + # Cleanup stale caches (older than 24 hours) + if not Path(GIT_CLONE_CACHE).exists(): + os.makedirs(GIT_CLONE_CACHE) + chmod(GIT_CLONE_CACHE, 0o700) + chown(GIT_CLONE_CACHE, "root", "root") + + for cache in Path(GIT_CLONE_CACHE).iterdir(): + if cache.is_dir() and (time.time() - cache.stat().st_ctime) > 24 * 3600: + try: + shutil.rmtree(cache) + except Exception as e: + logger.debug(f"Uhoh, failed to cleanup cache {cache} ? {e}") + + # There's a cache mechanism to avoid re-git-cloning the same stuff over and over again + # which can be ~costly, for example when checking up the PRE-UPGRADE notifs for app upgrades + if revision != "HEAD": + git_clone_cache = Path(GIT_CLONE_CACHE) / revision + if git_clone_cache.exists(): + logger.debug( + f"Reusing cache for {url} (branch={branch}, revision={revision}" + ) + shutil.copytree(git_clone_cache, dest_dir, dirs_exist_ok=True) + return revision + + logger.debug(f"Fetching {url} (branch={branch}, revision={revision}") + + git_ls_remote = check_output( + ["git", "ls-remote", "--symref", url, "HEAD"], + env={"GIT_TERMINAL_PROMPT": "0", "LC_ALL": "C"}, + shell=False, + ) + + if not branch: + default_branch = None + try: + for line in git_ls_remote.split("\n"): + # Look for the line formated like : + # ref: refs/heads/master HEAD + if "ref: refs/heads/" in line: + line = line.replace("/", " ").replace("\t", " ") + default_branch = line.split()[3] + except Exception: + pass + + if not default_branch: + logger.warning("Failed to parse default branch, trying 'main'") + branch = "main" + else: + if default_branch in ["testing", "dev"]: + logger.warning( + f"Trying 'master' branch instead of default '{default_branch}'" + ) + branch = "master" + else: + branch = default_branch + + logger.debug(m18n.n("downloading")) + + # Download only specified commit + # We don't use git clone because, git clone can't download + # a specific revision only + ref = branch if revision == "HEAD" else revision + assert ref + subprocess.check_call( + ["git", "init", dest_dir], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + cmds = [ + ["git", "remote", "add", "origin", url], + ["git", "fetch", "--depth=1", "origin", ref], + ["git", "reset", "--hard", "FETCH_HEAD"], + ] + for cmd in cmds: + subprocess.check_call( + cmd, cwd=dest_dir, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + + logger.debug(m18n.n("done")) + + if revision == "HEAD": + try: + # Get git last commit hash + cmd2 = f"git ls-remote --exit-code {url} {branch} | awk '{{print $1}}'" + actual_revision = check_output(cmd2) + except Exception as e: + logger.warning(f"cannot get last commit hash because: {e}") + actual_revision = "HEAD" + else: + actual_revision = revision + # Save as cache + git_clone_cache = Path(GIT_CLONE_CACHE) / revision + if git_clone_cache.exists(): + shutil.rmtree(git_clone_cache) + shutil.copytree(dest_dir, git_clone_cache) + + return actual_revision + + +def _extract_app_from_gitrepo( + url: str, branch: str | None = None, revision: str = "HEAD", app_info: dict = {} +) -> tuple[AppManifest, str]: + extracted_app_folder = _make_tmp_workdir_for_app() + + try: + actual_revision = _git_clone_light(extracted_app_folder, url, branch, revision) + except Exception as e: + logger.error(e) + raise YunohostError("app_sources_fetch_failed") + else: + logger.debug(m18n.n("done")) + + manifest = _get_manifest_of_app(extracted_app_folder) + + # Store remote repository info into the returned manifest + manifest["remote"] = { + "type": "git", + "url": url, + "branch": branch, + "revision": actual_revision, + } + if revision != "HEAD": + manifest["lastUpdate"] = app_info.get("lastUpdate") + + manifest["quality"] = { + "level": app_info.get("level", -1), + "state": app_info.get("state", "thirdparty"), + } + manifest["antifeatures"] = app_info.get("antifeatures", []) + manifest["potential_alternative_to"] = app_info.get("potential_alternative_to", []) + + return manifest, extracted_app_folder + + +def _is_installed(app: str) -> bool: + return os.path.isdir(APPS_SETTING_PATH + app) + + +def _assert_is_installed(app: str) -> None: + if not _is_installed(app): + installed_apps = "\n - " + "\n - ".join(sorted(_installed_apps())) + raise YunohostValidationError( + "app_not_installed", app=app, all_apps=installed_apps + ) + + +def _installed_apps() -> list[str]: + return os.listdir(APPS_SETTING_PATH) + + +class AppRequirementCheckResult(TypedDict): + id: str + passed: bool + error: str + + +def _check_manifest_requirements( + manifest: AppManifest, action: Literal["install", "upgrade"], app: str +) -> Iterator[AppRequirementCheckResult]: + """Check if required packages are met from the manifest""" + + app_base_id = manifest["id"] + logger.debug(m18n.n("app_requirements_checking", app=app)) + + # Packaging format + if manifest["packaging_format"] not in [1, 2]: + raise YunohostValidationError("app_packaging_format_not_supported") + + # Yunohost version + required_yunohost_version = ( + manifest["integration"].get("yunohost", "4.3").strip(">= ") + ) + current_yunohost_version = get_ynh_package_version("yunohost")["version"] + + yield { + "id": "required_yunohost_version", + "passed": version.parse(required_yunohost_version) + <= version.parse(current_yunohost_version), + "error": m18n.n( + "app_yunohost_version_not_supported", + current=current_yunohost_version, + required=required_yunohost_version, + ), + } + + # Architectures + arch_requirement = manifest["integration"]["architectures"] + arch = system_arch() + + yield { + "id": "arch", + "passed": arch_requirement in ["all", "?"] or arch in arch_requirement, + "error": m18n.n( + "app_arch_not_supported", + current=arch, + required=", ".join(arch_requirement) + if arch_requirement != "all" + else "all", + ), + } + + # Multi-instance + if action == "install": + multi_instance = manifest["integration"]["multi_instance"] is True + if not multi_instance: + apps = _installed_apps() + sibling_apps = [ + a for a in apps if a == app_base_id or a.startswith(f"{app_base_id}__") + ] + multi_instance = len(sibling_apps) == 0 + + yield { + "id": "install", + "passed": multi_instance, + "error": m18n.n("app_already_installed", app=app_base_id), + } + + # Disk + if action == "install": + root_free_space = free_space_in_directory("/") + var_free_space = free_space_in_directory("/var") + if manifest["integration"]["disk"] == "?": + has_enough_disk = True + else: + disk_req_bin = human_to_binary(manifest["integration"]["disk"]) + has_enough_disk = ( + root_free_space > disk_req_bin and var_free_space > disk_req_bin + ) + free_space = binary_to_human(min(root_free_space, var_free_space)) + + yield { + "id": "disk", + "passed": has_enough_disk, + "error": m18n.n( + "app_not_enough_disk", + current=free_space, + required=manifest["integration"]["disk"], + ), + } + + # Ram + ram_requirement = manifest["integration"]["ram"] + ram, swap = ram_available() + # Is "include_swap" really useful ? We should probably decide wether to always include it or not instead + if ram_requirement.get("include_swap", False): + ram += swap + + if ram_requirement["build"] == "?" or ram > human_to_binary( + ram_requirement["build"] + ): + can_build = True + # When upgrading, compare the available ram to (build - runtime), because the app is already running + elif ( + action == "upgrade" + and ram_requirement["runtime"] != "?" + and ram + > human_to_binary(ram_requirement["build"]) + - human_to_binary(ram_requirement["runtime"]) + ): + can_build = True + else: + can_build = False + + # Before upgrading, the application is probably already running, + # and RAM rarely increases significantly from one version to the next. + can_run = ( + ram_requirement["runtime"] == "?" + or ram > human_to_binary(ram_requirement["runtime"]) + or action == "upgrade" + ) + + # 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"] + ) + elif ram_requirement["build"] != "?": + max_build_runtime = ram_requirement["build"] + else: + max_build_runtime = ram_requirement["runtime"] + + yield { + "id": "ram", + "passed": can_build and can_run, + "error": m18n.n( + "app_not_enough_ram", + current=binary_to_human(ram), + required=max_build_runtime, + ), + } + + +def _guess_webapp_path_requirement(app_folder: str) -> str: + # If there's only one "domain" and "path", validate that domain/path + # is an available url and normalize the path. + + manifest = _get_manifest_of_app(app_folder) + raw_questions = manifest["install"] + + domain_questions = [ + question + for question in raw_questions.values() + if question.get("type") == "domain" + ] + path_questions = [ + question + for question in raw_questions.values() + if question.get("type") == "path" + ] + + if len(domain_questions) == 0 and len(path_questions) == 0: + return "" + if len(domain_questions) == 1 and len(path_questions) == 1: + return "domain_and_path" + if len(domain_questions) == 1 and len(path_questions) == 0: + if manifest.get("packaging_format", 0) < 2: + # This is likely to be a full-domain app... + + # Confirm that this is a full-domain app This should cover most cases + # ... though anyway the proper solution is to implement some mechanism + # in the manifest for app to declare that they require a full domain + # (among other thing) so that we can dynamically check/display this + # requirement on the webadmin form and not miserably fail at submit time + + # Full-domain apps typically declare something like path_url="/" or path=/ + # and use ynh_webpath_register or yunohost_app_checkurl inside the install script + install_script_content = read_file( + os.path.join(app_folder, "scripts/install") + ) + + if re.search( + r"\npath(_url)?=[\"']?/[\"']?", install_script_content + ) and re.search(r"ynh_webpath_register", install_script_content): + return "full_domain" + + else: + # For packaging v2 apps, check if there's a permission with url being a string + perm_resource = manifest.get("resources", {}).get("permissions") + if perm_resource is not None and isinstance( + perm_resource.get("main", {}).get("url"), str + ): + return "full_domain" + + return "?" + + +def _validate_webpath_requirement( + args: dict[str, Any], path_requirement: str, ignore_app: str | None = None +) -> None: + domain = args.get("domain") + path = args.get("path") + + if not domain and not path: + return None + + if path_requirement == "domain_and_path": + assert domain and path + _assert_no_conflicting_apps(domain, path, ignore_app=ignore_app) + + elif path_requirement == "full_domain": + assert domain + _assert_no_conflicting_apps( + domain, "/", full_domain=True, ignore_app=ignore_app + ) + + +def _get_conflicting_apps( + domain: str, path: str, ignore_app: str | None = None +) -> list[tuple[str, str, str]]: + """ + Return a list of all conflicting apps with a domain/path (it can be empty) + + Keyword argument: + domain -- The domain for the web path (e.g. your.domain.tld) + path -- The path to check (e.g. /coffee) + ignore_app -- An optional app id to ignore (c.f. the change_url usecase) + """ + + from ..app import app_map + from ..domain import _assert_domain_exists + from .form import DomainOption, WebPathOption + + domain = DomainOption.normalize(domain) + path = WebPathOption.normalize(path) + + # Abort if domain is unknown + _assert_domain_exists(domain) + + # Fetch apps map + apps_map = app_map(raw=True) + + # Loop through all apps to check if path is taken by one of them + conflicts = [] + if domain in apps_map: + # Loop through apps + for p, a in apps_map[domain].items(): + if a["id"] == ignore_app: + continue + if path == p or ( + not (path.startswith("/.well-known/") or p.startswith("/.well-known/")) + and (path == "/" or p == "/") + ): + conflicts.append((p, a["id"], a["label"])) + + return conflicts + + +def _assert_no_conflicting_apps( + domain: str, path: str, ignore_app: str | None = None, full_domain: bool = False +) -> None: + conflicts = _get_conflicting_apps(domain, path, ignore_app) + + if conflicts: + apps = [] + for path, app_id, app_label in conflicts: + apps.append(f" * {domain}{path} → {app_label} ({app_id})") + + if full_domain: + raise YunohostValidationError("app_full_domain_unavailable", domain=domain) + else: + raise YunohostValidationError( + "app_location_unavailable", apps="\n".join(apps) + ) + + +def _make_environment_for_app_script( + app, + args={}, + args_prefix="APP_ARG_", + workdir=None, + action=None, + force_include_app_settings=False, +) -> dict[str, str]: + from ..log import OperationLogger + + manifest = _get_manifest_of_app(workdir if workdir else app) + + 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(), + } + + if workdir: + env_dict["YNH_APP_BASEDIR"] = workdir + + if action: + env_dict["YNH_APP_ACTION"] = action + + for arg_name, arg_value in args.items(): + arg_name_upper = arg_name.upper() + env_dict[f"YNH_{args_prefix}{arg_name_upper}"] = str(arg_value) + + # If packaging format v2, load all settings + if manifest["packaging_format"] >= 2 or force_include_app_settings: + env_dict["app"] = app + 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 + + 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 + + +def _parse_app_instance_name(app_instance_name: str) -> tuple[str, int]: + """ + Parse a Yunohost app instance name and extracts the original appid + and the application instance number + + 'yolo' -> ('yolo', 1) + 'yolo1' -> ('yolo1', 1) + 'yolo__0' -> ('yolo__0', 1) + 'yolo__1' -> ('yolo', 1) + 'yolo__23' -> ('yolo', 23) + 'yolo__42__72' -> ('yolo__42', 72) + 'yolo__23qdqsd' -> ('yolo__23qdqsd', 1) + 'yolo__23qdqsd56' -> ('yolo__23qdqsd56', 1) + """ + match = re_app_instance_name.match(app_instance_name) + assert match, f"Could not parse app instance name : {app_instance_name}" + appid = match.groupdict().get("appid") + app_instance_nb_ = match.groupdict().get("appinstancenb") or "1" + if not appid: + raise Exception(f"Could not parse app instance name : {app_instance_name}") + if not str(app_instance_nb_).isdigit(): + raise Exception(f"Could not parse app instance name : {app_instance_name}") + else: + app_instance_nb = int(str(app_instance_nb_)) + + return (appid, app_instance_nb) + + +def _next_instance_number_for_app(app: str) -> int: + # Get list of sibling apps, such as {app}, {app}__2, {app}__4 + apps = _installed_apps() + sibling_app_ids = [a for a in apps if a == app or a.startswith(f"{app}__")] + + # Find the list of ids, such as [1, 2, 4] + sibling_ids = [_parse_app_instance_name(a)[1] for a in sibling_app_ids] + + # Find the first 'i' that's not in the sibling_ids list already + i = 1 + while True: + if i not in sibling_ids: + return i + else: + i += 1 + + +def _make_tmp_workdir_for_app(app: str | None = None) -> str: + from tempfile import mkdtemp + + # Create parent dir if it doesn't exists yet + if not os.path.exists(APPS_TMP_WORKDIRS): + os.makedirs(APPS_TMP_WORKDIRS) + + now = int(time.time()) + + # Cleanup old dirs (if any) + for dir_ in os.listdir(APPS_TMP_WORKDIRS): + path = os.path.join(APPS_TMP_WORKDIRS, dir_) + # We only delete folders older than an arbitary 12 hours + # This is to cover the stupid case of upgrades + # Where many app will call 'yunohost backup create' + # from the upgrade script itself, + # which will also call this function while the upgrade + # script itself is running in one of those dir... + # It could be that there are other edge cases + # such as app-install-during-app-install + if ( + os.stat(path).st_mtime < now - 12 * 3600 + and os.stat(path).st_ctime < now - 12 * 3600 + ): + shutil.rmtree(path) + + tmpdir = mkdtemp(prefix="app_", dir=APPS_TMP_WORKDIRS) + + # Copy existing app scripts, conf, ... if an app arg was provided + if app: + os.system(f"cp -a {APPS_SETTING_PATH}/{app}/* {tmpdir}") + + return tmpdir + + +def _assert_system_is_sane_for_app(manifest: AppManifest, when: Literal["pre", "post"]): + from ..service import service_status + + logger.debug("Checking that required services are up and running...") + + # 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 + + services = ["nginx", "fail2ban"] + + # Wait if a service is reloading + test_nb = 0 + + while test_nb < 16: + if not any(s for s in services if service_status(s)["status"] == "reloading"): + break + time.sleep(0.5) + test_nb += 1 + + # List services currently down and raise an exception if any are found + services_status = {s: service_status(s) for s in services} + faulty_services = [ + f"{s} ({status['status']})" + for s, status in services_status.items() + if status["status"] != "running" + ] + + if faulty_services: + if when == "pre": + raise YunohostValidationError( + "app_action_cannot_be_ran_because_required_services_down", + services=", ".join(faulty_services), + ) + elif when == "post": + raise YunohostError( + "app_action_broke_system", services=", ".join(faulty_services) + ) + + if dpkg_is_broken(): + if when == "pre": + raise YunohostValidationError("dpkg_is_broken") + elif when == "post": + raise YunohostError("this_action_broke_dpkg") + + +def _notification_is_dismissed(name, settings): + # Check for _dismiss_notiication_$name setting and also auto-dismiss + # notifications after one week (otherwise people using mostly CLI would + # never really dismiss the notification and it would be displayed forever) + + if name == "POST_INSTALL": + return ( + settings.get("_dismiss_notification_post_install") + or (int(time.time()) - settings.get("install_time", 0)) / (24 * 3600) > 7 + ) + elif name == "POST_UPGRADE": + # Check on update_time also implicitly prevent the post_upgrade notification + # from being displayed after install, because update_time is only set during upgrade + return ( + settings.get("_dismiss_notification_post_upgrade") + or (int(time.time()) - settings.get("update_time", 0)) / (24 * 3600) > 7 + ) + else: + return False + + +def _filter_and_hydrate_notifications( + notifications, current_version=None, data={} +) -> dict[str, str]: + def is_version_more_recent_than_current_version(name, current_version): + current_version = str(current_version) + return _parse_app_version(name) > _parse_app_version(current_version) + + 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() + if current_version is None + or name == "main" + 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: dict[str, str], force=False) -> None: + if not notifications: + return + + for name, content in notifications.items(): + print("==========") + print(content) + print("==========") + + # i18n: confirm_notifications_read + _ask_confirmation( + "confirm_notifications_read", kind="simple", force=force, inform_sse=True + ) + + +def _ask_confirmation( + question: str, + params: dict = {}, + kind: Literal["simple", "soft", "hard"] = "hard", + force: bool = False, + inform_sse: bool = False, +) -> None: + """ + Ask confirmation + + Keyword argument: + question -- m18n key or string + params -- dict of values passed to the string formating + kind -- "hard": ask with "Yes, I understand", "soft": "Y/N", "simple": "press enter" + force -- Will not ask for confirmation + + """ + if force or Moulinette.interface.type == "api": + return + + # If ran from the CLI in a non-interactive context, + # skip confirmation (except in hard mode) + if not os.isatty(1) and kind in ["simple", "soft"]: + return + + if inform_sse: + import logging + + from ..log import OperationLogger + + active_sse_handlers = [ + o.sse_handler + for o in OperationLogger._instances + if o.sse_handler and o.started_at is not None and o.ended_at is None + ] + if active_sse_handlers: + active_sse_handlers[0].emit( + logging.LogRecord( + "?", + logging.INFO, + "", + 0, + "The CLI is currently waiting for confirmation before continuing.", + {}, + None, + ) + ) + + if kind == "simple": + answer = Moulinette.prompt( + m18n.n(question, answers="Press enter to continue", **params), + color="yellow", + ) + answer = True + elif kind == "soft": + answer = Moulinette.prompt( + m18n.n(question, answers="Y/N", **params), color="yellow" + ) + answer = answer.upper() == "Y" + else: + answer = Moulinette.prompt( + m18n.n(question, answers="Yes, I understand", **params), color="red" + ) + answer = answer == "Yes, I understand" + + if not answer: + raise YunohostError("aborting") diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py new file mode 100644 index 0000000..9e2157d --- /dev/null +++ b/src/utils/configpanel.py @@ -0,0 +1,968 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import glob +import os +import re +from collections import OrderedDict +from collections.abc import Generator +from logging import getLogger +from typing import TYPE_CHECKING, Any, Iterator, Literal, Sequence, Type, Union, cast + +from moulinette import Moulinette, m18n +from moulinette.interfaces.cli import colorize +from pydantic import BaseModel, Extra, ValidationError, validator + +from .error import YunohostError, YunohostValidationError +from .file_utils import mkdir, read_toml, read_yaml, write_to_yaml +from .form import ( + AnyOption, + BaseInputOption, + BaseOption, + BaseReadonlyOption, + FileOption, + OptionsModel, + OptionType, + Translation, + build_form, + evaluate_simple_js_expression, + parse_prefilled_values, + prompt_or_validate_form, +) +from .i18n import _value_for_locale + +if TYPE_CHECKING: + from pydantic.fields import ModelField + from pydantic.typing import AbstractSetIntStr, MappingIntStrAny + + from ..log import OperationLogger + from .form import FormModel, Hooks + +if TYPE_CHECKING: + from .logging import YunohostLogger + + logger = cast(YunohostLogger, getLogger("yunohost.configpanel")) +else: + logger = getLogger("yunohost.configpanel") + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭╮╮╭─╮┌─╮┌─╴╷ ╭─╴ │ +# │ ││││ ││ │├─╴│ ╰─╮ │ +# │ ╵╵╵╰─╯└─╯╰─╴╰─╴╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + +CONFIG_PANEL_VERSION_SUPPORTED = 1.0 + + +class ContainerModel(BaseModel): + id: str + name: Translation | None = None + services: list[str] = [] + help: Translation | None = None + + def translate(self, i18n_key: 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 m18n.key_exists(f"{i18n_key}_{self.id}_{key}"): + setattr(self, key, m18n.n(f"{i18n_key}_{self.id}_{key}")) + + +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: bool | str = True + optional: bool = True + collapsed: bool = False + is_action_section: bool = False + bind: 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: Translation | None = None, + services: list[str] = [], + help: Translation | None = None, + visible: bool | str = True, + optional: bool = True, + collapsed: bool = False, + bind: str | None = None, + **kwargs: dict[str, Any], + ) -> 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, + collapsed=collapsed, + 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) # type: ignore + + def translate(self, i18n_key: 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: 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: Translation | None = None, + services: list[str] = [], + help: Translation | None = None, + bind: str | None = None, + **kwargs: dict[str, Any], + ) -> 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: 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: 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: str | None = None, + **kwargs: dict[str, Any], + ) -> 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_panel(self, panel_id: str) -> PanelModel | None: + for panel in self.panels: + if panel.id == panel_id: + return panel + return None + + def get_section(self, section_id: str) -> SectionModel | None: + for section in self.sections: + if section.id == section_id: + return section + return None + + def get_option(self, option_id: str) -> 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"], + ) -> Generator[tuple[PanelModel, SectionModel | None, BaseOption | None]]: + 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[str | None] + RawConfig = OrderedDict[str, Any] + RawSettings = dict[str, Any] + ConfigPanelGetMode = Literal["classic", "full", "export"] + + +def parse_filter_key(key: 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: 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: ConfigPanelModel | None = None + form: Union["FormModel", None] = None + raw_settings: "RawSettings" = {} + hooks: "Hooks" = {} + + @classmethod + def list(cls) -> list[str]: + """ + List available config panel + """ + assert cls.save_path_tpl + try: + entities = [ + re.match( + "^" + cls.save_path_tpl.format(entity="(?p)") + "$", f + ).group("entity") # type: ignore + for f in glob.glob(cls.save_path_tpl.format(entity="*")) + if os.path.isfile(f) + ] + except FileNotFoundError: + entities = [] + return entities + + def __init__( + self, + entity: str, + config_path: str | None = None, + save_path: str | None = None, + creation: bool = False, + ) -> None: + self.entity = entity + self.config_path = config_path + if not config_path: + self.config_path = self.config_path_tpl.format( + entity=entity, entity_type=self.entity_type + ) + self.save_path = save_path + if not save_path and self.save_path_tpl: + self.save_path = self.save_path_tpl.format(entity=entity) + + if ( + self.save_path + and self.save_mode != "diff" + and not creation + and not os.path.exists(self.save_path) + ): + raise YunohostValidationError( + f"{self.entity_type}_unknown", + **{self.entity_type: entity}, # type: ignore[arg-type] + ) + if self.save_path and creation and os.path.exists(self.save_path): + raise YunohostValidationError( + f"{self.entity_type}_exists", + **{self.entity_type: entity}, # type: ignore[arg-type] + ) + + # Search for hooks in the config panel + self.hooks = { + func: getattr(self, func) + for func in dir(self) + if callable(getattr(self, func)) + and re.match("^(validate|post_ask)__", func) + } + + def get( + self, key: str | None = None, mode: "ConfigPanelGetMode" = "classic" + ) -> Any: + self.filter_key = parse_filter_key(key) + self.config, self.form = self._get_config_panel(prevalidate=False) + + panel_id, section_id, option_id = self.filter_key + + # In 'classic' mode, we display the current value if key refer to an option + if option_id and mode == "classic": + option = self.config.get_option(option_id) + + if option is None: + # FIXME i18n + raise YunohostValidationError( + f"Couldn't find any option with id {option_id}", raw_msg=True + ) + + 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") + + if mode == "full": + 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": # type: ignore + continue + + for option in section.options: + # FIXME not sure why option resolves as possibly `None` + option = cast(AnyOption, option) # type: ignore + + 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: str | None = None, + value: Any = None, + args: str | None = None, + args_file: 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( + "You should either provide a value, or a serie of args/args_file, but not both at the same time", + raw_msg=True, + ) + + 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") + if option_id and value is not None: + prefilled_answers = {option_id: value} + else: + prefilled_answers = parse_prefilled_values(args, args_file) + + 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.form, self.config, previous_settings) + except YunohostError: + raise + # Script got manually interrupted ... + # N.B. : KeyboardInterrupt does not inherit from Exception + except (KeyboardInterrupt, EOFError): + error = m18n.n("operation_interrupted") + logger.error(m18n.n("config_apply_failed", error=error)) + raise + # Something wrong happened in Yunohost's code (most probably hook_exec) + except Exception: + import traceback + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error(m18n.n("config_apply_failed", error=error)) + raise + finally: + # Delete files uploaded from API + # FIXME : this is currently done in the context of config panels, + # but could also happen in the context of app install ... (or anywhere else + # where we may parse args etc...) + FileOption.clean_upload_dirs() + + self._reload_services() + + logger.success("Config updated as expected") + + 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.config, self.form = self._get_config_panel() + + for panel, section, option in self.config.iter_children(): + assert panel and section and option + if option.type == OptionType.button: + key = f"{panel.id}.{section.id}.{option.id}" + assert option.ask + actions[key] = _value_for_locale(option.ask) + + return actions + + def run_action( + self, + key: str | None = None, + args: str | None = None, + args_file: 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.config, self.form = self._get_config_panel() + + # FIXME: should also check that there's indeed a key called action + 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") + prefilled_answers = parse_prefilled_values(args, args_file) + + 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 ... + + if operation_logger: + operation_logger.start() + + try: + 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=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=key, error=error)) + raise + finally: + # Delete files uploaded from API + # FIXME : this is currently done in the context of config panels, + # but could also happen in the context of app install ... (or anywhere else + # where we may parse args etc...) + FileOption.clean_upload_dirs() + + # FIXME: i18n + logger.success(f"Action {action_id} successful") + + if operation_logger: + operation_logger.success() + + def _get_raw_config(self) -> "RawConfig": + assert self.config_path + if not os.path.exists(self.config_path): + raise YunohostValidationError("config_no_panel") + + return read_toml(self.config_path) # type: ignore[return-value] + + 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 {} # type: ignore[return-value] + + def _get_partial_raw_config(self) -> "RawConfig": + def filter_keys( + data: "RawConfig", + key: str, + model: 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} + ) + + raw_config = self._get_raw_config() + + panel_id, section_id, option_id = self.filter_key + + try: + 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(): + assert option + 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() + try: + config = ConfigPanelModel(**raw_config) + except ValidationError as e: + raise YunohostError( + "Error while parsing config panel: " + e.errors()[0]["msg"], + raw_msg=True, + ) + 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 + ) + + return (config, settings) + + def _ask( + self, + config: ConfigPanelModel, + form: "FormModel", + prefilled_answers: dict[str, Any] = {}, + action_id: str | None = None, + hooks: "Hooks" = {}, + ) -> "FormModel": + # FIXME could be turned into a staticmethod + logger.debug("Ask unanswered question and prevalidate data") + + interactive = Moulinette.interface.type == "cli" and os.isatty(1) + verbose = action_id is None or len(list(config.options)) > 1 + + if interactive: + config.translate() + + for panel in config.panels: + if interactive and verbose: + Moulinette.display( + colorize(f"\n{'=' * 40}\n>>>> {panel.name}\n{'=' * 40}", "purple") + ) + + # 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 interactive and verbose and section.name: + Moulinette.display(colorize(f"\n# {section.name}", "purple")) + + # filter action section options in case of multiple buttons + options = [ + option + for option in section.options + if option.type is not OptionType.button or option.id == action_id + ] + + form = prompt_or_validate_form( + options, + form, + prefilled_answers=prefilled_answers, + context=context, + hooks=hooks, + ) + + return form + + def _apply( + self, + form: "FormModel", + config: ConfigPanelModel, + 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...") + + assert self.save_path + dir_path = os.path.dirname(os.path.realpath(self.save_path)) + if not os.path.exists(dir_path): + mkdir(dir_path, mode=0o700) + + 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) + # 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 + } + + 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] + + # Save the settings to the .yaml file + assert self.save_path + write_to_yaml(self.save_path, current_settings) # type: ignore[arg-type] + + def _run_action(self, form: "FormModel", action_id: str) -> None: + raise NotImplementedError() + + def _reload_services(self) -> None: + from ..service import service_reload_or_restart + + services_to_reload = self.config.services if self.config else [] + + 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) diff --git a/src/utils/dns.py b/src/utils/dns.py new file mode 100644 index 0000000..4580f3e --- /dev/null +++ b/src/utils/dns.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from functools import cache +from typing import Literal + +import dns.exception +import dns.resolver + +from .file_utils import read_file + +SPECIAL_USE_TLDS = ["home.arpa", "internal", "local", "localhost", "onion", "test"] + +YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] + + +def is_yunohost_dyndns_domain(domain: str) -> bool: + return any( + domain.endswith(f".{dyndns_domain}") for dyndns_domain in YNH_DYNDNS_DOMAINS + ) + + +def is_special_use_tld(domain: str) -> bool: + return any(domain.endswith(f".{tld}") for tld in SPECIAL_USE_TLDS) + + +# Lazy dev caching to avoid re-reading the file multiple time when calling +# dig() often during same yunohost operation +@cache +def external_resolvers() -> list[str]: + resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n") + external_resolvers_ = [ + r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver") + ] + + # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6 + # will be tried anyway resulting in super-slow dig requests that'll wait + # until timeout... + external_resolvers_ = [r for r in external_resolvers_ if ":" not in r] + + return external_resolvers_ + + +def dig( + qname: str, + rdtype: str = "A", + timeout: int = 5, + resolvers: Literal["local"] | Literal["force_external"] | list[str] = "local", + edns_size: int = 1500, + full_answers: bool = False, +) -> ( + tuple[Literal["ok"], dns.resolver.Answer | list[str]] + | tuple[Literal["nok"], tuple[str, dns.exception.DNSException]] +): + """ + Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf + """ + + # It's very important to do the request with a qname ended by . + # If we don't and the domain fail, dns resolver try a second request + # by concatenate the qname with the end of the "hostname" + if not qname.endswith("."): + qname += "." + + if resolvers == "local": + resolvers = ["127.0.0.1"] + elif resolvers == "force_external": + resolvers = external_resolvers() + else: + assert isinstance(resolvers, list) + + resolver = dns.resolver.Resolver(configure=False) + resolver.use_edns(0, 0, edns_size) + resolver.nameservers = resolvers + # resolver.timeout is used to trigger the next DNS query on resolvers list. + # In python-dns 1.16, this value is set to 2.0. However, this means that if + # the 3 first dns resolvers in list are down, we wait 6 seconds before to + # run the DNS query to a DNS resolvers up... + # In diagnosis dnsrecords, with 10 domains this means at least 12min, too long. + resolver.timeout = 1.0 + # resolver.lifetime is the timeout for resolver.query() + # By default set it to 5 seconds to allow 4 resolvers to be unreachable. + resolver.lifetime = timeout + + answers: dns.resolver.Answer | list[str] + try: + answers = resolver.query(qname, rdtype) + except ( + dns.resolver.NXDOMAIN, + dns.resolver.NoNameservers, + dns.resolver.NoAnswer, + dns.exception.Timeout, + ) as err: + return ("nok", (err.__class__.__name__, err)) + + if not full_answers: + answers = [answer.to_text() for answer in answers] + + return ("ok", answers) diff --git a/src/utils/error.py b/src/utils/error.py new file mode 100644 index 0000000..477041a --- /dev/null +++ b/src/utils/error.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from typing import Any + +from moulinette import m18n +from moulinette.core import MoulinetteAuthenticationError, MoulinetteError + + +class YunohostError(MoulinetteError): + http_code = 500 + + """ + Yunohost base exception + + The (only?) main difference with MoulinetteError being that keys + are translated via m18n.n (namespace) instead of m18n.g (global?) + """ + + def __init__( + self, + key: str, + raw_msg: bool = False, + log_ref: str | None = None, + error_details: str | None = None, + *args: Any, + **kwargs: Any, + ) -> None: + self.key = key # Saving the key is useful for unit testing + self.kwargs = kwargs # Saving the key is useful for unit testing + self.log_ref = log_ref + self.error_details = error_details + if raw_msg: + msg = key + else: + msg = m18n.n(key, *args, **kwargs) + + super(YunohostError, self).__init__(msg, raw_msg=True) + + def content(self) -> dict[str, str] | str: + if self.log_ref: + return {"error": self.strerror, "log_ref": self.log_ref} + elif self.error_details: + return {"error": self.strerror, "details": self.error_details} + else: + return super().content() + + +class YunohostValidationError(YunohostError): + http_code = 400 + + def content(self) -> dict[str, str]: + return {"error": self.strerror, "error_key": self.key, **self.kwargs} + + +class YunohostAuthenticationError(MoulinetteAuthenticationError): + pass diff --git a/src/utils/file_utils.py b/src/utils/file_utils.py new file mode 100644 index 0000000..ee46d60 --- /dev/null +++ b/src/utils/file_utils.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import errno +import json +import os +import shutil +from collections import OrderedDict +from pathlib import Path +from typing import Any, TextIO + +import toml +import yaml + +from .error import YunohostError + +Jsonable = ( + str | int | float | bool | None | dict["Jsonable", "Jsonable"] | list["Jsonable"] +) + + +def read_file(file_path: str) -> str: + """ + Read a regular text file + + Keyword argument: + file_path -- Path to the text file + """ + assert isinstance(file_path, str), ( + "Error: file_path '{}' should be a string but is of type '{}' instead".format( + file_path, + type(file_path), + ) + ) + + # Check file exists + if not os.path.isfile(file_path): + raise YunohostError("file_not_exist", path=file_path) + + # Open file and read content + try: + with open(file_path, "r") as f: + file_content = f.read() + except IOError as e: + raise YunohostError("cannot_open_file", file=file_path, error=str(e)) + except Exception as e: + raise YunohostError("unknown_error_reading_file", file=file_path, error=str(e)) + + return file_content + + +def read_json(file_path: str) -> Jsonable: + """ + Read a json file + + Keyword argument: + file_path -- Path to the json file + """ + + # Read file + file_content = read_file(file_path) + + # Try to load json to check if it's syntaxically correct + try: + loaded_json = json.loads(file_content) + except ValueError as e: + raise YunohostError("corrupted_json", ressource=file_path, error=str(e)) + + return loaded_json + + +def read_yaml(file_: str | Path | TextIO) -> Jsonable: + """ + Safely read a yaml file + + Keyword argument: + file -- Path or stream to the yaml file + """ + + # Read file + file_path = file_ if isinstance(file_, str) else file_.name + file_content = read_file(file_) if isinstance(file_, str) else file_ + + # Try to load yaml to check if it's syntaxically correct + try: + loaded_yaml = yaml.safe_load(file_content) # type: ignore[arg-type] + except Exception as e: + raise YunohostError("corrupted_yaml", ressource=file_path, error=str(e)) + + return loaded_yaml + + +def read_toml(file_path: str) -> Jsonable: + """ + Safely read a toml file + + Keyword argument: + file_path -- Path to the toml file + """ + + # Read file + file_content = read_file(file_path) + + # Try to load toml to check if it's syntactically correct + try: + loaded_toml = toml.loads(file_content, _dict=OrderedDict) + except Exception as e: + raise YunohostError("corrupted_toml", ressource=file_path, error=str(e)) + + return loaded_toml + + +def write_to_file( + file_path: str, data: str | bytes | list, file_mode: str = "w" +) -> None: + """ + Write a single string or a list of string to a text file. + The text file will be overwritten by default. + + Keyword argument: + file_path -- Path to the output file + data -- The data to write (must be a string or list of string) + file_mode -- Mode used when writing the file. Option meant to be used + by append_to_file to avoid duplicating the code of this function. + """ + assert isinstance(data, str) or isinstance(data, bytes) or isinstance(data, list), ( + f"Error: data '{str(data)}' should be either a string or a list but is of type '{type(data)}'" + ) + assert not os.path.isdir(file_path), ( + "Error: file_path '%s' point to a dir, it should be a file" % file_path + ) + assert os.path.isdir(os.path.dirname(file_path)), ( + "Error: the path ('{}') base dir ('{}') is not a dir".format( + file_path, + os.path.dirname(file_path), + ) + ) + + # If data is a list, check elements are strings and build a single string + if isinstance(data, list): + for element in data: + assert isinstance(element, str), ( + "Error: element '{}' should be a string but is of type '{}' instead".format( + element, + type(element), + ) + ) + data = "\n".join(data) + + try: + with open(file_path, file_mode) as f: + f.write(data) + except IOError as e: + raise YunohostError("cannot_write_file", file=file_path, error=str(e)) + except Exception as e: + raise YunohostError("error_writing_file", file=file_path, error=str(e)) + + +def append_to_file(file_path: str, data: str | bytes | list) -> None: + """ + Append a single string or a list of string to a text file. + + Keyword argument: + file_path -- Path to the output file + data -- The data to write (must be a string or list of string) + """ + + write_to_file(file_path, data, file_mode="a") + + +def write_to_json( + file_path: str, data: Jsonable, sort_keys: bool = False, indent: int | None = None +) -> None: + """ + Write a dictionnary or a list to a json file + + Keyword argument: + file_path -- Path to the output json file + data -- The data to write (must be a dict or a list) + """ + + # Assumptions + assert isinstance(file_path, str), ( + "Error: file_path '{}' should be a string but is of type '{}' instead".format( + file_path, + type(file_path), + ) + ) + assert isinstance(data, dict) or isinstance(data, list), ( + "Error: data '{}' should be a dict or a list but is of type '{}' instead".format( + data, + type(data), + ) + ) + assert not os.path.isdir(file_path), ( + "Error: file_path '%s' point to a dir, it should be a file" % file_path + ) + assert os.path.isdir(os.path.dirname(file_path)), ( + "Error: the path ('{}') base dir ('{}') is not a dir".format( + file_path, + os.path.dirname(file_path), + ) + ) + + # Write dict to file + try: + with open(file_path, "w") as f: + json.dump(data, f, sort_keys=sort_keys, indent=indent) + except IOError as e: + raise YunohostError("cannot_write_file", file=file_path, error=str(e)) + except Exception as e: + raise YunohostError("error_writing_file", file=file_path, error=str(e)) + + +def write_to_yaml(file_path: str, data: Jsonable) -> None: + """ + Write a dictionnary or a list to a yaml file + + Keyword argument: + file_path -- Path to the output yaml file + data -- The data to write (must be a dict or a list) + """ + # Assumptions + assert isinstance(file_path, str) + assert isinstance(data, dict) or isinstance(data, list) + assert not os.path.isdir(file_path) + assert os.path.isdir(os.path.dirname(file_path)) + + # Write dict to file + try: + with open(file_path, "w") as f: + yaml.safe_dump(data, f, default_flow_style=False) + except IOError as e: + raise YunohostError("cannot_write_file", file=file_path, error=str(e)) + except Exception as e: + raise YunohostError("error_writing_file", file=file_path, error=str(e)) + + +def mkdir( + path: str, + mode: int = 0o0777, + parents: bool = False, + uid: str | int | None = None, + gid: str | int | None = None, + force: bool = False, +) -> None: + """Create a directory with optional features + + Create a directory and optionaly set its permissions to mode and its + owner and/or group. If path refers to an existing path, nothing is done + unless force is True. + + Keyword arguments: + - path -- The directory to create + - mode -- Numeric path mode to set + - parents -- Make parent directories as needed + - uid -- Numeric uid or user name + - gid -- Numeric gid or group name + - force -- Force directory creation and owning even if the path exists + + """ + if os.path.exists(path) and not force: + raise Exception(f"Folder {path} already exists") + + if parents: + # Create parents directories as needed + head, tail = os.path.split(path) + if not tail: + head, tail = os.path.split(head) + if head and tail and not os.path.exists(head): + try: + mkdir(head, mode, parents, uid, gid, force) + except OSError as e: + if e.errno != errno.EEXIST: + raise + if tail == os.curdir: + return + + # Create directory and set permissions + try: + oldmask = os.umask(000) + os.mkdir(path, mode) + os.umask(oldmask) + except OSError: + # mimic Python3.2+ os.makedirs exist_ok behaviour + if not force or not os.path.isdir(path): + raise + + if uid is not None or gid is not None: + chown(path, uid, gid) + + +def chown( + path: str, + uid: str | int | None = None, + gid: str | int | None = None, + recursive: bool = False, +) -> None: + """Change the owner and/or group of a path + + Keyword arguments: + - uid -- Numeric uid or user name + - gid -- Numeric gid or group name + - recursive -- Operate on path recursively + + """ + + import grp + from pwd import getpwnam + + if uid is None and gid is None: + raise ValueError("either uid or gid argument is required") + + # Retrieve uid/gid + if isinstance(uid, str): + try: + uid = getpwnam(uid).pw_uid + except KeyError: + raise YunohostError("unknown_user", user=uid) + elif uid is None: + uid = -1 + if isinstance(gid, str): + try: + gid = grp.getgrnam(gid).gr_gid + except KeyError: + raise YunohostError("unknown_group", group=gid) + elif gid is None: + gid = -1 + + try: + os.chown(path, uid, gid) + if recursive and os.path.isdir(path): + for root, dirs, files in os.walk(path): + for d in dirs: + os.chown(os.path.join(root, d), uid, gid) + for f in files: + os.chown(os.path.join(root, f), uid, gid) + except Exception as e: + raise YunohostError("error_changing_file_permissions", path=path, error=str(e)) + + +def chmod( + path: str, mode: int, fmode: int | None = None, recursive: bool = False +) -> None: + """Change the mode of a path + + Keyword arguments: + - mode -- Numeric path mode to set + - fmode -- Numeric file mode to set in case of a recursive directory + - recursive -- Operate on path recursively + + """ + + try: + os.chmod(path, mode) + if recursive and os.path.isdir(path): + if fmode is None: + fmode = mode + for root, dirs, files in os.walk(path): + for d in dirs: + os.chmod(os.path.join(root, d), mode) + for f in files: + os.chmod(os.path.join(root, f), fmode) + except Exception as e: + raise YunohostError("error_changing_file_permissions", path=path, error=str(e)) + + +def rm(path: str, recursive: bool = False, force: bool = False) -> None: + """Remove a file or directory + + Keyword arguments: + - path -- The path to remove + - recursive -- Remove directories and their contents recursively + - force -- Ignore nonexistent files + + """ + if recursive and os.path.isdir(path): + shutil.rmtree(path, ignore_errors=force) + else: + try: + os.remove(path) + except OSError as e: + if not force: + raise YunohostError("error_removing", path=path, error=str(e)) + + +def cp(source: str, dest: str, recursive: bool = False, **kwargs: Any) -> str: + if recursive and os.path.isdir(source): + return shutil.copytree(source, dest, symlinks=True, **kwargs) + else: + return shutil.copy2(source, dest, follow_symlinks=False, **kwargs) + + +def download_text(url: str, timeout: int = 30, expected_status_code: int = 200) -> str: + """ + Download text from a url and returns the raw text + + Keyword argument: + url -- The url to download the data from + timeout -- Number of seconds allowed for download to effectively start + before giving up + expected_status_code -- Status code expected from the request. Can be + None to ignore the status code. + """ + import requests # lazy loading this module for performance reasons + + # Assumptions + assert isinstance(url, str) + + # Download file + try: + r = requests.get(url, timeout=timeout) + # SSL exceptions + except requests.exceptions.SSLError: + raise YunohostError("download_ssl_error", url=url) + # Invalid URL + except requests.exceptions.ConnectionError: + raise YunohostError("invalid_url", url=url) + # Timeout exceptions + except requests.exceptions.Timeout: + raise YunohostError("download_timeout", url=url) + # Unknown stuff + except Exception as e: + raise YunohostError("download_unknown_error", url=url, error=str(e)) + # Assume error if status code is not 200 (OK) + if expected_status_code is not None and r.status_code != expected_status_code: + raise YunohostError( + "download_bad_status_code", url=url, code=str(r.status_code) + ) + + return r.text + + +def download_json( + url: str, timeout: int = 30, expected_status_code: int = 200 +) -> Jsonable: + """ + Download json from a url and returns the loaded json object + + Keyword argument: + url -- The url to download the data from + timeout -- Number of seconds allowed for download to effectively start + before giving up + """ + # Fetch the data + text = download_text(url, timeout, expected_status_code) + + # Try to load json to check if it's syntaxically correct + try: + loaded_json = json.loads(text) + except ValueError as e: + raise YunohostError("corrupted_json", ressource=url, error=str(e)) + + return loaded_json diff --git a/src/utils/form.py b/src/utils/form.py new file mode 100644 index 0000000..b2affcc --- /dev/null +++ b/src/utils/form.py @@ -0,0 +1,2331 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 YunoHost Contributors +# +# This file is part of YunoHost (see https://yunohost.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import ast +import datetime +import operator as op +import os +import re +import shutil +import tempfile +import urllib.parse +from enum import Enum +from logging import getLogger +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + ClassVar, + Iterable, + Literal, + Mapping, + Type, + Union, + cast, + overload, +) + +from moulinette import Moulinette, m18n +from moulinette.interfaces.cli import colorize +from pydantic import ( + BaseModel, + Extra, + ValidationError, + create_model, + root_validator, + validator, +) +from pydantic.color import Color +from pydantic.fields import Field +from pydantic.networks import EmailStr, HttpUrl +from pydantic.types import constr + +from ..log import OperationLogger +from ..utils.error import YunohostError, YunohostValidationError +from ..utils.i18n import _value_for_locale +from .file_utils import read_yaml, write_to_file + +if TYPE_CHECKING: + from pydantic.fields import FieldInfo, ModelField + +logger = getLogger("yunohost.form") + + +# ╭───────────────────────────────────────────────────────╮ +# │ ┌─╴╷ ╷╭─┐╷ │ +# │ ├─╴│╭╯├─┤│ │ +# │ ╰─╴╰╯ ╵ ╵╰─╴ │ +# ╰───────────────────────────────────────────────────────╯ + + +# Those js-like evaluate functions are used to eval safely visible attributes +# The goal is to evaluate in the same way than js simple-evaluate +# https://github.com/shepherdwind/simple-evaluate +def evaluate_simple_ast( + node: ast.Expression | ast.expr, context: dict[str, Any] | None = None +) -> Any: + if context is None: + context = {} + + operators: dict[type, Callable[..., Any]] = { + ast.Not: op.not_, + ast.Mult: op.mul, + ast.Div: op.truediv, # number + ast.Mod: op.mod, # number + ast.Add: op.add, # str + ast.Sub: op.sub, # number + ast.USub: op.neg, # Negative number + ast.Gt: op.gt, + ast.Lt: op.lt, + ast.GtE: op.ge, + ast.LtE: op.le, + ast.Eq: op.eq, + ast.NotEq: op.ne, + } + context["true"] = True + context["false"] = False + context["null"] = None + + # Variable + if isinstance(node, ast.Name): # Variable + return context[node.id] + + # Python <=3.7 String + elif isinstance(node, ast.Str): + return node.s + + # Python <=3.7 Number + elif isinstance(node, ast.Num): + return node.n + + # Boolean, None and Python 3.8 for Number, Boolean, String and None + elif isinstance(node, (ast.Constant, ast.NameConstant)): + return node.value + + # + - * / % + elif ( + isinstance(node, ast.BinOp) and type(node.op) in operators + ): # + left = evaluate_simple_ast(node.left, context) + right = evaluate_simple_ast(node.right, context) + if type(node.op) is ast.Add: + if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 + left = str(left) + right = str(right) + elif type(left) is type(right): # support "111" - "1" -> 110 + left = float(left) + right = float(right) + + return operators[type(node.op)](left, right) + + # Comparison + # JS and Python don't give the same result for multi operators + # like True == 10 > 2. + elif ( + isinstance(node, ast.Compare) and len(node.comparators) == 1 + ): # + left = evaluate_simple_ast(node.left, context) + right = evaluate_simple_ast(node.comparators[0], context) + operator = node.ops[0] + if isinstance(left, (int, float)) or isinstance(right, (int, float)): + try: + left = float(left) + right = float(right) + except ValueError: + return type(operator) is ast.NotEq + try: + return operators[type(operator)](left, right) + except TypeError: # support "e" > 1 -> False like in JS + return False + + # and / or + elif isinstance(node, ast.BoolOp): # + for value in node.values: + value = evaluate_simple_ast(value, context) + if isinstance(node.op, ast.And) and not value: + return False + elif isinstance(node.op, ast.Or) and value: + return True + return isinstance(node.op, ast.And) + + # not / USub (it's negation number -\d) + elif isinstance(node, ast.UnaryOp): # e.g., -1 + return operators[type(node.op)](evaluate_simple_ast(node.operand, context)) + + # match function call + elif isinstance(node, ast.Call) and node.func.__dict__.get("id") == "match": + return re.match( + evaluate_simple_ast(node.args[1], context), + context[node.args[0].id], # type: ignore + ) + + # Unauthorized opcode + else: + opcode = str(type(node)) + raise YunohostError( + f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True + ) + + +def js_to_python(expr: str) -> str: + in_string = None + py_expr = "" + i = 0 + escaped = False + for char in expr: + if char in r"\"'": + # Start a string + if not in_string: + in_string = char + + # Finish a string + elif in_string == char and not escaped: + in_string = None + + # If we are not in a string, replace operators + elif not in_string: + if char == "!" and expr[i + 1] != "=": + char = "not " + elif char in "|&" and py_expr[-1:] == char: + py_expr = py_expr[:-1] + char = " and " if char == "&" else " or " + + # Determine if next loop will be in escaped mode + escaped = char == "\\" and not escaped + py_expr += char + i += 1 + return py_expr + + +def evaluate_simple_js_expression( + expr: str, context: Mapping[str, Any] = {} +) -> int | float | str | bool | None | re.Match[str]: + if not expr.strip(): + return False + node = ast.parse(js_to_python(expr), mode="eval").body + return evaluate_simple_ast(node, context) # type: ignore + + +# ╭───────────────────────────────────────────────────────╮ +# │ ╭─╮┌─╮╶┬╴╶┬╴╭─╮╭╮╷╭─╴ │ +# │ │ │├─╯ │ │ │ ││││╰─╮ │ +# │ ╰─╯╵ ╵ ╶┴╴╰─╯╵╰╯╶─╯ │ +# ╰───────────────────────────────────────────────────────╯ + + +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" + + +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 = 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](/dev/packaging/advanced/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](/dev/packaging/advanced/config_panels#the-bind-statement) + """ + + type: OptionType + id: str + mode: Mode = "bash" # TODO use "python" as default mode with AppConfigPanel setuping it to "bash" + ask: Translation | None + readonly: bool = False + visible: JSExpression | bool = True + bind: str | None = None + name: 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: str | None, values: Values) -> str: + if value is None: + return values["id"] # type: ignore + 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) # type: ignore + + 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: 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](/dev/packaging/advanced/config_panels#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: Translation | None = None + style: State = State.success + icon: str | None = None + enabled: 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) # type: ignore + + +# ╭───────────────────────────────────────────────────────╮ +# │ 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: Translation | None = None + example: str | None = None + placeholder: str | None = None + redact: bool = False + optional: bool = False # FIXME keep required as default? + default: Any = None + _annotation: Any = Any + _none_as_empty_str: ClassVar[bool] = True + + @validator("default", pre=True) + def check_empty_default(value: Any) -> Any: + if value == "": + return None + return value + + @staticmethod + def humanize(value: Any, option: Union["BaseOption", dict[Any, Any]] = {}) -> str: + if value is None: + return "" + return str(value) + + @staticmethod + def normalize(value: Any, option: Union["BaseOption", dict[Any, Any]] = {}) -> Any: + if isinstance(value, str): + value = value.strip() + return value + + @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 + + @property + def _validators(self) -> dict[str, Callable[[Any, "ModelField"], Any]]: + return { + "pre": self._value_pre_validator, + "post": self._value_post_validator, + } + + 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: + attrs["allow_mutation"] = False + + if self.example: + attrs["examples"] = [self.example] + + if self.default is not None: + attrs["default"] = self.default + else: + attrs["default"] = ... if not self.optional else None + + return attrs + + 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 self._dynamic_annotation | None + ) + field = Field(default=attrs.pop("default", None), **attrs) + + return (anno, field) + + def _get_prompt_message(self, value: Any) -> str: + message = super()._get_prompt_message(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 value and isinstance(value, str): + data_to_redact.append(value) + + data_to_redact += [ + urllib.parse.quote(data) + for data in data_to_redact + if urllib.parse.quote(data) != data + ] + + for operation_logger in OperationLogger._instances: + operation_logger.data_to_redact.extend(data_to_redact) + + return value + + +# ─ STRINGS ─────────────────────────────────────────────── + + +class BaseStringOption(BaseInputOption): + default: str | None = None + pattern: 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 StringOption(BaseStringOption): + r""" + Ask for a simple string. + + #### 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" + ``` + + #### Properties + + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + - `pattern` (optional): `Pattern`, a regex to match the value against + """ + + 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: ClassVar[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: str | None, field: "ModelField") -> 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=forbidden_chars + ) + + # If it's an optional argument the value should be empty or strong enough + from ..utils.password import assert_password_is_strong_enough + + assert_password_is_strong_enough("user", value) + + return value + + +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: str | None = None + _annotation = Color + + @staticmethod + def humanize( + value: Color | str | None, option: Union["BaseOption", dict[Any, Any]] = {} + ) -> str: + if isinstance(value, Color): + value.as_named(fallback=True) + + return super(ColorOption, ColorOption).humanize(value, option) + + @staticmethod + def normalize( + value: Color | str | None, option: Union["BaseOption", dict[Any, Any]] = {} + ) -> str: + if isinstance(value, Color): + return value.as_hex() + + return super(ColorOption, ColorOption).normalize(value, option) + + @classmethod + def _value_post_validator( + cls, value: Color | None, field: "ModelField" + ) -> str | None: + if isinstance(value, Color): + return value.as_hex() + + return super()._value_post_validator(value, field) # type: ignore + + +# ─ NUMERIC ─────────────────────────────────────────────── + + +class NumberOption(BaseInputOption): + """ + Ask for a number (an integer). + + #### 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: int | None = None + min: int | None = None + max: int | None = None + step: int | None = None + _annotation = int + _none_as_empty_str = False + + @staticmethod + def normalize( + value: Any, option: Union["BaseOption", dict[Any, Any]] = {} + ) -> int | None: + if isinstance(value, int): + return value + + if isinstance(value, str): + value = value.strip() + + if isinstance(value, str) and value.isdigit(): + return int(value) + + if value in [None, ""]: + return None + + option = option.dict() if isinstance(option, BaseOption) else option + raise YunohostValidationError( + "app_argument_invalid", + name=option.get("id"), + error=m18n.n("invalid_number"), + ) + + 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 + + return attrs + + @classmethod + def _value_pre_validator(cls, value: int | None, field: "ModelField") -> int | None: + value = super()._value_pre_validator(value, field) + + if value is None: + return None + + return value + + +# ─ BOOLEAN ─────────────────────────────────────────────── + + +class BooleanOption(BaseInputOption): + """ + Ask for a boolean. + Renders as a switch in the web-admin and a yes/no prompt in CLI. + + #### 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: bool | int | str | None = 0 + _annotation = bool | int | str # type: ignore + _yes_answers: ClassVar[set[str]] = {"1", "yes", "y", "true", "t", "on"} + _no_answers: ClassVar[set[str]] = {"0", "no", "n", "false", "f", "off"} + _none_as_empty_str = False + + @staticmethod + def humanize(value: Any, option: Union["BaseOption", dict[Any, Any]] = {}) -> str: + option = option.dict() if isinstance(option, BaseOption) else option + + yes = option.get("yes", 1) + no = option.get("no", 0) + + value = BooleanOption.normalize(value, option) + + if value == yes: + return "yes" + if value == no: + return "no" + if value is None: + return "" + + raise YunohostValidationError( + "app_argument_choice_invalid", + name=option.get("id"), + value=value, + choices="yes/no", + ) + + @staticmethod + def normalize(value: Any, option: Union["BaseOption", dict[Any, Any]] = {}) -> Any: + option = option.dict() if isinstance(option, BaseOption) else option + + if isinstance(value, str): + value = value.strip() + + technical_yes = option.get("yes", 1) + technical_no = option.get("no", 0) + + no_answers = BooleanOption._no_answers + yes_answers = BooleanOption._yes_answers + + assert str(technical_yes).lower() not in no_answers, ( + f"'yes' value can't be in {no_answers}" + ) + assert str(technical_no).lower() not in yes_answers, ( + f"'no' value can't be in {yes_answers}" + ) + + no_answers.add(str(technical_no).lower()) + yes_answers.add(str(technical_yes).lower()) + + strvalue = str(value).lower() + + if strvalue in yes_answers: + return technical_yes + if strvalue in no_answers: + return technical_no + + if strvalue in ["none", ""]: + return None + + raise YunohostValidationError( + "app_argument_choice_invalid", + name=option.get("id"), + value=strvalue, + choices="yes/no", + ) + + def get(self, key: str, default: Any = None) -> Any: + return getattr(self, key, default) + + 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: Any) -> str: + message = super()._get_prompt_message(value) + + if not self.readonly: + message += " [yes | no]" + + return message + + @classmethod + def _value_post_validator(cls, value: 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(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. + + Can also take a timestamp as value that will output as an ISO date string. + + #### Example + + ```toml + [section.my_option_id] + type = "date" + default = "2070-12-31" + ``` + + #### Properties + + - [common inputs properties](#common-inputs-properties) + - `default`: `""` + """ + + type: Literal[OptionType.date] = OptionType.date + default: str | None = None + _annotation = datetime.date + + @classmethod + def _value_post_validator( + cls, value: datetime.date | None, field: "ModelField" + ) -> str | None: + if isinstance(value, datetime.date): + return value.isoformat() + + return super()._value_post_validator(value, field) # type: ignore + + +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: str | int | None = None + _annotation = datetime.time + + @classmethod + def _value_post_validator( + cls, value: datetime.date | None, field: "ModelField" + ) -> 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) # type: ignore + + +# ─ LOCATIONS ───────────────────────────────────────────── + + +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: EmailStr | None = None + _annotation = EmailStr + + +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: Any, option: Union["BaseOption", dict[Any, Any]] = {}) -> 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("id"), + error="Argument for path should be a string.", + ) + + if not value.strip(): + if option.get("optional"): + return "" + # Hmpf here we could just have a "else" case + # but we also want WebPathOption.normalize("") to return "/" + # (i.e. if no option is provided, hence .get("optional") is None + elif option.get("optional") is False: + raise YunohostValidationError( + "app_argument_invalid", + name=option.get("id"), + error="Option is mandatory", + ) + + return "/" + value.strip().strip(" /") + + +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: HttpUrl | None, field: "ModelField" + ) -> str | None: + if isinstance(value, HttpUrl): + return str(value) + + return super()._value_post_validator(value, field) # type: ignore + + +# ─ FILE ────────────────────────────────────────────────── + + +class FileOption(BaseInputOption): + r""" + Ask for file. + Renders a file prompt in the web-admin and ask for a path in CLI. + + #### 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: list[str] | None = None # currently only used by the web-admin + default: str | None = None + _annotation = str # TODO could be Path at some point + _upload_dirs: ClassVar[set[str]] = set() + + @property + def _validators(self) -> dict[str, Callable[[Any, "ModelField"], Any]]: + 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) -> None: + # Delete files uploaded from API + for upload_dir in cls._upload_dirs: + if os.path.exists(upload_dir): + shutil.rmtree(upload_dir) + + @classmethod + def _base_value_post_validator( + cls, value: Any, field: "ModelField" + ) -> tuple[bytes, str | None]: + import mimetypes + from base64 import b64decode + from pathlib import Path + + from magic import Magic + + if Moulinette.interface.type != "api" or ( + isinstance(value, str) and value.startswith("/") + ): + path = Path(value) + if not (path.exists() and path.is_absolute() and path.is_file()): + raise YunohostValidationError( + f"File {value} 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 file type '{mimetype}', expected a type among '{', '.join(accept_list)}'.", + raw_msg=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.add(upload_dir) + + logger.debug(f"Saving file {field.name} for file question into {file_path}") + + write_to_file(file_path, content, file_mode="wb") + + return file_path + + @classmethod + def _python_value_post_validator(cls, value: str, field: "ModelField") -> str: + """File handling for "python" config panels""" + + import hashlib + from pathlib import Path + + 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 BaseChoicesOption(BaseInputOption): + # FIXME probably forbid choices to be None? + filter: 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: dict[str, Any] | list[Any] | None + + @validator("choices", pre=True, check_fields=False) + def parse_comalist_choices( + value: str | dict[str, Any] | list[Any] | None, + ) -> 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) -> object | Type[str]: + # The bunch of type: ignore is because self.choices is not defined... + if self.choices is not None: # type: ignore + choices = ( + self.choices if isinstance(self.choices, list) else self.choices.keys() # type: ignore + ) + 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: # type: ignore + value = self.choices[value] # type: ignore + + return f"{colorize(message, 'purple')} {value}" + + if self.choices: # type: ignore + # Prevent displaying a shitload of choices + # (e.g. 100+ available users when choosing an app admin...) + choices = ( + list(self.choices.keys()) # type: ignore + if isinstance(self.choices, dict) # type: ignore + else self.choices # type: ignore + ) + 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 `