Merge branch 'dev' into dev

This commit is contained in:
Alexandre Aubin 2023-02-06 17:48:07 +01:00 committed by GitHub
commit e39c89e087
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 140 additions and 56 deletions

15
debian/changelog vendored
View file

@ -1,3 +1,18 @@
yunohost (11.1.5.5) stable; urgency=low
- admin->admins migration: try to handle boring case where the 'first' user cant be identified because it doesnt have the root@ alias (8485ebc7)
- appsv2: ignore the old/ugly/legacy removal of apt deps when removing the php conf, because that's handled by the apt resource (3bbba640)
- appsv2: moar fixes for v1->v2 upgrade not getting the proper env context (fb54da2e)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 04 Feb 2023 18:51:03 +0100
yunohost (11.1.5.4) stable; urgency=low
- appsv2: typo in ports resource doc x_x (0e787acb)
- appsv2: fix permission provisioning for fulldomain apps + fix apps not properly getting removed after failed resources init (476908bd)
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 03 Feb 2023 20:43:04 +0100
yunohost (11.1.5.3) stable; urgency=low yunohost (11.1.5.3) stable; urgency=low
- helpers/appsv2: replacement of __PHPVERSION__ should use the phpversion setting, not YNH_PHP_VERSION (13d4e16e) - helpers/appsv2: replacement of __PHPVERSION__ should use the phpversion setting, not YNH_PHP_VERSION (13d4e16e)

View file

@ -283,7 +283,7 @@ ynh_remove_fpm_config() {
# If the PHP version used is not the default version for YunoHost # If the PHP version used is not the default version for YunoHost
# The second part with YNH_APP_PURGE is an ugly hack to guess that we're inside the remove script # The second part with YNH_APP_PURGE is an ugly hack to guess that we're inside the remove script
# (we don't actually care about its value, we just check its not empty hence it exists) # (we don't actually care about its value, we just check its not empty hence it exists)
if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ]; then if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] && [ -n "${YNH_APP_PURGE:-}" ] && dpkg --compare-versions ${YNH_APP_PACKAGING_FORMAT:-0} lt 2; then
# Remove app dependencies ... but ideally should happen via an explicit call from packager # Remove app dependencies ... but ideally should happen via an explicit call from packager
ynh_remove_app_dependencies ynh_remove_app_dependencies
fi fi

View file

