From 682217800468f568e5a5cf587b418b8a71d5886e Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Wed, 24 Oct 2012 11:51:32 -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 | 851 ++++++++++++++++++++ tests/drush_testcase.inc | 57 ++ tests/permissionstestdir/INSTALL.txt | 1 + tests/permissionstestdir/includes/bootstrap.inc | 1 + tests/permissionstestdir/index.php | 1 + tests/permissionstestdir/misc/drupal.js | 1 + tests/permissionstestdir/quickstart.html | 1 + tests/permissionstestdir/robots.txt | 1 + .../sites/default/files/uploadfile.txt | 1 + .../permissionstestdir/sites/default/settings.php | 1 + tests/permsTest.php | 385 +++++++++ 12 files changed, 1305 insertions(+) create mode 100644 commands/perms/perms.drush.inc create mode 100644 tests/permissionstestdir/INSTALL.txt create mode 100644 tests/permissionstestdir/includes/bootstrap.inc create mode 100644 tests/permissionstestdir/index.php create mode 100644 tests/permissionstestdir/misc/drupal.js create mode 100644 tests/permissionstestdir/quickstart.html create mode 100644 tests/permissionstestdir/robots.txt create mode 100644 tests/permissionstestdir/sites/default/files/uploadfile.txt create mode 100644 tests/permissionstestdir/sites/default/settings.php create mode 100644 tests/permsTest.php 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..7e394ff --- /dev/null +++ b/commands/perms/perms.drush.inc @@ -0,0 +1,851 @@ + "Set appropriate ownership and permissions of files and directories within a Drupal web directory.", + '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. Suitable for use in a typical site running on a dedicated server. The web server user / php process MUST be a member of the group www-data.', + '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 both the owner and group of "Files" and "Private". Other users not in the www-data group will not be able to read any files. Suitable for use in a typical site running on a shared server. The web server user / php process MUST be a member of the group "www-data".', + '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. Suitable for use in a typical development environment. The web server user / php process SHOULD NOT be a member of the group "devs".', + 'drush perms bob www-data' => 'Set permissions with "bob" as the user and group owner of code files. Data files will be owned by "www-data". Other files will be world-readable.', + 'sudo chown -R www-admin . && drush perms --skip-set-owner www-data' => 'Only run unpriviledged commands from Drush.', + 'drush perms --sudo=all www-admin:www-data' => 'Instruct Drush to call sudo before executing priviledged commands. n.b. This is preferable to using `sudo drush ...`, which gives all enabled contrib modules an opportunity to run arbitrary code as the superuser.', + 'drush perms --pipe www-admin:www-data > perms.sh && chmod +x perms.sh && sudo ./perms.sh' => 'Generate a script and run it via sudo. Even more secure than the --sudo option.', + ), + 'arguments' => array( + 'code 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"). Optional; if not specified, forces --skip-set-owner and sets permissions only.', + 'data owner:group' => 'The user and group that will own the "Files" and "Private" directories and files. Optional; default is to use the code owner and group.', + ), + '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', + ), + 'doc-exceptions' => array( + 'description' => 'Filename pattern of files in the top-level directory that should NOT be treated like documentation, even though they match the pattern given in --doc-patterns. Optional; default is robots.txt', + 'value' => 'required', + 'example-value' => 'robots.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-files' => array( + 'description' => 'Octal permissions for settings.php configuration files. Optional; default is 0440.', + 'value' => 'required', + 'example-value' => '0440', + ), + 'data-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', + ), + 'data-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', + ), + 'code-files-group' => array( + 'description' => 'The group who will own the code (.php) and documentation (.txt) files. Alternative to specifying owner via the commandline argument; allows setting a group without setting the user.', + 'value' => 'required', + 'example-value' => 'www-data', + ), + 'code-files-owner' => array( + 'description' => 'The user who will own the code (.php) and documentation (.txt) files.', + 'value' => 'required', + 'example-value' => 'www-admin', + ), + 'data-files-group' => array( + 'description' => 'The group who will own the user-provided and Drupal-generated files.', + 'value' => 'required', + 'example-value' => 'www-data', + ), + 'data-files-owner' => array( + 'description' => 'The user who will own the user-provided and Drupal-generated files.', + 'value' => 'required', + 'example-value' => 'www-data', + ), + 'dir' => array( + 'description' => 'Apply permissions changes at the specified directory. Optional; defaults to Drupal root.', + 'value' => 'required', + 'example-value' => '.', + ), + '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 --data-dirs and --data-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 --data-files=0666 --data-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', + ), + 'pipe' => 'Output a script instead of executing the commands.', + 'no-variables' => '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', + ), + ), + 'aliases' => array('perms'), + 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, + ); + + 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($code_files_owner_group = '', $data_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-files' => '0440', + 'doc-files' => $lax ? '0664' : '0400', + 'code-files' => $lax ? '0664' : '0644', + 'code-dirs' => $lax ? '0775' : '0755', + 'data-files' => $lax ? '0666' : '0664', + 'data-dirs' => $lax ? '0777' : '0775', + ); + $permission_variable_keys = array_keys($vars); + // Process owner and group variables + if (!drush_get_option('skip-set-owner', FALSE)) { + if (!is_array($code_files_owner_group)) { + $code_files_owner_group = explode(':', $code_files_owner_group); + } + if (count($code_files_owner_group) < 2) { + $code_files_owner_group[1] = $code_files_owner_group[0]; + } + if (empty($data_files_owner_group)) { + $data_files_owner_group = $code_files_owner_group; + } + if (!is_array($data_files_owner_group)) { + $data_files_owner_group = explode(':', $data_files_owner_group); + } + if (count($data_files_owner_group) < 2) { + $data_files_owner_group[1] = $data_files_owner_group[0]; + } + $vars['code-files-owner'] = $code_files_owner_group[0]; + $vars['code-files-group'] = $code_files_owner_group[1]; + $vars['data-files-owner'] = $data_files_owner_group[0]; + $vars['data-files-group'] = $data_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); + } + // --strict takes precidence over user-specified permission values. + // If --not-world-readable or --not-group-writable was specified, strip + // permission bits off of the provided permission values. + if ($not_world_readable || $not_group_writable) { + foreach ($permission_variable_keys as $name) { + $value = base_convert($vars[$name], 8, 10); + if ($not_world_readable) { + $value = (int)$value & base_convert('0770', 8, 10); + } + if ($not_group_writable) { + $value = (int)$value & base_convert('0757', 8, 10); + } + $value = '0' . base_convert($value, 10, 8); + $vars[$name] = $value; + } + } + drush_set_context('DRUSH_PERMS', $vars); +} + +/* + * Implementats drush_hook_COMMAND_validate(). + */ +function drush_perms_permissions_validate($code_files_owner_group = '', $data_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_context('DRUSH_PIPE', 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_context('DRUSH_PIPE', FALSE) && !drush_get_option('skip-set-owner', FALSE)) { + // Make sure that the username is valid. + if (!posix_getpwnam($vars['code-files-owner'])) { + return drush_set_error( + 'ERROR_USER_INVALID', + dt('The user !user must be a valid user.', array('!user' => $vars['code-files-owner'])) + ); + } + + if (!posix_getpwnam($vars['data-files-owner'])) { + return drush_set_error( + 'ERROR_USER_INVALID', + dt('The user !user must be a valid user.', array('!user' => $vars['data-files-owner'])) + ); + } + + // Make sure that the group is valid. + if (!posix_getgrnam($vars['code-files-group'])) { + return drush_set_error( + 'ERROR_GROUP_INVALID', + dt('The group !group must be a valid group.', array('!group' => $vars['code-files-group'])) + ); + } + + if (!posix_getgrnam($vars['data-files-group'])) { + return drush_set_error( + 'ERROR_GROUP_INVALID', + dt('The group !group must be a valid group.', array('!group' => $vars['data-files-group'])) + ); + } + } + + drush_set_context('DRUSH_PERMS', $vars); +} + +/** + * Implements drush_COMMANDFILE_COMMANDNAME(). + */ +function drush_perms_permissions($code_files_owner_group = '', $data_files_owner_group = '') { + $vars = drush_get_context('DRUSH_PERMS'); + + // Determine the base directory to operate on + if (!$base_dir = drush_get_option('dir')) { + drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_ROOT); + if (!$base_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.') + ); + } + } + // We always work with absolute paths; adjust base dir if relative path provided + if (!drush_is_absolute_path($base_dir)) { + if ($base_dir == '.') { + $base_dir = ''; + } + $base_dir = drush_get_context('DRUSH_OLDCWD', getcwd()) . str_replace('./', '', $base_dir); + } + + // If we are in --pipe mode, print out the script header + $script = drush_get_context('DRUSH_PIPE', FALSE); + if ($script) { + $command_list['script-header'] = "#!/bin/bash"; + + // If we are not in 'script' mode, replace data values with variables + if (!drush_get_option('no-variables', FALSE)) { + foreach($vars as $key => $value) { + $name = str_replace("-", "_", strtoupper($key)); + $command_list['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_perms_get_option_list('files', '%files,%private'); + $exclude_files_list = _drush_perms_get_option_list('exclude', array()); + $settings_list = _drush_perms_get_option_list('settings', '%settings'); + $doc_pattern_list = _drush_perms_get_option_list('doc-patterns', '*.txt,quickstart.html'); + $doc_exceptions_list = _drush_perms_get_option_list('doc-exceptions', 'robots.txt'); + + // respect --skip-set-owner, and only chgrp if specified. + $owner = drush_get_option('skip-set-owner', FALSE) ? '' : $vars['code-files-owner']; + $group = array_key_exists('code-files-group', $vars) ? $vars['code-files-group'] : ''; + $files_owner = drush_get_option('skip-set-owner', FALSE) ? '' : $vars['data-files-owner']; + $files_group = array_key_exists('data-files-group', $vars) ? $vars['data-files-group'] : ''; + + // chown everything that needs to be chown'ed + if (!empty($owner) || !empty($group)) { + $command_list['chown-default'] = _drush_perms_build_chown_command($base_dir, $owner, $group); + } + + // Run chmod on all directories and files at the root. + $command_list['chmod-code-dirs'] = _drush_perms_build_chmod_command($base_dir, 'd', $vars['code-dirs']); + $command_list['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['data-dirs']; + $file_perms_differ = $vars['code-files'] != $vars['data-files']; + foreach($user_files_list as $path_key) { + $absolute_path = _drush_perms_evaluate_path($base_dir, $path_key); + if ($absolute_path && !_drush_perms_is_excluded($exclude_files_list, $base_dir, $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) && ((!empty($files_owner)) || (!empty($files_group)))) { + $command_list['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)) { + $command_list['chown-default']['!prune']['terms']['exclude-' . $path_key] = array( + 'tmpl' => '-path ^path', + '^path' => $absolute_path, + ); + } + // Similarly, make our own chmod commands for user dirs and files if needed + if ($dir_perms_differ || ($relative_path === FALSE)) { + $command_list['data-dirs'] = _drush_perms_build_chmod_command($absolute_path, 'd', $vars['data-dirs']); + } + if ($dir_perms_differ && ($relative_path !== FALSE)) { + $command_list['chmod-code-dirs']['!prune']['terms']['exclude-' . $path_key] = array( + 'tmpl' => '-path ^path', + '^path' => $absolute_path, + ); + } + if ($file_perms_differ || ($relative_path === FALSE)) { + $command_list['data-files'] = _drush_perms_build_chmod_command($absolute_path, 'f', $vars['data-files']); + } + if ($file_perms_differ && ($relative_path !== FALSE)) { + $command_list['chmod-code-files']['!prune']['terms']['exclude-' . $path_key] = array( + 'tmpl' => '-path ^path', + '^path' => $absolute_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) { + foreach (array_keys($command_list) as $key) { + if (is_array($command_list) && isset($command_list[$key]['!tests'])) { + $path_base_dir = $command_list[$key]['^base_dir']; + $relative_path = _drush_perms_relative_path($path_base_dir, $absolute_path); + if (!empty($relative_path)) { + $command_list[$key]['!prune']['terms']['exclude-' . $path_key] = array( + 'tmpl' => '-path ^path', + '^path' => $absolute_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); + if (!empty($relative_path)) { + // Build a chmod command for settings. Settings are always included + // in the chown command 'chown-default'. + $command_list['file-' . $settings_key] = array( + 'tmpl' => 'chmod !permissions !file', + '!permissions' => $vars['settings-files'], + '!file' => $absolute_path, + ); + // Exclude settings.php from the 'find' command that sets code file permissions + $command_list['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) { + $command_list['doc-files'] = _drush_perms_build_docs_chmod_command($base_dir, FALSE, $doc_pattern_list, $doc_exceptions_list, $vars['doc-files']); + $command_list['not-doc-files'] = _drush_perms_build_docs_chmod_command($base_dir, TRUE, $doc_pattern_list, $doc_exceptions_list, $vars['code-files']); + // We skip the files at the immediate root, as these files are covered by the commands we built above. + // Adding a `-mindepth 2` term interferes with -prune, so we instead use a pattern of '*/*' to + // mean "path contains a slash" (that is, path is not a file at the root). We also must prepend + // with "$base_dir/". + $command_list['chmod-code-files']['!tests']['not-at-root'] = array( + 'tmpl' => '-path ^path', + '^path' => $base_dir . '/*/*', + ); + } + + // In sudo mode, add a 'sudo' to each command + $sudo = drush_get_option('sudo', FALSE); + if ($sudo) { + foreach($command_list 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)))) { + $command_list[$key]['!command']['tmpl'] = 'sudo ' . $command_list[$key]['!command']['tmpl']; + } + } + } + // If in audit mode, convert the commands to 'echo'; + $audit = drush_get_option('audit', FALSE); + if ($audit) { + foreach($command_list as $key => $command_record) { + if (is_array($command_record) && isset($command_record['!command'])) { + $command_list[$key]['!command']['tmpl'] = 'echo ' . $command_list[$key]['!command']['tmpl']; + } + } + } + + // Print or execute each command + foreach($command_list as $key => $command_record) { + $cmd = _drush_perms_render_command($command_record); + if ($script) { + drush_print_pipe($cmd . "\n"); + } + elseif (!empty($cmd) && ($cmd[0] != '#')) { + $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 ($test_path == $base_path) { + $result = './'; + } + elseif (substr($test_path, 0, strlen($base_path)) == $base_path) { + if (($test_path[strlen($base_path)] == '/') || ($test_path[strlen($base_path)] == DIRECTORY_SEPARATOR)) { + $result = substr($test_path, strlen($base_path) + 1); + } + else { + $result = FALSE; + } + } + else { + $result = FALSE; + } + } + return $result; +} + +function _drush_perms_is_excluded($exclude_files_list, $base_dir, $test_path) { + foreach ($exclude_files_list as $excluded) { + $excluded_absolute = _drush_perms_absolute_path($base_dir, $excluded); + $relative_path = _drush_perms_relative_path($excluded_absolute, $test_path); + if ($relative_path !== FALSE) { + return TRUE; + } + } + + return FALSE; +} + +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 !prune \( !tests -print0 \) | xargs -0r !command", + // ^base_dir will be shell escaped before being inserted into tmpl + '^base_dir' => $base_dir, + '!options' => array(), + // !prune might have terms added to it later by caller + '!prune' => array( + 'terms' => array( + '#alter' => array('or-terms', 'parenthesize'), + ), + // #alter condition-not-empty will cause this item to be skipped if + // none of the preceding terms generated any output. + 'prune' => array( + '#alter' => array('condition-not-empty'), + 'tmpl' => '-prune -o', + ), + ), + // !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 !prune \( !tests -print0 \) | xargs -0r !command", + // ^base_dir will be shell escaped before being inserted into tmpl + '^base_dir' => $base_dir, + '!options' => array(), + // !prune might have terms added to it later by caller + '!prune' => array( + 'terms' => array( + '#alter' => array('or-terms', 'parenthesize'), + ), + // #alter condition-not-empty will cause this item to be skipped if + // none of the preceding terms generated any output. + 'prune' => array( + '#alter' => array('condition-not-empty'), + 'tmpl' => '-prune -o', + ), + ), + // !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, $not, $doc_pattern_list, $doc_exceptions_list, $permissions) { + $result = _drush_perms_build_chmod_command($base_dir, 'f', $permissions); + $result['!options']['maxdepth'] = '-maxdepth 1'; + $doc_exceptions = array('#alter' => array('or-terms', 'parenthesize')); + foreach ($doc_exceptions_list as $key => $item) { + $doc_exceptions['doc-' . $item] = array( + 'tmpl' => '-path ^item', + '^item' => $base_dir . '/' . $item, + ); + } + if ((count($doc_exceptions_list) > 0) && ($not)) { + $result['!tests']['path-start-paren'] = '\('; + $result['!tests']['doc-exceptions-list'] = $doc_exceptions; + $result['!tests']['exceptions-list-or'] = '-o'; + } + if (count($doc_pattern_list) > 0) { + if ($not) { + $result['!tests']['pattern-list-not'] = '\\!'; + } + $result['!tests']['doc-pattern-list']['#alter'] = array('or-terms', 'parenthesize'); + foreach ($doc_pattern_list as $key => $pattern) { + if ($pattern[0] != '*') { + $pattern = $base_dir . '/' . $pattern; + } + $result['!tests']['doc-pattern-list']['doc-' . $pattern] = array( + 'tmpl' => '-path ^pattern', + '^pattern' => $pattern, + ); + } + } + if ((count($doc_exceptions_list) > 0) && (!$not)) { + $result['!tests']['exceptions-list-not'] = '\\!'; + $result['!tests']['doc-exceptions-list'] = $doc_exceptions; + } + if ((count($doc_exceptions_list) > 0) && ($not)) { + $result['!tests']['path-end-paren'] = '\)'; + } + return $result; +} + +/** + * Convert a command record into an executable command string. + * + * @param mixed $command_list + * 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($command_or_command_list, $replacements = array()) { + $result = ''; + if (is_array($command_or_command_list)) { + $data = array(); + if (array_key_exists('tmpl', $command_or_command_list)) { + $tmpl = $command_or_command_list['tmpl']; + unset($command_or_command_list['tmpl']); + } + foreach ($command_or_command_list as $key => $info) { + if ($key[0] != '#') { + if (is_array($info) && array_key_exists('#alter', $info)) { + foreach ($info['#alter'] as $fn) { + $fn_name = '_drush_perms_pre_render_' . str_replace('-', '_', $fn); + if (function_exists($fn_name)) { + $fn_name($info); + } + } + } + $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); + } + if (is_array($info) && array_key_exists('#alter', $info)) { + foreach ($info['#alter'] as $fn) { + $fn_name = '_drush_perms_post_render_' . str_replace('-', '_', $fn); + if (function_exists($fn_name)) { + $value = $fn_name($info, $value, $data); + } + } + } + if (isset($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 = $command_or_command_list; + // Allow simple results to be replaced by variables where appropriate. + $result = str_replace(array_keys($replacements), array_values($replacements), $result); + } + return $result; +} + +/** + * If there is no data in '$data' yet, then erase the current term; + * otherwise, allow the current term to be processed and added to + * the result. + */ +function _drush_perms_post_render_condition_not_empty($info, $value, $data) { + if (_drush_perms_count_terms($data) == 0) { + return NULL; + } + else { + return $value; + } +} + +/** + * If there is more than one term in the provided set of terms, + * then add an "or" term ("-o") between each one. + */ +function _drush_perms_pre_render_or_terms(&$info) { + $added_term = FALSE; + $i = array(); + foreach ($info as $key => $value) { + if ($key[0] != '#') { + if ($added_term) { + $i['or-' . $key] = '-o'; + } + $added_term = TRUE; + } + $i[$key] = $value; + } + if ($added_term) { + $info = $i; + } +} + +/** + * If there is more than one term in the provided set of terms, + * then add parenthesis around all of the terms. + */ +function _drush_perms_pre_render_parenthesize(&$info) { + if (_drush_perms_count_terms($info) > 1) { + $i = array_reverse($info, TRUE); + $i['begin_paren'] = '\('; + $i = array_reverse($i, TRUE); + $i['end_paren'] = '\)'; + $info = $i; + } +} + +/** + * Count the number of 'terms' in the provided array. + * Ignore processing directives -- keys that begin with '#'. + */ +function _drush_perms_count_terms($info) { + $count = 0; + if (is_array($info)) { + foreach ($info as $k => $v) { + if (($k[0] != '#') && (!empty($v))) { + $count = $count + 1; + } + } + } + return $count; +} + +/** + * We would prefer drush_get_option_list to return an empty + * array if the user provides an empty string. By default, + * it returns an array with a single empty item in this instance. + */ +function _drush_perms_get_option_list($key, $default) { + $result = drush_get_option_list($key, $default); + if ($result == array(0 => '')) { + return array(); + } + else { + return $result; + } +} diff --git a/tests/drush_testcase.inc b/tests/drush_testcase.inc index 6207fe0..c78fccc 100644 --- a/tests/drush_testcase.inc +++ b/tests/drush_testcase.inc @@ -532,6 +532,63 @@ function unish_init() { } /** + * Same code as _drush_recursive_copy, except always overwrites + * and does not preserve permissions + */ +function unish_copy_dir($src, $dest) { + if(is_dir($src)) { + if(!is_dir($dest)) { + mkdir($dest, 0777, TRUE); + } + $dir_handle = opendir($src); + while($file = readdir($dir_handle)) { + if ($file != "." && $file != "..") { + if (unish_copy_dir("$src/$file", "$dest/$file") !== TRUE) { + return FALSE; + } + } + } + closedir($dir_handle); + } + elseif (is_link($src)) { + symlink(readlink($src), $dest); + } + elseif (copy($src, $dest) !== TRUE) { + return FALSE; + } + + return TRUE; +} + +/** + * Simpler version of drush_scan_directory + */ +function unish_scan_directory($dir) { + $files = array(); + $dir_handle = opendir($dir); + $dirs = array(); + while($file = readdir($dir_handle)) { + if ($file != "." && $file != "..") { + $filename = $dir . '/' . $file; + if (is_dir($filename)) { + $dirs[] = $filename; + } + else { + $files[$filename] = $file; + } + } + } + closedir($dir_handle); + asort($files); + asort($dirs); + foreach ($dirs as $filename) { + $files[$filename] = $file; + $files = array_merge($files, unish_scan_directory($filename)); + } + return $files; +} + +/** * A slightly less functional copy of drush_backend_parse_output(). */ function parse_backend_output($string) { diff --git a/tests/permissionstestdir/INSTALL.txt b/tests/permissionstestdir/INSTALL.txt new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/INSTALL.txt @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permissionstestdir/includes/bootstrap.inc b/tests/permissionstestdir/includes/bootstrap.inc new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/includes/bootstrap.inc @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permissionstestdir/index.php b/tests/permissionstestdir/index.php new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/index.php @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permissionstestdir/misc/drupal.js b/tests/permissionstestdir/misc/drupal.js new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/misc/drupal.js @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permissionstestdir/quickstart.html b/tests/permissionstestdir/quickstart.html new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/quickstart.html @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permissionstestdir/robots.txt b/tests/permissionstestdir/robots.txt new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/robots.txt @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permissionstestdir/sites/default/files/uploadfile.txt b/tests/permissionstestdir/sites/default/files/uploadfile.txt new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/sites/default/files/uploadfile.txt @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permissionstestdir/sites/default/settings.php b/tests/permissionstestdir/sites/default/settings.php new file mode 100644 index 0000000..b5976ab --- /dev/null +++ b/tests/permissionstestdir/sites/default/settings.php @@ -0,0 +1 @@ +Placeholder file for permissions testing diff --git a/tests/permsTest.php b/tests/permsTest.php new file mode 100644 index 0000000..6db45e8 --- /dev/null +++ b/tests/permsTest.php @@ -0,0 +1,385 @@ + NULL, + 'dir' => '/srv/www/drupalroot', + 'files' => 'sites/default/files', + 'settings' => 'sites/default/settings.php', + ); + foreach ($this->testDataForScriptOutput() as $key => $info) { + $test_options = $info['options'] + $default_options; + $this->drush('perms', $info['args'], $test_options); + $actual = $this->getOutput(); + // Remove doubled-up spaces + $actual = preg_replace('/ +/', ' ', $actual); + // We build a header to include in assertEquals simply to make it evident + // which data-driven test failed, should a failure occur. + $header = "# '$key' Test:\n\$ drush perms " . implode(' ', $info['args']) . $this->buildDescriptiveOptionList($info['options']) . "\n"; + $this->assertEquals($header . trim($info['expected']), $header . trim($actual)); + } + } + + + function testDataForScriptOutput() { + return array ( + 'Default Values' => array( + 'args' => array('owner:group'), + 'options' => array(), + 'expected' => <<<'EOT' + +#!/bin/bash +SETTINGS_FILES='0440' +DOC_FILES='0400' +CODE_FILES='0644' +CODE_DIRS='0755' +DATA_FILES='0664' +DATA_DIRS='0775' +CODE_FILES_OWNER='owner' +CODE_FILES_GROUP='group' +DATA_FILES_OWNER='owner' +DATA_FILES_GROUP='group' +find /srv/www/drupalroot -path /srv/www/drupalroot/sites/default/files -prune -o \( \( \! -group $CODE_FILES_GROUP -o \! -user $CODE_FILES_OWNER \) -print0 \) | xargs -0r chown $CODE_FILES_OWNER:$CODE_FILES_GROUP -- +find /srv/www/drupalroot -path /srv/www/drupalroot/sites/default/files -prune -o \( -type d \! -perm $CODE_DIRS -print0 \) | xargs -0r chmod $CODE_DIRS -- +find /srv/www/drupalroot -path /srv/www/drupalroot/sites/default/files -prune -o \( -type f \! -perm $CODE_FILES \! -path sites/default/settings.php -path '/srv/www/drupalroot/*/*' -print0 \) | xargs -0r chmod $CODE_FILES -- +find /srv/www/drupalroot/sites/default/files \( \( \! -group $DATA_FILES_GROUP -o \! -user $DATA_FILES_OWNER \) -print0 \) | xargs -0r chown $DATA_FILES_OWNER:$DATA_FILES_GROUP -- +find /srv/www/drupalroot/sites/default/files \( -type d \! -perm $DATA_DIRS -print0 \) | xargs -0r chmod $DATA_DIRS -- +find /srv/www/drupalroot/sites/default/files \( -type f \! -perm $DATA_FILES -print0 \) | xargs -0r chmod $DATA_FILES -- +chmod $SETTINGS_FILES /srv/www/drupalroot/sites/default/settings.php +find /srv/www/drupalroot -maxdepth 1 \( -type f \! -perm $DOC_FILES \( -path '*.txt' -o -path /srv/www/drupalroot/quickstart.html \) \! -path /srv/www/drupalroot/robots.txt -print0 \) | xargs -0r chmod $DOC_FILES -- +find /srv/www/drupalroot -maxdepth 1 \( -type f \! -perm $CODE_FILES \( -path /srv/www/drupalroot/robots.txt -o \! \( -path '*.txt' -o -path /srv/www/drupalroot/quickstart.html \) \) -print0 \) | xargs -0r chmod $CODE_FILES -- + +EOT + + ), + 'Default Values in "no-variables" mode' => array( + 'args' => array('owner:group'), + 'options' => array('no-variables' => NULL), + 'expected' => <<<'EOT' + +#!/bin/bash +find /srv/www/drupalroot \( \( \! -group group -o \! -user owner \) -print0 \) | xargs -0r chown owner:group -- +find /srv/www/drupalroot -path /srv/www/drupalroot/sites/default/files -prune -o \( -type d \! -perm 0755 -print0 \) | xargs -0r chmod 0755 -- +find /srv/www/drupalroot -path /srv/www/drupalroot/sites/default/files -prune -o \( -type f \! -perm 0644 \! -path sites/default/settings.php -path '/srv/www/drupalroot/*/*' -print0 \) | xargs -0r chmod 0644 -- +find /srv/www/drupalroot/sites/default/files \( -type d \! -perm 0775 -print0 \) | xargs -0r chmod 0775 -- +find /srv/www/drupalroot/sites/default/files \( -type f \! -perm 0664 -print0 \) | xargs -0r chmod 0664 -- +chmod 0440 /srv/www/drupalroot/sites/default/settings.php +find /srv/www/drupalroot -maxdepth 1 \( -type f \! -perm 0400 \( -path '*.txt' -o -path /srv/www/drupalroot/quickstart.html \) \! -path /srv/www/drupalroot/robots.txt -print0 \) | xargs -0r chmod 0400 -- +find /srv/www/drupalroot -maxdepth 1 \( -type f \! -perm 0644 \( -path /srv/www/drupalroot/robots.txt -o \! \( -path '*.txt' -o -path /srv/www/drupalroot/quickstart.html \) \) -print0 \) | xargs -0r chmod 0644 -- + +EOT + ), + 'Strict with misc excluded' => array( + 'args' => array('owner:group'), + 'options' => array('strict' => NULL, 'exclude' => 'misc'), + 'expected' => <<<'EOT' + +#!/bin/bash +SETTINGS_FILES='0440' +DOC_FILES='0400' +CODE_FILES='0640' +CODE_DIRS='0750' +DATA_FILES='0640' +DATA_DIRS='0750' +CODE_FILES_OWNER='owner' +CODE_FILES_GROUP='group' +DATA_FILES_OWNER='owner' +DATA_FILES_GROUP='group' +find /srv/www/drupalroot \( -path /srv/www/drupalroot/sites/default/files -o -path /srv/www/drupalroot/misc \) -prune -o \( \( \! -group $CODE_FILES_GROUP -o \! -user $CODE_FILES_OWNER \) -print0 \) | xargs -0r chown $CODE_FILES_OWNER:$CODE_FILES_GROUP -- +find /srv/www/drupalroot \( -path /srv/www/drupalroot/sites/default/files -o -path /srv/www/drupalroot/misc \) -prune -o \( -type d \! -perm $CODE_DIRS -print0 \) | xargs -0r chmod $CODE_DIRS -- +find /srv/www/drupalroot \( -path /srv/www/drupalroot/sites/default/files -o -path /srv/www/drupalroot/misc \) -prune -o \( -type f \! -perm $CODE_FILES \! -path sites/default/settings.php -path '/srv/www/drupalroot/*/*' -print0 \) | xargs -0r chmod $CODE_FILES -- +find /srv/www/drupalroot/sites/default/files \( \( \! -group $DATA_FILES_GROUP -o \! -user $DATA_FILES_OWNER \) -print0 \) | xargs -0r chown $DATA_FILES_OWNER:$DATA_FILES_GROUP -- +find /srv/www/drupalroot/sites/default/files \( -type d \! -perm $DATA_DIRS -print0 \) | xargs -0r chmod $DATA_DIRS -- +find /srv/www/drupalroot/sites/default/files \( -type f \! -perm $DATA_FILES -print0 \) | xargs -0r chmod $DATA_FILES -- +chmod $SETTINGS_FILES /srv/www/drupalroot/sites/default/settings.php +find /srv/www/drupalroot -maxdepth 1 \( -type f \! -perm $DOC_FILES \( -path '*.txt' -o -path /srv/www/drupalroot/quickstart.html \) \! -path /srv/www/drupalroot/robots.txt -print0 \) | xargs -0r chmod $DOC_FILES -- +find /srv/www/drupalroot -maxdepth 1 \( -type f \! -perm $CODE_FILES \( -path /srv/www/drupalroot/robots.txt -o \! \( -path '*.txt' -o -path /srv/www/drupalroot/quickstart.html \) \) -print0 \) | xargs -0r chmod $CODE_FILES -- + +EOT + + ), + ); + } + + // + // Run some permissions tests and check to see if the permissions of + // the modified folders comes out correctly. + // + // Note that all of these tests will run with --skip-set-owner, so + // that the tests will run without superuser access. This means that + // not all available operations are testable by the execution tests. + // + function testPermissionsExecution() { + $testDirName = 'permissionstestdir'; + $src = dirname(__FILE__) . "/$testDirName"; + $testDirectory = UNISH_SANDBOX . "/$testDirName"; + $default_options = array( + 'skip-set-owner' => NULL, + 'dir' => $testDirectory, + 'files' => 'sites/default/files', + 'settings' => 'sites/default/settings.php', + ); + foreach ($this->testDataForExecution() as $key => $info) { + if (unish_copy_dir($src, $testDirectory)) { + + $test_options = $info['options'] + $default_options; + $this->drush('perms', $info['args'], $test_options); + $actual = $this->getOutput(); + // If the 'pipe' option is set, then we will + // write the output to a file, execute it, and + // take the output from the script run as the + // "actual $actual". + if (array_key_exists('pipe', $info['options'])) { + $tmp_script = UNISH_SANDBOX . '/perms_script.sh'; + file_put_contents($tmp_script, $actual); + chmod($tmp_script, 0777); + $output = array(); + exec($tmp_script, $output); + unlink($tmp_script); + $actual = implode("\n", $output); + } + + // Remove doubled-up spaces + $actual = preg_replace('/ +/', ' ', $actual); + // Replace the test directory with 'DIR' + $actual = str_replace($testDirectory, 'DIR', $actual); + + if (!array_key_exists('audit', $info['options'])) { + $files = unish_scan_directory($testDirectory); + foreach ($files as $filename => $name) { + $perms = substr(str_pad(base_convert(fileperms($filename), 10, 8), 6, '0', STR_PAD_LEFT), 2); + $line = $perms . ' ' . str_replace($testDirectory . '/', '', $filename); + $actual .= "$line\n"; + } + } + + // We build a header to include in assertEquals simply to make it evident + // which data-driven test failed, should a failure occur. + $header = "# '$key' Test:\n\$ drush perms " . implode(' ', $info['args']) . $this->buildDescriptiveOptionList($info['options']) . "\n"; + $this->assertEquals($header . trim($info['expected']), $header . trim($actual)); + unish_file_delete_recursive($testDirectory); + } + else { + // We could not copy our test directory. Boom. + $this->assertTrue(FALSE); + } + } + + } + + function testDataForExecution() { + return array ( + 'Default Values' => array( + 'args' => array(), + 'options' => array(), + 'expected' => <<<'EOT' + +0400 INSTALL.txt +0644 index.php +0400 quickstart.html +0644 robots.txt +0755 includes +0644 includes/bootstrap.inc +0755 misc +0644 misc/drupal.js +0755 sites +0755 sites/default +0440 sites/default/settings.php +0775 sites/default/files +0664 sites/default/files/uploadfile.txt + +EOT + ), + 'Default Values script execution' => array( + 'args' => array(), + 'options' => array('pipe' => NULL), + 'expected' => <<<'EOT' + +0400 INSTALL.txt +0644 index.php +0400 quickstart.html +0644 robots.txt +0755 includes +0644 includes/bootstrap.inc +0755 misc +0644 misc/drupal.js +0755 sites +0755 sites/default +0440 sites/default/settings.php +0775 sites/default/files +0664 sites/default/files/uploadfile.txt + +EOT + ), + 'Default Values no-variables script execution' => array( + 'args' => array(), + 'options' => array('pipe' => NULL, 'no-variables' => NULL), + 'expected' => <<<'EOT' + +0400 INSTALL.txt +0644 index.php +0400 quickstart.html +0644 robots.txt +0755 includes +0644 includes/bootstrap.inc +0755 misc +0644 misc/drupal.js +0755 sites +0755 sites/default +0440 sites/default/settings.php +0775 sites/default/files +0664 sites/default/files/uploadfile.txt + +EOT + ), + 'Strict' => array( + 'args' => array(), + 'options' => array('strict' => NULL), + 'expected' => <<<'EOT' + +0400 INSTALL.txt +0640 index.php +0400 quickstart.html +0640 robots.txt +0750 includes +0640 includes/bootstrap.inc +0750 misc +0640 misc/drupal.js +0750 sites +0750 sites/default +0440 sites/default/settings.php +0750 sites/default/files +0640 sites/default/files/uploadfile.txt + +EOT + ), + 'Lax' => array( + 'args' => array(), + 'options' => array('lax' => NULL), + 'expected' => <<<'EOT' + +0664 INSTALL.txt +0664 index.php +0664 quickstart.html +0664 robots.txt +0775 includes +0664 includes/bootstrap.inc +0775 misc +0664 misc/drupal.js +0775 sites +0775 sites/default +0440 sites/default/settings.php +0777 sites/default/files +0666 sites/default/files/uploadfile.txt + +EOT + ), + 'Strict with files excluded' => array( + 'args' => array(), + 'options' => array('strict' => NULL, 'exclude' => 'sites/default/files'), + 'expected' => <<<'EOT' + +0400 INSTALL.txt +0640 index.php +0400 quickstart.html +0640 robots.txt +0750 includes +0640 includes/bootstrap.inc +0750 misc +0640 misc/drupal.js +0750 sites +0750 sites/default +0440 sites/default/settings.php +0775 sites/default/files +0664 sites/default/files/uploadfile.txt + +EOT + ), + # For this test, we redirect 'files' to 'sites' and use + # 'sites/default/files' as the 'files' subdirectory. + 'Strict with files subdirectory excluded' => array( + 'args' => array(), + 'options' => array('strict' => NULL, 'files' => 'sites', 'exclude' => 'sites/default/files', 'data-dirs' => '555', 'data-files' => '444'), + 'expected' => <<<'EOT' + +0400 INSTALL.txt +0640 index.php +0400 quickstart.html +0640 robots.txt +0750 includes +0640 includes/bootstrap.inc +0750 misc +0640 misc/drupal.js +0550 sites +0550 sites/default +0440 sites/default/settings.php +0775 sites/default/files +0664 sites/default/files/uploadfile.txt + +EOT + ), + 'Audit strict with misc excluded' => array( + 'args' => array(), + 'options' => array('strict' => NULL, 'exclude' => 'misc', 'audit' => NULL), + 'expected' => <<<'EOT' + +chmod 0750 -- DIR DIR/includes DIR/sites DIR/sites/default DIR/sites/default/files +chmod 0640 -- DIR/includes/bootstrap.inc DIR/sites/default/settings.php DIR/sites/default/files/uploadfile.txt +chmod 0400 -- DIR/INSTALL.txt DIR/quickstart.html +chmod 0640 -- DIR/robots.txt DIR/index.php + +EOT + ), + 'Strict with misc excluded' => array( + 'args' => array(), + 'options' => array('strict' => NULL, 'exclude' => 'misc'), + 'expected' => <<<'EOT' + +0400 INSTALL.txt +0640 index.php +0400 quickstart.html +0640 robots.txt +0750 includes +0640 includes/bootstrap.inc +0775 misc +0664 misc/drupal.js +0750 sites +0750 sites/default +0440 sites/default/settings.php +0750 sites/default/files +0640 sites/default/files/uploadfile.txt + +EOT + ), + ); + } + + // Convert an array of flag => value pairs into --flag=value + function buildDescriptiveOptionList($options) { + $descriptiveOutput = ''; + + foreach ($options as $key => $value) { + if (!isset($value)) { + $descriptiveOutput .= " --$key"; + } + else { + $descriptiveOutput .= " --$key=$value"; + } + } + + return $descriptiveOutput; + } +} -- 1.7.9.5