diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7a6521386..ae3051ac4 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -371,8 +371,6 @@ def app_change_url(operation_logger, app, domain, path): # Retrieve arguments list for change_url script # TODO: Allow to specify arguments args_odict = _parse_args_from_manifest(manifest, 'change_url') - args_list = [value[0] for value in args_odict.values()] - args_list.append(app) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app, args=args_odict) @@ -404,7 +402,7 @@ def app_change_url(operation_logger, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), - args=args_list, env=env_dict)[0] != 0: + env=env_dict)[0] != 0: msg = "Failed to change '%s' url." % app logger.error(msg) operation_logger.error(msg) @@ -432,7 +430,7 @@ def app_change_url(operation_logger, app, domain, path): logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) - hook_callback('post_app_change_url', args=args_list, env=env_dict) + hook_callback('post_app_change_url', env=env_dict) def app_upgrade(app=[], url=None, file=None, force=False): @@ -530,8 +528,6 @@ def app_upgrade(app=[], url=None, file=None, force=False): # Retrieve arguments list for upgrade script # TODO: Allow to specify arguments args_odict = _parse_args_from_manifest(manifest, 'upgrade') - args_list = [value[0] for value in args_odict.values()] - args_list.append(app_instance_name) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict) @@ -560,7 +556,7 @@ def app_upgrade(app=[], url=None, file=None, force=False): upgrade_failed = True try: upgrade_retcode = hook_exec(extracted_app_folder + '/scripts/upgrade', - args=args_list, env=env_dict)[0] + env=env_dict)[0] upgrade_failed = True if upgrade_retcode != 0 else False if upgrade_failed: @@ -637,7 +633,7 @@ def app_upgrade(app=[], url=None, file=None, force=False): # So much win logger.success(m18n.n('app_upgraded', app=app_instance_name)) - hook_callback('post_app_upgrade', args=args_list, env=env_dict) + hook_callback('post_app_upgrade', env=env_dict) operation_logger.success() permission_sync_to_user() @@ -754,10 +750,6 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Validate domain / path availability for webapps _validate_and_normalize_webpath(manifest, args_odict, extracted_app_folder) - # build arg list tq - args_list = [value[0] for value in args_odict.values()] - args_list.append(app_instance_name) - # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) @@ -830,7 +822,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict + env=env_dict )[0] # "Common" app install failure : the script failed and returned exit code != 0 install_failed = True if install_retcode != 0 else False @@ -946,7 +938,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu logger.success(m18n.n('installation_complete')) - hook_callback('post_app_install', args=args_list, env=env_dict) + hook_callback('post_app_install', env=env_dict) def dump_app_log_extract_for_debugging(operation_logger): @@ -1033,8 +1025,6 @@ def app_remove(operation_logger, app): os.system('chown -R admin: /tmp/yunohost_remove') os.system('chmod -R u+rX /tmp/yunohost_remove') - args_list = [app] - env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app) env_dict["YNH_APP_ID"] = app_id @@ -1046,7 +1036,6 @@ def app_remove(operation_logger, app): try: ret = hook_exec('/tmp/yunohost_remove/scripts/remove', - args=args_list, env=env_dict)[0] # Here again, calling hook_exec could fail miserably, or get # manually interrupted (by mistake or because script was stuck) @@ -1059,7 +1048,7 @@ def app_remove(operation_logger, app): if ret == 0: logger.success(m18n.n('app_removed', app=app)) - hook_callback('post_app_remove', args=args_list, env=env_dict) + hook_callback('post_app_remove', env=env_dict) else: logger.warning(m18n.n('app_not_properly_removed', app=app)) @@ -1470,7 +1459,6 @@ def app_action_run(operation_logger, app, action, args=None): # Retrieve arguments list for install script args_dict = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {} args_odict = _parse_args_for_action(actions[action], args=args_dict) - args_list = [value[0] for value in args_odict.values()] env_dict = _make_environment_for_app_script(app, args=args_odict, args_prefix="ACTION_") env_dict["YNH_ACTION"] = action @@ -1489,7 +1477,6 @@ def app_action_run(operation_logger, app, action, args=None): retcode = hook_exec( path, - args=args_list, env=env_dict, chdir=cwd, user=action_declaration.get("user", "root"), @@ -2775,7 +2762,7 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): } for arg_name, arg_value_and_type in args.items(): - env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = arg_value_and_type[0] + env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(arg_value_and_type[0]) return env_dict @@ -3139,6 +3126,33 @@ def _patch_legacy_helpers(app_folder): "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?", "replace": r"", "important": False + }, + # Old $1, $2 in backup/restore scripts... + "app=$2": { + "only_for": ["scripts/backup", "scripts/restore"], + "pattern": r"app=\$2", + "replace": r"app=$YNH_APP_INSTANCE_NAME", + "important": True + }, + # Old $1, $2 in backup/restore scripts... + "backup_dir=$1": { + "only_for": ["scripts/backup", "scripts/restore"], + "pattern": r"backup_dir=\$1", + "replace": r"backup_dir=.", + "important": True + }, + # Old $1, $2 in backup/restore scripts... + "restore_dir=$1": { + "only_for": ["scripts/restore"], + "pattern": r"restore_dir=\$1", + "replace": r"restore_dir=.", + "important": True + }, + # Old $1, $2 in install scripts... + # We ain't patching that shit because it ain't trivial to patch all args... + "domain=$1": { + "only_for": ["scripts/install"], + "important": True } } @@ -3157,6 +3171,11 @@ def _patch_legacy_helpers(app_folder): show_warning = False for helper, infos in stuff_to_replace.items(): + + # Ignore if not relevant for this file + if infos.get("only_for") and not any(filename.endswith(f) for f in infos["only_for"]): + continue + # If helper is used, attempt to patch the file if helper in content and infos["pattern"]: content = infos["pattern"].sub(infos["replace"], content) @@ -3164,11 +3183,11 @@ def _patch_legacy_helpers(app_folder): if infos["important"]: show_warning = True - # If the helpert is *still* in the content, it means that we + # If the helper is *still* in the content, it means that we # couldn't patch the deprecated helper in the previous lines. In # that case, abort the install or whichever step is performed if helper in content and infos["important"]: - raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.") + raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", raw_msg=True) if replaced_stuff: diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 7b37d1bf2..e7904bf8d 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -686,8 +686,10 @@ class BackupManager(): app_script = os.path.join(app_setting_path, 'scripts/backup') subprocess.call(['install', '-Dm555', app_script, tmp_script]) - hook_exec(tmp_script, args=[tmp_app_bkp_dir, app], - raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict)[0] + hook_exec(tmp_script, + raise_on_error=True, + chdir=tmp_app_bkp_dir, + env=env_dict)[0] self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) @@ -1383,7 +1385,6 @@ class RestoreManager(): # Execute app restore script hook_exec(restore_script, - args=[app_backup_in_archive, app_instance_name], chdir=app_backup_in_archive, raise_on_error=True, env=env_dict)[0] @@ -1408,7 +1409,7 @@ class RestoreManager(): operation_logger.start() # Execute remove script - if hook_exec(remove_script, args=[app_instance_name], + if hook_exec(remove_script, env=env_dict_remove)[0] != 0: msg = m18n.n('app_not_properly_removed', app=app_instance_name) logger.warning(msg) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 067a5d7a7..b11926f70 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -212,7 +212,7 @@ def hook_list(action, list_by='name', show_info=False): return {'hooks': result} -def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, +def hook_callback(action, hooks=[], args=None, chdir=None, env=None, pre_callback=None, post_callback=None): """ Execute all scripts binded to an action @@ -221,7 +221,6 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, action -- Action name hooks -- List of hooks names to execute args -- Ordered list of arguments to pass to the scripts - no_trace -- Do not print each command that will be executed chdir -- The directory from where the scripts will be executed env -- Dictionnary of environment variables to export pre_callback -- An object to call before each script execution with @@ -282,7 +281,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, hook_args = pre_callback(name=name, priority=priority, path=path, args=args) hook_return = hook_exec(path, args=hook_args, chdir=chdir, env=env, - no_trace=no_trace, raise_on_error=True)[1] + raise_on_error=True)[1] except YunohostError as e: state = 'failed' hook_return = {} @@ -298,8 +297,8 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, return result -def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user="root", return_format="json"): +def hook_exec(path, args=None, raise_on_error=False, + chdir=None, env=None, return_format="json"): """ Execute hook from a file with arguments @@ -307,11 +306,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, path -- Path of the script to execute args -- Ordered list of arguments to pass to the script raise_on_error -- Raise if the script returns a non-zero exit code - no_trace -- Do not print each command that will be executed chdir -- The directory from where the script will be executed env -- Dictionnary of environment variables to export - user -- User with which to run the command - """ # Validate hook path @@ -350,7 +346,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, if hook_type == 'text/x-python': returncode, returndata = _hook_exec_python(path, args, env, loggers) else: - returncode, returndata = _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, loggers) + returncode, returndata = _hook_exec_bash(path, args, chdir, env, return_format, loggers) # Check and return process' return code if returncode is None: @@ -365,7 +361,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, return returncode, returndata -def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, loggers): +def _hook_exec_bash(path, args, chdir, env, return_format, loggers): from moulinette.utils.process import call_async_output @@ -393,27 +389,20 @@ def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, logge f.write('') env['YNH_STDRETURN'] = stdreturn - # Construct command to execute - if user == "root": - command = ['sh', '-c'] - else: - command = ['sudo', '-n', '-u', user, '-H', 'sh', '-c'] + # use xtrace on fd 7 which is redirected to stdout + env['BASH_XTRACEFD'] = "7" + cmd = '/bin/bash -x "{script}" {args} 7>&1' + cmd = cmd.format(script=cmd_script, args=cmd_args) - if no_trace: - cmd = '/bin/bash "{script}" {args}' - else: - # use xtrace on fd 7 which is redirected to stdout - cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1' + logger.debug("Executing command '%s'" % cmd) - # prepend environment variables - cmd = '{0} {1}'.format( - ' '.join(['{0}={1}'.format(k, shell_quote(v)) - for k, v in env.items()]), cmd) - command.append(cmd.format(script=cmd_script, args=cmd_args)) + _env = os.environ.copy() + _env.update(env) - logger.debug("Executing command '%s'" % ' '.join(command)) - - returncode = call_async_output(command, loggers, shell=False, cwd=chdir) + returncode = call_async_output( + cmd, loggers, shell=False, cwd=chdir, + env=_env + ) raw_content = None try: diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index 2bda72852..1aaefb993 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -3,7 +3,7 @@ import pytest import sys import moulinette -from moulinette import m18n +from moulinette import m18n, msettings from yunohost.utils.error import YunohostError from contextlib import contextmanager sys.path.append("..") @@ -78,3 +78,4 @@ def pytest_cmdline_main(config): sys.path.insert(0, "/usr/lib/moulinette/") import yunohost yunohost.init(debug=config.option.yunodebug) + msettings["interface"] = "test" diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 9798d7519..9fe8c3176 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -219,12 +219,20 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) user_group_update(groupname='all_users', add=username, force=True, sync_perm=True) + # Trigger post_user_create hooks + env_dict = { + "YNH_USER_USERNAME": username, + "YNH_USER_MAIL": mail, + "YNH_USER_PASSWORD": password, + "YNH_USER_FIRSTNAME": firstname, + "YNH_USER_LASTNAME": lastname + } + + hook_callback('post_user_create', args=[username, mail], env=env_dict) + # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) - hook_callback('post_user_create', - args=[username, mail, password, firstname, lastname]) - return {'fullname': fullname, 'username': username, 'mail': mail} @@ -302,6 +310,7 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= from yunohost.app import app_ssowatconf from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface + from yunohost.hook import hook_callback domains = domain_list()['domains'] @@ -312,16 +321,21 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= if not result: raise YunohostError('user_unknown', user=username) user = result[0] + env_dict = { + "YNH_USER_USERNAME": username + } # Get modifications from arguments new_attr_dict = {} if firstname: new_attr_dict['givenName'] = [firstname] # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + user['sn'][0]] + env_dict["YNH_USER_FIRSTNAME"] = firstname if lastname: new_attr_dict['sn'] = [lastname] # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = [user['givenName'][0] + ' ' + lastname] + env_dict["YNH_USER_LASTNAME"] = lastname if lastname and firstname: new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname] @@ -337,6 +351,7 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= assert_password_is_strong_enough("user", change_password) new_attr_dict['userPassword'] = [_hash_user_password(change_password)] + env_dict["YNH_USER_PASSWORD"] = change_password if mail: main_domain = _get_maindomain() @@ -381,6 +396,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= raise YunohostError('mail_alias_remove_failed', mail=mail) new_attr_dict['mail'] = user['mail'] + if 'mail' in new_attr_dict: + env_dict["YNH_USER_MAILS"] = ','.join(new_attr_dict['mail']) + if add_mailforward: if not isinstance(add_mailforward, list): add_mailforward = [add_mailforward] @@ -400,8 +418,12 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= raise YunohostError('mail_forward_remove_failed', mail=mail) new_attr_dict['maildrop'] = user['maildrop'] + if 'maildrop' in new_attr_dict: + env_dict["YNH_USER_MAILFORWARDS"] = ','.join(new_attr_dict['maildrop']) + if mailbox_quota is not None: new_attr_dict['mailuserquota'] = [mailbox_quota] + env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota operation_logger.start() @@ -410,6 +432,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= except Exception as e: raise YunohostError('user_update_failed', user=username, error=e) + # Trigger post_user_update hooks + hook_callback('post_user_update', env=env_dict) + logger.success(m18n.n('user_updated')) app_ssowatconf() return user_info(username)