@ -48,7 +48,7 @@ ynh_replace_string() {
ynh_handle_getopts_args "$@" ynh_handle_getopts_args "$@"
set +o xtrace # set +x set +o xtrace # set +x
local delimit=@ local delimit=$'\001'
# Escape the delimiter if it's in the string. # Escape the delimiter if it's in the string.
match_string=${match_string//${delimit}/"\\${delimit}"} match_string=${match_string//${delimit}/"\\${delimit}"}
replace_string=${replace_string//${delimit}/"\\${delimit}"} replace_string=${replace_string//${delimit}/"\\${delimit}"}

View file

@ -568,7 +568,7 @@ ynh_read_var_in_file() {
var_part+='\s*' var_part+='\s*'
# Extract the part after assignation sign # Extract the part after assignation sign
local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)"
if [[ "$expression_with_comment" == "YNH_NULL" ]]; then if [[ "$expression_with_comment" == "YNH_NULL" ]]; then
set -o xtrace # set -x set -o xtrace # set -x
echo YNH_NULL echo YNH_NULL
@ -614,15 +614,14 @@ ynh_write_var_in_file() {
set +o xtrace # set +x set +o xtrace # set +x
# Get the line number after which we search for the variable # Get the line number after which we search for the variable
local line_number=1 local after_line_number=1
if [[ -n "$after" ]]; then if [[ -n "$after" ]]; then
line_number=$(grep -m1 -n $after $file | cut -d: -f1) after_line_number=$(grep -m1 -n $after $file | cut -d: -f1)
if [[ -z "$line_number" ]]; then if [[ -z "$after_line_number" ]]; then
set -o xtrace # set -x set -o xtrace # set -x
return 1 return 1
fi fi
fi fi
local range="${line_number},\$ "
local filename="$(basename -- "$file")" local filename="$(basename -- "$file")"
local ext="${filename##*.}" local ext="${filename##*.}"
@ -647,17 +646,21 @@ ynh_write_var_in_file() {
var_part+='\s*' var_part+='\s*'
# Extract the part after assignation sign # Extract the part after assignation sign
local expression_with_comment="$(tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL | head -n1)" local expression_with_comment="$((tail +$after_line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)"
if [[ "$expression_with_comment" == "YNH_NULL" ]]; then if [[ "$expression_with_comment" == "YNH_NULL" ]]; then
set -o xtrace # set -x set -o xtrace # set -x
return 1 return 1
fi fi
local value_line_number="$(tail +$after_line_number ${file} | grep -m1 -n -i -P $var_part'\K.*$' | cut -d: -f1)"
value_line_number=$((after_line_number + value_line_number))
local range="${after_line_number},${value_line_number} "
# Remove comments if needed # Remove comments if needed
local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")"
endline=${expression_with_comment#"$expression"} endline=${expression_with_comment#"$expression"}
endline="$(echo "$endline" | sed 's/\\/\\\\/g')" endline="$(echo "$endline" | sed 's/\\/\\\\/g')"
value="$(echo "$value" | sed 's/\\/\\\\/g')" value="$(echo "$value" | sed 's/\\/\\\\/g')"
value=${value//&/"\&"}
local first_char="${expression:0:1}" local first_char="${expression:0:1}"
delimiter=$'\001' delimiter=$'\001'
if [[ "$first_char" == '"' ]]; then if [[ "$first_char" == '"' ]]; then

View file

@ -9,7 +9,7 @@ source /usr/share/yunohost/helpers
# Backup destination # Backup destination
backup_dir="${1}/data/multimedia" backup_dir="${1}/data/multimedia"
if [ -e "/home/yunohost.multimedia/.nobackup" ]; then if [ ! -e "/home/yunohost.multimedia" ] || [ -e "/home/yunohost.multimedia/.nobackup" ]; then
exit 0 exit 0
fi fi

View file

@ -345,9 +345,11 @@
"domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!", "domain_config_cert_summary_selfsigned": "WARNING: Current certificate is self-signed. Browsers will display a spooky warning to new visitors!",
"domain_config_cert_validity": "Validity", "domain_config_cert_validity": "Validity",
"domain_config_default_app": "Default app", "domain_config_default_app": "Default app",
"domain_config_default_app_help": "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the user portal login form.",
"domain_config_mail_in": "Incoming emails", "domain_config_mail_in": "Incoming emails",
"domain_config_mail_out": "Outgoing emails", "domain_config_mail_out": "Outgoing emails",
"domain_config_xmpp": "Instant messaging (XMPP)", "domain_config_xmpp": "Instant messaging (XMPP)",
"domain_config_xmpp_help": "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled",
"domain_created": "Domain created", "domain_created": "Domain created",
"domain_creation_failed": "Unable to create domain {domain}: {error}", "domain_creation_failed": "Unable to create domain {domain}: {error}",
"domain_deleted": "Domain deleted", "domain_deleted": "Domain deleted",

View file

@ -9,8 +9,6 @@ name = "Features"
type = "app" type = "app"
filter = "is_webapp" filter = "is_webapp"
default = "_none" default = "_none"
# FIXME: i18n
help = "People will automatically be redirected to this app when opening this domain. If no app is specified, people are redirected to the user portal login form."
[feature.mail] [feature.mail]
@ -27,8 +25,6 @@ name = "Features"
[feature.xmpp.xmpp] [feature.xmpp.xmpp]
type = "boolean" type = "boolean"
default = 0 default = 0
# FIXME: i18n
help = "NB: some XMPP features will require that you update your DNS records and regenerate your Lets Encrypt certificate to be enabled"
[dns] [dns]
name = "DNS" name = "DNS"
@ -67,7 +63,6 @@ name = "Certificate"
visible = "acme_eligible == false || acme_eligible == null" visible = "acme_eligible == false || acme_eligible == null"
[cert.cert.cert_no_checks] [cert.cert.cert_no_checks]
ask = "Ignore diagnosis checks"
type = "boolean" type = "boolean"
default = false default = false
visible = "acme_eligible == false || acme_eligible == null" visible = "acme_eligible == false || acme_eligible == null"

View file

@ -1074,10 +1074,14 @@ def app_install(
if packaging_format >= 2: if packaging_format >= 2:
from yunohost.utils.resources import AppResourceManager from yunohost.utils.resources import AppResourceManager
try:
AppResourceManager(app_instance_name, wanted=manifest, current={}).apply( AppResourceManager(app_instance_name, wanted=manifest, current={}).apply(
rollback_and_raise_exception_if_failure=True, rollback_and_raise_exception_if_failure=True,
operation_logger=operation_logger, operation_logger=operation_logger,
) )
except (KeyboardInterrupt, EOFError, Exception) as e:
shutil.rmtree(app_setting_path)
raise e
else: else:
# Initialize the main permission for the app # Initialize the main permission for the app
# The permission is initialized with no url associated, and with tile disabled # The permission is initialized with no url associated, and with tile disabled
@ -2651,6 +2655,9 @@ def _guess_webapp_path_requirement(app_folder: str) -> str:
if len(domain_questions) == 1 and len(path_questions) == 1: if len(domain_questions) == 1 and len(path_questions) == 1:
return "domain_and_path" return "domain_and_path"
if len(domain_questions) == 1 and len(path_questions) == 0: if len(domain_questions) == 1 and len(path_questions) == 0:
if manifest.get("packaging_format", 0) < 2:
# This is likely to be a full-domain app... # This is likely to be a full-domain app...
# Confirm that this is a full-domain app This should cover most cases # Confirm that this is a full-domain app This should cover most cases
@ -2668,6 +2675,12 @@ def _guess_webapp_path_requirement(app_folder: str) -> str:
) and re.search(r"ynh_webpath_register", install_script_content): ) and re.search(r"ynh_webpath_register", install_script_content):
return "full_domain" return "full_domain"
else:
# For packaging v2 apps, check if there's a permission with url being a string
perm_resource = manifest.get("resources", {}).get("permissions")
if perm_resource is not None and isinstance(perm_resource.get("main", {}).get("url"), str):
return "full_domain"
return "?" return "?"

View file

@ -2295,7 +2295,7 @@ def backup_create(
) )
backup_manager.backup() backup_manager.backup()
logger.success(m18n.n("backup_created", name=name)) logger.success(m18n.n("backup_created", name=backup_manager.name))
operation_logger.success() operation_logger.success()
return { return {

View file

@ -591,7 +591,10 @@ def _get_registrar_config_section(domain):
# TODO : add a help tip with the link to the registar's API doc (c.f. Lexicon's README) # TODO : add a help tip with the link to the registar's API doc (c.f. Lexicon's README)
registrar_list = read_toml(DOMAIN_REGISTRAR_LIST_PATH) registrar_list = read_toml(DOMAIN_REGISTRAR_LIST_PATH)
registrar_credentials = registrar_list[registrar] registrar_credentials = registrar_list.get(registrar)
if registrar_credentials is None:
logger.warning(f"Registrar {registrar} unknown / Should be added to YunoHost's registrar_list.toml by the development team!")
registrar_credentials = {}
for credential, infos in registrar_credentials.items(): for credential, infos in registrar_credentials.items():
infos["default"] = infos.get("default", "") infos["default"] = infos.get("default", "")
infos["optional"] = infos.get("optional", "False") infos["optional"] = infos.get("optional", "False")

View file

@ -624,14 +624,28 @@ class DomainConfigPanel(ConfigPanel):
f"domain_config_cert_summary_{status['summary']}" f"domain_config_cert_summary_{status['summary']}"
) )
# Other specific strings used in config panels
# i18n: domain_config_cert_renew_help
# FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ... # FIXME: Ugly hack to save the cert status and reinject it in _load_current_values ...
self.cert_status = status self.cert_status = status
return toml 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): def _load_current_values(self):
# TODO add mechanism to share some settings with other domains on the same zone # TODO add mechanism to share some settings with other domains on the same zone
super()._load_current_values() super()._load_current_values()

View file

@ -46,6 +46,19 @@ class MyMigration(Migration):
new_admin_user = user new_admin_user = user
break break
# For some reason some system have no user with root@ alias,
# but the user does has admin / postmaster / ... alias
# ... try to find it instead otherwise this creashes the migration
# later because the admin@, postmaster@, .. aliases will already exist
if not new_admin_user:
for user in all_users:
aliases = user_info(user).get("mail-aliases", [])
if any(alias.startswith(f"admin@{main_domain}") for alias in aliases) \
or any(alias.startswith(f"postmaster@{main_domain}") for alias in aliases):
new_admin_user = user
break
self.ldap_migration_started = True self.ldap_migration_started = True
if new_admin_user: if new_admin_user:

View file

@ -6,6 +6,8 @@ from mock import patch
from .conftest import message, raiseYunohostError, get_test_apps_dir from .conftest import message, raiseYunohostError, get_test_apps_dir
from moulinette.utils.text import random_ascii
from yunohost.app import app_install, app_remove, app_ssowatconf from yunohost.app import app_install, app_remove, app_ssowatconf
from yunohost.app import _is_installed from yunohost.app import _is_installed
from yunohost.backup import ( from yunohost.backup import (
@ -236,8 +238,9 @@ def add_archive_system_from_4p2():
def test_backup_only_ldap(mocker): def test_backup_only_ldap(mocker):
# Create the backup # Create the backup
with message(mocker, "backup_created"): name = random_ascii(8)
backup_create(system=["conf_ldap"], apps=None) with message(mocker, "backup_created", name=name):
backup_create(name=name, system=["conf_ldap"], apps=None)
archives = backup_list()["archives"] archives = backup_list()["archives"]
assert len(archives) == 1 assert len(archives) == 1
@ -261,9 +264,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(mocker):
name = random_ascii(8)
# Create the backup # Create the backup
with message(mocker, "backup_created"): with message(mocker, "backup_created", name=name):
backup_create(system=[], apps=None) backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"] archives = backup_list()["archives"]
assert len(archives) == 1 assert len(archives) == 1
@ -294,9 +298,10 @@ def test_backup_and_restore_all_sys(mocker):
@pytest.mark.with_system_archive_from_4p2 @pytest.mark.with_system_archive_from_4p2
def test_restore_system_from_Ynh4p2(monkeypatch, mocker): def test_restore_system_from_Ynh4p2(monkeypatch, mocker):
name = random_ascii(8)
# Backup current system # Backup current system
with message(mocker, "backup_created"): with message(mocker, "backup_created", name=name):
backup_create(system=[], apps=None) backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"] archives = backup_list()["archives"]
assert len(archives) == 2 assert len(archives) == 2
@ -393,16 +398,17 @@ def test_backup_app_with_no_restore_script(mocker):
@pytest.mark.clean_opt_dir @pytest.mark.clean_opt_dir
def test_backup_with_different_output_directory(mocker): def test_backup_with_different_output_directory(mocker):
name = random_ascii(8)
# Create the backup # Create the backup
with message(mocker, "backup_created"): with message(mocker, "backup_created", name=name):
backup_create( backup_create(
system=["conf_ynh_settings"], system=["conf_ynh_settings"],
apps=None, apps=None,
output_directory="/opt/test_backup_output_directory", output_directory="/opt/test_backup_output_directory",
name="backup", name=name,
) )
assert os.path.exists("/opt/test_backup_output_directory/backup.tar") assert os.path.exists(f"/opt/test_backup_output_directory/{name}.tar")
archives = backup_list()["archives"] archives = backup_list()["archives"]
assert len(archives) == 1 assert len(archives) == 1
@ -416,13 +422,14 @@ def test_backup_with_different_output_directory(mocker):
@pytest.mark.clean_opt_dir @pytest.mark.clean_opt_dir
def test_backup_using_copy_method(mocker): def test_backup_using_copy_method(mocker):
# Create the backup # Create the backup
with message(mocker, "backup_created"): name = random_ascii(8)
with message(mocker, "backup_created", name=name):
backup_create( backup_create(
system=["conf_ynh_settings"], system=["conf_ynh_settings"],
apps=None, apps=None,
output_directory="/opt/test_backup_output_directory", output_directory="/opt/test_backup_output_directory",
methods=["copy"], methods=["copy"],
name="backup", name=name,
) )
assert os.path.exists("/opt/test_backup_output_directory/info.json") assert os.path.exists("/opt/test_backup_output_directory/info.json")
@ -565,8 +572,9 @@ def test_backup_and_restore_permission_app(mocker):
def _test_backup_and_restore_app(mocker, app): def _test_backup_and_restore_app(mocker, app):
# Create a backup of this app # Create a backup of this app
with message(mocker, "backup_created"): name = random_ascii(8)
backup_create(system=None, apps=[app]) with message(mocker, "backup_created", name=name):
backup_create(name=name, system=None, apps=[app])
archives = backup_list()["archives"] archives = backup_list()["archives"]
assert len(archives) == 1 assert len(archives) == 1
@ -628,8 +636,9 @@ def test_restore_archive_with_custom_hook(mocker):
os.system("touch %s/99-yolo" % custom_restore_hook_folder) os.system("touch %s/99-yolo" % custom_restore_hook_folder)
# Backup with custom hook system # Backup with custom hook system
with message(mocker, "backup_created"): name = random_ascii(8)
backup_create(system=[], apps=None) with message(mocker, "backup_created", name=name):
backup_create(name=name, system=[], apps=None)
archives = backup_list()["archives"] archives = backup_list()["archives"]
assert len(archives) == 1 assert len(archives) == 1
@ -666,5 +675,6 @@ def test_backup_binds_are_readonly(mocker, monkeypatch):
) )
# Create the backup # Create the backup
with message(mocker, "backup_created"): name = random_ascii(8)
backup_create(system=[]) with message(mocker, "backup_created", name=name):
backup_create(name=name, system=[])

View file

@ -1359,7 +1359,7 @@ class GroupQuestion(Question):
super().__init__(question, context) super().__init__(question, context)
self.choices = list(user_group_list(short=True)["groups"]) self.choices = list(user_group_list(short=True, include_primary_groups=False)["groups"])
def _human_readable_group(g): def _human_readable_group(g):
# i18n: visitors # i18n: visitors

View file

@ -179,7 +179,7 @@ class AppResource:
tmpdir = _make_tmp_workdir_for_app(app=self.app) tmpdir = _make_tmp_workdir_for_app(app=self.app)
env_ = _make_environment_for_app_script( env_ = _make_environment_for_app_script(
self.app, workdir=tmpdir, action=f"{action}_{self.type}" self.app, workdir=tmpdir, action=f"{action}_{self.type}", include_app_settings=True,
) )
env_.update(env) env_.update(env)
@ -320,6 +320,16 @@ class PermissionsResource(AppResource):
# Delete legacy is_public setting if not already done # Delete legacy is_public setting if not already done
self.delete_setting("is_public") self.delete_setting("is_public")
# Detect that we're using a full-domain app,
# in which case we probably need to automagically
# define the "path" setting with "/"
if (
isinstance(self.permissions["main"]["url"], str)
and self.get_setting("domain")
and not self.get_setting("path")
):
self.set_setting("path", "/")
existing_perms = user_permission_list(short=True, apps=[self.app])[ existing_perms = user_permission_list(short=True, apps=[self.app])[
"permissions" "permissions"
] ]
@ -338,6 +348,11 @@ class PermissionsResource(AppResource):
or self.get_setting(f"init_{perm}_permission") or self.get_setting(f"init_{perm}_permission")
or [] or []
) )
# If we're choosing 'visitors' from the init_{perm}_permission question, add all_users too
if not infos["allowed"] and init_allowed == "visitors":
init_allowed = ["visitors", "all_users"]
permission_create( permission_create(
perm_id, perm_id,
allowed=init_allowed, allowed=init_allowed,
@ -755,9 +770,10 @@ class PortsResource(AppResource):
##### Example: ##### Example:
```toml ```toml
[resources.port] [resources.ports]
# (empty should be fine for most apps... though you can customize stuff if absolutely needed) # (empty should be fine for most apps... though you can customize stuff if absolutely needed)
main.default = 12345 # if you really want to specify a prefered value .. but shouldnt matter in the majority of cases main.default = 12345 # if you really want to specify a prefered value .. but shouldnt matter in the majority of cases
xmpp_client.default = 5222 # if you need another port, pick a name for it (here, "xmpp_client") xmpp_client.default = 5222 # if you need another port, pick a name for it (here, "xmpp_client")