diff --git a/data/helpers.d/apt b/data/helpers.d/apt index e77fb8a5c..cea850f6e 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -208,6 +208,8 @@ ynh_package_install_from_equivs() { ynh_package_is_installed "$pkgname" } +YNH_INSTALL_APP_DEPENDENCIES_REPLACE="true" + # Define and install dependencies with a equivs control file # # This helper can/should only be called once per app @@ -245,19 +247,57 @@ ynh_install_app_dependencies() { dependencies="$(echo "$dependencies" | sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g')" fi + # Check for specific php dependencies which requires sury + # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" + # The (?<=php) syntax corresponds to lookbehind ;) + local specific_php_version=$(echo $dependencies | grep -oP '(?<=php)[0-9.]+(?=-|\>)' | sort -u) + + # Ignore case where the php version found is the one available in debian vanilla + [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" + + if [[ -n "$specific_php_version" ]] + then + # Cover a small edge case where a packager could have specified "php7.4-pwet php5-gni" which is confusing + [[ $(echo $specific_php_version | wc -l) -eq 1 ]] \ + || ynh_die --message="Inconsistent php versions in dependencies ... found : $specific_php_version" + + dependencies+=", php${specific_php_version}, php${specific_php_version}-fpm, php${specific_php_version}-common" + + ynh_add_sury + fi + + + # The first time we run ynh_install_app_dependencies, we will replace the + # entire control file (This is in particular meant to cover the case of + # upgrade script where ynh_install_app_dependencies is called with this + # expected effect) Otherwise, any subsequent call will add dependencies + # to those already present in the equivs control file. + if [[ $YNH_INSTALL_APP_DEPENDENCIES_REPLACE == "true" ]] + then + YNH_INSTALL_APP_DEPENDENCIES_REPLACE="false" + else + local current_dependencies="" + if ynh_package_is_installed --package="${dep_app}-ynh-deps" + then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " + current_dependencies=${current_dependencies// | /|} + fi + dependencies="$current_dependencies, $dependencies" + fi + # # Epic ugly hack to fix the goddamn dependency nightmare of sury # Sponsored by the "Djeezusse Fokin Kraiste Why Do Adminsys Has To Be So Fucking Complicated I Should Go Grow Potatoes Instead Of This Shit" collective # https://github.com/YunoHost/issues/issues/1407 # # If we require to install php dependency - if echo $dependencies | grep --quiet 'php'; then + if grep --quiet 'php' <<< "$dependencies"; then # And we have packages from sury installed (7.0.33-10+weirdshiftafter instead of 7.0.33-0 on debian) if dpkg --list | grep "php7.0" | grep --quiet --invert-match "7.0.33-0+deb9"; then # And sury ain't already in sources.lists if ! grep --recursive --quiet "^ *deb.*sury" /etc/apt/sources.list*; then # Re-add sury - ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 + ynh_add_sury fi fi fi @@ -275,14 +315,43 @@ EOF ynh_package_install_from_equivs /tmp/${dep_app}-ynh-deps.control \ || ynh_die --message="Unable to install dependencies" # Install the fake package and its dependencies rm /tmp/${dep_app}-ynh-deps.control + ynh_app_setting_set --app=$app --key=apt_dependencies --value="$dependencies" + + if [[ -n "$specific_php_version" ]] + then + # Set the default php version back as the default version for php-cli. + update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION + + # Store phpversion into the config of this app + ynh_app_setting_set --app=$app --key=phpversion --value=$specific_php_version + + # Integrate new php-fpm service in yunohost + yunohost service add php${specific_php_version}-fpm --log "/var/log/php${phpversion}-fpm.log" + elif grep --quiet 'php' <<< "$dependencies"; then + # Store phpversion into the config of this app + ynh_app_setting_set --app=$app --key=phpversion --value=$YNH_DEFAULT_PHP_VERSION + fi } +# Add sury repository with adequate pin strategy +# +# [internal] +# +# usage: ynh_add_sury +# +ynh_add_sury() { + + # Add an extra repository for those packages + ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 + +} + + # Add dependencies to install with ynh_install_app_dependencies # # usage: ynh_add_app_dependencies --package=phpversion [--replace] # | arg: -p, --package= - Packages to add as dependencies for the app. -# | arg: -r, --replace - Replace dependencies instead of adding to existing ones. # # Requires YunoHost version 3.8.1 or higher. ynh_add_app_dependencies() { @@ -290,22 +359,11 @@ ynh_add_app_dependencies() { local legacy_args=pr local -A args_array=([p]=package= [r]=replace) local package - local replace # Manage arguments with getopts ynh_handle_getopts_args "$@" - replace=${replace:-0} - local current_dependencies="" - if [ $replace -eq 0 ]; then - local dep_app=${app//_/-} # Replace all '_' by '-' - if ynh_package_is_installed --package="${dep_app}-ynh-deps"; then - current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " - fi - - current_dependencies=${current_dependencies// | /|} - fi - - ynh_install_app_dependencies "${current_dependencies}${package}" + ynh_print_warn --message="Packagers: ynh_add_app_dependencies is deprecated and is now only an alias to ynh_install_app_dependencies" + ynh_install_app_dependencies "${package}" } # Remove fake package and its dependencies @@ -316,8 +374,25 @@ ynh_add_app_dependencies() { # # Requires YunoHost version 2.6.4 or higher. ynh_remove_app_dependencies() { - local dep_app=${app//_/-} # Replace all '_' by '-' - ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. + local dep_app=${app//_/-} # Replace all '_' by '-' + + local current_dependencies="" + if ynh_package_is_installed --package="${dep_app}-ynh-deps"; then + current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${dep_app}-ynh-deps) " + current_dependencies=${current_dependencies// | /|} + fi + + ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. + + # Check if this app used a specific php version ... in which case we check + # if the corresponding php-fpm is still there. Otherwise, we remove the + # service from yunohost as well + + local specific_php_version=$(echo $current_dependencies | tr '-' ' ' | grep -o -E "\" | sed 's/php//g' | sort | uniq) + [[ "$specific_php_version" != "$YNH_DEFAULT_PHP_VERSION" ]] || specific_php_version="" + if [[ -n "$specific_php_version" ]] && ! ynh_package_is_installed --package="php${specific_php_version}-fpm"; then + yunohost service remove php${specific_php_version}-fpm + fi } # Install packages from an extra repository properly. @@ -350,7 +425,7 @@ ynh_install_extra_app_dependencies() { ynh_install_extra_repo --repo="$repo" $key --priority=995 --name=$name # Install requested dependencies from this extra repository. - ynh_add_app_dependencies --package="$package" + ynh_install_app_dependencies "$package" # Remove this extra repository after packages are installed ynh_remove_extra_repo --name=$app diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate index 6f9726beb..80b761711 100644 --- a/data/helpers.d/logrotate +++ b/data/helpers.d/logrotate @@ -90,6 +90,11 @@ $logfile { EOF mkdir --parents $(dirname "$logfile") # Create the log directory, if not exist cat ${app}-logrotate | $customtee /etc/logrotate.d/$app >/dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) + + if ynh_user_exists --username="$app"; then + chown $app:$app "$logfile" + chmod o-rwx "$logfile" + fi } # Remove the app's logrotate config. diff --git a/data/helpers.d/php b/data/helpers.d/php index 0aff50ee3..407d60240 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -107,7 +107,7 @@ ynh_add_fpm_config() { ynh_install_php --phpversion="$phpversion" "$additionnal_packages" elif [ -n "$package" ]; then # Install the additionnal packages from the default repository - ynh_add_app_dependencies --package="$package" + ynh_install_app_dependencies "$package" fi if [ $dedicated_service -eq 1 ]; then @@ -313,35 +313,12 @@ ynh_install_php() { ynh_handle_getopts_args "$@" package=${package:-} - # Store phpversion into the config of this app - ynh_app_setting_set $app phpversion $phpversion - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]; then ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION" fi - # Create the file if doesn't exist already - touch /etc/php/ynh_app_version - - # Do not add twice the same line - if ! grep --quiet "$YNH_APP_INSTANCE_NAME:" "/etc/php/ynh_app_version"; then - # Store the ID of this app and the version of PHP requested for it - echo "$YNH_APP_INSTANCE_NAME:$phpversion" | tee --append "/etc/php/ynh_app_version" - fi - - # Add an extra repository for those packages - ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 - - # Install requested dependencies from this extra repository. - # Install PHP-FPM first, otherwise PHP will install apache as a dependency. - ynh_add_app_dependencies --package="php${phpversion}-fpm" - ynh_add_app_dependencies --package="php$phpversion php${phpversion}-common $package" - - # Set the default PHP version back as the default version for php-cli. - update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION - - # Advertise service in admin panel - yunohost service add php${phpversion}-fpm --log "/var/log/php${phpversion}-fpm.log" + ynh_install_app_dependencies "$package" + ynh_app_setting_set --app=$app --key=phpversion --value=$specific_php_version } # Remove the specific version of PHP used by the app. @@ -351,33 +328,8 @@ ynh_install_php() { # usage: ynh_install_php # # Requires YunoHost version 3.8.1 or higher. -ynh_remove_php() { - # Get the version of PHP used by this app - local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) - - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] || [ -z "$phpversion" ]; then - if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]; then - ynh_print_err "Do not use ynh_remove_php to remove php$YNH_DEFAULT_PHP_VERSION !" - fi - return 0 - fi - - # Create the file if doesn't exist already - touch /etc/php/ynh_app_version - - # Remove the line for this app - sed --in-place "/$YNH_APP_INSTANCE_NAME:$phpversion/d" "/etc/php/ynh_app_version" - - # If no other app uses this version of PHP, remove it. - if ! grep --quiet "$phpversion" "/etc/php/ynh_app_version"; then - # Remove the service from the admin panel - if ynh_package_is_installed --package="php${phpversion}-fpm"; then - yunohost service remove php${phpversion}-fpm - fi - - # Purge PHP dependencies for this version. - ynh_package_autopurge "php$phpversion php${phpversion}-fpm php${phpversion}-common" - fi +ynh_remove_php () { + ynh_remove_app_dependencies } # Define the values to configure PHP-FPM diff --git a/data/helpers.d/utils b/data/helpers.d/utils index a4afdaf88..453a1ab94 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -699,6 +699,28 @@ properly with chmod/chown." echo $TMP_DIR } +_acceptable_path_to_delete() { + local file=$1 + + local forbidden_paths=$(ls -d / /* /{var,home,usr}/* /etc/{default,sudoers.d,yunohost,cron*}) + + # Legacy : A couple apps still have data in /home/$app ... + if [[ -n "$app" ]] + then + forbidden_paths=$(echo "$forbidden_paths" | grep -v "/home/$app") + fi + + # Use realpath to normalize the path .. + # i.e convert ///foo//bar//..///baz//// to /foo/baz + file=$(realpath --no-symlinks "$file") + if [ -z "$file" ] || grep -q -x -F "$file" <<< "$forbidden_paths"; then + return 1 + else + return 0 + fi +} + + # Remove a file or a directory securely # # usage: ynh_secure_remove --file=path_to_remove @@ -714,28 +736,18 @@ ynh_secure_remove() { ynh_handle_getopts_args "$@" set +o xtrace # set +x - local forbidden_path=" \ - /var/www \ - /home/yunohost.app" - if [ $# -ge 2 ]; then ynh_print_warn --message="/!\ Packager ! You provided more than one argument to ynh_secure_remove but it will be ignored... Use this helper with one argument at time." fi if [[ -z "$file" ]]; then ynh_print_warn --message="ynh_secure_remove called with empty argument, ignoring." - elif [[ "$forbidden_path" =~ "$file" || - - "$file" =~ ^/[[:alnum:]]+$ || - - "${file:${#file}-1}" = "/" ]]; then # Match all paths or subpaths in $forbidden_path - # Match all first level paths from / (Like /var, /root, etc...) - # Match if the path finishes by /. Because it seems there is an empty variable - ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete." - elif [ -e "$file" ]; then - rm --recursive "$file" - else + elif [[ ! -e $file ]]; then ynh_print_info --message="'$file' wasn't deleted because it doesn't exist." + elif ! _acceptable_path_to_delete "$file"; then + ynh_print_warn --message="Not deleting '$file' because it is not an acceptable path to delete." + else + rm --recursive "$file" fi set -o xtrace # set -x diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq index 0c016bcb9..ee2ff1a1f 100755 --- a/data/hooks/conf_regen/43-dnsmasq +++ b/data/hooks/conf_regen/43-dnsmasq @@ -32,6 +32,7 @@ do_pre_regen() { # add domain conf files for domain in $YNH_DOMAINS; do + [[ ! $domain =~ \.local$ ]] || continue export domain ynh_render_template "domain.tpl" "${dnsmasq_dir}/${domain}" done @@ -40,8 +41,10 @@ do_pre_regen() { conf_files=$(ls -1 /etc/dnsmasq.d \ | awk '/^[^\.]+\.[^\.]+.*$/ { print $1 }') for domain in $conf_files; do - [[ $YNH_DOMAINS =~ $domain ]] \ - || touch "${dnsmasq_dir}/${domain}" + if [[ ! $YNH_DOMAINS =~ $domain ]] && [[ ! $domain =~ \.local$ ]] + then + touch "${dnsmasq_dir}/${domain}" + fi done } diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index ee8511f83..c7e937979 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -78,6 +78,20 @@ service quota-warning { } } +service stats { + unix_listener stats-reader { + user = vmail + group = mail + mode = 0660 + } + + unix_listener stats-writer { + user = vmail + group = mail + mode = 0660 + } +} + plugin { sieve = /var/mail/sievescript/%n/.dovecot.sieve sieve_dir = /var/mail/sievescript/%n/scripts/ diff --git a/debian/changelog b/debian/changelog index a10c83888..def37d6b2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,23 @@ +yunohost (4.3.1.3) testing; urgency=low + + - [fix] app: repo url branch names may contain dots (38cff4a9) + + -- Alexandre Aubin Thu, 07 Oct 2021 18:31:09 +0200 + +yunohost (4.3.1.2) testing; urgency=low + + - [fix] apps: upgrade was broken because of typo ([#1350](https://github.com/YunoHost/yunohost/pull/1350)) + - [enh] apps: in app_info, return a new is_webapp info meant to be used by API/webadmin (4cd5e9b6) + - [fix] configpanel: handle case where file question didnt get modified from webadmin, in which case self.value contains a path (54d901ad) + - [fix] configpanel: bind_key -> bind_key_ to prevent yunohost from redacting key names which leads to broken log metadata.yml somehow (941cc294) + - [enh] questions: Add visible attribute support in cli (74256845) + - [enh] helpers: Simplify apt/php dependencies helpers ([#1018](https://github.com/YunoHost/yunohost/pull/1018)) + - [enh] helpers: In logrotate helper, enforce decent permissions on log file if app user exists ([#1352](https://github.com/YunoHost/yunohost/pull/1352)) + + Thanks to all contributors <3 ! (Éric Gaspar, Kay0u, ljf) + + -- Alexandre Aubin Thu, 07 Oct 2021 10:42:06 +0200 + yunohost (4.3.1.1) testing; urgency=low - [enh] app helpers: Update n version ([#1347](https://github.com/YunoHost/yunohost/pull/1347)) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2b8d71abf..fb544cab2 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -79,7 +79,7 @@ re_app_instance_name = re.compile( ) APP_REPO_URL = re.compile( - r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_]+)?(\.git)?/?$" + r"^https://[a-zA-Z0-9-_.]+/[a-zA-Z0-9-_./]+/[a-zA-Z0-9-_.]+_ynh(/?(-/)?tree/[a-zA-Z0-9-_.]+)?(\.git)?/?$" ) APP_FILES_TO_COPY = [ @@ -169,7 +169,7 @@ def app_info(app, full=False): ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {}) ret["upgradable"] = _app_upgradable(ret) - ret["is_webapp"] = ("domain" in settings and "path" in settings) + ret["is_webapp"] = "domain" in settings and "path" in settings ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index cf2c6c2c3..7b4c6e2e3 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -68,6 +68,7 @@ def test_repo_url_definition(): assert _is_app_repo_url( "https://gitlab.domainepublic.net/Neutrinet/neutrinet_ynh/-/tree/unstable" ) + assert _is_app_repo_url("https://github.com/YunoHost-Apps/foobar_ynh/tree/1.23.4") assert _is_app_repo_url("git@github.com:YunoHost-Apps/foobar_ynh.git") assert not _is_app_repo_url("github.com/YunoHost-Apps/foobar_ynh") diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index b39990b73..c21ff8c40 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -15,7 +15,7 @@ from yunohost.utils.config import ( PathQuestion, BooleanQuestion, FileQuestion, - evaluate_simple_js_expression + evaluate_simple_js_expression, ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -2095,97 +2095,95 @@ def test_normalize_path(): assert PathQuestion.normalize("macnuggets/") == "/macnuggets" assert PathQuestion.normalize("////macnuggets///") == "/macnuggets" + def test_simple_evaluate(): context = { - 'a1': 1, - 'b2': 2, - 'c10': 10, - 'foo': 'bar', - 'comp': '1>2', - 'empty': '', - 'lorem': 'Lorem ipsum dolor et si qua met!', - 'warning': 'Warning! This sentence will fail!', - 'quote': "Je s'apelle Groot", - 'and_': '&&', - 'object': { 'a': 'Security risk' } + "a1": 1, + "b2": 2, + "c10": 10, + "foo": "bar", + "comp": "1>2", + "empty": "", + "lorem": "Lorem ipsum dolor et si qua met!", + "warning": "Warning! This sentence will fail!", + "quote": "Je s'apelle Groot", + "and_": "&&", + "object": {"a": "Security risk"}, } supported = { - '42': 42, - '9.5': 9.5, - "'bopbidibopbopbop'": 'bopbidibopbopbop', - 'true': True, - 'false': False, - 'null': None, - + "42": 42, + "9.5": 9.5, + "'bopbidibopbopbop'": "bopbidibopbopbop", + "true": True, + "false": False, + "null": None, # Math - '1 * (2 + 3 * (4 - 3))': 5, - '1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3': True, - '(9 - 2) * 3 - 10': 11, - '12 - 2 * -2 + (3 - 4) * 3.1': 12.9, - '9 / 12 + 12 * 3 - 5': 31.75, - '9 / 12 + 12 * (3 - 5)': -23.25, - '12 > 13.1': False, - '12 < 14': True, - '12 <= 14': True, - '12 >= 14': False, - '12 == 14': False, - '12 % 5 > 3': False, - '12 != 14': True, - '9 - 1 > 10 && 3 * 5 > 10': False, - '9 - 1 > 10 || 3 * 5 > 10': True, - 'a1 > 0 || a1 < -12': True, - 'a1 > 0 && a1 < -12': False, - 'a1 + 1 > 0 && -a1 > -12': True, - '-(a1 + 1) < 0 || -(a1 + 2) > -12': True, - '-a1 * 2': -2, - '(9 - 2) * 3 - c10': 11, - '(9 - b2) * 3 - c10': 11, - 'c10 > b2': True, - + "1 * (2 + 3 * (4 - 3))": 5, + "1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3": True, + "(9 - 2) * 3 - 10": 11, + "12 - 2 * -2 + (3 - 4) * 3.1": 12.9, + "9 / 12 + 12 * 3 - 5": 31.75, + "9 / 12 + 12 * (3 - 5)": -23.25, + "12 > 13.1": False, + "12 < 14": True, + "12 <= 14": True, + "12 >= 14": False, + "12 == 14": False, + "12 % 5 > 3": False, + "12 != 14": True, + "9 - 1 > 10 && 3 * 5 > 10": False, + "9 - 1 > 10 || 3 * 5 > 10": True, + "a1 > 0 || a1 < -12": True, + "a1 > 0 && a1 < -12": False, + "a1 + 1 > 0 && -a1 > -12": True, + "-(a1 + 1) < 0 || -(a1 + 2) > -12": True, + "-a1 * 2": -2, + "(9 - 2) * 3 - c10": 11, + "(9 - b2) * 3 - c10": 11, + "c10 > b2": True, # String - "foo == 'bar'":True, - "foo != 'bar'":False, - 'foo == "bar" && 1 > 0':True, - '!!foo': True, - '!foo': False, - 'foo': 'bar', + "foo == 'bar'": True, + "foo != 'bar'": False, + 'foo == "bar" && 1 > 0': True, + "!!foo": True, + "!foo": False, + "foo": "bar", '!(foo > "baa") || 1 > 2': False, '!(foo > "baa") || 1 < 2': True, 'empty == ""': True, '1 == "1"': True, '1.0 == "1"': True, '1 == "aaa"': False, - "'I am ' + b2 + ' years'": 'I am 2 years', + "'I am ' + b2 + ' years'": "I am 2 years", "quote == 'Je s\\'apelle Groot'": True, "lorem == 'Lorem ipsum dolor et si qua met!'": True, "and_ == '&&'": True, "warning == 'Warning! This sentence will fail!'": True, - # Match "match(lorem, '^Lorem [ia]psumE?')": bool, "match(foo, '^Lorem [ia]psumE?')": None, "match(lorem, '^Lorem [ia]psumE?') && 1 == 1": bool, - # No code "": False, " ": False, } trigger_errors = { - "object.a": YunohostError, # Keep unsupported, for security reasons - 'a1 ** b2': YunohostError, # Keep unsupported, for security reasons - '().__class__.__bases__[0].__subclasses__()': YunohostError, # Very dangerous code - 'a1 > 11 ? 1 : 0': SyntaxError, - 'c10 > b2 == false': YunohostError, # JS and Python doesn't do the same thing for this situation - 'c10 > b2 == true': YunohostError, + "object.a": YunohostError, # Keep unsupported, for security reasons + "a1 ** b2": YunohostError, # Keep unsupported, for security reasons + "().__class__.__bases__[0].__subclasses__()": YunohostError, # Very dangerous code + "a1 > 11 ? 1 : 0": SyntaxError, + "c10 > b2 == false": YunohostError, # JS and Python doesn't do the same thing for this situation + "c10 > b2 == true": YunohostError, } for expression, result in supported.items(): if result == bool: assert bool(evaluate_simple_js_expression(expression, context)), expression else: - assert evaluate_simple_js_expression(expression, context) == result, expression + assert ( + evaluate_simple_js_expression(expression, context) == result + ), expression for expression, error in trigger_errors.items(): with pytest.raises(error): evaluate_simple_js_expression(expression, context) - diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index fbaecfbe0..4ee62c6f7 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -55,24 +55,24 @@ def evaluate_simple_ast(node, context={}): operators = { ast.Not: op.not_, ast.Mult: op.mul, - ast.Div: op.truediv, # number - ast.Mod: op.mod, # number - ast.Add: op.add, #str - ast.Sub: op.sub, #number - ast.USub: op.neg, # Negative number + ast.Div: op.truediv, # number + ast.Mod: op.mod, # number + ast.Add: op.add, # str + ast.Sub: op.sub, # number + ast.USub: op.neg, # Negative number ast.Gt: op.gt, ast.Lt: op.lt, ast.GtE: op.ge, ast.LtE: op.le, ast.Eq: op.eq, - ast.NotEq: op.ne + ast.NotEq: op.ne, } - context['true'] = True - context['false'] = False - context['null'] = None + context["true"] = True + context["false"] = False + context["null"] = None # Variable - if isinstance(node, ast.Name): # Variable + if isinstance(node, ast.Name): # Variable return context[node.id] # Python <=3.7 String @@ -88,14 +88,16 @@ def evaluate_simple_ast(node, context={}): return node.value # + - * / % - elif isinstance(node, ast.BinOp) and type(node.op) in operators: # + elif ( + isinstance(node, ast.BinOp) and type(node.op) in operators + ): # left = evaluate_simple_ast(node.left, context) right = evaluate_simple_ast(node.right, context) if type(node.op) == ast.Add: - if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 + if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 left = str(left) right = str(right) - elif type(left) != type(right): # support "111" - "1" -> 110 + elif type(left) != type(right): # support "111" - "1" -> 110 left = float(left) right = float(right) @@ -104,7 +106,9 @@ def evaluate_simple_ast(node, context={}): # Comparison # JS and Python don't give the same result for multi operators # like True == 10 > 2. - elif isinstance(node, ast.Compare) and len(node.comparators) == 1: # + elif ( + isinstance(node, ast.Compare) and len(node.comparators) == 1 + ): # left = evaluate_simple_ast(node.left, context) right = evaluate_simple_ast(node.comparators[0], context) operator = node.ops[0] @@ -116,11 +120,11 @@ def evaluate_simple_ast(node, context={}): return type(operator) == ast.NotEq try: return operators[type(operator)](left, right) - except TypeError: # support "e" > 1 -> False like in JS + except TypeError: # support "e" > 1 -> False like in JS return False # and / or - elif isinstance(node, ast.BoolOp): # + elif isinstance(node, ast.BoolOp): # for value in node.values: value = evaluate_simple_ast(value, context) if isinstance(node.op, ast.And) and not value: @@ -130,20 +134,22 @@ def evaluate_simple_ast(node, context={}): return isinstance(node.op, ast.And) # not / USub (it's negation number -\d) - elif isinstance(node, ast.UnaryOp): # e.g., -1 + elif isinstance(node, ast.UnaryOp): # e.g., -1 return operators[type(node.op)](evaluate_simple_ast(node.operand, context)) # match function call - elif isinstance(node, ast.Call) and node.func.__dict__.get('id') == 'match': + elif isinstance(node, ast.Call) and node.func.__dict__.get("id") == "match": return re.match( - evaluate_simple_ast(node.args[1], context), - context[node.args[0].id] + evaluate_simple_ast(node.args[1], context), context[node.args[0].id] ) # Unauthorized opcode else: opcode = str(type(node)) - raise YunohostError(f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True) + raise YunohostError( + f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True + ) + def js_to_python(expr): in_string = None @@ -162,7 +168,7 @@ def js_to_python(expr): # If we are not in a string, replace operators elif not in_string: - if char == "!" and expr[i +1] != "=": + if char == "!" and expr[i + 1] != "=": char = "not " elif char in "|&" and py_expr[-1:] == char: py_expr = py_expr[:-1] @@ -171,15 +177,17 @@ def js_to_python(expr): # Determine if next loop will be in escaped mode escaped = char == "\\" and not escaped py_expr += char - i+=1 + i += 1 return py_expr + def evaluate_simple_js_expression(expr, context={}): if not expr.strip(): return False node = ast.parse(js_to_python(expr), mode="eval").body return evaluate_simple_ast(node, context) + class ConfigPanel: def __init__(self, config_path, save_path=None): self.config_path = config_path @@ -648,7 +656,9 @@ class Question(object): def ask_if_needed(self): - if self.visible and not evaluate_simple_js_expression(self.visible, context=self.context): + if self.visible and not evaluate_simple_js_expression( + self.visible, context=self.context + ): # FIXME There could be several use case if the question is not displayed: # - we doesn't want to give a specific value # - we want to keep the previous value @@ -1231,7 +1241,6 @@ def ask_questions_and_parse_answers( else: answers = {} - out = [] for raw_question in raw_questions: diff --git a/tests/test_helpers.d/ynhtest_secure_remove.sh b/tests/test_helpers.d/ynhtest_secure_remove.sh new file mode 100644 index 000000000..04d85fa7a --- /dev/null +++ b/tests/test_helpers.d/ynhtest_secure_remove.sh @@ -0,0 +1,71 @@ +ynhtest_acceptable_path_to_delete() { + + mkdir -p /home/someuser + mkdir -p /home/$app + mkdir -p /home/yunohost.app/$app + mkdir -p /var/www/$app + touch /var/www/$app/bar + touch /etc/cron.d/$app + + ! _acceptable_path_to_delete / + ! _acceptable_path_to_delete //// + ! _acceptable_path_to_delete " //// " + ! _acceptable_path_to_delete /var + ! _acceptable_path_to_delete /var/www + ! _acceptable_path_to_delete /var/cache + ! _acceptable_path_to_delete /usr + ! _acceptable_path_to_delete /usr/bin + ! _acceptable_path_to_delete /home + ! _acceptable_path_to_delete /home/yunohost.backup + ! _acceptable_path_to_delete /home/yunohost.app + ! _acceptable_path_to_delete /home/yunohost.app/ + ! _acceptable_path_to_delete ///home///yunohost.app/// + ! _acceptable_path_to_delete /home/yunohost.app/$app/.. + ! _acceptable_path_to_delete ///home///yunohost.app///$app///..// + ! _acceptable_path_to_delete /home/yunohost.app/../$app/.. + ! _acceptable_path_to_delete /home/someuser + ! _acceptable_path_to_delete /home/yunohost.app//../../$app + ! _acceptable_path_to_delete " /home/yunohost.app/// " + ! _acceptable_path_to_delete /etc/cron.d/ + ! _acceptable_path_to_delete /etc/yunohost/ + + _acceptable_path_to_delete /home/yunohost.app/$app + _acceptable_path_to_delete /home/yunohost.app/$app/bar + _acceptable_path_to_delete /etc/cron.d/$app + _acceptable_path_to_delete /var/www/$app/bar + _acceptable_path_to_delete /var/www/$app + + rm /var/www/$app/bar + rm /etc/cron.d/$app + rmdir /home/yunohost.app/$app + rmdir /home/$app + rmdir /home/someuser + rmdir /var/www/$app +} + +ynhtest_secure_remove() { + + mkdir -p /home/someuser + mkdir -p /home/yunohost.app/$app + mkdir -p /var/www/$app + mkdir -p /var/whatever + touch /var/www/$app/bar + touch /etc/cron.d/$app + + ! ynh_secure_remove --file="/home/someuser" + ! ynh_secure_remove --file="/home/yunohost.app/" + ! ynh_secure_remove --file="/var/whatever" + ynh_secure_remove --file="/home/yunohost.app/$app" + ynh_secure_remove --file="/var/www/$app" + ynh_secure_remove --file="/etc/cron.d/$app" + + test -e /home/someuser + test -e /home/yunohost.app + test -e /var/whatever + ! test -e /home/yunohost.app/$app + ! test -e /var/www/$app + ! test -e /etc/cron.d/$app + + rmdir /home/someuser + rmdir /var/whatever +}