From c0b7b03ecd96fcc98153af269b8c1c0c758ae628 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Sat, 20 Oct 2012 10:09:46 -0700 Subject: Issue #990812 by Greg Anderson, greg.1.anderson, colan: Add a "permissions" subcommand to fix/set all file permissions --- commands/core/core.drush.inc | 4 + commands/perms/perms.drush.inc | 644 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 648 insertions(+) create mode 100644 commands/perms/perms.drush.inc diff --git a/commands/core/core.drush.inc b/commands/core/core.drush.inc index 72c0614..4c027ea 100644 --- a/commands/core/core.drush.inc +++ b/commands/core/core.drush.inc @@ -472,6 +472,10 @@ function _core_path_aliases($project = '') { $paths['%root'] = $drupal_root; if ($site_root = drush_get_context('DRUSH_DRUPAL_SITE_ROOT')) { $paths['%site'] = $site_root; + $settings_file = $site_root . '/settings.php'; + if (file_exists($settings_file)) { + $paths['%settings'] = $settings_file; + } if (is_dir($modules_path = conf_path() . '/modules')) { $paths['%modules'] = $modules_path; } diff --git a/commands/perms/perms.drush.inc b/commands/perms/perms.drush.inc new file mode 100644 index 0000000..b27b400 --- /dev/null +++ b/commands/perms/perms.drush.inc @@ -0,0 +1,644 @@ + "Set appropriate ownership and permissions of files and directories within a Drupal web directory.", + 'arguments' => array( + 'default owner:group' => 'The user and group that will own most directories and files. For security reasons, it is recommended that this user should be neither "root" (the superuser) nor the web user (e.g. "www-data" or "apache"). Required.', + 'files owner:group' => 'The user and group that will own the "Files" and "Private" directories and files. Optional; default is to use the default owner and group.', + ), + 'required-arguments' => 1, + 'options' => array( + 'doc-files' => array( + 'description' => 'Octal permissions for documentation files such as "INSTALL.txt". Optional; default is 0400.', + 'value' => 'required', + 'example-value' => '0400', + ), + 'doc-patterns' => array( + 'description' => 'Filename pattern of files in the top-level directory that should be treated like documentation. Optional; default is *.txt,quickstart.html', + 'value' => 'required', + 'example-value' => '*.txt', + ), + 'code-files' => array( + 'description' => 'Octal permissions for files that need to be read by (but not written to) the web server such as PHP files. Optional; default is 0644 or 0640 (strict).', + 'value' => 'required', + 'example-value' => '0644', + ), + 'code-dirs' => array( + 'description' => 'Octal permissions for directories that need to be read by (but not written to) the web server such as directories containing PHP files. Optional; default is 0755 or 750 (strict).', + 'value' => 'required', + 'example-value' => '0755', + ), + 'settings' => array( + 'description' => 'Octal permissions for settings.php configuration files. Optional; default is 0440.', + 'value' => 'required', + 'example-value' => '0440', + ), + 'user-files' => array( + 'description' => 'Octal permissions for user-uploaded and Drupal-generated files (files in the "Files" and "Private" directories) that need to be readable and writable by the php process. Optional; default is 0664 or 0640 (strict).', + 'value' => 'required', + 'example-value' => '0664', + ), + 'user-dirs' => array( + 'description' => 'Octal permissions for directories that contain user-uploaded and Drupal-generated files that need to be readable and writable by the php process. Optional; default is 0775 or 0750 (strict).', + 'value' => 'required', + 'example-value' => '0775', + ), + 'dir' => 'Apply permissions changes at the specified directory. Optional; defaults to Drupal root.', + 'not-world-readable' => 'Prevent users who are not the file owner and not in the applicable group from accessing files in the webroot. Optional; defaults to world-readable, except for settings.php.', + 'not-group-writable' => 'Changes the defaults for --user-dirs and --user-files to be the same as the defaults for --code-dirs and --code-files, respectively.', + 'lax' => 'Make writable files writable by any user. Optional; synonym for --code-files=0664 --code-dirs=0775 --user-files=0666 --user-dirs=0777 --doc-files=664.', + 'strict' => 'Optional; synonym for --not-world-readable --not-group-writable. Overrides --lax.', + 'skip-set-owner' => 'Presumes that the owner of the files is already correct, and skips setting it. Allows execution by unpriviledged user.', + 'sudo' => array( + 'description' => 'Call sudo before commands that set file ownership. If --sudo=all is specified, then sudo is also used before commands that set file permissions.', + 'value' => 'optional', + 'example-value' => 'all', + ), + 'script' => 'Output a script instead of executing the commands.', + 'direct' => 'In --script mode, do not assign variables; use values directly.', + 'audit' => 'Change nothing; only show files and folders that do not match requested permissions.', + 'files' => array( + 'description' => 'Comma-separated list of paths to files directories. Optional; default is "%files,%private".', + 'value' => 'required', + 'example-value' => '%files,%private', + ), + 'settings' => array( + 'description' => 'Comma-separated list of paths to settings files. Optional; default is "%settings".', + 'value' => 'required', + 'example-value' => '%settings', + ), + 'exclude' => array( + 'description' => 'Comma-separated list of paths to directories that should never be touched (e.g. network-mounted shared folders). Optional.', + 'value' => 'required', + 'example-value' => '%private', + ), + ), + 'examples' => array( + 'drush permissions www-admin:www-data' => 'Set permissions with "www-admin" as the user owner and "www-data" as the group owner. "Files" and "Private" will be writable by the group "www-data"; other files will be writable by the owner and world readable.', + 'drush permissions www-admin:www-data www-data --strict' => 'Set permissions with "www-admin" as the user owner and "www-data" as the group owner of the code files, and "www-data" as the owner of "Files" and "Private". Other users not in the www-data group will not be able to read any files.', + 'drush perms bob:devs www-data:devs --lax' => 'Set permissions with "bob" as the user owner and "devs" as the group owner. "Files" and "Private" will be world-writable. Members of group "devs" will be able to write to all files (except settings.php). Other files will be world-readable.', + 'drush perms bob www-data' => 'Set permissions with "bob" as the user and group owner". Writable files will be owned by "www-data". Other files will be world-readable.', + 'sudo chown -R www-admin . && drush perms --skip-owner www-data' => 'Only run unpriviledged commands from Drush.', + 'drush perms --sudo www-admin:www-data' => 'Instruct Drush to call sudo before executing priviledged commands.', + 'drush perms --script www-admin:www-data > perms.sh && chmod +x perms.sh && sudo ./perms.sh' => 'Have Drush generate a script suitable for execution via sudo.' + ), + 'aliases' => array('perms'), + // Just need the root directory. + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT, + ); + + return $items; +} + +/** + * Implementats hook_drush_help(). + */ +function perms_drush_help($section) { + switch ($section) { + case 'drush:permissions': + return dt("Set appropriate ownership and permissions of files and directories within a Drupal web directory."); + } +} + +/** + * Implementats drush_hook_COMMAND_pre_validate(). + * + * We will interpret the parameters upfront, and allow validate hooks to operate + * on the calculated values, if desired. + */ +function drush_perms_permissions_pre_validate($default_owner_group = '', $files_owner_group = '') { + $lax = drush_get_option('lax', FALSE); + $not_world_readable = drush_get_option('not-world-readable', FALSE); + $not_group_writable = drush_get_option('not-group-writable', FALSE); + if (drush_get_option('strict', FALSE)) { + $lax = FALSE; + $not_world_readable = TRUE; + $not_group_writable = TRUE; + } + $vars = array( + 'settings' => '0440', + 'doc-files' => $lax ? '0664' : '0400', + 'code-files' => $lax ? '0664' : '0644', + 'code-dirs' => $lax ? '0775' : '0755', + 'user-files' => $lax ? '0666' : '0664', + 'user-dirs' => $lax ? '0777' : '0775', + ); + // If --not-world-readable or --not-group-writable was specified, strip + // permission bits off of the default values. + if ($not_world_readable || $not_group_writable) { + foreach ($vars as $name => $value) { + $value = base_convert($value, 8, 10); + if ($not_world_readable) { + $value = $value & base_convert('0770', 8, 10); + } + if ($not_group_writable) { + $value = $value & base_convert('0757', 8, 10); + } + $value = '0' . base_convert($value, 10, 8); + $vars[$name] = $value; + } + } + // Process owner and group variables + if (!is_array($default_owner_group)) { + $default_owner_group = explode(':', $default_owner_group); + } + if (count($default_owner_group) < 2) { + $default_owner_group[1] = $default_owner_group[0]; + } + if (empty($files_owner_group)) { + $files_owner_group = $default_owner_group; + } + if (!is_array($files_owner_group)) { + $files_owner_group = explode(':', $files_owner_group); + } + if (count($files_owner_group) < 2) { + $files_owner_group[1] = $files_owner_group[0]; + } + $vars['default-owner'] = $default_owner_group[0]; + $vars['default-group'] = $default_owner_group[1]; + $vars['files-owner'] = $files_owner_group[0]; + $vars['files-group'] = $files_owner_group[1]; + // Allow user to give specific values for each permission via options + foreach ($vars as $name => $value) { + $vars[$name] = drush_get_option($name, $value); + } + drush_set_context('DRUSH_PERMS', $vars); +} + +/* + * Implementats drush_hook_COMMAND_validate(). + */ +function drush_perms_permissions_validate($default_owner_group = '', $files_owner_group = '') { + $vars = drush_get_context('DRUSH_PERMS'); + + // Make sure that we've got a POSIX system. + if (!function_exists('posix_getpwuid')) { + return drush_set_error( + 'ERROR_NON_POSIX', + dt('Currently, this command can only be run on a POSIX system.') + ); + } +/* + // We could try to automatically skip change owner commands if we are not running via sudo. + // Perhaps erroring out is preferable. + $name = posix_getpwuid(posix_geteuid()); + if ($name['name'] !== 'root') { + if (!drush_get_option('skip-set-owner', FALSE) && !drush_get_option('sudo', FALSE) && !drush_get_option('script', FALSE)) { + drush_log(dt("You must run as root in order to change file ownership."), 'warning'); + drush_set_option('skip-set-owner', TRUE); + } + } +*/ + // Skip user/group validation if generating a script (perhaps to run on a different machine) + if (!drush_get_option('script', FALSE)) { + // Make sure that the username is valid. + if (!posix_getpwnam($vars['default-owner'])) { + return drush_set_error( + 'ERROR_USER_INVALID', + dt('The user !user must be a valid user.', array('!user' => $vars['default-owner'])) + ); + } + + if (!posix_getpwnam($vars['files-owner'])) { + return drush_set_error( + 'ERROR_USER_INVALID', + dt('The user !user must be a valid user.', array('!user' => $vars['files-owner'])) + ); + } + + // Make sure that the group is valid. + if (!posix_getgrnam($vars['default-group'])) { + return drush_set_error( + 'ERROR_GROUP_INVALID', + dt('The group !group must be a valid group.', array('!group' => $vars['default-group'])) + ); + } + + if (!posix_getgrnam($vars['files-group'])) { + return drush_set_error( + 'ERROR_GROUP_INVALID', + dt('The group !group must be a valid group.', array('!group' => $vars['files-group'])) + ); + } + } + + drush_set_context('DRUSH_PERMS', $vars); +} + +/** + * Implements drush_COMMANDFILE_COMMANDNAME(). + */ +function drush_perms_permissions($default_owner_group = '', $files_owner_group = '') { + $vars = drush_get_context('DRUSH_PERMS'); + + // Determine the base directory to operate on + if (!$base_dir = drush_get_option('dir', drush_get_context('DRUSH_DRUPAL_ROOT'))) { + return drush_set_error( + 'ERROR_CANNOT_GET_BASE_DIR', + dt('Cannot determine the directory to operate on. Specify a Drupal site to use, or pass in --dir=. for the current working directory.') + ); + } + + // If we are in --script mode, print out the script header + $script = drush_get_option('script', FALSE); + if ($script) { + $exec['script-header'] = "#!/bin/bash"; + + // If we are not in 'script' mode, replace data values with variables + if (!drush_get_option('direct', FALSE)) { + foreach($vars as $key => $value) { + $name = '$' . str_replace("-", "_", strtoupper($key)); + $exec['assign-' . $key] = "$name='$value'"; + $vars[$key] = array('#escape' => FALSE, 'value' => $name); + } + } + } + + // Fill in variables for the files (%files, %private) and settings (%settings) + // options, so that users can select arbitrary files and directories to handle + // specially. + $user_files_list = drush_get_option_list('files', '%files,%private'); + $exclude_files_list = drush_get_option_list('exclude', array()); + $settings_list = drush_get_option_list('settings', '%settings'); + $doc_pattern_list = drush_get_option_list('doc-patterns', '*.txt,quickstart.html'); + + // respect --skip-set-owner, and only chgrp if specified. + $owner = drush_get_option('skip-set-owner', FALSE) ? '' : $vars['default-owner']; + $group = $vars['default-group']; + $files_owner = drush_get_option('skip-set-owner', FALSE) ? '' : $vars['files-owner']; + $files_group = $vars['files-group']; + + // chown everything that needs to be chown'ed + $exec['chown-default'] = _drush_perms_build_chown_command($base_dir, $owner, $group); + + // Run chmod on all directories and files at the root. + $exec['chmod-code-dirs'] = _drush_perms_build_chmod_command($base_dir, 'd', $vars['code-dirs']); + $exec['chmod-code-files'] = _drush_perms_build_chmod_command($base_dir, 'f', $vars['code-files']); + + // By default, we'll avoid running chown and chmod commands + // on %files and %private, since they will have their own + // user / group settings. If, by chance, the modifications + // for %files and %private are the same, then we'll leave these + // directories in the main operation. + $owner_differs = (($owner != $files_owner) || ($group != $files_group)); + $dir_perms_differ = $vars['code-dirs'] != $vars['user-dirs']; + $file_perms_differ = $vars['code-files'] != $vars['user-files']; + foreach($user_files_list as $path_key) { + $absolute_path = _drush_perms_evaluate_path($base_dir, $path_key); + if ($absolute_path) { + $relative_path = _drush_perms_relative_path($base_dir, $absolute_path); + // We need to build a separate chown command if the file owners are different + // than the default owner, or if the file path is outside the base_dir + if ($owner_differs || ($relative_path === FALSE)) { + $exec['chown-' . $path_key] = _drush_perms_build_chown_command($absolute_path, $files_owner, $files_group); + } + // We need to exclude the file path from the default path if the + // owners are different and the file path is inside the base_dir + if ($owner_differs && ($relative_path !== FALSE)) { + $exec['chown-default']['!tests']['exclude-' . $path_key] = array( + 'tmpl' => '-prune ^path', + '^path' => $relative_path, + ); + } + // Similarly, make our own chmod commands for user dirs and files if needed + if ($dir_perms_differ || ($relative_path === FALSE)) { + $exec['user-dirs'] = _drush_perms_build_chmod_command($absolute_path, 'd', $vars['user-dirs']); + } + if ($dir_perms_differ && ($relative_path !== FALSE)) { + $exec['chmod-code-dirs']['!tests']['exclude-' . $path_key] = array( + 'tmpl' => '-prune ^path', + '^path' => $relative_path, + ); + } + if ($file_perms_differ || ($relative_path === FALSE)) { + $exec['user-files'] = _drush_perms_build_chmod_command($absolute_path, 'd', $vars['user-files']); + } + if ($file_perms_differ && ($relative_path !== FALSE)) { + $exec['chmod-code-files']['!tests']['exclude-' . $path_key] = array( + 'tmpl' => '-prune ^path', + '^path' => $relative_path, + ); + } + } + } + // Exclude the directories specified via --exclude in every command + // that is operating from $base_dir + foreach($exclude_files_list as $path_key) { + $absolute_path = _drush_perms_evaluate_path($base_dir, $path_key); + if ($absolute_path) { + $relative_path = _drush_perms_relative_path($base_dir, $absolute_path); + foreach (array_keys($exec) as $key) { + if (is_array($exec) && isset($exec[$key]['!tests']) && ($base_dir == $exec[$key]['^base_dir'])) { + $exec[$key]['!tests']['exclude-' . $path_key] = array( + 'tmpl' => '-prune ^path', + '^path' => $relative_path, + ); + } + } + } + } + + // Next, process settings.php (or the user-supplied substitutes for same) + foreach($settings_list as $settings_key) { + $absolute_path = _drush_perms_evaluate_path($base_dir, $settings_key); + if ($absolute_path) { + $relative_path = _drush_perms_relative_path($base_dir, $absolute_path); + // Build a chmod command for settings. Settings are always included + // in the chown command 'chown-default'. + $exec['file-' . $settings_key] = array( + 'tmpl' => 'chmod !permissions !file', + '!permissions' => $vars['settings'], + '!file' => $absolute_path, + ); + // Exclude settings.php from the 'find' command that sets code file permissions + $exec['chmod-code-files']['!tests']['exclude-' . $settings_key] = array( + 'tmpl' => '\\! -path ^file', + '^file' => $relative_path, + ); + } + } + + // If the doc files have different permissions, then exclude them from + // the 'chmod-code-files' command and gen up our own chmod that operates only + // on the specified file patterns at the Drupal root. + $doc_perms_differ = $vars['code-files'] != $vars['doc-files']; + if ($doc_perms_differ) { + $exec['doc-files'] = _drush_perms_build_docs_chmod_command($base_dir, $doc_pattern_list, FALSE, $vars['doc-files']); + $exec['not-doc-files'] = _drush_perms_build_docs_chmod_command($base_dir, $doc_pattern_list, TRUE, $vars['code-files']); + // We skip the files at the immediate root, as these files are covered by the commands we built above + $exec['chmod-code-files']['!options']['mindepth'] = '-mindepth 2'; + } + + // In sudo mode, add a 'sudo' to each command + $sudo = drush_get_option('sudo', FALSE); + if ($sudo) { + foreach($exec as $key => $command_record) { + // If --sudo=all was specified, then add a 'sudo' to every command. + // Otherwise, only add 'sudo' to commands flagged as '#priviledged' + if (is_array($command_record) && isset($command_record['!command']) && (($sudo === 'all') || (array_key_exists('#priviledged', $command_record)))) { + $exec[$key]['!command']['tmpl'] = 'sudo ' . $exec[$key]['!command']['tmpl']; + } + } + } + // If in audit mode, convert the commands to 'echo'; + $audit = drush_get_option('audit', FALSE); + if ($audit) { + foreach($exec as $key => $command_record) { + if (is_array($command_record) && isset($command_record['!command'])) { + $exec[$key]['!command']['tmpl'] = 'echo ' . $exec[$key]['!command']['tmpl']; + } + } + } + + // Print or execute each command + foreach($exec as $key => $command_record) { + $cmd = _drush_perms_render_command($command_record); + if ($script) { + drush_print($cmd); + } + else { + $result = drush_op_system($cmd); + if ($result > 0) { + return FALSE; + } + } + } + if (!$script && !$audit) { + drush_log(dt("Permissions change complete."), 'success'); + } + return TRUE; +} + +function _drush_perms_relative_path($base_path, $test_path) { + $result = $test_path; + if (drush_is_absolute_path($test_path)) { + if ((substr($test_path, 0, strlen($base_path)) == $base_path) && (($test_path[strlen($base_path)] == '/') || ($test_path[strlen($base_path)] == DIRECTORY_SEPARATOR))) { + $result = substr($test_path, strlen($base_path) + 1); + } + else { + $result = FALSE; + } + } + return $result; +} + +function _drush_perms_absolute_path($base_path, $test_path) { + if (drush_is_absolute_path($test_path)) { + return $test_path; + } + else { + return $base_path . DIRECTORY_SEPARATOR . $test_path; + } +} + +function _drush_perms_evaluate_path($base_dir, $path) { + if ($path[0] == '%') { + $path = _drush_core_directory($path, 'path', TRUE); + return (strpos($path, '%') === FALSE) ? $path : FALSE; + } + else { + return _drush_perms_absolute_path($base_dir, $path); + } +} + +/** + * Generate a command record that will chown files via 'find' and 'exec' + */ +function _drush_perms_build_chown_command($base_dir, $owner, $group) { + $result = array(); + $chmod_template = "!cmd ^arg --"; + $test_owner_template = "\\( \\! -group ^group -o \\! -user ^owner \\)"; + $priviledged = TRUE; + $command = 'chown'; + $arg = $owner; + $kind = '-owner'; + if (empty($owner) || empty($group)) { + $template = "! !kind ^arg"; + } + if (empty($owner)) { + $command = 'chgrp'; + $arg = $group; + $kind = '-group'; + $priviledged = FALSE; + } + elseif (!empty($group)) { + $chmod_template = "!cmd ^owner:^group --"; + } + if (!empty($arg)) { + $result = array( + 'tmpl' => "find ^base_dir !options !tests -print0 | xargs -0r !command", + // ^base_dir will be shell escaped before being inserted into tmpl + '^base_dir' => $base_dir, + '!options' => array(), + // !tests will be replaced by the space-separated options list provided + '!tests' => array( + 'test-owner' => array( + 'tmpl' => $test_owner_template, + '!kind' => $kind, + '^arg' => $arg, + '^owner' => $owner, + '^group' => $group, + ), + ), + // !command will be recursively evaluated and inserted into 'tmpl'. + '!command' => array( + 'tmpl' => $chmod_template, + '!cmd' => $command, + '^arg' => $arg, + '^owner' => $owner, + '^group' => $group, + ) + ); + if ($priviledged) { + // Mark #priviledged to indicate the superuser priviledges + // are always necessary to run this command (changing owner). + $result['#priviledged'] = TRUE; + } + } + return $result; +} + +/** + * Generate a command record that will chmod files via 'find' and 'exec' + * + * Ex: + * find /path -type f ! -perm 0755 -print0 | xargs -0r chmod 0755 -- + */ +function _drush_perms_build_chmod_command($base_dir, $type, $permissions) { + return array( + 'tmpl' => "find ^base_dir !options !tests -print0 | xargs -0r !command", + // ^base_dir will be shell escaped before being inserted into tmpl + '^base_dir' => $base_dir, + '!options' => array(), + // !tests will be replaced by the space-separated options list provided + '!tests' => array( + 'type' => array( + 'tmpl' => '-type !type', + '!type' => $type, + ), + 'perm' => array( + 'tmpl' => '\\! -perm !permissions', + '!permissions' => $permissions, + ), + ), + // !command will be recursively evaluated and inserted into 'tmpl'. + '!command' => array( + 'tmpl' => "chmod !permissions --", + '!permissions' => $permissions, + ) + ); +} + +/** + * Generate a command record that will chmod documentation files via 'find' and 'exec' + * + * Ex: + * find /path -maxdepth 1 -type f ! ( -path '*.txt' -o -path 'quickstart.html' ) -print0 | xargs -0r chmod 0400 -- + */ +function _drush_perms_build_docs_chmod_command($base_dir, $doc_pattern_list, $not, $permissions) { + $result = _drush_perms_build_chmod_command($base_dir, 'f', $permissions); + $doc_pattern_tests = array(); + $multiple = FALSE; + foreach ($doc_pattern_list as $key => $pattern) { + if (!empty($doc_pattern_tests)) { + $doc_pattern_tests['or-' . $pattern] = '-o'; + $multiple = TRUE; + } + $doc_pattern_tests['doc-' . $pattern] = array( + 'tmpl' => '-path ^pattern', + '^pattern' => $pattern, + ); + } + $result['!options']['maxdepth'] = '-maxdepth 1'; + if ($not) { + $result['!tests']['not'] = '\\!'; + } + if ($multiple) { + $result['!tests']['doc-pattern-list'] = array( + 'list-start' => '\\(', + 'list' => $doc_pattern_tests, + 'list-end' => '\\)', + ); + } + else { + $result['!tests']['doc-pattern-list'] = $doc_pattern_tests; + } + return $result; +} + +/** + * Convert a command record into an executable command string. + * + * @param mixed $exec + * The command to render. Processing depends on the structure of the data. + * string - returns the string itself + * array + * 'tmpl' => 'command template with !replacements', + * '!var' => array(...), // nested command record + * '^var' => array(...), // data is escaped for shell + * array + * 'id1' => array(...), // nested command record + * 'id2' => array(...), // nested command record + * + * Command records with a 'tmpl' are expanded much as the t() function + * is, with the data from the other elements in the array also being + * evaluated and used as substitutions in the template. + * + * If no template is provided, then every item in the array is evaluated + * as a command template, and all are concatinated together, separated by + * spaces, and returned. The id is ignored, except for the fact that the + * value will be escaped if the id begins with "^". + */ +function _drush_perms_render_command($exec, $replacements = array()) { + $result = ''; + if (is_array($exec)) { + $data = array(); + if (array_key_exists('tmpl', $exec)) { + $tmpl = $exec['tmpl']; + unset($exec['tmpl']); + } + foreach ($exec as $key => $info) { + if ($key[0] != '#') { + $escape = (is_array($info) && array_key_exists('#escape', $info)) ? $info['#escape'] : 'default'; + $value = _drush_perms_render_command($info, $replacements); + if ($escape == 'default') { + $escape = ($key[0] == '^'); + } + if ($escape) { + $value = drush_escapeshellarg($value); + } + $data[$key] = $value; + } + } + // First array form: array with 'tmpl' and replacements + if (isset($tmpl)) { + $result = str_replace(array_keys($data), array_values($data), $tmpl); + } + // Second array form: array of command records to be concatinated + else { + $result = implode(" ", $data); + } + } + else { + // String form: just return the data unmodified. + $result = $exec; + // Allow simple results to be replaced by variables such as %files where appropriate. + $result = str_replace(array_keys($replacements), array_values($replacements), $result); + } + return $result; +} -- 1.7.9.5