Merge branch 'bookworm' into legacy_cleanup

This commit is contained in:
Alexandre Aubin 2023-06-13 21:30:01 +02:00
commit b4dcd0fb22
74 changed files with 6657 additions and 3486 deletions

View file

@ -6,6 +6,8 @@ on:
pull_request:
# The branches below must be a subset of the branches above
branches: [ "dev" ]
paths-ignore:
- 'src/tests/**'
schedule:
- cron: '43 12 * * 3'

View file

@ -1,78 +0,0 @@
#!/bin/bash
#=================================================
# N UPDATING HELPER
#=================================================
# This script is meant to be run by GitHub Actions.
# It is derived from the Updater script from the YunoHost-Apps organization.
# It aims to automate the update of `n`, the Node version management system.
#=================================================
# FETCHING LATEST RELEASE AND ITS ASSETS
#=================================================
# Fetching information
source helpers/nodejs
current_version="$n_version"
repo="tj/n"
# Some jq magic is needed, because the latest upstream release is not always the latest version (e.g. security patches for older versions)
version=$(curl --silent "https://api.github.com/repos/$repo/releases" | jq -r '.[] | select( .prerelease != true ) | .tag_name' | sort -V | tail -1)
# Later down the script, we assume the version has only digits and dots
# Sometimes the release name starts with a "v", so let's filter it out.
if [[ ${version:0:1} == "v" || ${version:0:1} == "V" ]]; then
version=${version:1}
fi
# Setting up the environment variables
echo "Current version: $current_version"
echo "Latest release from upstream: $version"
echo "VERSION=$version" >> $GITHUB_ENV
# For the time being, let's assume the script will fail
echo "PROCEED=false" >> $GITHUB_ENV
# Proceed only if the retrieved version is greater than the current one
if ! dpkg --compare-versions "$current_version" "lt" "$version" ; then
echo "::warning ::No new version available"
exit 0
# Proceed only if a PR for this new version does not already exist
elif git ls-remote -q --exit-code --heads https://github.com/${GITHUB_REPOSITORY:-YunoHost/yunohost}.git ci-auto-update-n-v$version ; then
echo "::warning ::A branch already exists for this update"
exit 0
fi
#=================================================
# UPDATE SOURCE FILES
#=================================================
asset_url="https://github.com/tj/n/archive/v${version}.tar.gz"
echo "Handling asset at $asset_url"
# Create the temporary directory
tempdir="$(mktemp -d)"
# Download sources and calculate checksum
filename=${asset_url##*/}
curl --silent -4 -L $asset_url -o "$tempdir/$filename"
checksum=$(sha256sum "$tempdir/$filename" | head -c 64)
# Delete temporary directory
rm -rf $tempdir
echo "Calculated checksum for n v${version} is $checksum"
#=================================================
# GENERIC FINALIZATION
#=================================================
# Replace new version in helper
sed -i -E "s/^n_version=.*$/n_version=$version/" helpers/nodejs
# Replace checksum in helper
sed -i -E "s/^n_checksum=.*$/n_checksum=$checksum/" helpers/nodejs
# The Action will proceed only if the PROCEED environment variable is set to true
echo "PROCEED=true" >> $GITHUB_ENV
exit 0

View file

@ -21,7 +21,8 @@ jobs:
git config --global user.name 'yunohost-bot'
git config --global user.email 'yunohost-bot@users.noreply.github.com'
# Run the updater script
/bin/bash .github/workflows/n_updater.sh
wget https://raw.githubusercontent.com/tj/n/master/bin/n --output-document=helpers/vendor/n/n
[[ -z "$(git diff helpers/vendor/n/n)" ]] || echo "PROCEED=true" >> $GITHUB_ENV
- name: Commit changes
id: commit
if: ${{ env.PROCEED == 'true' }}

View file

@ -7,7 +7,6 @@ generate-helpers-doc:
image: "before-install"
needs: []
before_script:
- apt-get update -y && apt-get install git hub -y
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
script:

View file

@ -3,34 +3,33 @@
########################################
# later we must fix lint and format-check jobs and remove "allow_failure"
lint39:
lint311:
stage: lint
image: "before-install"
needs: []
allow_failure: true
script:
- tox -e py39-lint
- tox -e py311-lint
invalidcode39:
invalidcode311:
stage: lint
image: "before-install"
needs: []
script:
- tox -e py39-invalidcode
- tox -e py311-invalidcode
mypy:
stage: lint
image: "before-install"
needs: []
script:
- tox -e py39-mypy
- tox -e py311-mypy
black:
stage: lint
image: "before-install"
needs: []
before_script:
- apt-get update -y && apt-get install git hub -y
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
- hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo
@ -38,7 +37,7 @@ black:
script:
# create a local branch that will overwrite distant one
- git checkout -b "ci-format-${CI_COMMIT_REF_NAME}" --no-track
- tox -e py39-black-run
- tox -e py311-black-run
- '[ $(git diff | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit
- git commit -am "[CI] Format code with Black" || true
- git push -f origin "ci-format-${CI_COMMIT_REF_NAME}":"ci-format-${CI_COMMIT_REF_NAME}"

View file

@ -1,7 +1,6 @@
.install_debs: &install_debs
- apt-get update -o Acquire::Retries=3
- DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ${CI_PROJECT_DIR}/*.deb
- pip3 install -U mock pip pytest pytest-cov pytest-mock pytest-sugar requests-mock tox ansi2html black jinja2 "packaging<22"
.test-stage:
stage: test

View file

@ -16,7 +16,6 @@ autofix-translated-strings:
image: "before-install"
needs: []
before_script:
- apt-get update -y && apt-get install git hub -y
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
- hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo

View file

@ -132,12 +132,8 @@ def main() -> bool:
)
continue
# Only broadcast IPv4 because IPv6 is buggy ... because we ain't using python3-ifaddr >= 0.1.7
# Buster only ships 0.1.6
# Bullseye ships 0.1.7
# To be re-enabled once we're on bullseye...
# ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"]
ips: List[str] = interfaces[interface]["ipv4"]
# Broadcast IPv4 and IPv6
ips: List[str] = interfaces[interface]["ipv4"] + interfaces[interface]["ipv6"]
# If at least one IP is listed
if not ips:

View file

@ -13,9 +13,8 @@ protocols = imap sieve {% if pop3_enabled == "True" %}pop3{% endif %}
mail_plugins = $mail_plugins quota notify push_notification
###############################################################################
# generated 2020-08-18, Mozilla Guideline v5.6, Dovecot 2.3.4, OpenSSL 1.1.1d, intermediate configuration
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.4&config=intermediate&openssl=1.1.1d&guideline=5.6
# generated 2023-06-13, Mozilla Guideline v5.7, Dovecot 2.3.19, OpenSSL 3.0.9, intermediate configuration
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.19&config=intermediate&openssl=3.0.9&guideline=5.7
ssl = required
@ -32,7 +31,7 @@ ssl_dh = </usr/share/yunohost/ffdhe2048.pem
# intermediate configuration
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl_cipher_list = 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 = no
###############################################################################

View file

@ -1,6 +1,6 @@
location ^~ '/.well-known/acme-challenge/'
{
default_type "text/plain";
alias /tmp/acme-challenge-public/;
alias /var/www/.well-known/acme-challenge-public/;
gzip off;
}

View file

@ -3,16 +3,16 @@ ssl_session_cache shared:SSL:50m; # about 200000 sessions
ssl_session_tickets off;
{% if compatibility == "modern" %}
# generated 2020-08-14, Mozilla Guideline v5.6, nginx 1.14.2, OpenSSL 1.1.1d, modern configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.14.2&config=modern&openssl=1.1.1d&guideline=5.6
# generated 2023-06-13, Mozilla Guideline v5.7, nginx 1.22.1, OpenSSL 3.0.9, modern configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.22.1&config=modern&openssl=3.0.9&guideline=5.7
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
{% else %}
# Ciphers with intermediate compatibility
# generated 2020-08-14, Mozilla Guideline v5.6, nginx 1.14.2, OpenSSL 1.1.1d, intermediate configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.14.2&config=intermediate&openssl=1.1.1d&guideline=5.6
# generated 2023-06-13, Mozilla Guideline v5.7, nginx 1.22.1, OpenSSL 3.0.9, intermediate configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.22.1&config=intermediate&openssl=3.0.9&guideline=5.7
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# Pre-defined FFDHE group (RFC 7919)
@ -26,7 +26,7 @@ ssl_dhparam /usr/share/yunohost/ffdhe2048.pem;
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
# https://observatory.mozilla.org/
{% if experimental == "True" %}
more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'";
more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data: blob: ; object-src https: data: 'unsafe-inline'; style-src https: data: 'unsafe-inline' ; script-src https: data: 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob:;";
{% else %}
more_set_headers "Content-Security-Policy : upgrade-insecure-requests";
{% endif %}

View file

@ -13,7 +13,7 @@ server {
include /etc/nginx/conf.d/acme-challenge.conf.inc;
location ^~ '/.well-known/ynh-diagnosis/' {
alias /tmp/.well-known/ynh-diagnosis/;
alias /var/www/.well-known/ynh-diagnosis/;
}
{% if mail_enabled == "True" %}

View file

@ -30,8 +30,8 @@ smtpd_tls_chain_files =
tls_server_sni_maps = hash:/etc/postfix/sni
{% if compatibility == "intermediate" %}
# generated 2020-08-18, Mozilla Guideline v5.6, Postfix 3.4.14, OpenSSL 1.1.1d, intermediate configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.4.14&config=intermediate&openssl=1.1.1d&guideline=5.6
# generated 2023-06-13, Mozilla Guideline v5.7, Postfix 3.7.5, OpenSSL 3.0.9, intermediate configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.7.5&config=intermediate&openssl=3.0.9&guideline=5.7
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
@ -41,10 +41,10 @@ smtpd_tls_mandatory_ciphers = medium
# not actually 1024 bits, this applies to all DHE >= 1024 bits
smtpd_tls_dh1024_param_file = /usr/share/yunohost/ffdhe2048.pem
tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
{% else %}
# generated 2020-08-18, Mozilla Guideline v5.6, Postfix 3.4.14, OpenSSL 1.1.1d, modern configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.4.14&config=modern&openssl=1.1.1d&guideline=5.6
# generated 2023-06-13, Mozilla Guideline v5.7, Postfix 3.7.5, OpenSSL 3.0.9, modern configuration
# https://ssl-config.mozilla.org/#server=postfix&version=3.7.5&config=modern&openssl=3.0.9&guideline=5.7
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2

164
debian/changelog vendored
View file

@ -1,3 +1,167 @@
yunohost (12.0.0) unstable; urgency=low
- Tmp changelog to prepare Bookworm
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 04 May 2023 20:30:19 +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 <alex.aubin@mailoo.org> Mon, 12 Jun 2023 17:41:26 +0200
yunohost (11.1.21.2) stable; urgency=low
- Aleks loves xargs syntax >_> (313a1647)
-- Alexandre Aubin <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> 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 <alex.aubin@mailoo.org> Tue, 28 Feb 2023 23:08:02 +0100
yunohost (11.1.11.2) stable; urgency=low
- Rebump version to flag as stable, not testing >_>

25
debian/control vendored
View file

@ -2,7 +2,7 @@ Source: yunohost
Section: utils
Priority: extra
Maintainer: YunoHost Contributors <contrib@yunohost.org>
Build-Depends: debhelper (>=9), debhelper-compat (= 13), dh-python, python3-all (>= 3.7), python3-yaml, python3-jinja2
Build-Depends: debhelper (>=9), debhelper-compat (= 13), dh-python, python3-all (>= 3.11), python3-yaml, python3-jinja2
Standards-Version: 3.9.6
Homepage: https://yunohost.org/
@ -14,9 +14,9 @@ Depends: ${python3:Depends}, ${misc:Depends}
, python3-psutil, python3-requests, python3-dnspython, python3-openssl
, python3-miniupnpc, python3-dbus, python3-jinja2
, python3-toml, python3-packaging, python3-publicsuffix2
, python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon,
, python3-ldap, python3-zeroconf (>=0.47), python3-lexicon,
, python-is-python3
, nginx, nginx-extras (>=1.18)
, nginx, nginx-extras (>=1.22)
, apt, apt-transport-https, apt-utils, dirmngr
, openssh-server, iptables, fail2ban, bind9-dnsutils
, openssl, ca-certificates, netcat-openbsd, iproute2
@ -32,23 +32,18 @@ Depends: ${python3:Depends}, ${misc:Depends}
Recommends: yunohost-admin
, ntp, inetutils-ping | iputils-ping
, bash-completion, rsyslog
, php7.4-common, php7.4-fpm, php7.4-ldap, php7.4-intl
, mariadb-server, php7.4-mysql
, php7.4-gd, php7.4-curl, php-php-gettext
, python3-pip
, unattended-upgrades
, libdbd-ldap-perl, libnet-dns-perl
, metronome (>=3.14.0)
Conflicts: iptables-persistent
, apache2
, bind9
, nginx-extras (>= 1.19)
, openssl (>= 1.1.1o-0)
, slapd (>= 2.4.58)
, dovecot-core (>= 1:2.3.14)
, redis-server (>= 5:6.1)
, fail2ban (>= 0.11.3)
, iptables (>= 1.8.8)
, nginx-extras (>= 1.23)
, openssl (>= 3.1)
, slapd (>= 2.6)
, dovecot-core (>= 1:2.4)
, redis-server (>= 5:7.1)
, fail2ban (>= 1.1)
, iptables (>= 1.8.10)
Description: manageable and configured self-hosting server
YunoHost aims to make self-hosting accessible to everyone. It configures
an email, Web and IM server alongside a LDAP base. It also provides

View file

@ -2,7 +2,7 @@ import ast
import datetime
import subprocess
version = (open("../debian/changelog").readlines()[0].split()[1].strip("()"),)
version = open("../debian/changelog").readlines()[0].split()[1].strip("()")
today = datetime.datetime.now().strftime("%d/%m/%Y")

View file

@ -111,3 +111,95 @@ ynh_remove_apps() {
done
fi
}
# Spawn a Bash shell with the app environment loaded
#
# usage: ynh_spawn_app_shell --app="app"
# | arg: -a, --app= - the app ID
#
# examples:
# ynh_spawn_app_shell --app="APP" <<< 'echo "$USER"'
# ynh_spawn_app_shell --app="APP" < /tmp/some_script.bash
#
# Requires YunoHost version 11.0.* or higher, and that the app relies on packaging v2 or higher.
# The spawned shell will have environment variables loaded and environment files sourced
# from the app's service configuration file (defaults to $app.service, overridable by the packager with `service` setting).
# If the app relies on a specific PHP version, then `php` will be aliased that version.
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)
if [ -n "$phpversion" ]
then
eval "php() { php${phpversion} \"\$@\"; }"
export -f php
fi
# Source the EnvironmentFiles from the app's service
local env_files=($(systemctl show $service.service -p "EnvironmentFiles" --value))
if [ ${#env_files[*]} -gt 0 ]
then
# set -/+a enables and disables new variables being automatically exported. Needed when using `source`.
set -a
for file in ${env_files[*]}
do
[[ $file = /* ]] && source $file
done
set +a
fi
# cd into the WorkingDirectory set in the service, or default to the install_dir
local env_dir=$(systemctl show $service.service -p "WorkingDirectory" --value)
[ -z $env_dir ] && env_dir=$install_dir;
cd $env_dir
# Spawn the app shell
su -s /bin/bash $app
}

View file

@ -370,7 +370,13 @@ ynh_remove_app_dependencies() {
apt-mark unhold ${dep_app}-ynh-deps
fi
ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used.
# 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.

View file

@ -329,7 +329,8 @@ ynh_store_file_checksum() {
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)
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
@ -374,7 +375,7 @@ ynh_backup_if_checksum_is_different() {
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)
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:"

View file

@ -308,8 +308,8 @@ ynh_script_progression() {
local progression_bar="${progress_string2:0:$effective_progression}${progress_string1:0:$expected_progression}${progress_string0:0:$left_progression}"
local print_exec_time=""
if [ $time -eq 1 ]; then
print_exec_time=" [$(date +%Hh%Mm,%Ss --date="0 + $exec_time sec")]"
if [ $time -eq 1 ] && [ "$exec_time" -gt 10 ]; then
print_exec_time=" [$(bc <<< "scale=1; $exec_time / 60" ) minutes]"
fi
ynh_print_info "[$progression_bar] > ${message}${print_exec_time}"

View file

@ -210,6 +210,9 @@ ynh_mysql_setup_db() {
# 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
}

View file

@ -1,32 +1,10 @@
#!/bin/bash
n_version=9.0.1
n_checksum=ad305e8ee9111aa5b08e6dbde23f01109401ad2d25deecacd880b3f9ea45702b
n_install_dir="/opt/node_n"
node_version_path="$n_install_dir/n/versions/node"
# N_PREFIX is the directory of n, it needs to be loaded as a environment variable.
export N_PREFIX="$n_install_dir"
# Install Node version management
#
# [internal]
#
# usage: ynh_install_n
#
# Requires YunoHost version 2.7.12 or higher.
ynh_install_n() {
# Build an app.src for n
echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz
SOURCE_SUM=${n_checksum}" >"$YNH_APP_BASEDIR/conf/n.src"
# Download and extract n
ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n
# Install n
(
cd "$n_install_dir/git"
PREFIX=$N_PREFIX make install 2>&1
)
}
# Load the version of node for an app, and set variables.
#
# usage: ynh_use_nodejs
@ -133,14 +111,10 @@ ynh_install_nodejs() {
test -x /usr/bin/node && mv /usr/bin/node /usr/bin/node_n
test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n
# If n is not previously setup, install it
if ! $n_install_dir/bin/n --version >/dev/null 2>&1; then
ynh_install_n
elif dpkg --compare-versions "$($n_install_dir/bin/n --version)" lt $n_version; then
ynh_install_n
fi
# Modify the default N_PREFIX in n script
# Install (or update if YunoHost vendor/ folder updated since last install) n
mkdir -p $n_install_dir/bin/
cp /usr/share/yunohost/helpers.d/vendor/n/n $n_install_dir/bin/n
# Tweak for n to understand it's installed in $N_PREFIX
ynh_replace_string --match_string="^N_PREFIX=\${N_PREFIX-.*}$" --replace_string="N_PREFIX=\${N_PREFIX-$N_PREFIX}" --target_file="$n_install_dir/bin/n"
# Restore /usr/local/bin in PATH

View file

@ -22,7 +22,10 @@ YNH_APP_BASEDIR=${YNH_APP_BASEDIR:-$(realpath ..)}
ynh_exit_properly() {
local exit_code=$?
rm -rf "/var/cache/yunohost/download/"
if [[ "${YNH_APP_ACTION:-}" =~ ^install$|^upgrade$|^restore$ ]]
then
rm -rf "/var/cache/yunohost/download/"
fi
if [ "$exit_code" -eq 0 ]; then
exit 0 # Exit without error if the script ended correctly
@ -62,7 +65,7 @@ ynh_abort_if_errors() {
}
# When running an app script with packaging format >= 2, auto-enable ynh_abort_if_errors except for remove script
if dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} ge 2 && [[ ${YNH_APP_ACTION} != "remove" ]]
if [[ "${YNH_CONTEXT:-}" != "regenconf" ]] && dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} ge 2 && [[ ${YNH_APP_ACTION} != "remove" ]]
then
ynh_abort_if_errors
fi
@ -71,39 +74,79 @@ fi
#
# 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 `app`
# | arg: -s, --source_id= - Name of the source, defaults to `main` (when the sources resource exists in manifest.toml) or (legacy) `app` otherwise
# | arg: -k, --keep= - Space-separated list of files/folders that will be backup/restored in $dest_dir, such as a config file you don't want to overwrite. For example 'conf.json secrets.json logs/'
# | arg: -r, --full_replace= - Remove previous sources before installing new sources
#
# #### New 'sources' resources
#
# (See also the resources documentation which may be more complete?)
#
# This helper will read infos from the 'sources' resources in the manifest.toml of the app
# and expect a structure like:
#
# ```toml
# [resources.sources]
# [resources.sources.main]
# url = "https://some.address.to/download/the/app/archive"
# sha256 = "0123456789abcdef" # The sha256 sum of the asset obtained from the URL
# ```
#
# ##### Optional flags
#
# ```text
# format = "tar.gz"/xz/bz2 # automatically guessed from the extension of the URL, but can be set explicitly. Will use `tar` to extract
# "zip" # automatically guessed from the extension of the URL, but can be set explicitly. Will use `unzip` to extract
# "docker" # useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract` to extract
# "whatever" # an arbitrary value, not really meaningful except to imply that the file won't be extracted
#
# in_subdir = true # default, there's an intermediate subdir in the archive before accessing the actual files
# false # sources are directly in the archive root
# n # (special cases) an integer representing a number of subdirs levels to get rid of
#
# extract = true # default if file is indeed an archive such as .zip, .tar.gz, .tar.bz2, ...
# = false # default if file 'format' is not set and the file is not to be extracted because it is not an archive but a script or binary or whatever asset.
# # in which case the file will only be `mv`ed to the location possibly renamed using the `rename` value
#
# rename = "whatever_your_want" # to be used for convenience when `extract` is false and the default name of the file is not practical
# platform = "linux/amd64" # (defaults to "linux/$YNH_ARCH") to be used in conjonction with `format = "docker"` to specify which architecture to extract for
# ```
#
# You may also define assets url and checksum per-architectures such as:
# ```toml
# [resources.sources]
# [resources.sources.main]
# amd64.url = "https://some.address.to/download/the/app/archive/when/amd64"
# amd64.sha256 = "0123456789abcdef"
# armhf.url = "https://some.address.to/download/the/app/archive/when/armhf"
# armhf.sha256 = "fedcba9876543210"
# ```
#
# In which case ynh_setup_source --dest_dir="$install_dir" will automatically pick the appropriate source depending on the arch
#
#
#
# #### Legacy format '.src'
#
# This helper will read `conf/${source_id}.src`, download and install the sources.
#
# The src file need to contains:
# ```
# SOURCE_URL=Address to download the app archive
# SOURCE_SUM=Control sum
# # (Optional) Program to check the integrity (sha256sum, md5sum...). Default: sha256
# SOURCE_SUM_PRG=sha256
# # (Optional) Archive format. Default: tar.gz
# SOURCE_SUM=Sha256 sum
# SOURCE_FORMAT=tar.gz
# # (Optional) Put false if sources are directly in the archive root. Default: true
# # Instead of true, SOURCE_IN_SUBDIR could be the number of sub directories to remove.
# SOURCE_IN_SUBDIR=false
# # (Optionnal) Name of the local archive (offline setup support). Default: ${src_id}.${src_format}
# SOURCE_FILENAME=example.tar.gz
# # (Optional) If it set as false don't extract the source. Default: true
# # (Useful to get a debian package or a python wheel.)
# SOURCE_EXTRACT=(true|false)
# # (Optionnal) Name of the plateform. Default: "linux/$YNH_ARCH"
# SOURCE_PLATFORM=linux/arm64/v8
# ```
#
# The helper will:
# - Check if there is a local source archive in `/opt/yunohost-apps-src/$APP_ID/$SOURCE_FILENAME`
# - Download `$SOURCE_URL` if there is no local archive
# - Check the integrity with `$SOURCE_SUM_PRG -c --status`
# - Download the specific URL if there is no local archive
# - Check the integrity with the specific sha256 sum
# - Uncompress the archive to `$dest_dir`.
# - If `$SOURCE_IN_SUBDIR` is true, the first level directory of the archive will be removed.
# - If `$SOURCE_IN_SUBDIR` is a numeric value, the N first level directories will be removed.
# - If `in_subdir` is true, the first level directory of the archive will be removed.
# - If `in_subdir` is a numeric value, the N first level directories will be removed.
# - Patches named `sources/patches/${src_id}-*.patch` will be applied to `$dest_dir`
# - Extra files in `sources/extra_files/$src_id` will be copied to dest_dir
#
@ -118,22 +161,66 @@ ynh_setup_source() {
local full_replace
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
source_id="${source_id:-app}"
keep="${keep:-}"
full_replace="${full_replace:-0}"
local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src"
if test -e $YNH_APP_BASEDIR/manifest.toml && cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq -e '.resources.sources' >/dev/null
then
source_id="${source_id:-main}"
local sources_json=$(cat $YNH_APP_BASEDIR/manifest.toml | toml_to_json | jq ".resources.sources[\"$source_id\"]")
if jq -re ".url" <<< "$sources_json"
then
local arch_prefix=""
else
local arch_prefix=".$YNH_ARCH"
fi
# Load value from configuration file (see above for a small doc about this file
# format)
local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_filename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_plateform=$(grep 'SOURCE_PLATFORM=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_url="$(jq -r "$arch_prefix.url" <<< "$sources_json" | sed 's/^null$//')"
local src_sum="$(jq -r "$arch_prefix.sha256" <<< "$sources_json" | sed 's/^null$//')"
local src_sumprg="sha256sum"
local src_format="$(jq -r ".format" <<< "$sources_json" | sed 's/^null$//')"
local src_in_subdir="$(jq -r ".in_subdir" <<< "$sources_json" | sed 's/^null$//')"
local src_extract="$(jq -r ".extract" <<< "$sources_json" | sed 's/^null$//')"
local src_platform="$(jq -r ".platform" <<< "$sources_json" | sed 's/^null$//')"
local src_rename="$(jq -r ".rename" <<< "$sources_json" | sed 's/^null$//')"
[[ -n "$src_url" ]] || ynh_die "No URL defined for source $source_id$arch_prefix ?"
[[ -n "$src_sum" ]] || ynh_die "No sha256 sum defined for source $source_id$arch_prefix ?"
if [[ -z "$src_format" ]]
then
if [[ "$src_url" =~ ^.*\.zip$ ]] || [[ "$src_url" =~ ^.*/zipball/.*$ ]]
then
src_format="zip"
elif [[ "$src_url" =~ ^.*\.tar\.gz$ ]] || [[ "$src_url" =~ ^.*\.tgz$ ]] || [[ "$src_url" =~ ^.*/tar\.gz/.*$ ]] || [[ "$src_url" =~ ^.*/tarball/.*$ ]]
then
src_format="tar.gz"
elif [[ "$src_url" =~ ^.*\.tar\.xz$ ]]
then
src_format="tar.xz"
elif [[ "$src_url" =~ ^.*\.tar\.bz2$ ]]
then
src_format="tar.bz2"
elif [[ -z "$src_extract" ]]
then
src_extract="false"
fi
fi
else
source_id="${source_id:-app}"
local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src"
# Load value from configuration file (see above for a small doc about this file
# format)
local src_url=$(grep 'SOURCE_URL=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_sum=$(grep 'SOURCE_SUM=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_format=$(grep 'SOURCE_FORMAT=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_rename=$(grep 'SOURCE_FILENAME=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_extract=$(grep 'SOURCE_EXTRACT=' "$src_file_path" | cut --delimiter='=' --fields=2-)
local src_platform=$(grep 'SOURCE_PLATFORM=' "$src_file_path" | cut --delimiter='=' --fields=2-)
fi
# Default value
src_sumprg=${src_sumprg:-sha256sum}
@ -141,33 +228,53 @@ ynh_setup_source() {
src_format=${src_format:-tar.gz}
src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]')
src_extract=${src_extract:-true}
if [ "$src_filename" = "" ]; then
src_filename="${source_id}.${src_format}"
if [[ "$src_extract" != "true" ]] && [[ "$src_extract" != "false" ]]
then
ynh_die "For source $source_id, expected either 'true' or 'false' for the extract parameter"
fi
# (Unused?) mecanism where one can have the file in a special local cache to not have to download it...
local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}"
mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/
src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}"
# (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_plateform="${src_plateform:-"linux/$YNH_ARCH"}"
src_platform="${src_platform:-"linux/$YNH_ARCH"}"
elif test -e "$local_src"; then
cp $local_src $src_filename
else
[ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?"
# NB. we have to declare the var as local first,
# otherwise 'local foo=$(false) || echo 'pwet'" does'nt work
# because local always return 0 ...
local out
# Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget)
out=$(wget --tries 3 --no-dns-cache --timeout 900 --no-verbose --output-document=$src_filename $src_url 2>&1) \
|| ynh_die --message="$out"
# 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
echo "${src_sum} ${src_filename}" | ${src_sumprg} --check --status \
|| ynh_die --message="Corrupt source for ${src_filename}: Expected ${src_sum} but got $(${src_sumprg} ${src_filename} | cut --delimiter=' ' --fields=1) (size: $(du -hs ${src_filename} | cut --delimiter=' ' --fields=1))."
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
@ -199,11 +306,16 @@ ynh_setup_source() {
_ynh_apply_default_permissions $dest_dir
fi
if ! "$src_extract"; then
mv $src_filename $dest_dir
elif [ "$src_format" = "docker" ]; then
/usr/share/yunohost/helpers.d/vendor/docker-image-extract/docker-image-extract -p $src_plateform -o $dest_dir $src_url 2>&1
elif [ "$src_format" = "zip" ]; then
if [[ "$src_extract" == "false" ]]; then
if [[ -z "$src_rename" ]]
then
mv $src_filename $dest_dir
else
mv $src_filename $dest_dir/$src_rename
fi
elif [[ "$src_format" == "docker" ]]; then
/usr/share/yunohost/helpers.d/vendor/docker-image-extract/docker-image-extract -p $src_platform -o $dest_dir $src_url 2>&1
elif [[ "$src_format" == "zip" ]]; then
# Zip format
# Using of a temp directory, because unzip doesn't manage --strip-components
if $src_in_subdir; then
@ -959,8 +1071,10 @@ _ynh_apply_default_permissions() {
fi
fi
# Crons should be owned by root otherwise they probably don't run
if echo "$target" | grep -q '^/etc/cron'
# Crons should be owned by root
# Also we don't want systemd conf, nginx conf or others stuff to be owned by the app,
# otherwise they could self-edit their own systemd conf and escalate privilege
if echo "$target" | grep -q '^/etc/cron\|/etc/php\|/etc/nginx/conf.d\|/etc/fail2ban\|/etc/systemd/system'
then
chmod 400 $target
chown root:root $target
@ -970,3 +1084,7 @@ _ynh_apply_default_permissions() {
int_to_bool() {
sed -e 's/^1$/True/g' -e 's/^0$/False/g'
}
toml_to_json() {
python3 -c 'import toml, json, sys; print(json.dumps(toml.load(sys.stdin)))'
}

21
helpers/vendor/n/LICENSE vendored Normal file
View file

@ -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.

1
helpers/vendor/n/README.md vendored Normal file
View file

@ -0,0 +1 @@
This is taken from https://github.com/tj/n/

1635
helpers/vendor/n/n vendored Executable file

File diff suppressed because it is too large Load diff

View file

@ -9,5 +9,5 @@ source /usr/share/yunohost/helpers
# Backup destination
backup_dir="${1}/data/xmpp"
ynh_backup /var/lib/metronome "${backup_dir}/var_lib_metronome"
ynh_backup /var/xmpp-upload/ "${backup_dir}/var_xmpp-upload"
[[ ! -d /var/lib/metronome ]] || ynh_backup /var/lib/metronome "${backup_dir}/var_lib_metronome" --not_mandatory
[[ ! -d /var/xmpp-upload ]] || ynh_backup /var/xmpp-upload/ "${backup_dir}/var_xmpp-upload" --not_mandatory

View file

@ -97,7 +97,7 @@ 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) &
sleep \$((RANDOM%3600)); yunohost tools update apps > /dev/null
EOF
# Cron job that renew lets encrypt certificates if there's any that needs renewal
@ -178,9 +178,20 @@ do_post_regen() {
chown root:admins /home/yunohost.backup/archives
chown root:root /var/cache/yunohost
[ ! -e /var/www/.well-known/ynh-diagnosis/ ] || chmod 775 /var/www/.well-known/ynh-diagnosis/
# 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
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
then
chown root:root /etc/php/*/fpm/pool.d/*.conf
chmod 644 /etc/php/*/fpm/pool.d/*.conf
fi
# Certs
# We do this with find because there could be a lot of them...
chown -R root:ssl-cert /etc/yunohost/certs

View file

@ -144,6 +144,12 @@ do_pre_regen() {
do_post_regen() {
regen_conf_files=$1
if ls -l /etc/nginx/conf.d/*.d/*.conf
then
chown root:root /etc/nginx/conf.d/*.d/*.conf
chmod 644 /etc/nginx/conf.d/*.d/*.conf
fi
[ -z "$regen_conf_files" ] && exit 0
# create NGINX conf directories for domains

View file

@ -24,6 +24,12 @@ do_pre_regen() {
do_post_regen() {
regen_conf_files=$1
if ls -l /etc/fail2ban/jail.d/*.conf
then
chown root:root /etc/fail2ban/jail.d/*.conf
chmod 644 /etc/fail2ban/jail.d/*.conf
fi
[[ -z "$regen_conf_files" ]] \
|| systemctl reload fail2ban
}

View file

@ -1,4 +1,11 @@
backup_dir="$1/data/xmpp"
cp -a $backup_dir/var_lib_metronome/. /var/lib/metronome
cp -a $backup_dir/var_xmpp-upload/. /var/xmpp-upload
if [[ -e $backup_dir/var_lib_metronome/ ]]
then
cp -a $backup_dir/var_lib_metronome/. /var/lib/metronome
fi
if [[ -e $backup_dir/var_xmpp-upload ]]
then
cp -a $backup_dir/var_xmpp-upload/. /var/xmpp-upload
fi

View file

@ -245,5 +245,17 @@
"migration_0021_main_upgrade": "بداية التحديث الرئيسي…",
"migration_0021_patching_sources_list": "تحديث ملف sources.lists…",
"pattern_firstname": "يجب أن يكون اسماً أولياً صالحاً (على الأقل 3 حروف)",
"yunohost_configured": "تم إعداد YunoHost الآن"
"yunohost_configured": "تم إعداد YunoHost الآن",
"global_settings_setting_backup_compress_tar_archives": "ضغط النُسخ الاحتياطية",
"diagnosis_description_apps": "التطبيقات",
"danger": "خطر:",
"diagnosis_basesystem_hardware": "بنية الخادم هي {virt} {arch}",
"diagnosis_basesystem_hardware_model": "طراز الخادم {model}",
"diagnosis_mail_queue_ok": "هناك {nb_pending} رسائل بريد إلكتروني معلقة في قوائم انتظار البريد",
"diagnosis_mail_ehlo_ok": "يمكن الوصول إلى خادم بريد SMTP من الخارج وبالتالي فهو قادر على استقبال رسائل البريد الإلكتروني!",
"diagnosis_dns_good_conf": "تم إعداد سجلات نظام أسماء النطاقات DNS بشكل صحيح للنطاق {domain} (category {category})",
"diagnosis_ip_dnsresolution_working": "تحليل اسم النطاق يعمل!",
"diagnosis_mail_blacklist_listed_by": "إن عنوان الـ IP الخاص بك أو نطاقك <code>{item}</code> مُدرَج ضمن قائمة سوداء على {blacklist_name}",
"diagnosis_mail_outgoing_port_25_ok": "خادم بريد SMTP قادر على إرسال رسائل البريد الإلكتروني (منفذ البريد الصادر 25 غير محظور).",
"user_already_exists": "المستخدم '{user}' موجود مِن قَبل"
}

View file

@ -692,5 +692,17 @@
"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_ok": "Gut, das aktuelle Zertifikat sieht gut aus!",
"app_change_url_require_full_domain": "{app} kann nicht auf diese neue URL verschoben werden, weil sie eine vollständige eigene Domäne benötigt (z.B. mit Pfad = /)",
"app_not_upgraded_broken_system_continue": "Die App '{failed_app}' konnte nicht aktualisiert werden und hat Ihr System in einen beschädigten Zustand versetzt (folglich wird --continue-on-failure ignoriert) und als Konsequenz wurde die Aktualisierung der folgenden Apps abgelehnt: {apps}",
"app_yunohost_version_not_supported": "Diese App setzt YunoHost >= {required} voraus aber die gegenwärtig installierte Version ist {current}",
"app_failed_to_upgrade_but_continue": "Die App {failed_app} konnte nicht aktualisiert werden und es wird anforderungsgemäss zur nächsten Aktualisierung fortgefahren. Starten sie 'yunohost log show {operation_logger_name}' um den Fehlerbericht zu sehen",
"app_not_upgraded_broken_system": "Die App '{failed_app}' konnte nicht aktualisiert werden und hat Ihr System in einen beschädigten Zustand versetzt und als Konzequenz wurde die Aktualisierung der folgenden Apps abgelehnt: {apps}",
"apps_failed_to_upgrade": "Diese Apps konnten nicht aktualisiert werden: {apps}",
"app_arch_not_supported": "Diese App kann nur auf bestimmten Architekturen {required} installiert werden, aber Ihre gegenwärtige Serverarchitektur ist {current}",
"app_not_enough_disk": "Diese App benötigt {required} freien Speicherplatz.",
"app_not_enough_ram": "Diese App benötigt {required} RAM um installiert/aktualisiert zu werden, aber es sind aktuell nur {current} verfügbar.",
"app_change_url_failed": "Kann die URL für {app} nicht ändern: {error}",
"app_change_url_script_failed": "Es ist ein Fehler im URL-Änderungs-Script aufgetreten",
"app_resource_failed": "Automatische Ressourcen-Allokation (provisioning), die Unterbindung des Zugriffts auf Ressourcen (deprovisioning) oder die Aktualisierung der Ressourcen für {app} schlug fehl: {error}"
}

View file

@ -26,7 +26,9 @@
"app_change_url_success": "{app} URL is now {domain}{path}",
"app_config_unable_to_apply": "Failed to apply config panel values.",
"app_config_unable_to_read": "Failed to read config panel values.",
"app_corrupt_source": "YunoHost was able to download the asset '{source_id}' ({url}) for {app}, but the asset doesn't match the expected checksum. This could mean that some temporary network failure happened on your server, OR the asset was somehow changed by the upstream maintainer (or a malicious actor?) and YunoHost packagers need to investigate and update the app manifest to reflect this change.\n Expected sha256 checksum: {expected_sha256}\n Downloaded sha256 checksum: {computed_sha256}\n Downloaded file size: {size}",
"app_extraction_failed": "Could not extract the installation files",
"app_failed_to_download_asset": "Failed to download asset '{source_id}' ({url}) for {app}: {out}",
"app_failed_to_upgrade_but_continue": "App {failed_app} failed to upgrade, continue to next upgrades as requested. Run 'yunohost log show {operation_logger_name}' to see failure log",
"app_full_domain_unavailable": "Sorry, this app must be installed on a domain of its own, but other apps are already installed on the domain '{domain}'. You could use a subdomain dedicated to this app instead.",
"app_id_invalid": "Invalid app ID",
@ -465,13 +467,17 @@
"group_creation_failed": "Could not create the group '{group}': {error}",
"group_deleted": "Group '{group}' deleted",
"group_deletion_failed": "Could not delete the group '{group}': {error}",
"group_mailalias_add": "The email alias '{mail}' will be added to the group '{group}'",
"group_mailalias_remove": "The email alias '{mail}' will be removed from the group '{group}'",
"group_no_change": "Nothing to change for group '{group}'",
"group_unknown": "The group '{group}' is unknown",
"group_update_aliases": "Updating aliases for group '{group}'",
"group_update_failed": "Could not update the group '{group}': {error}",
"group_updated": "Group '{group}' updated",
"group_user_add": "The user '{user}' will be added to the group '{group}'",
"group_user_already_in_group": "User {user} is already in group {group}",
"group_user_not_in_group": "User {user} is not in group {group}",
"group_user_remove": "The user '{user}' will be removed from the group '{group}'",
"hook_exec_failed": "Could not run script: {path}",
"hook_exec_not_terminated": "Script did not finish properly: {path}",
"hook_json_return_error": "Could not read return from hook {path}. Error: {msg}. Raw content: {raw_content}",
@ -570,7 +576,20 @@
"migration_0024_rebuild_python_venv_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}",
"migration_0024_rebuild_python_venv_failed": "Failed to rebuild the Python virtualenv for {app}. The app may not work as long as this is not resolved. You should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.",
"migration_0024_rebuild_python_venv_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`",
"migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bullseye and YunoHost 11.x",
"migration_0027_cleaning_up": "Cleaning up cache and packages not useful anymore...",
"migration_0027_hgjghjghjgeneral_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.",
"migration_0027_main_upgrade": "Starting main upgrade...",
"migration_0027_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}",
"migration_0027_not_buster2": "The current Debian distribution is not Bullseye! If you already ran the Bullseye->Bookworm migration, then this error is symptomatic of the fact that the migration procedure was not 100% succesful (otherwise YunoHost would have flagged it as completed). It is recommended to investigate what happened with the support team, who will need the **full** log of the `migration, which can be found in Tools > Logs in the webadmin.",
"migration_0027_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.",
"migration_0027_patch_yunohost_conflicts": "Applying patch to workaround conflict issue...",
"migration_0027_patching_sources_list": "Patching the sources.lists...",
"migration_0027_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}",
"migration_0027_start": "Starting migration to Bookworm",
"migration_0027_still_on_buster_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Bullseye",
"migration_0027_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Bookworm.",
"migration_0027_yunohost_upgrade": "Starting YunoHost core upgrade...",
"migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bookworm and YunoHost 12",
"migration_description_0022_php73_to_php74_pools": "Migrate php7.3-fpm 'pool' conf files to php7.4",
"migration_description_0023_postgresql_11_to_13": "Migrate databases from PostgreSQL 11 to 13",
"migration_description_0024_rebuild_python_venv": "Repair Python app after bullseye migration",

View file

@ -168,7 +168,7 @@
"diagnosis_failed": "Ezinezkoa izan da '{category}' ataleko diagnostikoa lortzea: {error}",
"diagnosis_ip_weird_resolvconf": "DNS ebazpena badabilela dirudi, baina antza denez moldatutako <code>/etc/resolv.conf</code> fitxategia erabiltzen ari zara.",
"diagnosis_dns_bad_conf": "DNS balio batzuk falta dira edo ez dira zuzenak {domain} domeinurako ({category} atala)",
"diagnosis_diskusage_ok": "<code>{mountpoint}</code> fitxategi-sistemak (<code>{device}</code> euskarrian) edukieraren {free} (%{free_percent}a) ditu erabilgarri oraindik ({total} orotara)!",
"diagnosis_diskusage_ok": "<code>{mountpoint}</code> fitxategi-sistemak (<code>{device}</code> euskarrian) edukieraren {free} (%{free_percent}a) ditu oraindik erabilgarri ({total} orotara)!",
"apps_catalog_update_success": "Aplikazioen katalogoa eguneratu da!",
"certmanager_warning_subdomain_dns_record": "'{subdomain}' azpidomeinuak ez dauka '{domain}'(e)k duen IP bera. Ezaugarri batzuk ez dira erabilgarri egongo hau zuzendu arte eta ziurtagiri bat birsortu arte.",
"app_argument_choice_invalid": "Hautatu ({choices}) aukeretako bat '{name}' argumenturako: '{value}' ez dago aukera horien artean",
@ -230,7 +230,7 @@
"certmanager_attempt_to_replace_valid_cert": "{domain} domeinurako egokia eta baliogarria den ziurtagiri bat ordezkatzen saiatzen ari zara! (Erabili --force mezu hau deuseztatu eta ziurtagiria ordezkatzeko)",
"diagnosis_backports_in_sources_list": "Dirudienez apt (pakete kudeatzailea) backports biltegia erabiltzeko konfiguratuta dago. Zertan ari zaren ez badakizu, ez zenuke backports biltegietako aplikaziorik instalatu beharko, ezegonkortasun eta gatazkak eragin ditzaketelako sistemarekin.",
"app_restore_failed": "Ezinezkoa izan da {app} lehengoratzea: {error}",
"diagnosis_apps_allgood": "Instalatutako aplikazioek oinarrizko pakete-jarraibideekin bat egiten dute",
"diagnosis_apps_allgood": "Instalatutako aplikazioak bat datoz oinarrizko pakete-jarraibideekin",
"diagnosis_apps_bad_quality": "Aplikazio hau hondatuta dagoela dio YunoHosten aplikazioen katalogoak. Agian behin-behineko kontua da arduradunak arazoa konpondu bitartean. Oraingoz, ezin da aplikazioa eguneratu.",
"diagnosis_apps_broken": "Aplikazio hau YunoHosten aplikazioen katalogoan hondatuta dagoela ageri da. Agian behin-behineko kontua da arduradunak konpondu bitartean. Oraingoz, ezin da aplikazioa eguneratu.",
"diagnosis_apps_deprecated_practices": "Instalatutako aplikazio honen bertsioak oraindik darabiltza zaharkitutako pakete-jarraibideak. Eguneratzea hausnartu beharko zenuke.",
@ -711,7 +711,7 @@
"domain_config_cert_summary": "Ziurtagiriaren egoera",
"domain_config_cert_summary_abouttoexpire": "Uneko ziurtagiria iraungitzear dago. Aurki berritu beharko litzateke automatikoki.",
"domain_config_cert_summary_letsencrypt": "Primeran! Baliozko Let's Encrypt zirutagiria erabiltzen ari zara!",
"domain_config_cert_summary_ok": "Ados, uneko ziurtagiriak itzura ona du!",
"domain_config_cert_summary_ok": "Ados, uneko ziurtagiriak itxura ona du!",
"domain_config_cert_validity": "Balizokotasuna",
"global_settings_setting_admin_strength_help": "Betekizun hauek pasahitza lehenbizikoz sortzerakoan edo aldatzerakoan baino ez dira bete behar",
"global_settings_setting_nginx_compatibility": "NGINXekin bateragarritasuna",
@ -752,5 +752,15 @@
"global_settings_setting_dns_exposure": "DNS ezarpenetan eta diagnostikoan kontuan hartzeko IP bertsioak",
"global_settings_setting_dns_exposure_help": "Ohart ongi: honek gomendatutako DNS ezarpenei eta diagnostikoari eragiten die soilik. Ez du eraginik sistemaren ezarpenetan.",
"diagnosis_ip_no_ipv6_tip_important": "IPv6 automatikoki ezarri ohi du sistemak edo hornitzaileak erabilgarri baldin badago. Bestela eskuz ezarri beharko dituzu aukera batzuk ondorengo dokumentazioan azaldu bezala: <a href='https://yunohost.org/#/ipv6'>https://yunohost.org/#/ipv6</a>.",
"pattern_fullname": "Baliozko izen oso bat izan behar da (gutxienez hiru karaktere)"
"pattern_fullname": "Baliozko izen oso bat izan behar da (gutxienez hiru karaktere)",
"app_change_url_failed": "Ezin izan da {app} aplikazioaren URLa aldatu: {error}",
"app_change_url_require_full_domain": "Ezin da {app} aplikazioa URL berri honetara aldatu domeinu oso bat behar duelako (i.e. with path = /)",
"app_change_url_script_failed": "Errorea gertatu da URLa aldatzeko aginduaren barnean",
"app_corrupt_source": "YunoHostek deskargatu du {app} aplikaziorako '{source_id}' ({url}) baliabidea baina ez dator bat espero zen 'checksum'arekin. Agian zerbitzariak interneteko konexioa galdu du tarte batez, EDO baliabidea nolabait moldatua izan da arduradunaren aldetik (edo partehartzaile maltzur batetik?) eta YunoHosten arduradunek egoera aztertu eta aplikazioaren manifestua eguneratu behar dute aldaketa hau kontuan hartzeko.\n Espero zen sha256 checksuma: {expected_sha256}\n Deskargatutakoaren sha256 checksuma: {computed_sha256}\n Deskargatutako fitxategiaren tamaina: {size}",
"app_failed_to_upgrade_but_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du, jarraitu hurrengo bertsio-berritzeetara eskatu bezala. Exekutatu 'yunohost log show {operation_logger_name}' errorearen erregistroa ikusteko",
"app_not_upgraded_broken_system": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du, beraz, ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}",
"app_not_upgraded_broken_system_continue": "{failed_app} aplikazioaren bertsio-berritzeak huts egin du eta sistema hondatu du (beraz, --continue-on-failure aukerari muzin egin zaio) eta ondorengo aplikazioen bertsio-berritzeak ezeztatu dira: {apps}",
"app_failed_to_download_asset": "{app} aplikaziorako '{source_id}' ({url}) baliabidea deskargatzeak huts egin du: {out}",
"apps_failed_to_upgrade": "Aplikazio hauen bertsio-berritzeak huts egin du: {apps}",
"apps_failed_to_upgrade_line": "\n * {app_id} (dagokion erregistroa ikusteko, exekutatu 'yunohost log show {operation_logger_name}')"
}

View file

@ -755,5 +755,16 @@
"domain_config_xmpp_help": "NB : certaines fonctions XMPP nécessiteront la mise à jour de vos enregistrements DNS et la régénération de votre certificat Lets Encrypt pour être activées",
"app_change_url_failed": "Impossible de modifier l'url de {app} : {error}",
"app_change_url_require_full_domain": "{app} ne peut pas être déplacée vers cette nouvelle URL car elle nécessite un domaine complet (c'est-à-dire avec un chemin = /)",
"app_change_url_script_failed": "Une erreur s'est produite dans le script de modification de l'url"
}
"app_change_url_script_failed": "Une erreur s'est produite dans le script de modification de l'url",
"app_failed_to_upgrade_but_continue": "La mise à jour de l'application {failed_app} a échoué, mais YunoHost va continuer avec les mises à jour suivantes comme demandé. Lancez 'yunohost log show {operation_logger_name}' pour voir le journal des échecs",
"app_not_upgraded_broken_system_continue": "L'application '{failed_app}' n'a pas réussi à se mettre à jour et a mis le système dans un état alternatif car quelque chose est au moins momentanément \"cassé\" (le paramètre --continue-on-failure est donc ignoré). La conséquence est que les mises à jour des applications suivantes ont été annulées : {apps}",
"app_not_upgraded_broken_system": "L'application '{failed_app}' n'a pas réussi à se mettre à jour et a mis le système dans un état de panne. Par conséquent, les mises à niveau des applications suivantes ont été annulées : {apps}",
"apps_failed_to_upgrade": "Ces applications n'ont pas pu être mises à jour : {apps}",
"apps_failed_to_upgrade_line": "\n * {app_id} (pour voir le journal correspondant, faites un 'yunohost log show {operation_logger_name}')",
"app_failed_to_download_asset": "Échec du téléchargement de la ressource '{source_id}' ({url}) pour {app} : {out}",
"app_corrupt_source": "YunoHost a pu télécharger la ressource '{source_id}' ({url}) pour {app}, malheureusement celle-ci ne correspond pas à la somme de contrôle attendue. Cela peut signifier qu'une défaillance temporaire du réseau s'est produite sur votre serveur, OU que la ressource a été modifiée par le mainteneur de l'application en amont (ou un acteur malveillant ?) et que les responsables du paquet de cette application pour YunoHost doivent investiguer et mettre à jour le manifeste de l'application pour refléter ce changement.\n Somme de contrôle sha256 attendue : {expected_sha256}\n Somme de contrôle sha256 téléchargée : {computed_sha256}\n Taille du fichier téléchargé : {size}",
"group_mailalias_add": "L'alias de courrier électronique '{mail}' sera ajouté au groupe '{group}'",
"group_user_add": "L'utilisateur '{user}' sera ajouté au groupe '{group}'",
"group_user_remove": "L'utilisateur '{user}' sera retiré du groupe '{group}'",
"group_mailalias_remove": "L'alias de courrier électronique '{mail}' sera supprimé du groupe '{group}'"
}

View file

@ -752,5 +752,15 @@
"domain_config_xmpp_help": "Nota: algunhas características de XMPP para ser utilizadas precisan que teñas ao día os rexistros DNS e rexeneres os certificados Lets Encrypt",
"app_change_url_failed": "Non se cambiou o url para {app}: {error}",
"app_change_url_require_full_domain": "{app} non se pode mover a este novo URL porque require un dominio completo propio (ex. con ruta = /)",
"app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url"
"app_change_url_script_failed": "Algo fallou ao executar o script de cambio de url",
"apps_failed_to_upgrade_line": "\n * {app_id} (para ver o rexistro correspondente executa 'yunohost log show {operation_logger_name}')",
"app_failed_to_upgrade_but_continue": "Fallou a actualización de {failed_app}, seguimos coas demáis actualizacións. Executa 'yunohost log show {operation_logger_name}' para ver o rexistro do fallo",
"app_not_upgraded_broken_system": "Fallou a actualización de '{failed_app}' e estragou o sistema, como consecuencia cancelouse a actualización das seguintes apps: {apps}",
"app_not_upgraded_broken_system_continue": "Fallou a actualización de '{failed_app}' e estragou o sistema (polo que ignórase --continue-on-failure), como consecuencia cancelouse a actualización das seguintes apps: {apps}",
"apps_failed_to_upgrade": "Fallou a actualización das seguintes aplicacións:{apps}",
"invalid_shell": "Intérprete de ordes non válido: {shell}",
"log_resource_snippet": "Aprovisionamento/desaprovisionamento/actualización dun recurso",
"app_resource_failed": "Fallou o aprovisionamento, desaprovisionamento ou actualización de recursos para {app}: {error}",
"app_failed_to_download_asset": "Fallou a descarga do recurso '{source_id}' ({url}) para {app}: {out}",
"app_corrupt_source": "YunoHost foi quen de descargar o recurso '{source_id}' ({url}) para {app}, pero a suma de comprobación para o recurso non concorda. Pode significar que houbo un fallo temporal na conexión do servidor á rede, OU que o recurso sufreu, dalgún xeito, cambios desde que os desenvolvedores orixinais (ou unha terceira parte maliciosa?), o equipo de YunoHost ten que investigar e actualizar o manifesto da app para mostrar este cambio.\n Suma sha256 agardada: {expected_sha256} \n Suma sha256 do descargado: {computed_sha256}\n Tamaño do ficheiro: {size}"
}

View file

@ -5,19 +5,19 @@
"app_already_installed": "{app} sudah terpasang",
"app_already_up_to_date": "{app} sudah dalam versi mutakhir",
"app_argument_required": "Argumen '{name}' dibutuhkan",
"app_change_url_identical_domains": "Domain)url_path yang lama dan baru identik ('{domain}{path}'), tak ada yang perlu dilakukan.",
"app_change_url_identical_domains": "Domain/url_path yang lama dan baru identik ('{domain}{path}'), tak ada yang perlu dilakukan.",
"app_change_url_no_script": "Aplikasi '{app_name}' belum mendukung pengubahan URL. Mungkin Anda harus memperbaruinya.",
"app_change_url_success": "URL {app} sekarang adalah {domain}{path}",
"app_id_invalid": "ID aplikasi tidak sah",
"app_install_failed": "Tidak dapat memasang {app}: {error}",
"app_install_files_invalid": "Berkas-berkas ini tidak dapat dipasang",
"app_install_script_failed": "Sebuah kesalahan terjadi pada script pemasangan aplikasi",
"app_install_files_invalid": "Berkas ini tidak dapat dipasang",
"app_install_script_failed": "Sebuah kesalahan terjadi pada skrip pemasangan aplikasi",
"app_manifest_install_ask_admin": "Pilih seorang administrator untuk aplikasi ini",
"app_manifest_install_ask_domain": "Pilih di domain mana aplikasi ini harus dipasang",
"app_not_installed": "Tidak dapat menemukan {app} di daftar aplikasi yang terpasang: {all_apps}",
"app_not_properly_removed": "{app} belum dihapus dengan benar",
"app_remove_after_failed_install": "Menghapus aplikasi mengikuti kegagalan pemasangan...",
"app_removed": "{app} dihapus",
"app_not_properly_removed": "{app} belum dilepas dengan benar",
"app_remove_after_failed_install": "Melepas aplikasi setelah kegagalan pemasangan...",
"app_removed": "{app} dilepas",
"app_restore_failed": "Tidak dapat memulihkan {app}: {error}",
"app_upgrade_some_app_failed": "Beberapa aplikasi tidak dapat diperbarui",
"app_upgraded": "{app} diperbarui",
@ -35,30 +35,361 @@
"app_upgrade_app_name": "Memperbarui {app}...",
"app_upgrade_failed": "Tidak dapat memperbarui {app}: {error}",
"app_start_install": "Memasang {app}...",
"app_start_remove": "Menghapus {app}...",
"app_start_remove": "Melepas {app}...",
"app_manifest_install_ask_password": "Pilih kata sandi administrasi untuk aplikasi ini",
"app_upgrade_several_apps": "Aplikasi-aplikasi berikut akan diperbarui: {apps}",
"app_upgrade_several_apps": "Aplikasi berikut akan diperbarui: {apps}",
"backup_app_failed": "Tidak dapat mencadangkan {app}",
"backup_archive_name_exists": "Arsip cadangan dengan nama ini sudah ada.",
"backup_created": "Cadangan dibuat",
"backup_created": "Cadangan dibuat: {name}",
"backup_creation_failed": "Tidak dapat membuat arsip cadangan",
"backup_delete_error": "Tidak dapat menghapus '{path}'",
"backup_deleted": "Cadangan dihapus",
"backup_deleted": "Cadangan dihapus: {name}",
"diagnosis_apps_issue": "Sebuah masalah ditemukan pada aplikasi {app}",
"backup_applying_method_tar": "Membuat arsip TAR cadangan...",
"backup_method_tar_finished": "Arsip TAR cadanagan dibuat",
"backup_method_tar_finished": "Arsip TAR cadangan dibuat",
"backup_nothings_done": "Tak ada yang harus disimpan",
"certmanager_cert_install_success": "Sertifikat Let's Encrypt sekarang sudah terpasang pada domain '{domain}'",
"backup_mount_archive_for_restore": "Menyiapkan arsip untuk pemulihan...",
"aborting": "Membatalkan.",
"action_invalid": "Tindakan tidak sah '{action}'",
"action_invalid": "Tindakan tidak valid '{action}'",
"app_action_cannot_be_ran_because_required_services_down": "Layanan yang dibutuhkan ini harus aktif untuk menjalankan tindakan ini: {services}. Coba memulai ulang layanan tersebut untuk melanjutkan (dan mungkin melakukan penyelidikan mengapa layanan tersebut nonaktif).",
"app_argument_choice_invalid": "Pilih nilai yang sah untuk argumen '{name}': '{value}' tidak termasuk pada pilihan yang tersedia ({choices})",
"app_argument_invalid": "Pilih nilai yang sah untuk argumen '{name}': {error}",
"app_argument_choice_invalid": "Pilih yang valid untuk argumen '{name}': '{value}' tidak termasuk pada pilihan yang tersedia ({choices})",
"app_argument_invalid": "Pilih yang valid untuk argumen '{name}': {error}",
"app_extraction_failed": "Tidak dapat mengekstrak berkas pemasangan",
"app_full_domain_unavailable": "Maaf, aplikasi ini harus dipasang pada domain sendiri, namun aplikasi lain sudah terpasang pada domain '{domain}'. Anda dapat menggunakan subdomain hanya untuk aplikasi ini.",
"app_location_unavailable": "URL ini mungkin tidak tersedia, atau terjadi konflik dengan aplikasi yang telah terpasang:\n{apps}",
"app_not_upgraded": "Aplikasi '{failed_app}' gagal diperbarui, oleh karena itu aplikasi-aplikasi berikut juga dibatalkan: {apps}",
"app_location_unavailable": "URL ini mungkin tidak tersedia atau terjadi konflik dengan aplikasi yang telah terpasang:\n{apps}",
"app_not_upgraded": "Aplikasi '{failed_app}' gagal diperbarui, oleh karena itu pembaruan aplikasi berikut juga dibatalkan: {apps}",
"app_config_unable_to_apply": "Gagal menerapkan nilai-nilai panel konfigurasi.",
"app_config_unable_to_read": "Gagal membaca nilai-nilai panel konfigurasi."
}
"app_config_unable_to_read": "Gagal membaca nilai-nilai panel konfigurasi.",
"permission_cannot_remove_main": "Menghapus izin utama tidak diperbolehkan",
"service_description_postgresql": "Menyimpan data aplikasi (basis data SQL)",
"restore_already_installed_app": "Aplikasi dengan ID '{app}' telah terpasang",
"app_change_url_require_full_domain": "{app} tidak dapat dipindah ke URL baru ini karena ini memerlukan domain penuh (tanpa jalur = /)",
"app_change_url_script_failed": "Galat terjadi di skrip pengubahan URL",
"app_not_enough_disk": "Aplikasi ini memerlukan {required} ruang kosong.",
"app_not_enough_ram": "Aplikasi ini memerlukan {required} RAM untuk pemasangan/pembaruan, tapi sekarang hanya tersedia {current} saja.",
"app_packaging_format_not_supported": "Aplikasi ini tidak dapat dipasang karena format pengemasan tidak didukung oleh YunoHost versi Anda. Anda sebaiknya memperbarui sistem Anda.",
"ask_admin_username": "Nama pengguna admin",
"backup_archive_broken_link": "Tidak dapat mengakses arsip cadangan (tautan rusak untuk {path})",
"backup_archive_open_failed": "Tidak dapat membuka arsip cadangan",
"certmanager_cert_install_success_selfsigned": "Sertifikat ditandai sendiri sekarang terpasang untuk '{domain}'",
"certmanager_cert_renew_failed": "Pembaruan ulang sertifikat Let's Encrypt gagal untuk {domains}",
"certmanager_cert_renew_success": "Sertifikat Let's Encrypt diperbarui untuk domain '{domain}'",
"diagnosis_apps_allgood": "Semua aplikasi yang dipasang mengikuti panduan penyusunan yang baik",
"diagnosis_basesystem_kernel": "Peladen memakai kernel Linux {kernel_version}",
"diagnosis_cache_still_valid": "(Tembolok masih valid untuk diagnosis {category}. Belum akan didiagnosis ulang!)",
"diagnosis_description_dnsrecords": "Rekaman DNS",
"diagnosis_description_ip": "Konektivitas internet",
"diagnosis_description_web": "Web",
"diagnosis_domain_expiration_error": "Beberapa domain akan kedaluwarsa SEGERA!",
"diagnosis_domain_expiration_not_found_details": "Informasi WHOIS untuk domain {domain} sepertinya tidak mengandung informasi tentang tanggal kedaluwarsa?",
"diagnosis_domain_expiration_warning": "Beberapa domain akan kedaluwarsa!",
"diagnosis_domain_expires_in": "{domain} kedaluwarsa dalam {days} hari.",
"diagnosis_everything_ok": "Sepertinya semuanya bagus untuk {category}!",
"diagnosis_ip_no_ipv6_tip": "Memiliki IPv6 tidaklah wajib agar sistem Anda bekerja, tapi itu akan membuat internet lebih sehat. IPv6 biasanya secara otomatis akan dikonfigurasikan oleh sistem atau penyedia peladen Anda jika tersedia. Jika belum dikonfigurasi, Anda mungkin harus mengonfigurasi beberapa hal secara manual seperti yang dijelaskan di dokumentasi di sini: <a href='https://yunohost.org/#/ipv6'>https://yunohost.org/#/ipv6</a>. 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: <a href='https://yunohost.org/#/ipv6'>https://yunohost.org/#/ipv6</a>.",
"diagnosis_ip_not_connected_at_all": "Peladen ini sepertinya tidak terhubung dengan internet sama sekali?",
"diagnosis_mail_queue_unavailable_details": "Galat: {error}",
"global_settings_setting_root_password_confirm": "Kata sandi root baru (konfirmasi)",
"global_settings_setting_smtp_allow_ipv6": "Perbolehkan IPv6",
"global_settings_setting_ssh_port": "Porta SSH",
"log_app_change_url": "Mengubah URL untuk aplikasi '{}'",
"log_app_config_set": "Menerapkan konfigurasi untuk aplikasi '{}'",
"log_app_install": "Memasang aplikasi '{}'",
"log_app_makedefault": "Membuat '{}' sebagai aplikasi baku",
"log_app_remove": "Melepas aplikasi '{}'",
"log_app_upgrade": "Memperbarui aplikasi '{}'",
"log_available_on_yunopaste": "Log ini sekarang sudah tersedia di {url}",
"log_backup_create": "Membuat arsip cadangan",
"log_backup_restore_app": "Memulihkan '{}' dari arsip cadangan",
"log_backup_restore_system": "Memulihkan sistem dari arsip cadangan",
"log_corrupted_md_file": "Berkas metadata YAML yang terkait dengan log rusak: '{md_file}\nGalat: {error}'",
"log_domain_config_set": "Memperbarui konfigurasi untuk domain '{}'",
"log_domain_main_domain": "Atur '{}' sebagai domain utama",
"log_domain_remove": "Hapus domain '{}' dari konfigurasi sistem",
"log_link_to_log": "Log penuh untuk tindakan ini: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_settings_reset": "Atur ulang pengaturan",
"log_tools_migrations_migrate_forward": "Menjalankan migrasi",
"log_tools_reboot": "Mulai ulang peladen Anda",
"log_tools_shutdown": "Matikan peladen Anda",
"log_tools_upgrade": "Perbarui paket sistem",
"migration_0021_main_upgrade": "Memulai pembaruan utama...",
"migration_0021_start": "Memulai migrasi ke Bullseye",
"migration_0021_yunohost_upgrade": "Memulai pembaruan YunoHost Core...",
"permission_updated": "Izin '{permission}' diperbarui",
"registrar_infos": "Info registrar",
"restore_already_installed_apps": "Aplikasi berikut tidak dapat dipulihkan karena mereka sudah terpasang: {apps}",
"restore_backup_too_old": "Arsip cadangan ini tidak dapat dipulihkan karena ini dihasilkan dari YunoHost dengan versi yang terlalu tua.",
"restore_failed": "Tidak dapat memulihkan sistem",
"restore_nothings_done": "Tidak ada yang dipulihkan",
"restore_running_app_script": "Memulihkan aplikasi {app}...",
"root_password_changed": "kata sandi root telah diubah",
"root_password_desynchronized": "Kata sandi administrasi telah diubah tapi YunoHost tidak dapat mengubahnya menjadi kata sandi root!",
"server_reboot_confirm": "Peladen akan dimulai ulang segera, apakan Anda yakin [{answers}]",
"server_shutdown": "Peladen akan dimatikan",
"server_shutdown_confirm": "Peladen akan dimatikan segera, apakah Anda yakin? [{answers}]",
"service_add_failed": "Tidak dapat menambahkan layanan '{service}'",
"service_added": "Layanan '{service}' ditambahkan",
"service_already_stopped": "Layanan '{service}' telah dihentikan",
"service_cmd_exec_failed": "Tidak dapat menjalankan perintah '{command}'",
"service_description_dnsmasq": "Mengurus DNS",
"service_description_dovecot": "Digunakan untuk memperbolehkan klien surel mengakses surel (via IMAP dan POP3)",
"service_description_metronome": "Mengelola akun XMPP",
"service_description_postfix": "Digunakan untuk mengirim dan menerima surel",
"service_description_slapd": "Menyimpan info terkait pengguna, domain, dan sejenisnya",
"service_description_ssh": "Memperbolehkan Anda untuk terhubung secara jarak jauh dengan peladen Anda via terminal (protokol SSH)",
"service_description_yunohost-firewall": "Mengelola pembukaan dan penutupan porta koneksi ke layanan",
"unbackup_app": "{app} tidak akan disimpan",
"user_deleted": "Pengguna dihapus",
"user_deletion_failed": "Tidak dapat menghapus pengguna {user}: {error}",
"user_import_bad_file": "Berkas CSV Anda tidak secara benar diformat, akan diabaikan untuk menghindari potensi data hilang",
"yunohost_postinstall_end_tip": "Proses pasca-pemasangan sudah selesai! Untuk menyelesaikan pengaturan Anda, pertimbangkan:\n - diagnosis masalah yang mungkin lewat bagian 'Diagnosis' di webadmin (atau 'yunohost diagnosis run' di cmd);\n - baca bagian 'Finalizing your setup' dan 'Getting to know YunoHost' di dokumentasi admin: https://yunohost.org/admindoc.",
"app_already_installed_cant_change_url": "Aplikasi ini sudah terpasang. URL tidak dapat diubah hanya dengan ini. Periksa `app changeurl` jika tersedia.",
"app_requirements_checking": "Memeriksa persyaratan untuk {app}...",
"backup_create_size_estimation": "Arsip ini akan mengandung data dengan ukuran {size}.",
"certmanager_certificate_fetching_or_enabling_failed": "Mencoba untuk menggunakan sertifikat baru untuk {domain} tidak bisa...",
"certmanager_no_cert_file": "Tidak dapat membuka berkas sertifikat untuk domain {domain} (berkas: {file})",
"diagnosis_basesystem_hardware": "Arsitektur perangkat keras peladen adalah {virt} {arch}",
"diagnosis_basesystem_ynh_inconsistent_versions": "Anda menjalankan versi paket YunoHost yang tidak konsisten... sepertinya karena pembaruan yang gagal.",
"diagnosis_basesystem_ynh_single_version": "versi {package}: {version} ({repo})",
"diagnosis_description_services": "Status layanan",
"diagnosis_description_systemresources": "Sumber daya sistem",
"diagnosis_domain_not_found_details": "Domain {domain} tidak ada di basis data WHOIS atau sudah kedaluwarsa!",
"diagnosis_http_ok": "Domain {domain} bisa dicapai dengan HTTP dari luar jaringan lokal.",
"diagnosis_ip_connected_ipv4": "Peladen ini terhubung ke internet lewat IPv4!",
"diagnosis_ip_no_ipv6": "Peladen ini sepertinya tidak memiliki IPv6.",
"domain_cert_gen_failed": "Tidak dapat membuat sertifikat",
"done": "Selesai",
"log_domain_add": "Menambahkan domain '{}' ke konfigurasi sistem",
"main_domain_changed": "Domain utama telah diubah",
"service_already_started": "Layanan '{service}' telah berjalan",
"service_description_fail2ban": "Melindungi dari berbagai macam serangan dari Internet",
"service_description_yunohost-api": "Mengelola interaksi antara antarmuka web YunoHost dengan sistem",
"this_action_broke_dpkg": "Tindakan ini merusak dpkg/APT (pengelola paket sistem)... Anda bisa mencoba menyelesaikan masalah ini dengan masuk lewat SSH dan menjalankan `sudo apt install --fix-broken` dan/atau `sudo dpkg --configure -a`.",
"app_manifest_install_ask_init_admin_permission": "Siapa yang boleh mengakses fitur admin untuk aplikasi ini? (Ini bisa diubah nanti)",
"admins": "Admin",
"all_users": "Semua pengguna YunoHost",
"app_action_failed": "Gagal menjalankan tindakan {action} untuk aplikasi {app}",
"unrestore_app": "{app} akan dipulihkan",
"user_already_exists": "Pengguna '{user}' telah ada",
"user_created": "Pengguna dibuat",
"user_creation_failed": "Tidak dapat membuat pengguna {user}: {error}",
"user_home_creation_failed": "Tidak dapat membuat folder home '{home}' untuk pengguna",
"app_manifest_install_ask_init_main_permission": "Siapa yang boleh mengakses aplikasi ini? (Ini bisa diubah nanti)",
"ask_admin_fullname": "Nama lengkap admin",
"ask_fullname": "Nama lengkap",
"backup_abstract_method": "Metode pencadangan ini belum diimplementasikan",
"backup_csv_addition_failed": "Tidak dapat menambahkan berkas ke cadangan dengan berkas CSV",
"config_action_failed": "Gagal menjalankan tindakan '{action}': {error}",
"config_validate_color": "Harus warna heksadesimal RGB yang valid",
"danger": "Peringatan:",
"diagnosis_basesystem_host": "Peladen memakai Debian {debian_version}",
"diagnosis_domain_expiration_not_found": "Tidak dapat memeriksa tanggal kedaluwarsa untuk beberapa domain",
"diagnosis_http_could_not_diagnose_details": "Galat: {error}",
"app_manifest_install_ask_path": "Pilih jalur URL (setelah domain) dimana aplikasi ini akan dipasang",
"certmanager_cert_signing_failed": "Tidak dapat memverifikasi sertifikat baru",
"config_validate_url": "Harus URL web yang valid",
"diagnosis_description_ports": "Penyingkapan porta",
"diagnosis_failed_for_category": "Diagnosis gagal untuk kategori '{category}': {error}",
"mail_unavailable": "Alamat surel ini hanya untuk kelompok admin",
"main_domain_change_failed": "Tidak dapat mengubah domain utama",
"diagnosis_ip_global": "IP Global: <code>{global}</code>",
"diagnosis_ip_dnsresolution_working": "Resolusi nama domain bekerja!",
"diagnosis_ip_local": "IP Lokal: <code>{local}</code>",
"diagnosis_ip_no_ipv4": "Peladen ini sepertinya tidak memiliki IPv4.",
"diagnosis_mail_ehlo_could_not_diagnose_details": "Galat: {error}",
"global_settings_setting_ssh_password_authentication_help": "Izinkan autentikasi kata sandi untuk SSH",
"password_listed": "Kata sandi ini termasuk dalam daftar kata sandi yang sering digunakan di dunia. Mohon untuk memilih yang lebih unik.",
"permission_not_found": "Izin '{permission}' tidak ditemukan",
"restore_not_enough_disk_space": "Ruang tidak cukup (ruang: {free_space} B, ruang yang dibutuhkan: {needed_space} B, margin aman: {margin} B)",
"server_reboot": "Peladen akan dimulai ulang",
"service_description_nginx": "Menyediakan akses untuk semua situs yang dihos di peladen Anda",
"service_description_rspamd": "Filter spam dan fitur terkait surel lainnya",
"service_remove_failed": "Tidak dapat menghapus layanan '{service}'",
"user_unknown": "Pengguna tidak diketahui: {user}",
"user_update_failed": "Tidak dapat memperbarui pengguna {user}: {error}",
"yunohost_configured": "YunoHost sudah terkonfigurasi",
"global_settings_setting_pop3_enabled": "Aktifkan POP3",
"log_user_import": "Mengimpor pengguna",
"app_start_backup": "Mengumpulkan berkas untuk dicadangkan untuk {app}...",
"app_upgrade_script_failed": "Galat terjadi di skrip pembaruan aplikasi",
"backup_csv_creation_failed": "Tidak dapat membuat berkas CSV yang dibutuhkan untuk pemulihan",
"certmanager_attempt_to_renew_valid_cert": "Sertifikat untuk domain '{domain}' belum akan kedaluwarsa! (Anda bisa menggunakan --force jika Anda tahu apa yang Anda lakukan)",
"extracting": "Mengekstrak...",
"system_username_exists": "Nama pengguna telah ada di daftar pengguna sistem",
"upgrade_complete": "Pembaruan selesai",
"upgrading_packages": "Memperbarui paket...",
"diagnosis_description_apps": "Aplikasi",
"diagnosis_description_basesystem": "Basis sistem",
"global_settings_setting_pop3_enabled_help": "Aktifkan protokol POP3 untuk peladen surel",
"password_confirmation_not_the_same": "Kata sandi dan untuk konfirmasinya tidak sama",
"restore_complete": "Pemulihan selesai",
"user_import_success": "Pengguna berhasil diimpor",
"user_updated": "Informasi pengguna diubah",
"visitors": "Pengunjung",
"yunohost_already_installed": "YunoHost sudah terpasang",
"yunohost_installing": "Memasang YunoHost...",
"yunohost_not_installed": "YunoHost tidak terpasang dengan benar. Jalankan 'yunohost tools postinstall'",
"restore_removing_tmp_dir_failed": "Tidak dapat menghapus direktori sementara yang dulu",
"app_sources_fetch_failed": "Tidak dapat mengambil berkas sumber, apakah URL-nya benar?",
"installation_complete": "Pemasangan selesai",
"app_arch_not_supported": "Aplikasi ini hanya bisa dipasang pada arsitektur {required}, tapi arsitektur peladen Anda adalah {current}",
"diagnosis_basesystem_hardware_model": "Model peladen adalah {model}",
"app_yunohost_version_not_supported": "Aplikasi ini memerlukan YunoHost >= {required}, tapi versi yang terpasang adalah {current}",
"ask_new_path": "Jalur baru",
"backup_cleaning_failed": "Tidak dapat menghapus folder cadangan sementara",
"diagnosis_description_mail": "Surel",
"diagnosis_description_regenconf": "Konfigurasi sistem",
"diagnosis_display_tip": "Untuk melihat masalah yang ditemukan, Anda bisa ke bagian Diagnosis di administrasi web atau jalankan 'yunohost diagnosis show --issues --human-readable'.",
"diagnosis_domain_expiration_success": "Domain Anda sudah terdaftar dan belum akan kedaluwarsa dalam waktu dekat.",
"diagnosis_failed": "Gagal mengambil hasil diagnosis untuk kategori '{category}': {error}",
"global_settings_setting_portal_theme": "Tema portal",
"global_settings_setting_portal_theme_help": "Informasi lebih lanjut tentang tema portal kustom ada di https://yunohost.org/theming",
"global_settings_setting_ssh_password_authentication": "Autentikasi kata sandi",
"certmanager_attempt_to_renew_nonLE_cert": "Sertifikat untuk domain '{domain}' tidak diterbitkan oleh Let's Encrypt. Tidak dapat memperbarui secara otomatis!",
"certmanager_cert_install_failed": "Pemasangan sertifikat Let's Encrypt gagal untuk {domains}",
"certmanager_cert_install_failed_selfsigned": "Pemasangan sertifikat ditandai sendiri (self-signed) gagal untuk {domains}",
"config_validate_email": "Harus surel yang valid",
"config_apply_failed": "Gagal menerapkan konfigurasi baru: {error}",
"diagnosis_basesystem_ynh_main_version": "Peladen memakai YunoHost {main_version} ({repo})",
"diagnosis_cant_run_because_of_dep": "Tidak dapat menjalankan diagnosis untuk {category} ketika ada masalah utama yang terkait dengan {dep}.",
"diagnosis_services_conf_broken": "Konfigurasi rusak untuk layanan {service}!",
"diagnosis_services_running": "Layanan {service} berjalan!",
"diagnosis_swap_ok": "Sistem ini memiliki {total} swap!",
"downloading": "Mengunduh...",
"pattern_password": "Harus paling tidak 3 karakter",
"pattern_password_app": "Maaf, kata sandi tidak dapat mengandung karakter berikut: {forbidden_chars}",
"pattern_port_or_range": "Harus angka porta yang valid (cth. 0-65535) atau jangkauan porta (cth. 100:200)",
"permission_already_exist": "Izin '{permission}' sudah ada",
"permission_cant_add_to_all_users": "Izin '{permission}' tidak dapat ditambahkan ke semua pengguna.",
"permission_created": "Izin '{permission}' dibuat",
"permission_creation_failed": "Tidak dapat membuat izin '{permission}': {error}",
"permission_deleted": "Izin '{permission}' dihapus",
"service_description_mysql": "Menyimpan data aplikasi (basis data SQL)",
"mailbox_disabled": "Surel dimatikan untuk pengguna {user}",
"log_user_update": "Memperbarui informasi untuk pengguna '{}'",
"apps_catalog_obsolete_cache": "Tembolok katalog aplikasi kosong atau sudah tua.",
"backup_actually_backuping": "Membuat arsip cadangan dari berkas yang dikumpulkan...",
"backup_applying_method_copy": "Menyalin semua berkas ke cadangan...",
"backup_archive_app_not_found": "Tidak dapat menemukan {app} di arsip cadangan",
"config_validate_date": "Harus tanggal yang valid seperti format YYYY-MM-DD",
"config_validate_time": "Harus waktu yang valid seperti HH:MM",
"diagnosis_ip_connected_ipv6": "Peladen ini terhubung ke internet lewat IPv6!",
"diagnosis_services_bad_status": "Layanan {service} {status} :(",
"global_settings_setting_root_password": "Kata sandi root baru",
"log_app_action_run": "Menjalankan tindakan dari aplikasi '{}'",
"log_settings_reset_all": "Atur ulang semua pengaturan",
"log_settings_set": "Terapkan pengaturan",
"service_removed": "Layanan '{service}' dihapus",
"service_restart_failed": "Tidak dapat memulai ulang layanan '{service}'\n\nLog layanan baru-baru ini:{logs}",
"ssowat_conf_generated": "Konfigurasi SSOwat diperbarui",
"system_upgraded": "Sistem diperbarui",
"tools_upgrade": "Memperbarui paket sistem",
"upnp_dev_not_found": "Tidak ada perangkat UPnP yang ditemukan",
"upnp_enabled": "UPnP dinyalakan",
"upnp_port_open_failed": "Tidak dapat membuka porta lewat UPnP",
"app_change_url_failed": "Tidak dapat mengubah URL untuk {app}: {error}",
"app_restore_script_failed": "Galat terjadi di skrip pemulihan aplikasi",
"app_label_deprecated": "Perintah ini sudah usang! Silakan untuk menggunakan perintah baru 'yunohost user permission update' untuk mengelola label aplikasi.",
"app_make_default_location_already_used": "Tidak dapat membuat '{app}' menjadi aplikasi baku untuk domain, '{domain}' telah dipakai oleh '{other_app}'",
"app_manifest_install_ask_is_public": "Bolehkan aplikasi ini dibuka untuk pengunjung awanama?",
"upnp_disabled": "UPnP dimatikan",
"global_settings_setting_smtp_allow_ipv6_help": "Perbolehkan penggunaan IPv6 untuk menerima dan mengirim surel",
"domain_config_default_app": "Aplikasi baku",
"diagnosis_diskusage_verylow": "Penyimpanan <code>{mountpoint}</code> (di perangkat <code>{device}</code>) hanya tinggal memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total}). Direkomendasikan untuk membersihkan ruang penyimpanan!",
"domain_config_api_protocol": "Protokol API",
"domain_config_cert_summary_letsencrypt": "Bagus! Anda menggunakan sertifikat Let's Encrypt yang valid!",
"domain_config_mail_out": "Surel keluar",
"domain_deletion_failed": "Tidak dapat menghapus domain {domain}: {error}",
"backup_copying_to_organize_the_archive": "Menyalin {size}MB untuk menyusun arsip",
"backup_method_copy_finished": "Salinan cadangan telah selesai",
"certmanager_domain_cert_not_selfsigned": "Sertifikat untuk domain {domain} bukan disertifikasi sendiri. Apakah Anda yakin ingin mengubahnya? (Gunakan '--force' jika iya)",
"diagnosis_diskusage_ok": "Penyimpanan <code>{mountpoint}</code> (di perangkat <code>{device}</code>) masih memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total})!",
"diagnosis_http_nginx_conf_not_up_to_date": "Konfigurasi nginx domain ini sepertinya diubah secara manual, itu mencegah YunoHost untuk mendiagnosis apakah domain ini terhubung ke HTTP.",
"domain_created": "Domain dibuat",
"migrations_running_forward": "Menjalankan migrasi {id}...",
"permission_deletion_failed": "Tidak dapat menghapus izin '{permission}': {error}",
"domain_config_cert_no_checks": "Abaikan pemeriksaan diagnosis",
"domain_config_cert_renew": "Perbarui sertifikat Let's Encrypt",
"domain_config_cert_summary": "Status sertifikat",
"domain_config_cert_summary_expired": "PENTING: Sertifikat saat ini tidak valid! HTTPS tidak akan bekerja sama sekali!",
"port_already_opened": "Porta {port} telah dibuka untuk koneksi {ip_version}",
"migrations_success_forward": "Migrasi {id} selesai",
"not_enough_disk_space": "Ruang kosong tidak cukup di '{path}'",
"password_too_long": "Pilih kata sandi yang lebih pendek dari 127 karakter",
"regenconf_file_backed_up": "Berkas konfigurasi '{conf}' dicadangkan ke '{backup}'",
"domain_creation_failed": "Tidak dapat membuat domain {domain}: {error}",
"domain_deleted": "Domain dihapus",
"regex_with_only_domain": "Anda tidak dapat menggunakan regex untuk domain, hanya untuk jalur",
"diagnosis_diskusage_low": "Penyimpanan <code>{mountpoint}</code> (di perangkat <code>{device}</code>) hanya tinggal memiliki {free} ({free_percent}%) ruang kosong yang tersedia (dari {total}).",
"domain_config_cert_summary_ok": "Oke, sertifikat saat ini terlihat bagus!",
"app_failed_to_upgrade_but_continue": "Gagal memperbarui aplikasi {failed_app}, melanjutkan pembaruan berikutnya seperti yang diminta. Jalankan 'yunohost log show {operation_logger_name}' untuk melihat log kegagalan",
"certmanager_attempt_to_replace_valid_cert": "Anda sedang mencoba untuk menimpa sertifikat yang valid untuk domain {domain}! (Gunakan --force untuk melewati ini)",
"permission_protected": "Izin {permission} dilindungi. Anda tidak dapat menambahkan atau menghapus kelompok pengunjung ke/dari izin ini.",
"permission_require_account": "Izin {permission} hanya masuk akal untuk pengguna yang memiliki akun, maka ini tidak dapat diaktifkan untuk pengunjung.",
"permission_update_failed": "Tidak dapat memperbarui izin '{permission}': {error}",
"apps_failed_to_upgrade": "Aplikasi berikut gagal untuk diperbarui:{apps}",
"backup_archive_name_unknown": "Arsip cadangan lokal tidak diketahui yang bernama '{name}'",
"diagnosis_http_nginx_conf_not_up_to_date_details": "Untuk memperbaiki ini, periksa perbedaannya dari CLI menggunakan <cmd>yunohost tools regen-conf nginx --dry-run --with-diff</cmd> dan jika Anda sudah, terapkan perubahannya menggunakan <cmd>yunohost tools regen-conf nginx --force</cmd>.",
"domain_config_auth_token": "Token autentikasi",
"domain_config_cert_install": "Pasang sertifikat Let's Encrypt",
"domain_config_cert_summary_abouttoexpire": "Sertifikat saat ini akan kedaluwarsa. Akan secara otomatis diperbarui secepatnya.",
"domain_config_mail_in": "Surel datang",
"password_too_simple_1": "Panjang kata sandi harus paling tidak 8 karakter",
"password_too_simple_2": "Panjang kata sandi harus paling tidak 8 karakter dan mengandung digit, huruf kapital, dan huruf kecil",
"password_too_simple_3": "Panjang kata sandi harus paling tidak 8 karakter dan mengandung digit, huruf kapital, huruf kecil, dan karakter khusus",
"password_too_simple_4": "Panjang kata sandi harus paling tidak 12 karakter dan mengandung digit, huruf kapital, huruf kecil, dan karakter khusus",
"port_already_closed": "Porta {port} telah ditutup untuk koneksi {ip_version}",
"service_description_yunomdns": "Membuat Anda bisa menemukan peladen Anda menggunakan 'yunohost.local' di jaringan lokal Anda",
"regenconf_file_copy_failed": "Tidak dapat menyalin berkas konfigurasi baru '{new}' ke '{conf}'",
"regenconf_file_kept_back": "Berkas konfigurasi '{conf}' seharusnya dihapus oleh regen-conf (kategori {category}) tapi tidak jadi.",
"regenconf_file_manually_modified": "Berkas konfigurasi '{conf}' telah diubah secara manual dan tidak akan diperbarui",
"regenconf_file_manually_removed": "Berkas konfigurasi '{conf}' telah dihapus secara manual dan tidak akan dibikin",
"regenconf_file_remove_failed": "Tidak dapat menghapus berkas konfigurasi '{conf}'",
"regenconf_file_removed": "Berkas konfigurasi '{conf}' dihapus",
"regenconf_file_updated": "Berkas konfigurasi '{conf}' diperbarui",
"regenconf_now_managed_by_yunohost": "Berkas konfigurasi '{conf}' sekarang dikelola oleh YunoHost (kategori {category})",
"regenconf_updated": "Konfigurasi diperbarui untuk '{category}'",
"log_user_group_delete": "Menghapus kelompok '{}'",
"backup_archive_cant_retrieve_info_json": "Tidak dapat memuat info untuk arsip '{archive}'... Berkas info.json tidak dapat didapakan (atau bukan json yang valid).",
"diagnosis_mail_blacklist_reason": "Alasan pendaftarhitaman adalah: {reason}",
"diagnosis_ports_unreachable": "Porta {port} tidak tercapai dari luar.",
"diagnosis_ram_verylow": "Sistem hanya memiliki {available} ({available_percent}%) RAM yang tersedia! (dari {total})",
"diagnosis_regenconf_allgood": "Semua berkas konfigurasi sesuai dengan rekomendasi konfigurasi!",
"diagnosis_security_vulnerable_to_meltdown": "Sepertinya sistem Anda rentan terhadap kerentanan keamanan Meltdown",
"diagnosis_security_vulnerable_to_meltdown_details": "Untuk memperbaiki ini, sebaiknya perbarui sistem Anda dan mulai ulang untuk memuat kernel linux yang baru (atau hubungi penyedia peladen Anda jika itu tidak bekerja). Kunjungi https://meltdownattack.com/ untuk informasi lebih lanjut.",
"domain_exists": "Domain telah ada",
"domain_uninstall_app_first": "Aplikasi berikut masih terpasang di domain Anda:\n{apps}\n\nSilakan lepas mereka menggunakan 'yunohost app remove id_aplikasi' atau pindahkan ke domain lain menggunakan 'yunohost app change-url id_aplikasi' sebelum melanjutkan ke penghapusan domain",
"group_creation_failed": "Tidak dapat membuat kelompok '{group}': {error}",
"group_deleted": "Kelompok '{group}' dihapus",
"log_letsencrypt_cert_install": "Memasang sertifikat Let's Encrypt di domain '{}'",
"log_permission_create": "Membuat izin '{}'",
"log_permission_delete": "Menghapus izin '{}'",
"backup_with_no_backup_script_for_app": "Aplikasi '{app}' tidak memiliki skrip pencadangan. Mengabaikan.",
"backup_system_part_failed": "Tidak dapat mencadangkan bagian '{part}' sistem",
"log_user_create": "Menambahkan pengguna '{}'",
"log_user_delete": "Menghapus pengguna '{}'",
"log_user_group_create": "Membuat kelompok '{}'",
"log_user_group_update": "Memperbarui kelompok '{}'",
"log_user_permission_update": "Memperbarui akses untuk izin '{}'",
"mail_alias_remove_failed": "Tidak dapat menghapus alias surel '{mail}'",
"diagnosis_mail_blacklist_ok": "IP dan domain yang digunakan oleh peladen ini sepertinya tidak didaftarhitamkan",
"diagnosis_dns_point_to_doc": "Silakan periksa dokumentasi di <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> jika Anda masih membutuhkan bantuan untuk mengatur rekaman DNS.",
"diagnosis_regenconf_manually_modified": "Berkas konfigurasi <code>{file}</code> sepertinya telah diubah manual.",
"backup_with_no_restore_script_for_app": "{app} tidak memiliki skrip pemulihan, Anda tidak akan bisa secara otomatis memulihkan cadangan aplikasi ini.",
"config_no_panel": "Tidak dapat menemukan panel konfigurasi.",
"confirm_app_install_warning": "Peringatan: Aplikasi ini mungkin masih bisa bekerja, tapi tidak terintegrasi dengan baik dengan YunoHost. Beberapa fitur seperti SSO dan pencadangan mungkin tidak tersedia. Tetap pasang? [{answers}] ",
"diagnosis_ports_ok": "Porta {port} tercapai dari luar.",
"diagnosis_ports_partially_unreachable": "Porta {port} tidak tercapai dari luar lewat IPv{failed}.",
"domain_remove_confirm_apps_removal": "Menghapus domain ini akan melepas aplikasi berikut:\n{apps}\n\nApakah Anda yakin? [{answers}]",
"domains_available": "Domain yang tersedia:",
"global_settings_reset_success": "Atur ulang pengaturan global",
"group_created": "Kelompok '{group}' dibuat",
"group_deletion_failed": "Tidak dapat menghapus kelompok '{group}': {error}",
"group_updated": "Kelompok '{group}' diperbarui",
"invalid_credentials": "Nama pengguna atau kata sandi salah",
"log_letsencrypt_cert_renew": "Memperbarui sertifikat Let's Encrypt '{}'",
"log_selfsigned_cert_install": "Memasang sertifikat ditandai sendiri pada domain '{}'",
"log_user_permission_reset": "Mengatur ulang izin '{}'",
"domain_config_xmpp": "Pesan Langsung (XMPP)"
}

View file

@ -379,7 +379,7 @@
"diagnosis_services_bad_status": "Lo servici {service} es {status} :(",
"diagnosis_swap_ok": "Lo sistèma a {total} descambi !",
"diagnosis_regenconf_allgood": "Totes los fichièrs de configuracion son confòrmes a la configuracion recomandada !",
"diagnosis_regenconf_manually_modified": "Lo fichièr de configuracion {file} foguèt modificat manualament.",
"diagnosis_regenconf_manually_modified": "Lo fichièr de configuracion <code>{file}</code> foguèt modificat manualament.",
"diagnosis_regenconf_manually_modified_details": "Es probablament bon tan que sabètz çò que fasètz ;) !",
"diagnosis_security_vulnerable_to_meltdown": "Semblatz èsser vulnerable a la vulnerabilitat de seguretat critica de Meltdown",
"diagnosis_description_basesystem": "Sistèma de basa",

View file

@ -80,7 +80,7 @@
"app_already_installed_cant_change_url": "Ta aplikacja jest już zainstalowana. URL nie może zostać zmieniony przy użyciu tej funkcji. Sprawdź czy można zmienić w `app changeurl`",
"app_id_invalid": "Nieprawidłowy identyfikator aplikacji(ID)",
"app_change_url_require_full_domain": "Nie można przenieść aplikacji {app} na nowy adres URL, ponieważ wymaga ona pełnej domeny (tj. ze ścieżką = /)",
"app_install_files_invalid": "Tych plików nie można zainstalować",
"app_install_files_invalid": "Te pliki nie mogą zostać zainstalowane.",
"app_make_default_location_already_used": "Nie można ustawić '{app}' jako domyślnej aplikacji w domenie '{domain}' ponieważ jest już używana przez '{other_app}'",
"app_change_url_identical_domains": "Stara i nowa domena/ścieżka_url są identyczne („{domain}{path}”), nic nie trzeba robić.",
"app_config_unable_to_read": "Nie udało się odczytać wartości panelu konfiguracji.",
@ -136,7 +136,7 @@
"backup_archive_corrupted": "Wygląda na to, że archiwum kopii zapasowej '{archive}' jest uszkodzone: {error}",
"backup_cleaning_failed": "Nie udało się wyczyścić folderu tymczasowej kopii zapasowej",
"backup_create_size_estimation": "Archiwum będzie zawierać około {size} danych.",
"app_location_unavailable": "Ten adres URL jest niedostępny lub powoduje konflikt z już zainstalowanymi aplikacja(mi):\n{apps}",
"app_location_unavailable": "Ten adres URL jest niedostępny lub koliduje z już zainstalowanymi aplikacjami:\n{apps}",
"app_restore_failed": "Nie można przywrócić {app}: {error}",
"app_restore_script_failed": "Wystąpił błąd w skrypcie przywracania aplikacji",
"app_full_domain_unavailable": "Przepraszamy, ta aplikacja musi być zainstalowana we własnej domenie, ale inna aplikacja jest już zainstalowana w tej domenie „{domain}”. Zamiast tego możesz użyć subdomeny dedykowanej tej aplikacji.",
@ -155,5 +155,64 @@
"app_manifest_install_ask_init_main_permission": "Kto powinien mieć dostęp do tej aplikacji? (Można to później zmienić)",
"ask_admin_fullname": "Pełne imię i nazwisko administratora",
"app_change_url_failed": "Nie udało się zmienić adresu URL aplikacji {app}: {error}",
"app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL"
}
"app_change_url_script_failed": "Wystąpił błąd w skrypcie zmiany adresu URL",
"app_failed_to_upgrade_but_continue": "Nie udało zaktualizować się aplikacji {failed_app}, przechodzenie do następnych aktualizacji według żądania. Uruchom komendę 'yunohost log show {operation_logger_name}', aby sprawdzić logi dotyczące błędów",
"certmanager_cert_signing_failed": "Nie udało się zarejestrować nowego certyfikatu",
"certmanager_cert_renew_success": "Pomyślne odnowienie certyfikatu Let's Encrypt dla domeny '{domain}'",
"backup_delete_error": "Nie udało się usunąć '{path}'",
"certmanager_attempt_to_renew_nonLE_cert": "Certyfikat dla domeny '{domain}' nie został wystawiony przez Let's Encrypt. Automatyczne odnowienie jest niemożliwe!",
"backup_archive_cant_retrieve_info_json": "Nieudane wczytanie informacji dla archiwum '{archive}'... Plik info.json nie może zostać odzyskany (lub jest niepoprawny).",
"backup_method_custom_finished": "Tworzenie kopii zapasowej według własnej metody '{method}' zakończone",
"backup_nothings_done": "Brak danych do zapisania",
"app_unsupported_remote_type": "Niewspierany typ zdalny użyty w aplikacji",
"backup_archive_name_unknown": "Nieznane, lokalne archiwum kopii zapasowej o nazwie '{name}'",
"backup_output_directory_not_empty": "Należy wybrać pusty katalog dla danych wyjściowych",
"certmanager_attempt_to_renew_valid_cert": "Certyfikat dla domeny '{domain}' nie jest bliski wygaśnięciu! (Możesz użyć komendy z dopiskiem --force jeśli wiesz co robisz)",
"certmanager_cert_install_success": "Pomyślna instalacja certyfikatu Let's Encrypt dla domeny '{domain}'",
"certmanager_attempt_to_replace_valid_cert": "Właśnie zamierzasz nadpisać dobry i poprawny certyfikat dla domeny '{domain}'! (Użyj komendy z dopiskiem --force, aby ominąć)",
"backup_method_copy_finished": "Zakończono tworzenie kopii zapasowej",
"certmanager_certificate_fetching_or_enabling_failed": "Próba użycia nowego certyfikatu dla {domain} zakończyła się niepowodzeniem...",
"backup_method_tar_finished": "Utworzono archiwum kopii zapasowej TAR",
"backup_mount_archive_for_restore": "Przygotowywanie archiwum do przywrócenia...",
"certmanager_cert_install_failed": "Nieudana instalacja certyfikatu Let's Encrypt dla {domains}",
"certmanager_cert_install_failed_selfsigned": "Nieudana instalacja certyfikatu self-signed dla {domains}",
"certmanager_cert_install_success_selfsigned": "Pomyślna instalacja certyfikatu self-signed dla domeny '{domain}'",
"certmanager_cert_renew_failed": "Nieudane odnowienie certyfikatu Let's Encrypt dla {domains}",
"apps_failed_to_upgrade": "Nieudana aktualizacja aplikacji: {apps}",
"backup_output_directory_required": "Musisz wybrać katalog dla kopii zapasowej",
"app_failed_to_download_asset": "Nie udało się pobrać zasobu '{source_id}' ({url}) dla {app}: {out}",
"backup_with_no_backup_script_for_app": "Aplikacja '{app}' nie posiada skryptu kopii zapasowej. Ignorowanie.",
"backup_with_no_restore_script_for_app": "Aplikacja {app} nie posiada skryptu przywracania, co oznacza, że nie będzie można automatycznie przywrócić kopii zapasowej tej aplikacji.",
"certmanager_acme_not_configured_for_domain": "Wyzwanie ACME nie może zostać uruchomione dla domeny {domain}, ponieważ jej konfiguracja nginx nie zawiera odpowiedniego fragmentu kodu... Upewnij się, że konfiguracja nginx jest aktualna, używając polecenia yunohost tools regen-conf nginx --dry-run --with-diff.",
"certmanager_domain_dns_ip_differs_from_public_ip": "Rekordy DNS dla domeny '{domain}' różnią się od adresu IP tego serwera. Sprawdź kategorię 'Rekordy DNS' (podstawowe) w diagnozie, aby uzyskać więcej informacji. Jeśli niedawno dokonałeś zmiany rekordu A, poczekaj, aż zostanie on zaktualizowany (można skorzystać z narzędzi online do sprawdzania propagacji DNS). (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)",
"confirm_app_install_danger": "UWAGA! Ta aplikacja jest wciąż w fazie eksperymentalnej (jeśli nie działa jawnie)! Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}",
"confirm_app_install_thirdparty": "UWAGA! Ta aplikacja nie jest częścią katalogu aplikacji YunoHost. Instalowanie aplikacji innych firm może naruszyć integralność i bezpieczeństwo systemu. Prawdopodobnie NIE powinieneś jej instalować, chyba że wiesz, co robisz. NIE ZOSTANIE udzielone wsparcie, jeśli ta aplikacja nie będzie działać poprawnie lub spowoduje uszkodzenie systemu... Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'",
"config_apply_failed": "Nie udało się zastosować nowej konfiguracji: {error}",
"config_cant_set_value_on_section": "Nie można ustawić pojedynczej wartości dla całej sekcji konfiguracji.",
"config_no_panel": "Nie znaleziono panelu konfiguracji.",
"config_unknown_filter_key": "Klucz filtru '{filter_key}' jest niepoprawny.",
"config_validate_email": "Proszę podać poprawny adres e-mail",
"backup_hook_unknown": "Nieznany jest hook kopii zapasowej '{hook}'.",
"backup_no_uncompress_archive_dir": "Nie istnieje taki katalog nieskompresowanego archiwum.",
"backup_output_symlink_dir_broken": "Twój katalog archiwum '{path}' to uszkodzony dowiązanie symboliczne. Być może zapomniałeś o ponownym zamontowaniu lub podłączeniu nośnika przechowującego, do którego on wskazuje.",
"backup_system_part_failed": "Nie można wykonać kopii zapasowej części systemu '{part}'",
"config_validate_color": "Powinien być poprawnym szesnastkowym kodem koloru RGB.",
"config_validate_date": "Data powinna być poprawna w formacie RRRR-MM-DD",
"config_validate_time": "Podaj poprawny czas w formacie GG:MM",
"certmanager_domain_not_diagnosed_yet": "Nie ma jeszcze wyników diagnozy dla domeny {domain}. Proszę ponownie uruchomić diagnozę dla kategorii 'Rekordy DNS' i 'Strona internetowa' w sekcji diagnozy, aby sprawdzić, czy domena jest gotowa do użycia Let's Encrypt. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)",
"certmanager_cannot_read_cert": "Wystąpił problem podczas próby otwarcia bieżącego certyfikatu dla domeny {domain} (plik: {file}), przyczyna: {reason}",
"certmanager_no_cert_file": "Nie można odczytać pliku certyfikatu dla domeny {domain} (plik: {file}).",
"certmanager_self_ca_conf_file_not_found": "Nie można znaleźć pliku konfiguracyjnego dla autorytetu samopodpisującego (plik: {file})",
"backup_running_hooks": "Uruchamianie hooków kopii zapasowej...",
"backup_permission": "Uprawnienia kopii zapasowej dla aplikacji {app}",
"certmanager_domain_cert_not_selfsigned": "Certyfikat dla domeny {domain} nie jest samopodpisany. Czy na pewno chcesz go zastąpić? (Użyj opcji '--force', aby to zrobić.)",
"config_action_disabled": "Nie można uruchomić akcji '{action}', ponieważ jest ona wyłączona. Upewnij się, że spełnione są jej ograniczenia. Pomoc: {help}",
"config_action_failed": "Nie udało się uruchomić akcji '{action}': {error}",
"config_forbidden_readonly_type": "Typ '{type}' nie może być ustawiony jako tylko do odczytu. Użyj innego typu, aby wyświetlić tę wartość (odpowiednie ID argumentu: '{id}')",
"config_forbidden_keyword": "Słowo kluczowe '{keyword}' jest zastrzeżone. Nie można tworzyć ani używać panelu konfiguracji z pytaniem o tym identyfikatorze.",
"backup_output_directory_forbidden": "Wybierz inną ścieżkę docelową. Kopie zapasowe nie mogą być tworzone w podfolderach /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ani /home/yunohost.backup/archives",
"confirm_app_insufficient_ram": "UWAGA! Ta aplikacja wymaga {required} pamięci RAM do zainstalowania/aktualizacji, a obecnie dostępne jest tylko {current}. Nawet jeśli aplikacja mogłaby działać, proces instalacji/aktualizacji wymaga dużej ilości pamięci RAM, więc serwer może się zawiesić i niepowodzenie może być katastrofalne. Jeśli mimo to jesteś gotów podjąć to ryzyko, wpisz '{answers}'",
"app_not_upgraded_broken_system": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu. W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}",
"app_not_upgraded_broken_system_continue": "Aplikacja '{failed_app}' nie powiodła się w procesie aktualizacji i spowodowała uszkodzenie systemu (parametr --continue-on-failure jest ignorowany). W rezultacie anulowane zostały aktualizacje następujących aplikacji: {apps}",
"certmanager_domain_http_not_working": "Domena {domain} wydaje się niedostępna przez HTTP. Sprawdź kategorię 'Strona internetowa' diagnostyki, aby uzyskać więcej informacji. (Jeśli wiesz, co robisz, użyj opcji '--no-checks', aby wyłączyć te sprawdzania.)"
}

View file

@ -325,5 +325,8 @@
"global_settings_setting_ssh_port": "SSH порт",
"global_settings_setting_webadmin_allowlist_help": "IP-адреса, разрешенные для доступа к веб-интерфейсу администратора. Разделенные запятыми.",
"global_settings_setting_webadmin_allowlist_enabled_help": "Разрешите доступ к веб-интерфейсу администратора только некоторым IP-адресам.",
"global_settings_setting_smtp_allow_ipv6_help": "Разрешить использование IPv6 для получения и отправки почты"
}
"global_settings_setting_smtp_allow_ipv6_help": "Разрешить использование IPv6 для получения и отправки почты",
"admins": "Администраторы",
"all_users": "Все пользователи YunoHost",
"app_action_failed": "Не удалось выполнить действие {action} для приложения {app}"
}

View file

@ -248,5 +248,16 @@
"ask_fullname": "Celé meno",
"all_users": "Všetci používatelia YunoHost",
"app_manifest_install_ask_init_main_permission": "Kto má mať prístup k tejto aplikácii? (Nastavenie môžete neskôr zmeniť)",
"certmanager_cert_install_failed": "Inštalácia Let's Encrypt certifikátu pre {domains} skončila s chybou"
}
"certmanager_cert_install_failed": "Inštalácia Let's Encrypt certifikátu pre {domains} skončila s chybou",
"app_arch_not_supported": "Túto aplikáciu možno nainštalovať iba na architektúrach {required}, ale Váš server beží na architektúre {current}",
"log_help_to_get_failed_log": "Akciu '{desc}' sa nepodarilo dokončiť. Ak potrebujete pomoc, zdieľajte, prosím, úplný záznam tejto operácie pomocou príkazu 'yunohost log share {name}'",
"operation_interrupted": "Bola akcia manuálne prerušená?",
"log_link_to_failed_log": "Akciu '{desc}' sa nepodarilo dokončiť. Ak potrebujete pomoc, poskytnite, prosím, úplný záznam tejto operácie <a href=\"#/tools/logs/{name}\">kliknutím sem</a>",
"app_change_url_failed": "Nepodarilo sa zmeniť URL adresu aplikácie {app}: {error}",
"app_yunohost_version_not_supported": "Táto aplikácia vyžaduje YunoHost >= {required}, ale aktuálne nainštalovaná verzia je {current}",
"config_action_failed": "Nepodarilo sa spustiť operáciu '{action}': {error}",
"app_change_url_script_failed": "Vo skripte na zmenu URL adresy sa vyskytla chyba",
"app_not_enough_disk": "Táto aplikácia vyžaduje {required} voľného miesta.",
"app_not_enough_ram": "Táto aplikácia vyžaduje {required} pamäte na inštaláciu/aktualizáciu, ale k dispozícii je momentálne iba {current}.",
"apps_failed_to_upgrade": "Nasledovné aplikácie nebolo možné aktualizovať: {apps}"
}

View file

@ -234,7 +234,7 @@
"group_already_exist_on_system": "Група {group} вже існує в групах системи",
"group_already_exist": "Група {group} вже існує",
"good_practices_about_user_password": "Зараз ви збираєтеся поставити новий пароль користувача. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).",
"good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адмініструванні. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольного гасла) і/або використання різних символів (великих, малих, цифр і спеціальних символів).",
"good_practices_about_admin_password": "Зараз ви збираєтеся поставити новий пароль адміністрування. Пароль повинен складатися не менше ніж з 8 символів, але хорошою практикою є використання більш довгого пароля (тобто парольної фрази) і/або використання різних символів (великих, малих, цифр і спеціальних символів).",
"global_settings_setting_smtp_relay_password": "Пароль SMTP-ретрансляції",
"global_settings_setting_smtp_relay_user": "Користувач SMTP-ретрансляції",
"global_settings_setting_smtp_relay_port": "Порт SMTP-ретрансляції",
@ -278,7 +278,7 @@
"domain_cannot_remove_main": "Ви не можете вилучити '{domain}', бо це основний домен, спочатку вам потрібно встановити інший домен в якості основного за допомогою 'yunohost domain main-domain -n <inshyi-domen>'; ось список доменів-кандидатів: {other_domains}",
"disk_space_not_sufficient_update": "Недостатньо місця на диску для оновлення цього застосунку",
"disk_space_not_sufficient_install": "Недостатньо місця на диску для встановлення цього застосунку",
"diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду <cmd>yunohost settings set security.ssh.ssh port -v YOUR_SSH_PORT</cmd>, щоб визначити порт SSH, і перевірте<cmd>yunohost tools regen-conf ssh --dry-run --with-diff</cmd> і <cmd>yunohost tools regen-conf ssh --force</cmd>, щоб скинути ваш конфіг на рекомендований YunoHost.",
"diagnosis_sshd_config_inconsistent_details": "Будь ласка, виконайте команду <cmd>yunohost settings set security.ssh.ssh port -v ВАШ_SSH_ПОРТ</cmd>, щоб визначити порт SSH, і перевірте<cmd>yunohost tools regen-conf ssh --dry-run --with-diff</cmd> і <cmd>yunohost tools regen-conf ssh --force</cmd>, щоб скинути ваш конфіг на рекомендований YunoHost.",
"diagnosis_sshd_config_inconsistent": "Схоже, що порт SSH був уручну змінений в /etc/ssh/sshd_config. Починаючи з версії YunoHost 4.2, доступний новий глобальний параметр 'security.ssh.ssh port', що дозволяє уникнути ручного редагування конфігурації.",
"diagnosis_sshd_config_insecure": "Схоже, що конфігурація SSH була змінена вручну і є небезпечною, оскільки не містить директив 'AllowGroups' або 'AllowUsers' для обмеження доступу авторизованих користувачів.",
"diagnosis_processes_killed_by_oom_reaper": "Деякі процеси було недавно вбито системою через брак пам'яті. Зазвичай це є симптомом нестачі пам'яті в системі або процесу, який з'їв дуже багато пам'яті. Зведення убитих процесів:\n{kills_summary}",
@ -346,7 +346,7 @@
"diagnosis_mail_outgoing_port_25_blocked_details": "Спочатку спробуйте розблокувати вихідний порт 25 в інтерфейсі вашого інтернет-маршрутизатора або в інтерфейсі вашого хостинг-провайдера. (Деякі хостинг-провайдери можуть вимагати, щоб ви відправили їм заявку в службу підтримки).",
"diagnosis_mail_outgoing_port_25_blocked": "Поштовий сервер SMTP не може відправляти електронні листи на інші сервери, оскільки вихідний порт 25 заблоковано в IPv{ipversion}.",
"app_manifest_install_ask_path": "Оберіть шлях URL (після домену), за яким має бути встановлено цей застосунок",
"yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - додавання першого користувача через розділ 'Користувачі' вебадмініструванні (або 'yunohost user create <username>' в командному рядку);\n - діагностика можливих проблем через розділ 'Діагностика' вебадмініструванні (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.",
"yunohost_postinstall_end_tip": "Післявстановлення завершено! Щоб завершити доналаштування, будь ласка, розгляньте наступні варіанти:\n - діагностика можливих проблем через розділ 'Діагностика' вебадмініструванні (або 'yunohost diagnosis run' в командному рядку);\n - прочитання розділів 'Завершення встановлення' і 'Знайомство з YunoHost' у документації адміністратора: https://yunohost.org/admindoc.",
"yunohost_not_installed": "YunoHost установлений неправильно. Будь ласка, запустіть 'yunohost tools postinstall'",
"yunohost_installing": "Установлення YunoHost...",
"yunohost_configured": "YunoHost вже налаштовано",
@ -488,7 +488,7 @@
"backup_method_custom_finished": "Користувацький спосіб резервного копіювання '{method}' завершено",
"backup_method_copy_finished": "Резервне копіювання завершено",
"backup_hook_unknown": "Гачок (hook) резервного копіювання '{hook}' невідомий",
"backup_deleted": "Резервна копія видалена",
"backup_deleted": "Резервна копія '{name}' видалена",
"backup_delete_error": "Не вдалося видалити '{path}'",
"backup_custom_mount_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'монтування'",
"backup_custom_backup_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'резервне копіювання'",
@ -496,7 +496,7 @@
"backup_csv_addition_failed": "Не вдалося додати файли для резервного копіювання в CSV-файл",
"backup_creation_failed": "Не вдалося створити архів резервного копіювання",
"backup_create_size_estimation": "Архів буде містити близько {size} даних.",
"backup_created": "Резервна копія створена",
"backup_created": "Резервна копія '{name}' створена",
"backup_couldnt_bind": "Не вдалося зв'язати {src} з {dest}.",
"backup_copying_to_organize_the_archive": "Копіювання {size} МБ для організації архіву",
"backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання",
@ -654,7 +654,7 @@
"global_settings_setting_admin_strength": "Надійність пароля адміністратора",
"global_settings_setting_user_strength": "Надійність пароля користувача",
"global_settings_setting_postfix_compatibility_help": "Компроміс між сумісністю і безпекою для сервера Postfix. Впливає на шифри (і інші аспекти, пов'язані з безпекою)",
"global_settings_setting_ssh_compatibility_help": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою)",
"global_settings_setting_ssh_compatibility_help": "Компроміс між сумісністю і безпекою для SSH-сервера. Впливає на шифри (і інші аспекти, пов'язані з безпекою).",
"global_settings_setting_ssh_password_authentication_help": "Дозволити автентифікацію паролем для SSH",
"global_settings_setting_ssh_port": "SSH-порт",
"global_settings_setting_webadmin_allowlist_help": "IP-адреси, яким дозволений доступ до вебадмініструванні. Через кому.",
@ -735,5 +735,36 @@
"visitors": "Відвідувачі",
"password_confirmation_not_the_same": "Пароль і його підтвердження не збігаються",
"password_too_long": "Будь ласка, виберіть пароль коротший за 127 символів",
"pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)"
}
"pattern_fullname": "Має бути дійсне повне ім’я (принаймні 3 символи)",
"app_failed_to_upgrade_but_continue": "Застосунок {failed_app} не вдалося оновити, продовжуйте наступні оновлення відповідно до запиту. Запустіть 'yunohost log show {operation_logger_name}', щоб побачити журнал помилок",
"app_not_upgraded_broken_system": "Застосунок '{failed_app}' не зміг оновитися і перевів систему в неробочий стан, і як наслідок, оновлення наступних застосунків було скасовано: {apps}",
"app_not_upgraded_broken_system_continue": "Застосунок '{failed_app}' не зміг оновитися і перевів систему у неробочий стан (тому --continue-on-failure ігнорується), і як наслідок, оновлення наступних застосунків було скасовано: {apps}",
"confirm_app_insufficient_ram": "НЕБЕЗПЕКА! Цей застосунок вимагає {required} оперативної пам'яті для встановлення/оновлення, але зараз доступно лише {current}. Навіть якби цей застосунок можна було б запустити, процес його встановлення/оновлення вимагає великої кількості оперативної пам'яті, тому ваш сервер може зависнути і вийти з ладу. Якщо ви все одно готові піти на цей ризик, введіть '{answers}'",
"invalid_shell": "Недійсна оболонка: {shell}",
"domain_config_default_app_help": "Користувачі будуть автоматично перенаправлятися на цей застосунок при відкритті цього домену. Якщо застосунок не вказано, люди будуть перенаправлені на форму входу на портал користувача.",
"domain_config_xmpp_help": "Примітка: для ввімкнення деяких функцій XMPP потрібно оновити записи DNS та відновити сертифікат Lets Encrypt",
"global_settings_setting_dns_exposure_help": "Примітка: Це стосується лише рекомендованої конфігурації DNS і діагностичних перевірок. Це не впливає на конфігурацію системи.",
"global_settings_setting_passwordless_sudo": "Дозвіл адміністраторам використовувати \"sudo\" без повторного введення пароля",
"app_change_url_failed": "Не вдалося змінити url для {app}: {error}",
"app_change_url_require_full_domain": "{app} не може бути переміщено на цю нову URL-адресу, оскільки для цього потрібен повний домен (тобто зі шляхом = /)",
"app_change_url_script_failed": "Виникла помилка всередині скрипта зміни URL-адреси",
"app_yunohost_version_not_supported": "Для роботи застосунку потрібен YunoHost мінімум версії {required}, але поточна встановлена версія {current}",
"app_arch_not_supported": "Цей застосунок можна встановити лише на архітектурах {required}, але архітектура вашого сервера {current}",
"global_settings_setting_dns_exposure": "Версії IP, які слід враховувати при конфігурації та діагностиці DNS",
"domain_cannot_add_muc_upload": "Ви не можете додавати домени, що починаються на 'muc.'. Такі імена зарезервовані для багатокористувацького чату XMPP, інтегрованого в YunoHost.",
"confirm_notifications_read": "ПОПЕРЕДЖЕННЯ: Перш ніж продовжити, перевірте сповіщення застосунку вище, там можуть бути важливі повідомлення. [{answers}]",
"global_settings_setting_portal_theme": "Тема порталу",
"global_settings_setting_portal_theme_help": "Подробиці щодо створення користувацьких тем порталу на https://yunohost.org/theming",
"diagnosis_ip_no_ipv6_tip_important": "Зазвичай IPv6 має бути автоматично налаштований системою або вашим провайдером, якщо він доступний. В іншому випадку, можливо, вам доведеться налаштувати деякі речі вручну, як описано в документації тут: <a href='https://yunohost.org/#/ipv6'>https://yunohost.org/#/ipv6</a>.",
"app_not_enough_disk": "Цей застосунок вимагає {required} вільного місця.",
"app_not_enough_ram": "Для встановлення/оновлення цього застосунку потрібно {required} оперативної пам'яті, але наразі доступно лише {current}.",
"app_resource_failed": "Не вдалося надати, позбавити або оновити ресурси для {app}: {error}",
"apps_failed_to_upgrade": "Ці застосунки не вдалося оновити:{apps}",
"apps_failed_to_upgrade_line": "\n * {app_id} (щоб побачити відповідний журнал, виконайте 'yunohost log show {operation_logger_name}')",
"group_mailalias_add": "Псевдонім електронної пошти '{mail}' буде додано до групи '{group}'",
"group_mailalias_remove": "Псевдонім електронної пошти '{mail}' буде вилучено з групи '{group}'",
"group_user_add": "Користувача '{user}' буде додано до групи '{group}'",
"group_user_remove": "Користувача '{user}' буде вилучено з групи '{group}'",
"app_corrupt_source": "YunoHost зміг завантажити ресурс '{source_id}' ({url}) для {app}, але він не відповідає очікуваній контрольній сумі. Це може означати, що на вашому сервері стався тимчасовий збій мережі, АБО ресурс був якимось чином змінений висхідним супровідником (або зловмисником?), і пакувальникам YunoHost потрібно дослідити і оновити маніфест застосунку, щоб відобразити цю зміну.\n Очікувана контрольна сума sha256: {expected_sha256}\n Обчислена контрольна сума sha256: {computed_sha256}\n Розмір завантаженого файлу: {size}",
"app_failed_to_download_asset": "Не вдалося завантажити ресурс '{source_id}' ({url}) для {app}: {out}"
}

View file

@ -954,6 +954,12 @@ app:
help: Delete the key
action: store_true
### app_shell()
shell:
action_help: Open an interactive shell with the app environment already loaded
arguments:
app:
help: App ID
### app_register_url()
register-url:

View file

@ -501,6 +501,15 @@
[pointhq.auth_token]
type = "string"
redact = true
[porkbun]
[porkbun.auth_key]
type = "string"
redact = true
[porkbun.auth_secret]
type = "string"
redact = true
[powerdns]
[powerdns.auth_token]

View file

@ -48,11 +48,10 @@ from moulinette.utils.filesystem import (
chmod,
)
from yunohost.utils.config import (
ConfigPanel,
ask_questions_and_parse_answers,
DomainQuestion,
PathQuestion,
from yunohost.utils.configpanel import ConfigPanel, ask_questions_and_parse_answers
from yunohost.utils.form import (
DomainOption,
WebPathOption,
hydrate_questions_with_choices,
)
from yunohost.utils.i18n import _value_for_locale
@ -431,10 +430,10 @@ def app_change_url(operation_logger, app, domain, path):
# Normalize path and domain format
domain = DomainQuestion.normalize(domain)
old_domain = DomainQuestion.normalize(old_domain)
path = PathQuestion.normalize(path)
old_path = PathQuestion.normalize(old_path)
domain = DomainOption.normalize(domain)
old_domain = DomainOption.normalize(old_domain)
path = WebPathOption.normalize(path)
old_path = WebPathOption.normalize(old_path)
if (domain, path) == (old_domain, old_path):
raise YunohostValidationError(
@ -534,7 +533,14 @@ def app_change_url(operation_logger, app, domain, path):
hook_callback("post_app_change_url", env=env_dict)
def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False, continue_on_failure=False):
def app_upgrade(
app=[],
url=None,
file=None,
force=False,
no_safety_backup=False,
continue_on_failure=False,
):
"""
Upgrade app
@ -748,6 +754,7 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False
).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
@ -857,8 +864,16 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False
else:
operation_logger.close()
logger.error(m18n.n("app_failed_to_upgrade_but_continue", failed_app=app_instance_name, operation_logger_name=operation_logger.name))
failed_to_upgrade_apps.append((app_instance_name, operation_logger.name))
logger.error(
m18n.n(
"app_failed_to_upgrade_but_continue",
failed_app=app_instance_name,
operation_logger_name=operation_logger.name,
)
)
failed_to_upgrade_apps.append(
(app_instance_name, operation_logger.name)
)
# Otherwise we're good and keep going !
now = int(time.time())
@ -923,7 +938,11 @@ def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False
if failed_to_upgrade_apps:
apps = ""
for app_id, operation_logger_name in failed_to_upgrade_apps:
apps += m18n.n("apps_failed_to_upgrade_line", app_id=app_id, operation_logger_name=operation_logger_name)
apps += m18n.n(
"apps_failed_to_upgrade_line",
app_id=app_id,
operation_logger_name=operation_logger_name,
)
logger.warning(m18n.n("apps_failed_to_upgrade", apps=apps))
@ -1153,6 +1172,7 @@ def app_install(
AppResourceManager(app_instance_name, wanted=manifest, current={}).apply(
rollback_and_raise_exception_if_failure=True,
operation_logger=operation_logger,
action="install",
)
except (KeyboardInterrupt, EOFError, Exception) as e:
shutil.rmtree(app_setting_path)
@ -1284,7 +1304,7 @@ def app_install(
AppResourceManager(
app_instance_name, wanted={}, current=manifest
).apply(rollback_and_raise_exception_if_failure=False)
).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():
@ -1423,7 +1443,9 @@ def app_remove(operation_logger, app, purge=False, force_workdir=None):
from yunohost.utils.resources import AppResourceManager
AppResourceManager(app, wanted={}, current=manifest).apply(
rollback_and_raise_exception_if_failure=False, purge_data_dir=purge
rollback_and_raise_exception_if_failure=False,
purge_data_dir=purge,
action="remove",
)
else:
# Remove all permission in LDAP
@ -1508,6 +1530,23 @@ def app_setting(app, key, value=None, delete=False):
_set_app_settings(app, app_settings)
def app_shell(app):
"""
Open an interactive shell with the app environment already loaded
Keyword argument:
app -- App ID
"""
subprocess.run(
[
"/bin/bash",
"-c",
"source /usr/share/yunohost/helpers && ynh_spawn_app_shell " + app,
]
)
def app_register_url(app, domain, path):
"""
Book/register a web path for a given app
@ -1523,8 +1562,8 @@ def app_register_url(app, domain, path):
permission_sync_to_user,
)
domain = DomainQuestion.normalize(domain)
path = PathQuestion.normalize(path)
domain = DomainOption.normalize(domain)
path = WebPathOption.normalize(path)
# We cannot change the url of an app already installed simply by changing
# the settings...
@ -1741,13 +1780,13 @@ class AppConfigPanel(ConfigPanel):
save_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/settings.yml")
config_path_tpl = os.path.join(APPS_SETTING_PATH, "{entity}/config_panel.toml")
def _load_current_values(self):
self.values = self._call_config_script("show")
def _run_action(self, action):
env = {key: str(value) for key, value in self.new_values.items()}
self._call_config_script(action, env=env)
def _get_raw_settings(self):
self.values = self._call_config_script("show")
def _apply(self):
env = {key: str(value) for key, value in self.new_values.items()}
return_content = self._call_config_script("apply", env=env)
@ -1862,19 +1901,8 @@ def _get_app_settings(app):
logger.error(m18n.n("app_not_correctly_installed", app=app))
return {}
# Stupid fix for legacy bullshit
# In the past, some setups did not have proper normalization for app domain/path
# Meaning some setups (as of January 2021) still have path=/foobar/ (with a trailing slash)
# resulting in stupid issue unless apps using ynh_app_normalize_path_stuff
# So we yolofix the settings if such an issue is found >_>
# A simple call to `yunohost app list` (which happens quite often) should be enough
# to migrate all app settings ... so this can probably be removed once we're past Bullseye...
if settings.get("path") != "/" and (
settings.get("path", "").endswith("/")
or not settings.get("path", "/").startswith("/")
):
settings["path"] = "/" + settings["path"].strip("/")
_set_app_settings(app, settings)
# Make the app id available as $app too
settings["app"] = app
if app == settings["id"]:
return settings
@ -2099,7 +2127,7 @@ def _hydrate_app_template(template, data):
varname = stuff.strip("_").lower()
if varname in data:
template = template.replace(stuff, data[varname])
template = template.replace(stuff, str(data[varname]))
return template
@ -2713,8 +2741,8 @@ def _get_conflicting_apps(domain, path, ignore_app=None):
from yunohost.domain import _assert_domain_exists
domain = DomainQuestion.normalize(domain)
path = PathQuestion.normalize(path)
domain = DomainOption.normalize(domain)
path = WebPathOption.normalize(path)
# Abort if domain is unknown
_assert_domain_exists(domain)
@ -2904,10 +2932,10 @@ def _assert_system_is_sane_for_app(manifest, when):
services = manifest.get("services", [])
# Some apps use php-fpm, php5-fpm or php7.x-fpm which is now php7.4-fpm
# Some apps use php-fpm, php5-fpm or php7.x-fpm which is now php8.2-fpm
def replace_alias(service):
if service in ["php-fpm", "php5-fpm", "php7.0-fpm", "php7.3-fpm"]:
return "php7.4-fpm"
if service in ["php-fpm", "php5-fpm", "php7.0-fpm", "php7.3-fpm", "php7.4-fpm"]:
return "php8.2-fpm"
else:
return service
@ -2916,7 +2944,7 @@ def _assert_system_is_sane_for_app(manifest, when):
# We only check those, mostly to ignore "custom" services
# (added by apps) and because those are the most popular
# services
service_filter = ["nginx", "php7.4-fpm", "mysql", "postfix"]
service_filter = ["nginx", "php8.2-fpm", "mysql", "postfix"]
services = [str(s) for s in services if s in service_filter]
if "nginx" not in services:
@ -2990,7 +3018,8 @@ def _notification_is_dismissed(name, settings):
def _filter_and_hydrate_notifications(notifications, current_version=None, data={}):
def is_version_more_recent_than_current_version(name):
def is_version_more_recent_than_current_version(name, current_version):
current_version = str(current_version)
# Boring code to handle the fact that "0.1 < 9999~ynh1" is False
if "~" in name:
@ -3004,7 +3033,7 @@ def _filter_and_hydrate_notifications(notifications, current_version=None, 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)
or is_version_more_recent_than_current_version(name, current_version)
}

View file

@ -1204,7 +1204,7 @@ class RestoreManager:
def _patch_legacy_php_versions_in_csv_file(self):
"""
Apply dirty patch to redirect php5 and php7.0 files to php7.4
Apply dirty patch to redirect php5 and php7.x files to php8.2
"""
from yunohost.utils.legacy import LEGACY_PHP_VERSION_REPLACEMENTS
@ -1528,6 +1528,7 @@ class RestoreManager:
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
@ -2375,6 +2376,7 @@ def backup_list(with_info=False, human_readable=False):
# (we do a realpath() to resolve symlinks)
archives = glob(f"{ARCHIVES_PATH}/*.tar.gz") + glob(f"{ARCHIVES_PATH}/*.tar")
archives = {os.path.realpath(archive) for archive in archives}
archives = {archive for archive in archives if os.path.exists(archive)}
archives = sorted(archives, key=lambda x: os.path.getctime(x))
# Extract only filename without the extension

View file

@ -41,8 +41,8 @@ from yunohost.log import OperationLogger
logger = getActionLogger("yunohost.certmanager")
CERT_FOLDER = "/etc/yunohost/certs/"
TMP_FOLDER = "/tmp/acme-challenge-private/"
WEBROOT_FOLDER = "/tmp/acme-challenge-public/"
TMP_FOLDER = "/var/www/.well-known/acme-challenge-private/"
WEBROOT_FOLDER = "/var/www/.well-known/acme-challenge-public/"
SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem"
ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem"

View file

@ -60,9 +60,9 @@ class MyDiagnoser(Diagnoser):
domains_to_check.append(domain)
self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16))
rm("/tmp/.well-known/ynh-diagnosis/", recursive=True, force=True)
mkdir("/tmp/.well-known/ynh-diagnosis/", parents=True)
os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce)
rm("/var/www/.well-known/ynh-diagnosis/", recursive=True, force=True)
mkdir("/var/www/.well-known/ynh-diagnosis/", parents=True, mode=0o0775)
os.system("touch /var/www/.well-known/ynh-diagnosis/%s" % self.nonce)
if not domains_to_check:
return

View file

@ -62,12 +62,12 @@ class MyDiagnoser(Diagnoser):
# Check quality level in catalog
if not app.get("from_catalog") or app["from_catalog"].get("state") != "working":
yield ("error", "diagnosis_apps_not_in_app_catalog")
yield ("warning", "diagnosis_apps_not_in_app_catalog")
elif (
not isinstance(app["from_catalog"].get("level"), int)
or app["from_catalog"]["level"] == 0
):
yield ("error", "diagnosis_apps_broken")
yield ("warning", "diagnosis_apps_broken")
elif app["from_catalog"]["level"] <= 4:
yield ("warning", "diagnosis_apps_bad_quality")

View file

@ -960,6 +960,9 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=
f"Pushing {record['type']} records is not properly supported by Lexicon/Godaddy."
)
continue
elif registrar == "gandi":
if record["name"] == base_dns_zone:
record["name"] = "@." + record["name"]
record["action"] = action
query = (

View file

@ -33,7 +33,8 @@ from yunohost.app import (
_get_conflicting_apps,
)
from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf
from yunohost.utils.config import ConfigPanel, Question
from yunohost.utils.configpanel import ConfigPanel
from yunohost.utils.form import BaseOption
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.log import is_unit_operation
@ -527,7 +528,7 @@ def domain_config_set(
"""
Apply a new domain configuration
"""
Question.operation_logger = operation_logger
BaseOption.operation_logger = operation_logger
config = DomainConfigPanel(domain)
return config.set(key, value, args, args_file, operation_logger=operation_logger)
@ -537,6 +538,83 @@ class DomainConfigPanel(ConfigPanel):
save_path_tpl = f"{DOMAIN_SETTINGS_DIR}/{{entity}}.yml"
save_mode = "diff"
def get(self, key="", mode="classic"):
result = super().get(key=key, mode=mode)
if mode == "full":
for panel, section, option in self._iterate():
# This injects:
# i18n: domain_config_cert_renew_help
# i18n: domain_config_default_app_help
# i18n: domain_config_xmpp_help
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
option["help"] = m18n.n(
self.config["i18n"] + "_" + option["id"] + "_help"
)
return self.config
return result
def _get_raw_config(self):
toml = super()._get_raw_config()
toml["feature"]["xmpp"]["xmpp"]["default"] = (
1 if self.entity == _get_maindomain() else 0
)
# Optimize wether or not to load the DNS section,
# e.g. we don't want to trigger the whole _get_registary_config_section
# when just getting the current value from the feature section
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
if not filter_key or filter_key[0] == "dns":
from yunohost.dns import _get_registrar_config_section
toml["dns"]["registrar"] = _get_registrar_config_section(self.entity)
# FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ...
self.registar_id = toml["dns"]["registrar"]["registrar"]["value"]
del toml["dns"]["registrar"]["registrar"]["value"]
# Cert stuff
if not filter_key or filter_key[0] == "cert":
from yunohost.certificate import certificate_status
status = certificate_status([self.entity], full=True)["certificates"][
self.entity
]
toml["cert"]["cert"]["cert_summary"]["style"] = status["style"]
# i18n: domain_config_cert_summary_expired
# i18n: domain_config_cert_summary_selfsigned
# i18n: domain_config_cert_summary_abouttoexpire
# i18n: domain_config_cert_summary_ok
# i18n: domain_config_cert_summary_letsencrypt
toml["cert"]["cert"]["cert_summary"]["ask"] = m18n.n(
f"domain_config_cert_summary_{status['summary']}"
)
# FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ...
self.cert_status = status
return toml
def _get_raw_settings(self):
# TODO add mechanism to share some settings with other domains on the same zone
super()._get_raw_settings()
# FIXME: Ugly hack to save the registar id/value and reinject it in _get_raw_settings ...
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
if not filter_key or filter_key[0] == "dns":
self.values["registrar"] = self.registar_id
# FIXME: Ugly hack to save the cert status and reinject it in _get_raw_settings ...
if not filter_key or filter_key[0] == "cert":
self.values["cert_validity"] = self.cert_status["validity"]
self.values["cert_issuer"] = self.cert_status["CA_type"]
self.values["acme_eligible"] = self.cert_status["ACME_eligible"]
self.values["summary"] = self.cert_status["summary"]
def _apply(self):
if (
"default_app" in self.future_values
@ -585,83 +663,6 @@ class DomainConfigPanel(ConfigPanel):
if stuff_to_regen_conf:
regen_conf(names=stuff_to_regen_conf)
def _get_toml(self):
toml = super()._get_toml()
toml["feature"]["xmpp"]["xmpp"]["default"] = (
1 if self.entity == _get_maindomain() else 0
)
# Optimize wether or not to load the DNS section,
# e.g. we don't want to trigger the whole _get_registary_config_section
# when just getting the current value from the feature section
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
if not filter_key or filter_key[0] == "dns":
from yunohost.dns import _get_registrar_config_section
toml["dns"]["registrar"] = _get_registrar_config_section(self.entity)
# FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ...
self.registar_id = toml["dns"]["registrar"]["registrar"]["value"]
del toml["dns"]["registrar"]["registrar"]["value"]
# Cert stuff
if not filter_key or filter_key[0] == "cert":
from yunohost.certificate import certificate_status
status = certificate_status([self.entity], full=True)["certificates"][
self.entity
]
toml["cert"]["cert"]["cert_summary"]["style"] = status["style"]
# i18n: domain_config_cert_summary_expired
# i18n: domain_config_cert_summary_selfsigned
# i18n: domain_config_cert_summary_abouttoexpire
# i18n: domain_config_cert_summary_ok
# i18n: domain_config_cert_summary_letsencrypt
toml["cert"]["cert"]["cert_summary"]["ask"] = m18n.n(
f"domain_config_cert_summary_{status['summary']}"
)
# FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ...
self.cert_status = status
return toml
def get(self, key="", mode="classic"):
result = super().get(key=key, mode=mode)
if mode == "full":
for panel, section, option in self._iterate():
# This injects:
# i18n: domain_config_cert_renew_help
# i18n: domain_config_default_app_help
# i18n: domain_config_xmpp_help
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
option["help"] = m18n.n(
self.config["i18n"] + "_" + option["id"] + "_help"
)
return self.config
return result
def _load_current_values(self):
# TODO add mechanism to share some settings with other domains on the same zone
super()._load_current_values()
# FIXME: Ugly hack to save the registar id/value and reinject it in _load_current_values ...
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
if not filter_key or filter_key[0] == "dns":
self.values["registrar"] = self.registar_id
# FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ...
if not filter_key or filter_key[0] == "cert":
self.values["cert_validity"] = self.cert_status["validity"]
self.values["cert_issuer"] = self.cert_status["CA_type"]
self.values["acme_eligible"] = self.cert_status["ACME_eligible"]
self.values["summary"] = self.cert_status["summary"]
def domain_action_run(domain, action, args=None):
import urllib.parse

View file

@ -402,7 +402,13 @@ def firewall_upnp(action="status", no_refresh=False):
# Discover UPnP device(s)
logger.debug("discovering UPnP devices...")
nb_dev = upnpc.discover()
try:
nb_dev = upnpc.discover()
except Exception:
logger.warning("Failed to find any UPnP device on the network")
nb_dev = -1
enabled = False
logger.debug("found %d UPnP device(s)", int(nb_dev))
if nb_dev < 1:
logger.error(m18n.n("upnp_dev_not_found"))

View file

@ -452,6 +452,8 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers):
logger.debug("Executing command '%s'" % command)
_env = os.environ.copy()
if "YNH_CONTEXT" in _env:
del _env["YNH_CONTEXT"]
_env.update(env)
# Remove the 'HOME' var which is causing some inconsistencies between

View file

@ -139,6 +139,7 @@ def regen_conf(
env["YNH_MAIN_DOMAINS"] = " ".join(
domain_list(exclude_subdomains=True)["domains"]
)
env["YNH_CONTEXT"] = "regenconf"
pre_result = hook_callback("conf_regen", names, pre_callback=_pre_call, env=env)

View file

@ -21,7 +21,8 @@ import subprocess
from moulinette import m18n
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.config import ConfigPanel, Question
from yunohost.utils.configpanel import ConfigPanel
from yunohost.utils.form import BaseOption
from moulinette.utils.log import getActionLogger
from yunohost.regenconf import regen_conf
from yunohost.firewall import firewall_reload
@ -81,7 +82,7 @@ def settings_set(operation_logger, key=None, value=None, args=None, args_file=No
value -- New value
"""
Question.operation_logger = operation_logger
BaseOption.operation_logger = operation_logger
settings = SettingsConfigPanel()
key = translate_legacy_settings_to_configpanel_settings(key)
return settings.set(key, value, args, args_file, operation_logger=operation_logger)
@ -124,6 +125,93 @@ class SettingsConfigPanel(ConfigPanel):
def __init__(self, config_path=None, save_path=None, creation=False):
super().__init__("settings")
def get(self, key="", mode="classic"):
result = super().get(key=key, mode=mode)
if mode == "full":
for panel, section, option in self._iterate():
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
option["help"] = m18n.n(
self.config["i18n"] + "_" + option["id"] + "_help"
)
return self.config
# Dirty hack to let settings_get() to work from a python script
if isinstance(result, str) and result in ["True", "False"]:
result = bool(result == "True")
return result
def reset(self, key="", operation_logger=None):
self.filter_key = key
# Read config panel toml
self._get_config_panel()
if not self.config:
raise YunohostValidationError("config_no_panel")
# Replace all values with default values
self.values = self._get_default_values()
BaseOption.operation_logger = operation_logger
if operation_logger:
operation_logger.start()
try:
self._apply()
except YunohostError:
raise
# Script got manually interrupted ...
# N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
error = m18n.n("operation_interrupted")
logger.error(m18n.n("config_apply_failed", error=error))
raise
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(m18n.n("config_apply_failed", error=error))
raise
logger.success(m18n.n("global_settings_reset_success"))
operation_logger.success()
def _get_raw_config(self):
toml = super()._get_raw_config()
# Dynamic choice list for portal themes
THEMEDIR = "/usr/share/ssowat/portal/assets/themes/"
try:
themes = [d for d in os.listdir(THEMEDIR) if os.path.isdir(THEMEDIR + d)]
except Exception:
themes = ["unsplash", "vapor", "light", "default", "clouds"]
toml["misc"]["portal"]["portal_theme"]["choices"] = themes
return toml
def _get_raw_settings(self):
super()._get_raw_settings()
# Specific logic for those settings who are "virtual" settings
# and only meant to have a custom setter mapped to tools_rootpw
self.values["root_password"] = ""
self.values["root_password_confirm"] = ""
# Specific logic for virtual setting "passwordless_sudo"
try:
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
self.values["passwordless_sudo"] = "!authenticate" in ldap.search(
"ou=sudo", "cn=admins", ["sudoOption"]
)[0].get("sudoOption", [])
except Exception:
self.values["passwordless_sudo"] = False
def _apply(self):
root_password = self.new_values.pop("root_password", None)
root_password_confirm = self.new_values.pop("root_password_confirm", None)
@ -169,93 +257,6 @@ class SettingsConfigPanel(ConfigPanel):
logger.error(f"Post-change hook for setting failed : {e}")
raise
def _get_toml(self):
toml = super()._get_toml()
# Dynamic choice list for portal themes
THEMEDIR = "/usr/share/ssowat/portal/assets/themes/"
try:
themes = [d for d in os.listdir(THEMEDIR) if os.path.isdir(THEMEDIR + d)]
except Exception:
themes = ["unsplash", "vapor", "light", "default", "clouds"]
toml["misc"]["portal"]["portal_theme"]["choices"] = themes
return toml
def _load_current_values(self):
super()._load_current_values()
# Specific logic for those settings who are "virtual" settings
# and only meant to have a custom setter mapped to tools_rootpw
self.values["root_password"] = ""
self.values["root_password_confirm"] = ""
# Specific logic for virtual setting "passwordless_sudo"
try:
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
self.values["passwordless_sudo"] = "!authenticate" in ldap.search(
"ou=sudo", "cn=admins", ["sudoOption"]
)[0].get("sudoOption", [])
except Exception:
self.values["passwordless_sudo"] = False
def get(self, key="", mode="classic"):
result = super().get(key=key, mode=mode)
if mode == "full":
for panel, section, option in self._iterate():
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
option["help"] = m18n.n(
self.config["i18n"] + "_" + option["id"] + "_help"
)
return self.config
# Dirty hack to let settings_get() to work from a python script
if isinstance(result, str) and result in ["True", "False"]:
result = bool(result == "True")
return result
def reset(self, key="", operation_logger=None):
self.filter_key = key
# Read config panel toml
self._get_config_panel()
if not self.config:
raise YunohostValidationError("config_no_panel")
# Replace all values with default values
self.values = self._get_default_values()
Question.operation_logger = operation_logger
if operation_logger:
operation_logger.start()
try:
self._apply()
except YunohostError:
raise
# Script got manually interrupted ...
# N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
error = m18n.n("operation_interrupted")
logger.error(m18n.n("config_apply_failed", error=error))
raise
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(m18n.n("config_apply_failed", error=error))
raise
logger.success(m18n.n("global_settings_reset_success"))
operation_logger.success()
# Meant to be a dict of setting_name -> function to call
post_change_hooks = {}

View file

@ -81,7 +81,7 @@ def user_ssh_add_key(username, key, comment):
parents=True,
uid=user["uid"][0],
)
chmod(os.path.join(user["homeDirectory"][0], ".ssh"), 0o600)
chmod(os.path.join(user["homeDirectory"][0], ".ssh"), 0o700)
# create empty file to set good permissions
write_to_file(authorized_keys_file, "")

View file

@ -1,5 +1,6 @@
import os
import pytest
from unittest.mock import Mock
import moulinette
from moulinette import m18n, Moulinette
@ -23,11 +24,15 @@ def get_test_apps_dir():
@contextmanager
def message(mocker, key, **kwargs):
mocker.spy(m18n, "n")
def message(key, **kwargs):
m = Mock(wraps=m18n.n)
old_m18n = m18n.n
m18n.n = m
yield
m18n.n.assert_any_call(key, **kwargs)
try:
m.assert_any_call(key, **kwargs)
finally:
m18n.n = old_m18n
@contextmanager
def raiseYunohostError(mocker, key, **kwargs):

View file

@ -5,6 +5,8 @@ import requests_mock
import glob
import shutil
from .conftest import message
from moulinette import m18n
from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml
@ -258,13 +260,12 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker):
assert "bar" in app_dict.keys()
def test_apps_catalog_load_with_oudated_api_version(mocker):
def test_apps_catalog_load_with_outdated_api_version():
# Initialize ...
_initialize_apps_catalog_system()
# Update
with requests_mock.Mocker() as m:
mocker.spy(m18n, "n")
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
_update_apps_catalog()
@ -282,10 +283,8 @@ def test_apps_catalog_load_with_oudated_api_version(mocker):
with requests_mock.Mocker() as m:
# Mock the server response with a dummy apps catalog
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
mocker.spy(m18n, "n")
app_dict = _load_apps_catalog()["apps"]
m18n.n.assert_any_call("apps_catalog_update_success")
with message("apps_catalog_update_success"):
app_dict = _load_apps_catalog()["apps"]
assert "foo" in app_dict.keys()
assert "bar" in app_dict.keys()

View file

@ -112,7 +112,7 @@ def app_expected_files(domain, app):
if app.startswith("legacy_app"):
yield "/var/www/%s/index.html" % app
yield "/etc/yunohost/apps/%s/settings.yml" % app
if "manifestv2" in app:
if "manifestv2" in app or "my_webapp" in app:
yield "/etc/yunohost/apps/%s/manifest.toml" % app
else:
yield "/etc/yunohost/apps/%s/manifest.json" % app
@ -330,7 +330,7 @@ def test_app_from_catalog():
app_install(
"my_webapp",
args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0",
args=f"domain={main_domain}&path=/site&with_sftp=0&password=superpassword&is_public=1&with_mysql=0&phpversion=none",
)
app_map_ = app_map(raw=True)
assert main_domain in app_map_
@ -392,9 +392,9 @@ def test_legacy_app_install_private(secondary_domain):
assert app_is_not_installed(secondary_domain, "legacy_app")
def test_legacy_app_install_unknown_domain(mocker):
def test_legacy_app_install_unknown_domain():
with pytest.raises(YunohostError):
with message(mocker, "app_argument_invalid"):
with message("app_argument_invalid"):
install_legacy_app("whatever.nope", "/legacy")
assert app_is_not_installed("whatever.nope", "legacy_app")
@ -421,12 +421,12 @@ def test_legacy_app_install_multiple_instances(secondary_domain):
assert app_is_not_installed(secondary_domain, "legacy_app__2")
def test_legacy_app_install_path_unavailable(mocker, secondary_domain):
def test_legacy_app_install_path_unavailable(secondary_domain):
# These will be removed in teardown
install_legacy_app(secondary_domain, "/legacy")
with pytest.raises(YunohostError):
with message(mocker, "app_location_unavailable"):
with message("app_location_unavailable"):
install_legacy_app(secondary_domain, "/")
assert app_is_installed(secondary_domain, "legacy_app")
@ -442,19 +442,19 @@ def test_legacy_app_install_with_nginx_down(mocker, secondary_domain):
install_legacy_app(secondary_domain, "/legacy")
def test_legacy_app_failed_install(mocker, secondary_domain):
def test_legacy_app_failed_install(secondary_domain):
# This will conflict with the folder that the app
# attempts to create, making the install fail
mkdir("/var/www/legacy_app/", 0o750)
with pytest.raises(YunohostError):
with message(mocker, "app_install_script_failed"):
with message("app_install_script_failed"):
install_legacy_app(secondary_domain, "/legacy")
assert app_is_not_installed(secondary_domain, "legacy_app")
def test_legacy_app_failed_remove(mocker, secondary_domain):
def test_legacy_app_failed_remove(secondary_domain):
install_legacy_app(secondary_domain, "/legacy")
# The remove script runs with set -eu and attempt to remove this
@ -486,52 +486,52 @@ def test_full_domain_app_with_conflicts(mocker, secondary_domain):
install_full_domain_app(secondary_domain)
def test_systemfuckedup_during_app_install(mocker, secondary_domain):
def test_systemfuckedup_during_app_install(secondary_domain):
with pytest.raises(YunohostError):
with message(mocker, "app_install_failed"):
with message(mocker, "app_action_broke_system"):
with message("app_install_failed"):
with message("app_action_broke_system"):
install_break_yo_system(secondary_domain, breakwhat="install")
assert app_is_not_installed(secondary_domain, "break_yo_system")
def test_systemfuckedup_during_app_remove(mocker, secondary_domain):
def test_systemfuckedup_during_app_remove(secondary_domain):
install_break_yo_system(secondary_domain, breakwhat="remove")
with pytest.raises(YunohostError):
with message(mocker, "app_action_broke_system"):
with message(mocker, "app_removed"):
with message("app_action_broke_system"):
with message("app_removed"):
app_remove("break_yo_system")
assert app_is_not_installed(secondary_domain, "break_yo_system")
def test_systemfuckedup_during_app_install_and_remove(mocker, secondary_domain):
def test_systemfuckedup_during_app_install_and_remove(secondary_domain):
with pytest.raises(YunohostError):
with message(mocker, "app_install_failed"):
with message(mocker, "app_action_broke_system"):
with message("app_install_failed"):
with message("app_action_broke_system"):
install_break_yo_system(secondary_domain, breakwhat="everything")
assert app_is_not_installed(secondary_domain, "break_yo_system")
def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain):
def test_systemfuckedup_during_app_upgrade(secondary_domain):
install_break_yo_system(secondary_domain, breakwhat="upgrade")
with pytest.raises(YunohostError):
with message(mocker, "app_action_broke_system"):
with message("app_action_broke_system"):
app_upgrade(
"break_yo_system",
file=os.path.join(get_test_apps_dir(), "break_yo_system_ynh"),
)
def test_failed_multiple_app_upgrade(mocker, secondary_domain):
def test_failed_multiple_app_upgrade(secondary_domain):
install_legacy_app(secondary_domain, "/legacy")
install_break_yo_system(secondary_domain, breakwhat="upgrade")
with pytest.raises(YunohostError):
with message(mocker, "app_not_upgraded"):
with message("app_not_upgraded"):
app_upgrade(
["break_yo_system", "legacy_app"],
file={
@ -548,37 +548,51 @@ class TestMockedAppUpgrade:
This class is here to test the logical workflow of app_upgrade and thus
mock nearly all side effects
"""
def setup_method(self, method):
self.apps_list = []
self.upgradable_apps_list = []
def _mock_app_upgrade(self, mocker):
# app list
self._installed_apps = mocker.patch("yunohost.app._installed_apps", side_effect=lambda: self.apps_list)
self._installed_apps = mocker.patch(
"yunohost.app._installed_apps", side_effect=lambda: self.apps_list
)
# just check if an app is really installed
mocker.patch("yunohost.app._is_installed", side_effect=lambda app: app in self.apps_list)
mocker.patch(
"yunohost.app._is_installed", side_effect=lambda app: app in self.apps_list
)
# app_dict =
mocker.patch("yunohost.app.app_info", side_effect=lambda app, full: {
"upgradable": "yes" if app in self.upgradable_apps_list else "no",
"manifest": {"id": app},
"version": "?",
})
mocker.patch(
"yunohost.app.app_info",
side_effect=lambda app, full: {
"upgradable": "yes" if app in self.upgradable_apps_list else "no",
"manifest": {"id": app},
"version": "?",
},
)
def custom_extract_app(app):
return ({
"version": "?",
"packaging_format": 1,
"id": app,
"notifications": {"PRE_UPGRADE": None, "POST_UPGRADE": None},
}, "MOCKED_BY_TEST")
return (
{
"version": "?",
"packaging_format": 1,
"id": app,
"notifications": {"PRE_UPGRADE": None, "POST_UPGRADE": None},
},
"MOCKED_BY_TEST",
)
# return (manifest, extracted_app_folder)
mocker.patch("yunohost.app._extract_app", side_effect=custom_extract_app)
# for [(name, passed, values, err), ...] in
mocker.patch("yunohost.app._check_manifest_requirements", return_value=[(None, True, None, None)])
mocker.patch(
"yunohost.app._check_manifest_requirements",
return_value=[(None, True, None, None)],
)
# raise on failure
mocker.patch("yunohost.app._assert_system_is_sane_for_app", return_value=True)
@ -593,12 +607,15 @@ class TestMockedAppUpgrade:
mocker.patch("os.path.exists", side_effect=custom_os_path_exists)
# manifest =
mocker.patch("yunohost.app.read_toml", return_value={
"arguments": {"install": []}
})
mocker.patch(
"yunohost.app.read_toml", return_value={"arguments": {"install": []}}
)
# install_failed, failure_message_with_debug_instructions =
self.hook_exec_with_script_debug_if_failure = mocker.patch("yunohost.hook.hook_exec_with_script_debug_if_failure", return_value=(False, ""))
self.hook_exec_with_script_debug_if_failure = mocker.patch(
"yunohost.hook.hook_exec_with_script_debug_if_failure",
return_value=(False, ""),
)
# settings =
mocker.patch("yunohost.app._get_app_settings", return_value={})
# return nothing
@ -644,7 +661,12 @@ class TestMockedAppUpgrade:
app_upgrade()
self.hook_exec_with_script_debug_if_failure.assert_called_once()
assert self.hook_exec_with_script_debug_if_failure.call_args.kwargs["env"]["YNH_APP_ID"] == "some_app"
assert (
self.hook_exec_with_script_debug_if_failure.call_args.kwargs["env"][
"YNH_APP_ID"
]
== "some_app"
)
def test_app_upgrade_continue_on_failure(self, mocker):
self._mock_app_upgrade(mocker)
@ -682,7 +704,10 @@ class TestMockedAppUpgrade:
raise Exception()
return True
mocker.patch("yunohost.app._assert_system_is_sane_for_app", side_effect=_assert_system_is_sane_for_app)
mocker.patch(
"yunohost.app._assert_system_is_sane_for_app",
side_effect=_assert_system_is_sane_for_app,
)
with pytest.raises(YunohostError):
app_upgrade()

View file

@ -202,10 +202,6 @@ def test_normalize_permission_path_with_bad_regex():
)
# Full Regex
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url(
"re:" + maindomain + "/yolo?+/", maindomain + "/path", "test_permission"
)
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url(
"re:" + maindomain + "/yolo[1-9]**/",

View file

@ -55,7 +55,7 @@ def setup_function(function):
if "with_legacy_app_installed" in markers:
assert not app_is_installed("legacy_app")
install_app("legacy_app_ynh", "/yolo")
install_app("legacy_app_ynh", "/yolo", "&is_public=true")
assert app_is_installed("legacy_app")
if "with_backup_recommended_app_installed" in markers:
@ -236,10 +236,10 @@ def add_archive_system_from_4p2():
#
def test_backup_only_ldap(mocker):
def test_backup_only_ldap():
# Create the backup
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(name=name, system=["conf_ldap"], apps=None)
archives = backup_list()["archives"]
@ -253,7 +253,7 @@ def test_backup_only_ldap(mocker):
def test_backup_system_part_that_does_not_exists(mocker):
# Create the backup
with message(mocker, "backup_hook_unknown", hook="doesnt_exist"):
with message("backup_hook_unknown", hook="doesnt_exist"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=["doesnt_exist"], apps=None)
@ -263,10 +263,10 @@ def test_backup_system_part_that_does_not_exists(mocker):
#
def test_backup_and_restore_all_sys(mocker):
def test_backup_and_restore_all_sys():
name = random_ascii(8)
# Create the backup
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"]
@ -284,7 +284,7 @@ def test_backup_and_restore_all_sys(mocker):
assert not os.path.exists("/etc/ssowat/conf.json")
# Restore the backup
with message(mocker, "restore_complete"):
with message("restore_complete"):
backup_restore(name=archives[0], force=True, system=[], apps=None)
# Check ssowat conf is back
@ -297,17 +297,17 @@ def test_backup_and_restore_all_sys(mocker):
@pytest.mark.with_system_archive_from_4p2
def test_restore_system_from_Ynh4p2(monkeypatch, mocker):
def test_restore_system_from_Ynh4p2(monkeypatch):
name = random_ascii(8)
# Backup current system
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 2
# Restore system archive from 3.8
try:
with message(mocker, "restore_complete"):
with message("restore_complete"):
backup_restore(
name=backup_list()["archives"][1], system=[], apps=None, force=True
)
@ -336,7 +336,7 @@ def test_backup_script_failure_handling(monkeypatch, mocker):
# with the expected error message key
monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
with message(mocker, "backup_app_failed", app="backup_recommended_app"):
with message("backup_app_failed", app="backup_recommended_app"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["backup_recommended_app"])
@ -363,7 +363,7 @@ def test_backup_not_enough_free_space(monkeypatch, mocker):
def test_backup_app_not_installed(mocker):
assert not _is_installed("wordpress")
with message(mocker, "unbackup_app", app="wordpress"):
with message("unbackup_app", app="wordpress"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["wordpress"])
@ -375,14 +375,14 @@ def test_backup_app_with_no_backup_script(mocker):
assert not os.path.exists(backup_script)
with message(
mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app"
"backup_with_no_backup_script_for_app", app="backup_recommended_app"
):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.with_backup_recommended_app_installed
def test_backup_app_with_no_restore_script(mocker):
def test_backup_app_with_no_restore_script():
restore_script = "/etc/yunohost/apps/backup_recommended_app/scripts/restore"
os.system("rm %s" % restore_script)
assert not os.path.exists(restore_script)
@ -391,16 +391,16 @@ def test_backup_app_with_no_restore_script(mocker):
# user...
with message(
mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app"
"backup_with_no_restore_script_for_app", app="backup_recommended_app"
):
backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.clean_opt_dir
def test_backup_with_different_output_directory(mocker):
def test_backup_with_different_output_directory():
name = random_ascii(8)
# Create the backup
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(
system=["conf_ynh_settings"],
apps=None,
@ -420,10 +420,10 @@ def test_backup_with_different_output_directory(mocker):
@pytest.mark.clean_opt_dir
def test_backup_using_copy_method(mocker):
def test_backup_using_copy_method():
# Create the backup
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(
system=["conf_ynh_settings"],
apps=None,
@ -442,8 +442,8 @@ def test_backup_using_copy_method(mocker):
@pytest.mark.with_wordpress_archive_from_4p2
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_wordpress_from_Ynh4p2(mocker):
with message(mocker, "restore_complete"):
def test_restore_app_wordpress_from_Ynh4p2():
with message("restore_complete"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
@ -461,7 +461,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker):
assert not _is_installed("wordpress")
with message(mocker, "app_restore_script_failed"):
with message("app_restore_script_failed"):
with raiseYunohostError(mocker, "restore_nothings_done"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
@ -494,7 +494,7 @@ def test_restore_app_not_in_backup(mocker):
assert not _is_installed("wordpress")
assert not _is_installed("yoloswag")
with message(mocker, "backup_archive_app_not_found", app="yoloswag"):
with message("backup_archive_app_not_found", app="yoloswag"):
with raiseYunohostError(mocker, "restore_nothings_done"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["yoloswag"]
@ -509,7 +509,7 @@ def test_restore_app_not_in_backup(mocker):
def test_restore_app_already_installed(mocker):
assert not _is_installed("wordpress")
with message(mocker, "restore_complete"):
with message("restore_complete"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
@ -525,22 +525,22 @@ def test_restore_app_already_installed(mocker):
@pytest.mark.with_legacy_app_installed
def test_backup_and_restore_legacy_app(mocker):
_test_backup_and_restore_app(mocker, "legacy_app")
def test_backup_and_restore_legacy_app():
_test_backup_and_restore_app("legacy_app")
@pytest.mark.with_backup_recommended_app_installed
def test_backup_and_restore_recommended_app(mocker):
_test_backup_and_restore_app(mocker, "backup_recommended_app")
def test_backup_and_restore_recommended_app():
_test_backup_and_restore_app("backup_recommended_app")
@pytest.mark.with_backup_recommended_app_installed_with_ynh_restore
def test_backup_and_restore_with_ynh_restore(mocker):
_test_backup_and_restore_app(mocker, "backup_recommended_app")
def test_backup_and_restore_with_ynh_restore():
_test_backup_and_restore_app("backup_recommended_app")
@pytest.mark.with_permission_app_installed
def test_backup_and_restore_permission_app(mocker):
def test_backup_and_restore_permission_app():
res = user_permission_list(full=True)["permissions"]
assert "permissions_app.main" in res
assert "permissions_app.admin" in res
@ -554,7 +554,7 @@ def test_backup_and_restore_permission_app(mocker):
assert res["permissions_app.admin"]["allowed"] == ["alice"]
assert res["permissions_app.dev"]["allowed"] == []
_test_backup_and_restore_app(mocker, "permissions_app")
_test_backup_and_restore_app("permissions_app")
res = user_permission_list(full=True)["permissions"]
assert "permissions_app.main" in res
@ -570,10 +570,10 @@ def test_backup_and_restore_permission_app(mocker):
assert res["permissions_app.dev"]["allowed"] == []
def _test_backup_and_restore_app(mocker, app):
def _test_backup_and_restore_app(app):
# Create a backup of this app
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(name=name, system=None, apps=[app])
archives = backup_list()["archives"]
@ -590,7 +590,7 @@ def _test_backup_and_restore_app(mocker, app):
assert app + ".main" not in user_permission_list()["permissions"]
# Restore the app
with message(mocker, "restore_complete"):
with message("restore_complete"):
backup_restore(system=None, name=archives[0], apps=[app])
assert app_is_installed(app)
@ -631,19 +631,19 @@ def test_restore_archive_with_bad_archive(mocker):
clean_tmp_backup_directory()
def test_restore_archive_with_custom_hook(mocker):
def test_restore_archive_with_custom_hook():
custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore")
os.system("touch %s/99-yolo" % custom_restore_hook_folder)
# Backup with custom hook system
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
# Restore system with custom hook
with message(mocker, "restore_complete"):
with message("restore_complete"):
backup_restore(
name=backup_list()["archives"][0], system=[], apps=None, force=True
)
@ -651,7 +651,7 @@ def test_restore_archive_with_custom_hook(mocker):
os.system("rm %s/99-yolo" % custom_restore_hook_folder)
def test_backup_binds_are_readonly(mocker, monkeypatch):
def test_backup_binds_are_readonly(monkeypatch):
def custom_mount_and_backup(self):
self._organize_files()
@ -676,5 +676,5 @@ def test_backup_binds_are_readonly(mocker, monkeypatch):
# Create the backup
name = random_ascii(8)
with message(mocker, "backup_created", name=name):
with message("backup_created", name=name):
backup_create(name=name, system=[])

View file

@ -39,7 +39,7 @@ def check_changeurl_app(path):
assert appmap[maindomain][path]["id"] == "change_url_app"
r = requests.get(
"https://127.0.0.1%s/" % path, headers={"domain": maindomain}, verify=False
"https://127.0.0.1%s/" % path, headers={"Host": maindomain}, verify=False
)
assert r.status_code == 200

View file

@ -59,7 +59,7 @@ def test_authenticate_with_wrong_password():
assert expected_msg in str(exception)
def test_authenticate_server_down(mocker):
def test_authenticate_server_down():
os.system("systemctl stop slapd && sleep 5")
LDAPAuth().authenticate_credentials(credentials="alice:Yunohost")

View file

@ -435,8 +435,8 @@ def test_permission_list():
#
def test_permission_create_main(mocker):
with message(mocker, "permission_created", permission="site.main"):
def test_permission_create_main():
with message("permission_created", permission="site.main"):
permission_create("site.main", allowed=["all_users"], protected=False)
res = user_permission_list(full=True)["permissions"]
@ -446,8 +446,8 @@ def test_permission_create_main(mocker):
assert res["site.main"]["protected"] is False
def test_permission_create_extra(mocker):
with message(mocker, "permission_created", permission="site.test"):
def test_permission_create_extra():
with message("permission_created", permission="site.test"):
permission_create("site.test")
res = user_permission_list(full=True)["permissions"]
@ -466,8 +466,8 @@ def test_permission_create_with_specific_user():
assert res["site.test"]["allowed"] == ["alice"]
def test_permission_create_with_tile_management(mocker):
with message(mocker, "permission_created", permission="site.main"):
def test_permission_create_with_tile_management():
with message("permission_created", permission="site.main"):
_permission_create_with_dummy_app(
"site.main",
allowed=["all_users"],
@ -483,8 +483,8 @@ def test_permission_create_with_tile_management(mocker):
assert res["site.main"]["show_tile"] is False
def test_permission_create_with_tile_management_with_main_default_value(mocker):
with message(mocker, "permission_created", permission="site.main"):
def test_permission_create_with_tile_management_with_main_default_value():
with message("permission_created", permission="site.main"):
_permission_create_with_dummy_app(
"site.main",
allowed=["all_users"],
@ -500,8 +500,8 @@ def test_permission_create_with_tile_management_with_main_default_value(mocker):
assert res["site.main"]["show_tile"] is True
def test_permission_create_with_tile_management_with_not_main_default_value(mocker):
with message(mocker, "permission_created", permission="wiki.api"):
def test_permission_create_with_tile_management_with_not_main_default_value():
with message("permission_created", permission="wiki.api"):
_permission_create_with_dummy_app(
"wiki.api",
allowed=["all_users"],
@ -517,8 +517,8 @@ def test_permission_create_with_tile_management_with_not_main_default_value(mock
assert res["wiki.api"]["show_tile"] is True
def test_permission_create_with_urls_management_without_url(mocker):
with message(mocker, "permission_created", permission="wiki.api"):
def test_permission_create_with_urls_management_without_url():
with message("permission_created", permission="wiki.api"):
_permission_create_with_dummy_app(
"wiki.api", allowed=["all_users"], domain=maindomain, path="/site"
)
@ -530,8 +530,8 @@ def test_permission_create_with_urls_management_without_url(mocker):
assert res["wiki.api"]["auth_header"] is True
def test_permission_create_with_urls_management_simple_domain(mocker):
with message(mocker, "permission_created", permission="site.main"):
def test_permission_create_with_urls_management_simple_domain():
with message("permission_created", permission="site.main"):
_permission_create_with_dummy_app(
"site.main",
allowed=["all_users"],
@ -553,8 +553,8 @@ def test_permission_create_with_urls_management_simple_domain(mocker):
@pytest.mark.other_domains(number=2)
def test_permission_create_with_urls_management_multiple_domain(mocker):
with message(mocker, "permission_created", permission="site.main"):
def test_permission_create_with_urls_management_multiple_domain():
with message("permission_created", permission="site.main"):
_permission_create_with_dummy_app(
"site.main",
allowed=["all_users"],
@ -575,14 +575,14 @@ def test_permission_create_with_urls_management_multiple_domain(mocker):
assert res["site.main"]["auth_header"] is True
def test_permission_delete(mocker):
with message(mocker, "permission_deleted", permission="wiki.main"):
def test_permission_delete():
with message("permission_deleted", permission="wiki.main"):
permission_delete("wiki.main", force=True)
res = user_permission_list()["permissions"]
assert "wiki.main" not in res
with message(mocker, "permission_deleted", permission="blog.api"):
with message("permission_deleted", permission="blog.api"):
permission_delete("blog.api", force=False)
res = user_permission_list()["permissions"]
@ -625,8 +625,8 @@ def test_permission_delete_main_without_force(mocker):
# user side functions
def test_permission_add_group(mocker):
with message(mocker, "permission_updated", permission="wiki.main"):
def test_permission_add_group():
with message("permission_updated", permission="wiki.main"):
user_permission_update("wiki.main", add="alice")
res = user_permission_list(full=True)["permissions"]
@ -634,8 +634,8 @@ def test_permission_add_group(mocker):
assert set(res["wiki.main"]["corresponding_users"]) == {"alice", "bob"}
def test_permission_remove_group(mocker):
with message(mocker, "permission_updated", permission="blog.main"):
def test_permission_remove_group():
with message("permission_updated", permission="blog.main"):
user_permission_update("blog.main", remove="alice")
res = user_permission_list(full=True)["permissions"]
@ -643,8 +643,8 @@ def test_permission_remove_group(mocker):
assert res["blog.main"]["corresponding_users"] == []
def test_permission_add_and_remove_group(mocker):
with message(mocker, "permission_updated", permission="wiki.main"):
def test_permission_add_and_remove_group():
with message("permission_updated", permission="wiki.main"):
user_permission_update("wiki.main", add="alice", remove="all_users")
res = user_permission_list(full=True)["permissions"]
@ -652,9 +652,9 @@ def test_permission_add_and_remove_group(mocker):
assert res["wiki.main"]["corresponding_users"] == ["alice"]
def test_permission_add_group_already_allowed(mocker):
def test_permission_add_group_already_allowed():
with message(
mocker, "permission_already_allowed", permission="blog.main", group="alice"
"permission_already_allowed", permission="blog.main", group="alice"
):
user_permission_update("blog.main", add="alice")
@ -663,9 +663,9 @@ def test_permission_add_group_already_allowed(mocker):
assert res["blog.main"]["corresponding_users"] == ["alice"]
def test_permission_remove_group_already_not_allowed(mocker):
def test_permission_remove_group_already_not_allowed():
with message(
mocker, "permission_already_disallowed", permission="blog.main", group="bob"
"permission_already_disallowed", permission="blog.main", group="bob"
):
user_permission_update("blog.main", remove="bob")
@ -674,8 +674,8 @@ def test_permission_remove_group_already_not_allowed(mocker):
assert res["blog.main"]["corresponding_users"] == ["alice"]
def test_permission_reset(mocker):
with message(mocker, "permission_updated", permission="blog.main"):
def test_permission_reset():
with message("permission_updated", permission="blog.main"):
user_permission_reset("blog.main")
res = user_permission_list(full=True)["permissions"]
@ -693,42 +693,42 @@ def test_permission_reset_idempotency():
assert set(res["blog.main"]["corresponding_users"]) == {"alice", "bob"}
def test_permission_change_label(mocker):
with message(mocker, "permission_updated", permission="wiki.main"):
def test_permission_change_label():
with message("permission_updated", permission="wiki.main"):
user_permission_update("wiki.main", label="New Wiki")
res = user_permission_list(full=True)["permissions"]
assert res["wiki.main"]["label"] == "New Wiki"
def test_permission_change_label_with_same_value(mocker):
with message(mocker, "permission_updated", permission="wiki.main"):
def test_permission_change_label_with_same_value():
with message("permission_updated", permission="wiki.main"):
user_permission_update("wiki.main", label="Wiki")
res = user_permission_list(full=True)["permissions"]
assert res["wiki.main"]["label"] == "Wiki"
def test_permission_switch_show_tile(mocker):
def test_permission_switch_show_tile():
# Note that from the actionmap the value is passed as string, not as bool
# Try with lowercase
with message(mocker, "permission_updated", permission="wiki.main"):
with message("permission_updated", permission="wiki.main"):
user_permission_update("wiki.main", show_tile="false")
res = user_permission_list(full=True)["permissions"]
assert res["wiki.main"]["show_tile"] is False
# Try with uppercase
with message(mocker, "permission_updated", permission="wiki.main"):
with message("permission_updated", permission="wiki.main"):
user_permission_update("wiki.main", show_tile="TRUE")
res = user_permission_list(full=True)["permissions"]
assert res["wiki.main"]["show_tile"] is True
def test_permission_switch_show_tile_with_same_value(mocker):
def test_permission_switch_show_tile_with_same_value():
# Note that from the actionmap the value is passed as string, not as bool
with message(mocker, "permission_updated", permission="wiki.main"):
with message("permission_updated", permission="wiki.main"):
user_permission_update("wiki.main", show_tile="True")
res = user_permission_list(full=True)["permissions"]
@ -806,7 +806,7 @@ def test_permission_main_url_regex():
def test_permission_main_url_bad_regex(mocker):
with raiseYunohostError(mocker, "invalid_regex"):
permission_url("blog.main", url="re:/[a-z]++reboy/.*")
permission_url("blog.main", url="re:/[a-z]+++reboy/.*")
@pytest.mark.other_domains(number=1)
@ -837,7 +837,7 @@ def test_permission_add_additional_regex():
def test_permission_add_additional_bad_regex(mocker):
with raiseYunohostError(mocker, "invalid_regex"):
permission_url("blog.main", add_url=["re:/[a-z]++reboy/.*"])
permission_url("blog.main", add_url=["re:/[a-z]+++reboy/.*"])
def test_permission_remove_additional_url():

File diff suppressed because it is too large Load diff

View file

@ -87,7 +87,7 @@ def test_ssh_conf_unmanaged():
assert SSHD_CONFIG in _get_conf_hashes("ssh")
def test_ssh_conf_unmanaged_and_manually_modified(mocker):
def test_ssh_conf_unmanaged_and_manually_modified():
_force_clear_hashes([SSHD_CONFIG])
os.system("echo ' ' >> %s" % SSHD_CONFIG)
@ -98,7 +98,7 @@ def test_ssh_conf_unmanaged_and_manually_modified(mocker):
assert SSHD_CONFIG in _get_conf_hashes("ssh")
assert SSHD_CONFIG in manually_modified_files()
with message(mocker, "regenconf_need_to_explicitly_specify_ssh"):
with message("regenconf_need_to_explicitly_specify_ssh"):
regen_conf(force=True)
assert SSHD_CONFIG in _get_conf_hashes("ssh")

View file

@ -91,8 +91,8 @@ def test_list_groups():
#
def test_create_user(mocker):
with message(mocker, "user_created"):
def test_create_user():
with message("user_created"):
user_create("albert", maindomain, "test123Ynh", fullname="Albert Good")
group_res = user_group_list()["groups"]
@ -102,8 +102,8 @@ def test_create_user(mocker):
assert "albert" in group_res["all_users"]["members"]
def test_del_user(mocker):
with message(mocker, "user_deleted"):
def test_del_user():
with message("user_deleted"):
user_delete("alice")
group_res = user_group_list()["groups"]
@ -112,7 +112,7 @@ def test_del_user(mocker):
assert "alice" not in group_res["all_users"]["members"]
def test_import_user(mocker):
def test_import_user():
import csv
from io import StringIO
@ -157,7 +157,7 @@ def test_import_user(mocker):
}
)
csv_io.seek(0)
with message(mocker, "user_import_success"):
with message("user_import_success"):
user_import(csv_io, update=True, delete=True)
group_res = user_group_list()["groups"]
@ -171,7 +171,7 @@ def test_import_user(mocker):
assert "alice" not in group_res["dev"]["members"]
def test_export_user(mocker):
def test_export_user():
result = user_export()
should_be = (
"username;firstname;lastname;password;mail;mail-alias;mail-forward;mailbox-quota;groups\r\n"
@ -182,8 +182,8 @@ def test_export_user(mocker):
assert result == should_be
def test_create_group(mocker):
with message(mocker, "group_created", group="adminsys"):
def test_create_group():
with message("group_created", group="adminsys"):
user_group_create("adminsys")
group_res = user_group_list()["groups"]
@ -192,8 +192,8 @@ def test_create_group(mocker):
assert group_res["adminsys"]["members"] == []
def test_del_group(mocker):
with message(mocker, "group_deleted", group="dev"):
def test_del_group():
with message("group_deleted", group="dev"):
user_group_delete("dev")
group_res = user_group_list()["groups"]
@ -262,46 +262,46 @@ def test_del_group_that_does_not_exist(mocker):
#
def test_update_user(mocker):
with message(mocker, "user_updated"):
def test_update_user():
with message("user_updated"):
user_update("alice", firstname="NewName", lastname="NewLast")
info = user_info("alice")
assert info["fullname"] == "NewName NewLast"
with message(mocker, "user_updated"):
with message("user_updated"):
user_update("alice", fullname="New2Name New2Last")
info = user_info("alice")
assert info["fullname"] == "New2Name New2Last"
def test_update_group_add_user(mocker):
with message(mocker, "group_updated", group="dev"):
def test_update_group_add_user():
with message("group_updated", group="dev"):
user_group_update("dev", add=["bob"])
group_res = user_group_list()["groups"]
assert set(group_res["dev"]["members"]) == {"alice", "bob"}
def test_update_group_add_user_already_in(mocker):
with message(mocker, "group_user_already_in_group", user="bob", group="apps"):
def test_update_group_add_user_already_in():
with message("group_user_already_in_group", user="bob", group="apps"):
user_group_update("apps", add=["bob"])
group_res = user_group_list()["groups"]
assert group_res["apps"]["members"] == ["bob"]
def test_update_group_remove_user(mocker):
with message(mocker, "group_updated", group="apps"):
def test_update_group_remove_user():
with message("group_updated", group="apps"):
user_group_update("apps", remove=["bob"])
group_res = user_group_list()["groups"]
assert group_res["apps"]["members"] == []
def test_update_group_remove_user_not_already_in(mocker):
with message(mocker, "group_user_not_in_group", user="jack", group="apps"):
def test_update_group_remove_user_not_already_in():
with message("group_user_not_in_group", user="jack", group="apps"):
user_group_update("apps", remove=["jack"])
group_res = user_group_list()["groups"]

View file

@ -631,7 +631,7 @@ def user_info(username):
has_value = re.search(r"Value=(\d+)", cmd_result)
if has_value:
storage_use = int(has_value.group(1))
storage_use = int(has_value.group(1)) * 1000
storage_use = binary_to_human(storage_use)
if is_limited:
@ -1189,6 +1189,7 @@ def user_group_update(
)
else:
operation_logger.related_to.append(("user", user))
logger.info(m18n.n("group_user_add", group=groupname, user=user))
new_group_members += users_to_add
@ -1202,6 +1203,7 @@ def user_group_update(
)
else:
operation_logger.related_to.append(("user", user))
logger.info(m18n.n("group_user_remove", group=groupname, user=user))
# Remove users_to_remove from new_group_members
# Kinda like a new_group_members -= users_to_remove
@ -1237,6 +1239,7 @@ def user_group_update(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
new_group_mail.append(mail)
logger.info(m18n.n("group_mailalias_add", group=groupname, mail=mail))
if remove_mailalias:
from yunohost.domain import _get_maindomain
@ -1256,6 +1259,9 @@ def user_group_update(
)
if mail in new_group_mail:
new_group_mail.remove(mail)
logger.info(
m18n.n("group_mailalias_remove", group=groupname, mail=mail)
)
else:
raise YunohostValidationError("mail_alias_remove_failed", mail=mail)

686
src/utils/configpanel.py Normal file
View file

@ -0,0 +1,686 @@
#
# Copyright (c) 2023 YunoHost Contributors
#
# This file is part of YunoHost (see https://yunohost.org)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import glob
import os
import re
import urllib.parse
from collections import OrderedDict
from typing import Union
from moulinette import Moulinette, m18n
from moulinette.interfaces.cli import colorize
from moulinette.utils.filesystem import mkdir, read_toml, read_yaml, write_to_yaml
from moulinette.utils.log import getActionLogger
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.form import (
OPTIONS,
BaseOption,
FileOption,
ask_questions_and_parse_answers,
evaluate_simple_js_expression,
)
from yunohost.utils.i18n import _value_for_locale
logger = getActionLogger("yunohost.configpanel")
CONFIG_PANEL_VERSION_SUPPORTED = 1.0
class ConfigPanel:
entity_type = "config"
save_path_tpl: Union[str, None] = None
config_path_tpl = "/usr/share/yunohost/config_{entity_type}.toml"
save_mode = "full"
@classmethod
def list(cls):
"""
List available config panel
"""
try:
entities = [
re.match(
"^" + cls.save_path_tpl.format(entity="(?p<entity>)") + "$", f
).group("entity")
for f in glob.glob(cls.save_path_tpl.format(entity="*"))
if os.path.isfile(f)
]
except FileNotFoundError:
entities = []
return entities
def __init__(self, entity, config_path=None, save_path=None, creation=False):
self.entity = entity
self.config_path = config_path
if not config_path:
self.config_path = self.config_path_tpl.format(
entity=entity, entity_type=self.entity_type
)
self.save_path = save_path
if not save_path and self.save_path_tpl:
self.save_path = self.save_path_tpl.format(entity=entity)
self.config = {}
self.values = {}
self.new_values = {}
if (
self.save_path
and self.save_mode != "diff"
and not creation
and not os.path.exists(self.save_path)
):
raise YunohostValidationError(
f"{self.entity_type}_unknown", **{self.entity_type: entity}
)
if self.save_path and creation and os.path.exists(self.save_path):
raise YunohostValidationError(
f"{self.entity_type}_exists", **{self.entity_type: entity}
)
# Search for hooks in the config panel
self.hooks = {
func: getattr(self, func)
for func in dir(self)
if callable(getattr(self, func))
and re.match("^(validate|post_ask)__", func)
}
def get(self, key="", mode="classic"):
self.filter_key = key or ""
# Read config panel toml
self._get_config_panel()
if not self.config:
raise YunohostValidationError("config_no_panel")
# Read or get values and hydrate the config
self._get_raw_settings()
self._hydrate()
# In 'classic' mode, we display the current value if key refer to an option
if self.filter_key.count(".") == 2 and mode == "classic":
option = self.filter_key.split(".")[-1]
value = self.values.get(option, None)
option_type = None
for _, _, option_ in self._iterate():
if option_["id"] == option:
option_type = OPTIONS[option_["type"]]
break
return option_type.normalize(value) if option_type else value
# Format result in 'classic' or 'export' mode
logger.debug(f"Formating result in '{mode}' mode")
result = {}
for panel, section, option in self._iterate():
if section["is_action_section"] and mode != "full":
continue
key = f"{panel['id']}.{section['id']}.{option['id']}"
if mode == "export":
result[option["id"]] = option.get("current_value")
continue
ask = None
if "ask" in option:
ask = _value_for_locale(option["ask"])
elif "i18n" in self.config:
ask = m18n.n(self.config["i18n"] + "_" + option["id"])
if mode == "full":
option["ask"] = ask
question_class = OPTIONS[option.get("type", "string")]
# FIXME : maybe other properties should be taken from the question, not just choices ?.
option["choices"] = question_class(option).choices
option["default"] = question_class(option).default
option["pattern"] = question_class(option).pattern
else:
result[key] = {"ask": ask}
if "current_value" in option:
question_class = OPTIONS[option.get("type", "string")]
result[key]["value"] = question_class.humanize(
option["current_value"], option
)
# FIXME: semantics, technically here this is not about a prompt...
if question_class.hide_user_input_in_prompt:
result[key][
"value"
] = "**************" # Prevent displaying password in `config get`
if mode == "full":
return self.config
else:
return result
def set(
self, key=None, value=None, args=None, args_file=None, operation_logger=None
):
self.filter_key = key or ""
# Read config panel toml
self._get_config_panel()
if not self.config:
raise YunohostValidationError("config_no_panel")
if (args is not None or args_file is not None) and value is not None:
raise YunohostValidationError(
"You should either provide a value, or a serie of args/args_file, but not both at the same time",
raw_msg=True,
)
if self.filter_key.count(".") != 2 and value is not None:
raise YunohostValidationError("config_cant_set_value_on_section")
# Import and parse pre-answered options
logger.debug("Import and parse pre-answered options")
self._parse_pre_answered(args, value, args_file)
# Read or get values and hydrate the config
self._get_raw_settings()
self._hydrate()
BaseOption.operation_logger = operation_logger
self._ask()
if operation_logger:
operation_logger.start()
try:
self._apply()
except YunohostError:
raise
# Script got manually interrupted ...
# N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
error = m18n.n("operation_interrupted")
logger.error(m18n.n("config_apply_failed", error=error))
raise
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(m18n.n("config_apply_failed", error=error))
raise
finally:
# Delete files uploaded from API
# FIXME : this is currently done in the context of config panels,
# but could also happen in the context of app install ... (or anywhere else
# where we may parse args etc...)
FileOption.clean_upload_dirs()
self._reload_services()
logger.success("Config updated as expected")
operation_logger.success()
def list_actions(self):
actions = {}
# FIXME : meh, loading the entire config panel is again going to cause
# stupid issues for domain (e.g loading registrar stuff when willing to just list available actions ...)
self.filter_key = ""
self._get_config_panel()
for panel, section, option in self._iterate():
if option["type"] == "button":
key = f"{panel['id']}.{section['id']}.{option['id']}"
actions[key] = _value_for_locale(option["ask"])
return actions
def run_action(self, action=None, args=None, args_file=None, operation_logger=None):
#
# FIXME : this stuff looks a lot like set() ...
#
self.filter_key = ".".join(action.split(".")[:2])
action_id = action.split(".")[2]
# Read config panel toml
self._get_config_panel()
# FIXME: should also check that there's indeed a key called action
if not self.config:
raise YunohostValidationError(f"No action named {action}", raw_msg=True)
# Import and parse pre-answered options
logger.debug("Import and parse pre-answered options")
self._parse_pre_answered(args, None, args_file)
# Read or get values and hydrate the config
self._get_raw_settings()
self._hydrate()
BaseOption.operation_logger = operation_logger
self._ask(action=action_id)
# FIXME: here, we could want to check constrains on
# the action's visibility / requirements wrt to the answer to questions ...
if operation_logger:
operation_logger.start()
try:
self._run_action(action_id)
except YunohostError:
raise
# Script got manually interrupted ...
# N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError):
error = m18n.n("operation_interrupted")
logger.error(m18n.n("config_action_failed", action=action, error=error))
raise
# Something wrong happened in Yunohost's code (most probably hook_exec)
except Exception:
import traceback
error = m18n.n("unexpected_error", error="\n" + traceback.format_exc())
logger.error(m18n.n("config_action_failed", action=action, error=error))
raise
finally:
# Delete files uploaded from API
# FIXME : this is currently done in the context of config panels,
# but could also happen in the context of app install ... (or anywhere else
# where we may parse args etc...)
FileOption.clean_upload_dirs()
# FIXME: i18n
logger.success(f"Action {action_id} successful")
operation_logger.success()
def _get_raw_config(self):
return read_toml(self.config_path)
def _get_config_panel(self):
# Split filter_key
filter_key = self.filter_key.split(".") if self.filter_key != "" else []
if len(filter_key) > 3:
raise YunohostError(
f"The filter key {filter_key} has too many sub-levels, the max is 3.",
raw_msg=True,
)
if not os.path.exists(self.config_path):
logger.debug(f"Config panel {self.config_path} doesn't exists")
return None
toml_config_panel = self._get_raw_config()
# Check TOML config panel is in a supported version
if float(toml_config_panel["version"]) < CONFIG_PANEL_VERSION_SUPPORTED:
logger.error(
f"Config panels version {toml_config_panel['version']} are not supported"
)
return None
# Transform toml format into internal format
format_description = {
"root": {
"properties": ["version", "i18n"],
"defaults": {"version": 1.0},
},
"panels": {
"properties": ["name", "services", "actions", "help"],
"defaults": {
"services": [],
"actions": {"apply": {"en": "Apply"}},
},
},
"sections": {
"properties": ["name", "services", "optional", "help", "visible"],
"defaults": {
"name": "",
"services": [],
"optional": True,
"is_action_section": False,
},
},
"options": {
"properties": [
"ask",
"type",
"bind",
"help",
"example",
"default",
"style",
"icon",
"placeholder",
"visible",
"optional",
"choices",
"yes",
"no",
"pattern",
"limit",
"min",
"max",
"step",
"accept",
"redact",
"filter",
"readonly",
"enabled",
# "confirm", # TODO: to ask confirmation before running an action
],
"defaults": {},
},
}
def _build_internal_config_panel(raw_infos, level):
"""Convert TOML in internal format ('full' mode used by webadmin)
Here are some properties of 1.0 config panel in toml:
- node properties and node children are mixed,
- text are in english only
- some properties have default values
This function detects all children nodes and put them in a list
"""
defaults = format_description[level]["defaults"]
properties = format_description[level]["properties"]
# Start building the ouput (merging the raw infos + defaults)
out = {key: raw_infos.get(key, value) for key, value in defaults.items()}
# Now fill the sublevels (+ apply filter_key)
i = list(format_description).index(level)
sublevel = list(format_description)[i + 1] if level != "options" else None
search_key = filter_key[i] if len(filter_key) > i else False
for key, value in raw_infos.items():
# Key/value are a child node
if (
isinstance(value, OrderedDict)
and key not in properties
and sublevel
):
# We exclude all nodes not referenced by the filter_key
if search_key and key != search_key:
continue
subnode = _build_internal_config_panel(value, sublevel)
subnode["id"] = key
if level == "root":
subnode.setdefault("name", {"en": key.capitalize()})
elif level == "sections":
subnode["name"] = key # legacy
subnode.setdefault("optional", raw_infos.get("optional", True))
# If this section contains at least one button, it becomes an "action" section
if subnode.get("type") == "button":
out["is_action_section"] = True
out.setdefault(sublevel, []).append(subnode)
# Key/value are a property
else:
if key not in properties:
logger.warning(f"Unknown key '{key}' found in config panel")
# Todo search all i18n keys
out[key] = (
value
if key not in ["ask", "help", "name"] or isinstance(value, dict)
else {"en": value}
)
return out
self.config = _build_internal_config_panel(toml_config_panel, "root")
try:
self.config["panels"][0]["sections"][0]["options"][0]
except (KeyError, IndexError):
raise YunohostValidationError(
"config_unknown_filter_key", filter_key=self.filter_key
)
# List forbidden keywords from helpers and sections toml (to avoid conflict)
forbidden_keywords = [
"old",
"app",
"changed",
"file_hash",
"binds",
"types",
"formats",
"getter",
"setter",
"short_setting",
"type",
"bind",
"nothing_changed",
"changes_validated",
"result",
"max_progression",
]
forbidden_keywords += format_description["sections"]
forbidden_readonly_types = ["password", "app", "domain", "user", "file"]
for _, _, option in self._iterate():
if option["id"] in forbidden_keywords:
raise YunohostError("config_forbidden_keyword", keyword=option["id"])
if (
option.get("readonly", False)
and option.get("type", "string") in forbidden_readonly_types
):
raise YunohostError(
"config_forbidden_readonly_type",
type=option["type"],
id=option["id"],
)
return self.config
def _get_default_values(self):
return {
option["id"]: option["default"]
for _, _, option in self._iterate()
if "default" in option
}
def _get_raw_settings(self):
"""
Retrieve entries in YAML file
And set default values if needed
"""
# Inject defaults if needed (using the magic .update() ;))
self.values = self._get_default_values()
# Retrieve entries in the YAML
if os.path.exists(self.save_path) and os.path.isfile(self.save_path):
self.values.update(read_yaml(self.save_path) or {})
def _hydrate(self):
# Hydrating config panel with current value
for _, section, option in self._iterate():
if option["id"] not in self.values:
allowed_empty_types = [
"alert",
"display_text",
"markdown",
"file",
"button",
]
if section["is_action_section"] and option.get("default") is not None:
self.values[option["id"]] = option["default"]
elif (
option["type"] in allowed_empty_types
or option.get("bind") == "null"
):
continue
else:
raise YunohostError(
f"Config panel question '{option['id']}' should be initialized with a value during install or upgrade.",
raw_msg=True,
)
value = self.values[option["name"]]
# Allow to use value instead of current_value in app config script.
# e.g. apps may write `echo 'value: "foobar"'` in the config file (which is more intuitive that `echo 'current_value: "foobar"'`
# For example hotspot used it...
# See https://github.com/YunoHost/yunohost/pull/1546
if (
isinstance(value, dict)
and "value" in value
and "current_value" not in value
):
value["current_value"] = value["value"]
# In general, the value is just a simple value.
# Sometimes it could be a dict used to overwrite the option itself
value = value if isinstance(value, dict) else {"current_value": value}
option.update(value)
self.values[option["id"]] = value.get("current_value")
return self.values
def _ask(self, action=None):
logger.debug("Ask unanswered question and prevalidate data")
if "i18n" in self.config:
for panel, section, option in self._iterate():
if "ask" not in option:
option["ask"] = m18n.n(self.config["i18n"] + "_" + option["id"])
# auto add i18n help text if present in locales
if m18n.key_exists(self.config["i18n"] + "_" + option["id"] + "_help"):
option["help"] = m18n.n(
self.config["i18n"] + "_" + option["id"] + "_help"
)
def display_header(message):
"""CLI panel/section header display"""
if Moulinette.interface.type == "cli" and self.filter_key.count(".") < 2:
Moulinette.display(colorize(message, "purple"))
for panel, section, obj in self._iterate(["panel", "section"]):
if (
section
and section.get("visible")
and not evaluate_simple_js_expression(
section["visible"], context=self.future_values
)
):
continue
# Ugly hack to skip action section ... except when when explicitly running actions
if not action:
if section and section["is_action_section"]:
continue
if panel == obj:
name = _value_for_locale(panel["name"])
display_header(f"\n{'='*40}\n>>>> {name}\n{'='*40}")
else:
name = _value_for_locale(section["name"])
if name:
display_header(f"\n# {name}")
elif section:
# filter action section options in case of multiple buttons
section["options"] = [
option
for option in section["options"]
if option.get("type", "string") != "button"
or option["id"] == action
]
if panel == obj:
continue
# Check and ask unanswered questions
prefilled_answers = self.args.copy()
prefilled_answers.update(self.new_values)
questions = ask_questions_and_parse_answers(
{question["name"]: question for question in section["options"]},
prefilled_answers=prefilled_answers,
current_values=self.values,
hooks=self.hooks,
)
self.new_values.update(
{
question.name: question.value
for question in questions
if question.value is not None
}
)
@property
def future_values(self):
return {**self.values, **self.new_values}
def __getattr__(self, name):
if "new_values" in self.__dict__ and name in self.new_values:
return self.new_values[name]
if "values" in self.__dict__ and name in self.values:
return self.values[name]
return self.__dict__[name]
def _parse_pre_answered(self, args, value, args_file):
args = urllib.parse.parse_qs(args or "", keep_blank_values=True)
self.args = {key: ",".join(value_) for key, value_ in args.items()}
if args_file:
# Import YAML / JSON file but keep --args values
self.args = {**read_yaml(args_file), **self.args}
if value is not None:
self.args = {self.filter_key.split(".")[-1]: value}
def _apply(self):
logger.info("Saving the new configuration...")
dir_path = os.path.dirname(os.path.realpath(self.save_path))
if not os.path.exists(dir_path):
mkdir(dir_path, mode=0o700)
values_to_save = self.future_values
if self.save_mode == "diff":
defaults = self._get_default_values()
values_to_save = {
k: v for k, v in values_to_save.items() if defaults.get(k) != v
}
# Save the settings to the .yaml file
write_to_yaml(self.save_path, values_to_save)
def _reload_services(self):
from yunohost.service import service_reload_or_restart
services_to_reload = set()
for panel, section, obj in self._iterate(["panel", "section", "option"]):
services_to_reload |= set(obj.get("services", []))
services_to_reload = list(services_to_reload)
services_to_reload.sort(key="nginx".__eq__)
if services_to_reload:
logger.info("Reloading services...")
for service in services_to_reload:
if hasattr(self, "entity"):
service = service.replace("__APP__", self.entity)
service_reload_or_restart(service)
def _iterate(self, trigger=["option"]):
for panel in self.config.get("panels", []):
if "panel" in trigger:
yield (panel, None, panel)
for section in panel.get("sections", []):
if "section" in trigger:
yield (panel, section, section)
if "option" in trigger:
for option in section.get("options", []):
yield (panel, section, option)

File diff suppressed because it is too large Load diff

View file

@ -163,32 +163,45 @@ def translate_legacy_default_app_in_ssowant_conf_json_persistent():
LEGACY_PHP_VERSION_REPLACEMENTS = [
("/etc/php5", "/etc/php/7.4"),
("/etc/php/7.0", "/etc/php/7.4"),
("/etc/php/7.3", "/etc/php/7.4"),
("/var/run/php5-fpm", "/var/run/php/php7.4-fpm"),
("/var/run/php/php7.0-fpm", "/var/run/php/php7.4-fpm"),
("/var/run/php/php7.3-fpm", "/var/run/php/php7.4-fpm"),
("php5", "php7.4"),
("php7.0", "php7.4"),
("php7.3", "php7.4"),
('YNH_PHP_VERSION="7.3"', 'YNH_PHP_VERSION="7.4"'),
("/etc/php5", "/etc/php/8.2"),
("/etc/php/7.0", "/etc/php/8.2"),
("/etc/php/7.3", "/etc/php/8.2"),
("/etc/php/7.4", "/etc/php/8.2"),
("/var/run/php5-fpm", "/var/run/php/php8.2-fpm"),
("/var/run/php/php7.0-fpm", "/var/run/php/php8.2-fpm"),
("/var/run/php/php7.3-fpm", "/var/run/php/php8.2-fpm"),
("/var/run/php/php7.4-fpm", "/var/run/php/php8.2-fpm"),
("php5", "php8.2"),
("php7.0", "php8.2"),
("php7.3", "php8.2"),
("php7.4", "php8.2"),
('YNH_PHP_VERSION="7.3"', 'YNH_PHP_VERSION="8.2"'),
('YNH_PHP_VERSION="7.4"', 'YNH_PHP_VERSION="8.2"'),
(
'phpversion="${phpversion:-7.0}"',
'phpversion="${phpversion:-7.4}"',
'phpversion="${phpversion:-8.2}"',
), # Many helpers like the composer ones use 7.0 by default ...
(
'phpversion="${phpversion:-7.3}"',
'phpversion="${phpversion:-8.2}"',
), # Many helpers like the composer ones use 7.0 by default ...
(
'phpversion="${phpversion:-7.4}"',
'phpversion="${phpversion:-8.2}"',
), # Many helpers like the composer ones use 7.0 by default ...
(
'"$phpversion" == "7.0"',
'$(bc <<< "$phpversion >= 7.4") -eq 1',
'$(bc <<< "$phpversion >= 8.2") -eq 1',
), # patch ynh_install_php to refuse installing/removing php <= 7.3
(
'"$phpversion" == "7.3"',
'$(bc <<< "$phpversion >= 7.4") -eq 1',
'$(bc <<< "$phpversion >= 8.2") -eq 1',
), # patch ynh_install_php to refuse installing/removing php <= 7.3
(
'"$phpversion" == "7.4"',
'$(bc <<< "$phpversion >= 8.2") -eq 1',
), # patch ynh_install_php to refuse installing/removing php <= 7.3
]
@ -217,15 +230,16 @@ def _patch_legacy_php_versions(app_folder):
def _patch_legacy_php_versions_in_settings(app_folder):
settings = read_yaml(os.path.join(app_folder, "settings.yml"))
if settings.get("fpm_config_dir") in ["/etc/php/7.0/fpm", "/etc/php/7.3/fpm"]:
settings["fpm_config_dir"] = "/etc/php/7.4/fpm"
if settings.get("fpm_service") in ["php7.0-fpm", "php7.3-fpm"]:
settings["fpm_service"] = "php7.4-fpm"
if settings.get("phpversion") in ["7.0", "7.3"]:
settings["phpversion"] = "7.4"
if settings.get("fpm_config_dir") in ["/etc/php/7.0/fpm", "/etc/php/7.3/fpm", "/etc/php/7.4/fpm"]:
settings["fpm_config_dir"] = "/etc/php/8.2/fpm"
if settings.get("fpm_service") in ["php7.0-fpm", "php7.3-fpm", "php7.4-fpm"]:
settings["fpm_service"] = "php8.2-fpm"
if settings.get("phpversion") in ["7.0", "7.3", "7.4"]:
settings["phpversion"] = "8.2"
# We delete these checksums otherwise the file will appear as manually modified
list_to_remove = [
"checksum__etc_php_7.4_fpm_pool",
"checksum__etc_php_7.3_fpm_pool",
"checksum__etc_php_7.0_fpm_pool",
"checksum__etc_nginx_conf.d",

View file

@ -21,6 +21,7 @@ import copy
import shutil
import random
import tempfile
import subprocess
from typing import Dict, Any, List
from moulinette import m18n
@ -30,7 +31,7 @@ from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file
from moulinette.utils.filesystem import (
rm,
)
from yunohost.utils.system import system_arch
from yunohost.utils.error import YunohostError, YunohostValidationError
logger = getActionLogger("yunohost.app_resources")
@ -258,6 +259,201 @@ ynh_abort_if_errors
# print(ret)
class SourcesResource(AppResource):
"""
Declare what are the sources / assets used by this app. Typically, this corresponds to some tarball published by the upstream project, that needs to be downloaded and extracted in the install dir using the ynh_setup_source helper.
This resource is intended both to declare the assets, which will be parsed by ynh_setup_source during the app script runtime, AND to prefetch and validate the sha256sum of those asset before actually running the script, to be able to report an error early when the asset turns out to not be available for some reason.
Various options are available to accomodate the behavior according to the asset structure
##### Example
```toml
[resources.sources]
[resources.sources.main]
url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz"
sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"
autoupdate.strategy = "latest_github_tag"
```
Or more complex examples with several element, including one with asset that depends on the arch
```toml
[resources.sources]
[resources.sources.main]
in_subdir = false
amd64.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.amd64.tar.gz"
amd64.sha256 = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"
i386.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.386.tar.gz"
i386.sha256 = "53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3"
armhf.url = "https://github.com/foo/bar/archive/refs/tags/v1.2.3.arm.tar.gz"
armhf.sha256 = "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865"
autoupdate.strategy = "latest_github_release"
autoupdate.asset.amd64 = ".*\\.amd64.tar.gz"
autoupdate.asset.i386 = ".*\\.386.tar.gz"
autoupdate.asset.armhf = ".*\\.arm.tar.gz"
[resources.sources.zblerg]
url = "https://zblerg.com/download/zblerg"
sha256 = "1121cfccd5913f0a63fec40a6ffd44ea64f9dc135c66634ba001d10bcf4302a2"
format = "script"
rename = "zblerg.sh"
```
##### Properties (for each source)
- `prefetch` : `true` (default) or `false`, wether or not to pre-fetch this asset during the provisioning phase of the resource. If several arch-dependent url are provided, YunoHost will only prefetch the one for the current system architecture.
- `url` : the asset's URL
- If the asset's URL depend on the architecture, you may instead provide `amd64.url`, `i386.url`, `armhf.url` and `arm64.url` (depending on what architectures are supported), using the same `dpkg --print-architecture` nomenclature as for the supported architecture key in the manifest
- `sha256` : the asset's sha256sum. This is used both as an integrity check, and as a layer of security to protect against malicious actors which could have injected malicious code inside the asset...
- Same as `url` : if the asset's URL depend on the architecture, you may instead provide `amd64.sha256`, `i386.sha256`, ...
- `format` : The "format" of the asset. It is typically automatically guessed from the extension of the URL (or the mention of "tarball", "zipball" in the URL), but can be set explicitly:
- `tar.gz`, `tar.xz`, `tar.bz2` : will use `tar` to extract the archive
- `zip` : will use `unzip` to extract the archive
- `docker` : useful to extract files from an already-built docker image (instead of rebuilding them locally). Will use `docker-image-extract`
- `whatever`: whatever arbitrary value, not really meaningful except to imply that the file won't be extracted (eg because it's a .deb to be manually installed with dpkg/apt, or a script, or ...)
- `in_subdir`: `true` (default) or `false`, depending on if there's an intermediate subdir in the archive before accessing the actual files. Can also be `N` (an integer) to handle special cases where there's `N` level of subdir to get rid of to actually access the files
- `extract` : `true` or `false`. Defaults to `true` for archives such as `zip`, `tar.gz`, `tar.bz2`, ... Or defaults to `false` when `format` is not something that should be extracted. When `extract = false`, the file will only be `mv`ed to the location, possibly renamed using the `rename` value
- `rename`: some string like `whatever_your_want`, to be used for convenience when `extract` is `false` and the default name of the file is not practical
- `platform`: for example `linux/amd64` (defaults to `linux/$YNH_ARCH`) to be used in conjonction with `format = "docker"` to specify which architecture to extract for
###### Regarding `autoupdate`
Strictly speaking, this has nothing to do with the actual app install. `autoupdate` is expected to contain metadata for automatic maintenance / update of the app sources info in the manifest. It is meant to be a simpler replacement for "autoupdate" Github workflow mechanism.
The infos are used by this script : https://github.com/YunoHost/apps/blob/master/tools/autoupdate_app_sources/autoupdate_app_sources.py which is ran by the YunoHost infrastructure periodically and will create the corresponding pull request automatically.
The script will rely on the code repo specified in the upstream section of the manifest.
`autoupdate.strategy` is expected to be one of :
- `latest_github_tag` : look for the latest tag (by sorting tags and finding the "largest" version). Then using the corresponding tar.gz url. Tags containing `rc`, `beta`, `alpha`, `start` are ignored, and actually any tag which doesn't look like `x.y.z` or `vx.y.z`
- `latest_github_release` : similar to `latest_github_tags`, but starting from the list of releases. Pre- or draft releases are ignored. Releases may have assets attached to them, in which case you can define:
- `autoupdate.asset = "some regex"` (when there's only one asset to use). The regex is used to find the appropriate asset among the list of all assets
- or several `autoupdate.asset.$arch = "some_regex"` (when the asset is arch-specific). The regex is used to find the appropriate asset for the specific arch among the list of assets
- `latest_github_commit` : will use the latest commit on github, and the corresponding tarball. If this is used for the 'main' source, it will also assume that the version is YYYY.MM.DD corresponding to the date of the commit.
It is also possible to define `autoupdate.upstream` to use a different Git(hub) repository instead of the code repository from the upstream section of the manifest. This can be useful when, for example, the app uses other assets such as plugin from a different repository.
##### Provision/Update
- For elements with `prefetch = true`, will download the asset (for the appropriate architecture) and store them in `/var/cache/yunohost/download/$app/$source_id`, to be later picked up by `ynh_setup_source`. (NB: this only happens during install and upgrade, not restore)
##### Deprovision
- Nothing (just cleanup the cache)
"""
type = "sources"
priority = 10
default_sources_properties: Dict[str, Any] = {
"prefetch": True,
"url": None,
"sha256": None,
}
sources: Dict[str, Dict[str, Any]] = {}
def __init__(self, properties: Dict[str, Any], *args, **kwargs):
for source_id, infos in properties.items():
properties[source_id] = copy.copy(self.default_sources_properties)
properties[source_id].update(infos)
super().__init__({"sources": properties}, *args, **kwargs)
def deprovision(self, context: Dict = {}):
if os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"):
rm(f"/var/cache/yunohost/download/{self.app}/", recursive=True)
def provision_or_update(self, context: Dict = {}):
# Don't prefetch stuff during restore
if context.get("action") == "restore":
return
for source_id, infos in self.sources.items():
if not infos["prefetch"]:
continue
if infos["url"] is None:
arch = system_arch()
if (
arch in infos
and isinstance(infos[arch], dict)
and isinstance(infos[arch].get("url"), str)
and isinstance(infos[arch].get("sha256"), str)
):
self.prefetch(source_id, infos[arch]["url"], infos[arch]["sha256"])
else:
raise YunohostError(
f"In resources.sources: it looks like you forgot to define url/sha256 or {arch}.url/{arch}.sha256",
raw_msg=True,
)
else:
if infos["sha256"] is None:
raise YunohostError(
f"In resources.sources: it looks like the sha256 is missing for {source_id}",
raw_msg=True,
)
self.prefetch(source_id, infos["url"], infos["sha256"])
def prefetch(self, source_id, url, expected_sha256):
logger.debug(f"Prefetching asset {source_id}: {url} ...")
if not os.path.isdir(f"/var/cache/yunohost/download/{self.app}/"):
mkdir(f"/var/cache/yunohost/download/{self.app}/", parents=True)
filename = f"/var/cache/yunohost/download/{self.app}/{source_id}"
# NB: we use wget and not requests.get() because we want to output to a file (ie avoid ending up with the full archive in RAM)
# AND the nice --tries, --no-dns-cache, --timeout options ...
p = subprocess.Popen(
[
"/usr/bin/wget",
"--tries=3",
"--no-dns-cache",
"--timeout=900",
"--no-verbose",
"--output-document=" + filename,
url,
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
out, _ = p.communicate()
returncode = p.returncode
if returncode != 0:
if os.path.exists(filename):
rm(filename)
raise YunohostError(
"app_failed_to_download_asset",
source_id=source_id,
url=url,
app=self.app,
out=out.decode(),
)
assert os.path.exists(
filename
), f"For some reason, wget worked but {filename} doesnt exists?"
computed_sha256 = check_output(f"sha256sum {filename}").split()[0]
if computed_sha256 != expected_sha256:
size = check_output(f"du -hs {filename}").split()[0]
rm(filename)
raise YunohostError(
"app_corrupt_source",
source_id=source_id,
url=url,
app=self.app,
expected_sha256=expected_sha256,
computed_sha256=computed_sha256,
size=size,
)
class PermissionsResource(AppResource):
"""
Configure the SSO permissions/tiles. Typically, webapps are expected to have a 'main' permission mapped to '/', meaning that a tile pointing to the `$domain/$path` will be available in the SSO for users allowed to access that app.
@ -266,7 +462,7 @@ class PermissionsResource(AppResource):
The list of allowed user/groups may be initialized using the content of the `init_{perm}_permission` question from the manifest, hence `init_main_permission` replaces the `is_public` question and shall contain a group name (typically, `all_users` or `visitors`).
##### Example:
##### Example
```toml
[resources.permissions]
main.url = "/"
@ -277,7 +473,7 @@ class PermissionsResource(AppResource):
admin.allowed = "admins" # Assuming the "admins" group exists (cf future developments ;))
```
##### Properties (for each perm name):
##### Properties (for each perm name)
- `url`: The relative URI corresponding to this permission. Typically `/` or `/something`. This property may be omitted for non-web permissions.
- `show_tile`: (default: `true` if `url` is defined) Wether or not a tile should be displayed for that permission in the user portal
- `allowed`: (default: nobody) The group initially allowed to access this perm, if `init_{perm}_permission` is not defined in the manifest questions. Note that the admin may tweak who is allowed/unallowed on that permission later on, this is only meant to **initialize** the permission.
@ -285,14 +481,14 @@ class PermissionsResource(AppResource):
- `protected`: (default: `false`) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'.
- `additional_urls`: (default: none) List of additional URL for which access will be allowed/forbidden
##### Provision/Update:
##### Provision/Update
- Delete any permissions that may exist and be related to this app yet is not declared anymore
- Loop over the declared permissions and create them if needed or update them with the new values
##### Deprovision:
##### Deprovision
- Delete all permission related to this app
##### Legacy management:
##### Legacy management
- Legacy `is_public` setting will be deleted if it exists
"""
@ -320,32 +516,65 @@ class PermissionsResource(AppResource):
def __init__(self, properties: Dict[str, Any], *args, **kwargs):
# FIXME : if url != None, we should check that there's indeed a domain/path defined ? ie that app is a webapp
# Validate packager-provided infos
for perm, infos in properties.items():
if "auth_header" in infos and not isinstance(
infos.get("auth_header"), bool
):
raise YunohostError(
f"In manifest, for permission '{perm}', 'auth_header' should be a boolean",
raw_msg=True,
)
if "show_tile" in infos and not isinstance(infos.get("show_tile"), bool):
raise YunohostError(
f"In manifest, for permission '{perm}', 'show_tile' should be a boolean",
raw_msg=True,
)
if "protected" in infos and not isinstance(infos.get("protected"), bool):
raise YunohostError(
f"In manifest, for permission '{perm}', 'protected' should be a boolean",
raw_msg=True,
)
if "additional_urls" in infos and not isinstance(
infos.get("additional_urls"), list
):
raise YunohostError(
f"In manifest, for permission '{perm}', 'additional_urls' should be a list",
raw_msg=True,
)
if "main" not in properties:
properties["main"] = copy.copy(self.default_perm_properties)
for perm, infos in properties.items():
properties[perm] = copy.copy(self.default_perm_properties)
properties[perm].update(infos)
if properties[perm]["show_tile"] is None:
properties[perm]["show_tile"] = bool(properties[perm]["url"])
if (
if properties["main"]["url"] is not None and (
not isinstance(properties["main"].get("url"), str)
or properties["main"]["url"] != "/"
):
raise YunohostError(
"URL for the 'main' permission should be '/' for webapps (or undefined/None for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/",
"URL for the 'main' permission should be '/' for webapps (or left undefined for non-webapps). Note that / refers to the install url of the app, i.e $domain.tld/$path/",
raw_msg=True,
)
super().__init__({"permissions": properties}, *args, **kwargs)
from yunohost.app import _get_app_settings, _hydrate_app_template
settings = _get_app_settings(self.app)
for perm, infos in self.permissions.items():
if infos.get("url") and "__DOMAIN__" in infos.get("url", ""):
infos["url"] = infos["url"].replace(
"__DOMAIN__", self.get_setting("domain")
)
infos["additional_urls"] = [
u.replace("__DOMAIN__", self.get_setting("domain"))
for u in infos.get("additional_urls", [])
]
if infos.get("url") and "__" in infos.get("url"):
infos["url"] = _hydrate_app_template(infos["url"], settings)
if infos.get("additional_urls"):
infos["additional_urls"] = [
_hydrate_app_template(url, settings)
for url in infos["additional_urls"]
]
def provision_or_update(self, context: Dict = {}):
from yunohost.permission import (
@ -441,22 +670,22 @@ class SystemuserAppResource(AppResource):
"""
Provision a system user to be used by the app. The username is exactly equal to the app id
##### Example:
##### Example
```toml
[resources.system_user]
# (empty - defaults are usually okay)
```
##### Properties:
##### Properties
- `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user
- `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user
- `home`: (default: `/var/www/__APP__`) Defines the home property for this user. NB: unfortunately you can't simply use `__INSTALL_DIR__` or `__DATA_DIR__` for now
##### Provision/Update:
##### Provision/Update
- will create the system user if it doesn't exists yet
- will add/remove the ssh/sftp.app groups
##### Deprovision:
##### Deprovision
- deletes the user and group
"""
@ -549,28 +778,28 @@ class InstalldirAppResource(AppResource):
"""
Creates a directory to be used by the app as the installation directory, typically where the app sources and assets are located. The corresponding path is stored in the settings as `install_dir`
##### Example:
##### Example
```toml
[resources.install_dir]
# (empty - defaults are usually okay)
```
##### Properties:
##### Properties
- `dir`: (default: `/var/www/__APP__`) The full path of the install dir
- `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the install dir
- `group`: (default: `__APP__:rx`) The group (and group permissions) for the install dir
##### Provision/Update:
##### Provision/Update
- during install, the folder will be deleted if it already exists (FIXME: is this what we want?)
- if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location
- otherwise, creates the directory if it doesn't exists yet
- (re-)apply permissions (only on the folder itself, not recursively)
- save the value of `dir` as `install_dir` in the app's settings, which can be then used by the app scripts (`$install_dir`) and conf templates (`__INSTALL_DIR__`)
##### Deprovision:
##### Deprovision
- recursively deletes the directory if it exists
##### Legacy management:
##### Legacy management
- In the past, the setting was called `final_path`. The code will automatically rename it as `install_dir`.
- As explained in the 'Provision/Update' section, the folder will also be moved if the location changed
@ -664,28 +893,30 @@ class DatadirAppResource(AppResource):
"""
Creates a directory to be used by the app as the data store directory, typically where the app multimedia or large assets added by users are located. The corresponding path is stored in the settings as `data_dir`. This resource behaves very similarly to install_dir.
##### Example:
##### Example
```toml
[resources.data_dir]
# (empty - defaults are usually okay)
```
##### Properties:
##### Properties
- `dir`: (default: `/home/yunohost.app/__APP__`) The full path of the data dir
- `subdirs`: (default: empty list) A list of subdirs to initialize inside the data dir. For example, `['foo', 'bar']`
- `owner`: (default: `__APP__:rwx`) The owner (and owner permissions) for the data dir
- `group`: (default: `__APP__:rx`) The group (and group permissions) for the data dir
##### Provision/Update:
##### Provision/Update
- if the dir path changed and a folder exists at the old location, the folder will be `mv`'ed to the new location
- otherwise, creates the directory if it doesn't exists yet
- (re-)apply permissions (only on the folder itself, not recursively)
- create each subdir declared and which do not exist already
- (re-)apply permissions (only on the folder itself and declared subdirs, not recursively)
- save the value of `dir` as `data_dir` in the app's settings, which can be then used by the app scripts (`$data_dir`) and conf templates (`__DATA_DIR__`)
##### Deprovision:
##### Deprovision
- (only if the purge option is chosen by the user) recursively deletes the directory if it exists
- also delete the corresponding setting
##### Legacy management:
##### Legacy management
- In the past, the setting may have been called `datadir`. The code will automatically rename it as `data_dir`.
- As explained in the 'Provision/Update' section, the folder will also be moved if the location changed
@ -701,11 +932,13 @@ class DatadirAppResource(AppResource):
default_properties: Dict[str, Any] = {
"dir": "/home/yunohost.app/__APP__",
"subdirs": [],
"owner": "__APP__:rwx",
"group": "__APP__:rx",
}
dir: str = ""
subdirs: list = []
owner: str = ""
group: str = ""
@ -727,7 +960,12 @@ class DatadirAppResource(AppResource):
)
shutil.move(current_data_dir, self.dir)
else:
mkdir(self.dir)
mkdir(self.dir, parents=True)
for subdir in self.subdirs:
full_path = os.path.join(self.dir, subdir)
if not os.path.isdir(full_path):
mkdir(full_path, parents=True)
owner, owner_perm = self.owner.split(":")
group, group_perm = self.group.split(":")
@ -747,6 +985,10 @@ class DatadirAppResource(AppResource):
# in which case we want to apply the perm to the pointed dir, not to the symlink
chmod(os.path.realpath(self.dir), perm_octal)
chown(os.path.realpath(self.dir), owner, group)
for subdir in self.subdirs:
full_path = os.path.join(self.dir, subdir)
chmod(os.path.realpath(full_path), perm_octal)
chown(os.path.realpath(full_path), owner, group)
self.set_setting("data_dir", self.dir)
self.delete_setting("datadir") # Legacy
@ -766,7 +1008,7 @@ class AptDependenciesAppResource(AppResource):
"""
Create a virtual package in apt, depending on the list of specified packages that the app needs. The virtual packages is called `$app-ynh-deps` (with `_` being replaced by `-` in the app name, see `ynh_install_app_dependencies`)
##### Example:
##### Example
```toml
[resources.apt]
packages = "nyancat, lolcat, sl"
@ -777,16 +1019,16 @@ class AptDependenciesAppResource(AppResource):
extras.yarn.packages = "yarn"
```
##### Properties:
##### Properties
- `packages`: Comma-separated list of packages to be installed via `apt`
- `packages_from_raw_bash`: A multi-line bash snippet (using triple quotes as open/close) which should echo additional packages to be installed. Meant to be used for packages to be conditionally installed depending on architecture, debian version, install questions, or other logic.
- `extras`: A dict of (repo, key, packages) corresponding to "extra" repositories to fetch dependencies from
##### Provision/Update:
##### Provision/Update
- The code literally calls the bash helpers `ynh_install_app_dependencies` and `ynh_install_extra_app_dependencies`, similar to what happens in v1.
- Note that when `packages` contains some phpX.Y-foobar dependencies, this will automagically define a `phpversion` setting equal to `X.Y` which can therefore be used in app scripts ($phpversion) or templates (`__PHPVERSION__`)
##### Deprovision:
##### Deprovision
- The code literally calls the bash helper `ynh_remove_app_dependencies`
"""
@ -845,7 +1087,7 @@ class PortsResource(AppResource):
Note that because multiple ports can be booked, each properties is prefixed by the name of the port. `main` is a special name and will correspond to the setting `$port`, whereas for example `xmpp_client` will correspond to the setting `$port_xmpp_client`.
##### Example:
##### Example
```toml
[resources.ports]
# (empty should be fine for most apps... though you can customize stuff if absolutely needed)
@ -857,21 +1099,21 @@ class PortsResource(AppResource):
xmpp_client.exposed = "TCP" # here, we're telling that the port needs to be publicly exposed on TCP on the firewall
```
##### Properties (for every port name):
##### Properties (for every port name)
- `default`: The prefered value for the port. If this port is already being used by another process right now, or is booked in another app's setting, the code will increment the value until it finds a free port and store that value as the setting. If no value is specified, a random value between 10000 and 60000 is used.
- `exposed`: (default: `false`) Wether this port should be opened on the firewall and be publicly reachable. This should be kept to `false` for the majority of apps than only need a port for internal reverse-proxying! Possible values: `false`, `true`(=`Both`), `Both`, `TCP`, `UDP`. This will result in the port being opened on the firewall, and the diagnosis checking that a program answers on that port.
- `fixed`: (default: `false`) Tells that the app absolutely needs the specific value provided in `default`, typically because it's needed for a specific protocol
##### Provision/Update (for every port name):
##### Provision/Update (for every port name)
- If not already booked, look for a free port, starting with the `default` value (or a random value between 10000 and 60000 if no `default` set)
- If `exposed` is not `false`, open the port in the firewall accordingly - otherwise make sure it's closed.
- The value of the port is stored in the `$port` setting for the `main` port, or `$port_NAME` for other `NAME`s
##### Deprovision:
##### Deprovision
- Close the ports on the firewall if relevant
- Deletes all the port settings
##### Legacy management:
##### Legacy management
- In the past, some settings may have been named `NAME_port` instead of `port_NAME`, in which case the code will automatically rename the old setting.
"""
@ -913,8 +1155,8 @@ class PortsResource(AppResource):
% port
)
# This second command is mean to cover (most) case where an app is using a port yet ain't currently using it for some reason (typically service ain't up)
cmd2 = f"grep --quiet \"port: '{port}'\" /etc/yunohost/apps/*/settings.yml"
return os.system(cmd1) == 0 and os.system(cmd2) == 0
cmd2 = f"grep --quiet --extended-regexp \"port: '?{port}'?\" /etc/yunohost/apps/*/settings.yml"
return os.system(cmd1) == 0 or os.system(cmd2) == 0
def provision_or_update(self, context: Dict = {}):
from yunohost.firewall import firewall_allow, firewall_disallow
@ -974,25 +1216,25 @@ class DatabaseAppResource(AppResource):
NB2: no automagic migration will happen in an suddenly change `type` from `mysql` to `postgresql` or viceversa in its life
##### Example:
##### Example
```toml
[resources.database]
type = "mysql" # or : "postgresql". Only these two values are supported
```
##### Properties:
##### Properties
- `type`: The database type, either `mysql` or `postgresql`
##### Provision/Update:
##### Provision/Update
- (Re)set the `$db_name` and `$db_user` settings with the sanitized app name (replacing `-` and `.` with `_`)
- If `$db_pwd` doesn't already exists, pick a random database password and store it in that setting
- If the database doesn't exists yet, create the SQL user and DB using `ynh_mysql_create_db` or `ynh_psql_create_db`.
##### Deprovision:
##### Deprovision
- Drop the DB using `ynh_mysql_remove_db` or `ynh_psql_remove_db`
- Deletes the `db_name`, `db_user` and `db_pwd` settings
##### Legacy management:
##### Legacy management
- In the past, the sql passwords may have been named `mysqlpwd` or `psqlpwd`, in which case it will automatically be renamed as `db_pwd`
"""

18
tox.ini
View file

@ -1,15 +1,15 @@
[tox]
envlist = py39-{lint,invalidcode},py39-black-{run,check}
envlist = py311-{lint,invalidcode},py311-black-{run,check}
[testenv]
skip_install=True
deps =
py39-{lint,invalidcode}: flake8
py39-black-{run,check}: black
py39-mypy: mypy >= 0.900
py311-{lint,invalidcode}: flake8
py311-black-{run,check}: black
py311-mypy: mypy >= 0.900
commands =
py39-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude src/vendor
py39-invalidcode: flake8 src bin maintenance --exclude src/tests,src/vendor --select F,E722,W605
py39-black-check: black --check --diff bin src doc maintenance tests
py39-black-run: black bin src doc maintenance tests
py39-mypy: mypy --ignore-missing-import --install-types --non-interactive --follow-imports silent src/ --exclude (acme_tiny|migrations)
py311-lint: flake8 src doc maintenance tests --ignore E402,E501,E203,W503,E741 --exclude src/vendor
py311-invalidcode: flake8 src bin maintenance --exclude src/tests,src/vendor --select F,E722,W605
py311-black-check: black --check --diff bin src doc maintenance tests
py311-black-run: black bin src doc maintenance tests
py311-mypy: mypy --ignore-missing-import --install-types --non-interactive --follow-imports silent src/ --exclude (acme_tiny|migrations)