drush_get_context('DRUSH_SITE_WIDE_COMMANDFILES'))); } } /** * Implementation of hook_drush_command(). */ function pm_drush_command() { $update_options = array( 'lock' => array( 'description' => 'Add a persistent lock to remove the specified projects from consideration during updates. Locks may be removed with the --unlock parameter, or overridden by specifically naming the project as a parameter to pm-update or pm-updatecode. The lock does not affect pm-download. See also the update_advanced project for similar and improved functionality.', 'example-value' => 'foo,bar', ), ); $update_suboptions = array( 'lock' => array( 'lock-message' => array( 'description' => 'A brief message explaining why a project is being locked; displayed during pm-updatecode. Optional.', 'example-value' => 'message', ), 'unlock' => array( 'description' => 'Remove the persistent lock from the specified projects so that they may be updated again.', 'example-value' => 'foo,bar', ), ), ); $items['pm-enable'] = array( 'description' => 'Enable one or more extensions (modules or themes).', 'arguments' => array( 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to enable all matches.', ), 'options' => array( 'resolve-dependencies' => 'Attempt to download any missing dependencies. At the moment, only works when the module name is the same as the project name.', 'skip' => 'Skip automatic downloading of libraries (c.f. devel).', ), 'aliases' => array('en'), 'engines' => array( 'release_info' => array( 'add-options-to-command' => FALSE, ), ), ); $items['pm-disable'] = array( 'description' => 'Disable one or more extensions (modules or themes).', 'arguments' => array( 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to disable multiple matches.', ), 'aliases' => array('dis'), 'engines' => array( 'version_control', 'package_handler', 'release_info' => array( 'add-options-to-command' => FALSE, ), ), ); $items['pm-info'] = array( 'description' => 'Show detailed info for one or more extensions (modules or themes).', 'arguments' => array( 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to show info for multiple matches. If no argument is provided it will show info for all available extensions.', ), 'aliases' => array('pmi'), 'outputformat' => array( 'default' => 'key-value-list', 'pipe-format' => 'json', 'formatted-filter' => '_drush_pm_info_format_table_data', 'field-labels' => array( 'extension' => 'Extension', 'project' => 'Project', 'type' => 'Type', 'title' => 'Title', 'description' => 'Description', 'version' => 'Version', 'date' => 'Date', 'package' => 'Package', 'core' => 'Core', 'php' => 'PHP', 'status' => 'Status', 'path' => 'Path', 'schema_version' => 'Schema version', 'files' => 'Files', 'requires' => 'Requires', 'required_by' => 'Required by', 'permissions' => 'Permissions', 'config' => 'Configure', 'engine' => 'Engine', 'base_theme' => 'Base theme', 'regions' => 'Regions', 'features' => 'Features', 'stylesheets' => 'Stylesheets', // 'media_' . $media => 'Media '. $media for each $info->info['stylesheets'] as $media => $files 'scripts' => 'Scripts', ), 'output-data-type' => 'format-table', ), ); $items['pm-projectinfo'] = array( 'description' => 'Show a report of available projects and their extensions.', 'arguments' => array( 'projects' => 'Optional. A list of installed projects to show.', ), 'options' => array( 'drush' => 'Optional. Only incude projects that have one or more Drush commands.', 'status' => array( 'description' => 'Filter by project status. Choices: enabled, disabled. A project is considered enabled when at least one of its extensions is enabled.', 'example-value' => 'enabled', ), ), 'outputformat' => array( 'default' => 'key-value-list', 'pipe-format' => 'json', 'field-labels' => array( 'label' => 'Name', 'type' => 'Type', 'version' => 'Version', 'status' => 'Status', 'extensions' => 'Extensions', 'drush' => 'Drush Commands', 'datestamp' => 'Datestamp', 'path' => 'Path', ), 'fields-default' => array('label', 'type', 'version', 'status', 'extensions', 'drush', 'datestamp', 'path'), 'fields-pipe' => array('label'), 'output-data-type' => 'format-table', ), 'aliases' => array('pmpi'), ); // Install command is reserved for the download and enable of projects including dependencies. // @see http://drupal.org/node/112692 for more information. // $items['install'] = array( // 'description' => 'Download and enable one or more modules', // ); $items['pm-uninstall'] = array( 'description' => 'Uninstall one or more modules.', 'arguments' => array( 'modules' => 'A list of modules.', ), 'aliases' => array('pmu'), ); $items['pm-list'] = array( 'description' => 'Show a list of available extensions (modules and themes).', 'callback arguments' => array(array(), FALSE), 'options' => array( 'type' => array( 'description' => 'Filter by extension type. Choices: module, theme.', 'example-value' => 'module', ), 'status' => array( 'description' => 'Filter by extension status. Choices: enabled, disabled and/or \'not installed\'. You can use multiple comma separated values. (i.e. --status="disabled,not installed").', 'example-value' => 'disabled', ), 'package' => 'Filter by project packages. You can use multiple comma separated values. (i.e. --package="Core - required,Other").', 'core' => 'Filter out extensions that are not in drupal core.', 'no-core' => 'Filter out extensions that are provided by drupal core.', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'list', 'field-labels' => array('package' => 'Package', 'name' => 'Name', 'type' => 'Type', 'status' => 'Status', 'version' => 'Version'), 'output-data-type' => 'format-table', ), 'aliases' => array('pml'), ); $items['pm-refresh'] = array( 'description' => 'Refresh update status information.', 'engines' => array( 'update_status' => array( 'add-options-to-command' => FALSE, ), ), 'aliases' => array('rf'), ); $items['pm-updatestatus'] = array( 'description' => 'Show a report of available minor updates to Drupal core and contrib projects.', 'arguments' => array( 'projects' => 'Optional. A list of installed projects to show.', ), 'options' => array( 'pipe' => 'Return a list of the projects with any extensions enabled that need updating, one project per line.', ) + $update_options, 'sub-options' => $update_suboptions, 'engines' => array( 'update_status', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'list', 'field-labels' => array('name' => 'Short Name', 'label' => 'Name', 'existing_version' => 'Installed Version', 'status' => 'Status', 'status_msg' => 'Message', 'candidate_version' => 'Proposed version'), 'fields-default' => array('label', 'existing_version', 'candidate_version', 'status_msg' ), 'fields-pipe' => array('name', 'existing_version', 'candidate_version', 'status_msg'), 'output-data-type' => 'format-table', ), 'aliases' => array('ups'), ); $items['pm-updatecode'] = array( 'description' => 'Update Drupal core and contrib projects to latest recommended releases.', 'examples' => array( 'drush pm-updatecode --no-core' => 'Update contrib projects, but skip core.', 'drush pm-updatestatus --format=csv --list-separator=" " --fields="name,existing_version,candidate_version,status_msg"' => 'To show a list of projects with their update status, use pm-updatestatus instead of pm-updatecode.', ), 'arguments' => array( 'projects' => 'Optional. A list of installed projects to update.', ), 'options' => array( 'notes' => 'Show release notes for each project to be updated.', 'no-core' => 'Only update modules and skip the core update.', 'check-updatedb' => 'Check to see if an updatedb is needed after updating the code. Default is on; use --check-updatedb=0 to disable.', ) + $update_options, 'sub-options' => $update_suboptions, 'aliases' => array('upc'), 'topics' => array('docs-policy'), 'engines' => array( 'version_control', 'package_handler', 'release_info' => array( 'add-options-to-command' => FALSE, ), 'update_status', ), ); // Merge all items from above. $items['pm-update'] = array( 'description' => 'Update Drupal core and contrib projects and apply any pending database updates (Same as pm-updatecode + updatedb).', 'aliases' => array('up'), 'allow-additional-options' => array('pm-updatecode', 'updatedb'), ); $items['pm-updatecode-postupdate'] = array( 'description' => 'Notify of pending db updates.', 'hidden' => TRUE, ); $items['pm-releasenotes'] = array( 'description' => 'Print release notes for given projects.', 'arguments' => array( 'projects' => 'A list of project names, with optional version. Defaults to \'drupal\'', ), 'options' => array( 'html' => dt('Display release notes in HTML rather than plain text.'), ), 'examples' => array( 'drush rln cck' => 'Prints the release notes for the recommended version of CCK project.', 'drush rln token-1.13' => 'View release notes of a specfic version of the Token project for my version of Drupal.', 'drush rln pathauto zen' => 'View release notes for the recommended version of Pathauto and Zen projects.', ), 'aliases' => array('rln'), 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'engines' => array( 'release_info', ), ); $items['pm-releases'] = array( 'description' => 'Print release information for given projects.', 'arguments' => array( 'projects' => 'A list of drupal.org project names. Defaults to \'drupal\'', ), 'examples' => array( 'drush pm-releases cck zen' => 'View releases for cck and Zen projects for your Drupal version.', ), 'options' => array( 'default-major' => 'Show releases compatible with the specified major version of Drupal.', ), 'aliases' => array('rl'), 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'csv', 'field-labels' => array( 'project' => 'Project', 'version' => 'Release', 'date' => 'Date', 'status' => 'Status', 'release_link' => 'Release link', 'download_link' => 'Download link', ), 'fields-default' => array('project', 'version', 'date', 'status'), 'fields-pipe' => array('project', 'version', 'date', 'status'), 'output-data-type' => 'format-table', ), 'engines' => array( 'release_info', ), ); $items['pm-download'] = array( 'description' => 'Download projects from drupal.org or other sources.', 'examples' => array( 'drush dl drupal' => 'Download latest recommended release of Drupal core.', 'drush dl drupal-7.x' => 'Download latest 7.x development version of Drupal core.', 'drush dl drupal-6' => 'Download latest recommended release of Drupal 6.x.', 'drush dl cck zen' => 'Download latest versions of CCK and Zen projects.', 'drush dl og-1.3' => 'Download a specfic version of Organic groups module for my version of Drupal.', 'drush dl diff-6.x-2.x' => 'Download a specific development branch of diff module for a specific Drupal version.', 'drush dl views --select' => 'Show a list of recent releases of the views project, prompt for which one to download.', 'drush dl webform --dev' => 'Download the latest dev release of webform.', 'drush dl webform --cache' => 'Download webform. Fetch and populate the download cache as needed.', ), 'arguments' => array( 'projects' => 'A comma delimited list of drupal.org project names, with optional version. Defaults to \'drupal\'', ), 'options' => array( 'destination' => array( 'description' => 'Path to which the project will be copied. If you\'re providing a relative path, note it is relative to the drupal root (if bootstrapped).', 'example-value' => 'path', ), 'use-site-dir' => 'Force to use the site specific directory. It will create the directory if it doesn\'t exist. If --destination is also present this option will be ignored.', 'notes' => 'Show release notes after each project is downloaded.', 'variant' => array( 'description' => "Only useful for install profiles. Possible values: 'full', 'projects', 'profile-only'.", 'example-value' => 'full', ), 'select' => "Select the version to download interactively from a list of available releases.", 'drupal-project-rename' => 'Alternate name for "drupal-x.y" directory when downloading Drupal project. Defaults to "drupal".', 'default-major' => array( 'description' => 'Specify the default major version of modules to download when there is no bootstrapped Drupal site. Defaults to "8".', 'example-value' => '7', ), 'skip' => 'Skip automatic downloading of libraries (c.f. devel).', 'pipe' => 'Returns a list of the names of the extensions (modules and themes) contained in the downloaded projects.', ), 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'aliases' => array('dl'), 'engines' => array( 'version_control', 'package_handler', 'release_info', ), ); return $items; } /** * @defgroup extensions Extensions management. * @{ * Functions to manage extensions. */ /** * Command argument complete callback. */ function pm_pm_enable_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_disable_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_uninstall_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_info_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_releasenotes_complete() { return pm_complete_projects(); } /** * Command argument complete callback. */ function pm_pm_releases_complete() { return pm_complete_projects(); } /** * Command argument complete callback. */ function pm_pm_updatecode_complete() { return pm_complete_projects(); } /** * Command argument complete callback. */ function pm_pm_update_complete() { return pm_complete_projects(); } /** * List extensions for completion. * * @return * Array of available extensions. */ function pm_complete_extensions() { if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $extension_info = drush_get_extensions(FALSE); return array('values' => array_keys($extension_info)); } } /** * List projects for completion. * * @return * Array of installed projects. */ function pm_complete_projects() { if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { return array('values' => array_keys(drush_get_projects())); } } /** * Sort callback function for sorting extensions. * * It will sort first by type, second by package and third by name. */ function _drush_pm_sort_extensions($a, $b) { $a_type = drush_extension_get_type($a); $b_type = drush_extension_get_type($b); if ($a_type == 'module' && $b_type == 'theme') { return -1; } if ($a_type == 'theme' && $b_type == 'module') { return 1; } $cmp = strcasecmp($a->info['package'], $b->info['package']); if ($cmp == 0) { $cmp = strcasecmp($a->info['name'], $b->info['name']); } return $cmp; } /** * Calculate an extension status based on current status and schema version. * * @param $extension * Object of a single extension info. * * @return * String describing extension status. Values: enabled|disabled|not installed */ function drush_get_extension_status($extension) { if ((drush_extension_get_type($extension) == 'module') && ($extension->schema_version == -1)) { $status = "not installed"; } else { $status = ($extension->status == 1)?'enabled':'disabled'; } return $status; } /** * Classify extensions as modules, themes or unknown. * * @param $extensions * Array of extension names, by reference. * @param $modules * Empty array to be filled with modules in the provided extension list. * @param $themes * Empty array to be filled with themes in the provided extension list. */ function drush_pm_classify_extensions(&$extensions, &$modules, &$themes, $extension_info) { _drush_pm_expand_extensions($extensions, $extension_info); foreach ($extensions as $extension) { if (!isset($extension_info[$extension])) { continue; } $type = drush_extension_get_type($extension_info[$extension]); if ($type == 'module') { $modules[$extension] = $extension; } else if ($type == 'theme') { $themes[$extension] = $extension; } } } /** * Obtain an array of installed projects off the extensions available. * * A project is considered to be 'enabled' when any of its extensions is * enabled. * If any extension lacks project information and it is found that the * extension was obtained from drupal.org's cvs or git repositories, a new * 'vcs' attribute will be set on the extension. Example: * $extensions[name]->vcs = 'cvs'; * * @param array $extensions * Array of extensions as returned by drush_get_extensions(). * * @return * Array of installed projects with info of version, status and provided * extensions. */ function drush_get_projects(&$extensions = NULL) { if (!isset($extensions)) { $extensions = drush_get_extensions(); } $projects = array( 'drupal' => array( 'label' => 'Drupal', 'version' => drush_drupal_version(), 'type' => 'core', 'extensions' => array(), ) ); if (isset($extensions['system']->info['datestamp'])) { $projects['drupal']['datestamp'] = $extensions['system']->info['datestamp']; } foreach ($extensions as $extension) { $extension_name = drush_extension_get_name($extension); $extension_path = drush_extension_get_path($extension); // Obtain the project name. It is not available in this cases: // 1. the extension is part of drupal core. // 2. the project was checked out from CVS/git and cvs_deploy/git_deploy // is not installed. // 3. it is not a project hosted in drupal.org. if (empty($extension->info['project'])) { if (isset($extension->info['version']) && ($extension->info['version'] == drush_drupal_version())) { $project = 'drupal'; } else { if (is_dir($extension_path . '/CVS') && (!drush_module_exists('cvs_deploy'))) { $extension->vcs = 'cvs'; drush_log(dt('Extension !extension is fetched from cvs. Ignoring.', array('!extension' => $extension_name)), LogLevel::DEBUG); } elseif (is_dir($extension_path . '/.git') && (!drush_module_exists('git_deploy'))) { $extension->vcs = 'git'; drush_log(dt('Extension !extension is fetched from git. Ignoring.', array('!extension' => $extension_name)), LogLevel::DEBUG); } continue; } } else { $project = $extension->info['project']; } // Create/update the project in $projects with the project data. if (!isset($projects[$project])) { $projects[$project] = array( // If there's an extension with matching name, pick its label. // Otherwise use just the project name. We avoid $extension->label // for the project label because the extension's label may have // no direct relation with the project name. For example, // "Text (text)" or "Number (number)" for the CCK project. 'label' => isset($extensions[$project]) ? $extensions[$project]->label : $project, 'type' => drush_extension_get_type($extension), 'version' => $extension->info['version'], 'status' => $extension->status, 'extensions' => array(), ); if (isset($extension->info['datestamp'])) { $projects[$project]['datestamp'] = $extension->info['datestamp']; } if (isset($extension->info['project status url'])) { $projects[$project]['status url'] = $extension->info['project status url']; } } else { // If any of the extensions is enabled, consider the project is enabled. if ($extension->status != 0) { $projects[$project]['status'] = $extension->status; } } $projects[$project]['extensions'][] = drush_extension_get_name($extension); } // Obtain each project's path and try to provide a better label for ones // with machine name. $reserved = array('modules', 'sites', 'themes'); foreach ($projects as $name => $project) { if ($name == 'drupal') { continue; } // If this project has no human label, see if we can find // one "main" extension whose label we could use. if ($project['label'] == $name) { // If there is only one extension, construct a label based on // the extension name. if (count($project['extensions']) == 1) { $extension = $extensions[$project['extensions'][0]]; $projects[$name]['label'] = $extension->info['name'] . ' (' . $name . ')'; } else { // Make a list of all of the extensions in this project // that do not depend on any other extension in this // project. $candidates = array(); foreach ($project['extensions'] as $e) { $has_project_dependency = FALSE; if (isset($extensions[$e]->info['dependencies']) && is_array($extensions[$e]->info['dependencies'])) { foreach ($extensions[$e]->info['dependencies'] as $dependent) { if (in_array($dependent, $project['extensions'])) { $has_project_dependency = TRUE; } } } if ($has_project_dependency === FALSE) { $candidates[] = $extensions[$e]->info['name']; } } // If only one of the modules is a candidate, use its name in the label if (count($candidates) == 1) { $projects[$name]['label'] = reset($candidates) . ' (' . $name . ')'; } } } drush_log(dt('Obtaining !project project path.', array('!project' => $name)), LogLevel::DEBUG); $path = _drush_pm_find_common_path($project['type'], $project['extensions']); // Prevent from setting a reserved path. For example it may happen in a case // where a module and a theme are declared as part of a same project. // There's a special case, a project called "sites", this is the reason for // the second condition here. if ($path == '.' || (in_array(basename($path), $reserved) && !in_array($name, $reserved))) { drush_log(dt('Error while trying to find the common path for enabled extensions of project !project. Extensions are: !extensions.', array('!project' => $name, '!extensions' => implode(', ', $project['extensions']))), LogLevel::ERROR); } else { $projects[$name]['path'] = $path; } } return $projects; } /** * Helper function to find the common path for a list of extensions in the aim to obtain the project name. * * @param $project_type * Type of project we're trying to find. Valid values: module, theme. * @param $extensions * Array of extension names. */ function _drush_pm_find_common_path($project_type, $extensions) { // Select the first path as the candidate to be the common prefix. $extension = array_pop($extensions); while (!($path = drupal_get_path($project_type, $extension))) { drush_log(dt('Unknown path for !extension !type.', array('!extension' => $extension, '!type' => $project_type)), LogLevel::WARNING); $extension = array_pop($extensions); } // If there's only one extension we are done. Otherwise, we need to find // the common prefix for all of them. if (count($extensions) > 0) { // Iterate over the other projects. while($extension = array_pop($extensions)) { $path2 = drupal_get_path($project_type, $extension); if (!$path2) { drush_log(dt('Unknown path for !extension !type.', array('!extension' => $extension, '!type' => $project_type)), LogLevel::DEBUG); continue; } // Option 1: same path. if ($path == $path2) { continue; } // Option 2: $path is a prefix of $path2. if (strpos($path2, $path) === 0) { continue; } // Option 3: $path2 is a prefix of $path. if (strpos($path, $path2) === 0) { $path = $path2; continue; } // Option 4: no one is a prefix of the other. Find the common // prefix by iteratively strip the rigthtmost piece of $path. // We will iterate until a prefix is found or path = '.', that on the // other hand is a condition theorically impossible to reach. do { $path = dirname($path); if (strpos($path2, $path) === 0) { break; } } while ($path != '.'); } } return $path; } /** * @} End of "defgroup extensions". */ /** * Command callback. Show a list of extensions with type and status. */ function drush_pm_list() { //--package $package_filter = array(); $package = strtolower(drush_get_option('package')); if (!empty($package)) { $package_filter = explode(',', $package); } if (!empty($package_filter) && (count($package_filter) == 1)) { drush_hide_output_fields('package'); } //--type $all_types = array('module', 'theme'); $type_filter = strtolower(drush_get_option('type')); if (!empty($type_filter)) { $type_filter = explode(',', $type_filter); } else { $type_filter = $all_types; } if (count($type_filter) == 1) { drush_hide_output_fields('type'); } foreach ($type_filter as $type) { if (!in_array($type, $all_types)) { //TODO: this kind of check can be implemented drush-wide return drush_set_error('DRUSH_PM_INVALID_PROJECT_TYPE', dt('!type is not a valid project type.', array('!type' => $type))); } } //--status $all_status = array('enabled', 'disabled', 'not installed'); $status_filter = strtolower(drush_get_option('status')); if (!empty($status_filter)) { $status_filter = explode(',', $status_filter); } else { $status_filter = $all_status; } if (count($status_filter) == 1) { drush_hide_output_fields('status'); } foreach ($status_filter as $status) { if (!in_array($status, $all_status)) { //TODO: this kind of check can be implemented drush-wide return drush_set_error('DRUSH_PM_INVALID_PROJECT_STATUS', dt('!status is not a valid project status.', array('!status' => $status))); } } $result = array(); $extension_info = drush_get_extensions(FALSE); uasort($extension_info, '_drush_pm_sort_extensions'); $major_version = drush_drupal_major_version(); foreach ($extension_info as $key => $extension) { if (!in_array(drush_extension_get_type($extension), $type_filter)) { unset($extension_info[$key]); continue; } $status = drush_get_extension_status($extension); if (!in_array($status, $status_filter)) { unset($extension_info[$key]); continue; } // Filter out core if --no-core specified. if (drush_get_option('no-core', FALSE)) { if ((($major_version >= 8) && ($extension->origin == 'core')) || (($major_version <= 7) && (strpos($extension->info['package'], 'Core') === 0))) { unset($extension_info[$key]); continue; } } // Filter out non-core if --core specified. if (drush_get_option('core', FALSE)) { if ((($major_version >= 8) && ($extension->origin != 'core')) || (($major_version <= 7) && (strpos($extension->info['package'], 'Core') !== 0))) { unset($extension_info[$key]); continue; } } // Filter by package. if (!empty($package_filter)) { if (!in_array(strtolower($extension->info['package']), $package_filter)) { unset($extension_info[$key]); continue; } } $row['package'] = $extension->info['package']; $row['name'] = $extension->label; $row['type'] = ucfirst(drush_extension_get_type($extension)); $row['status'] = ucfirst($status); // Suppress notice when version is not present. $row['version'] = @$extension->info['version']; $result[$key] = $row; unset($row); } // In Drush-5, we used to return $extension_info here. return $result; } /** * Helper function for pm-enable. */ function drush_pm_enable_find_project_from_extension($extension) { $result = drush_pm_lookup_extension_in_cache($extension); if (!isset($result)) { $release_info = drush_get_engine('release_info'); // If we can find info on a project that has the same name // as the requested extension, then we'll call that a match. $request = pm_parse_request($extension); if ($release_info->checkProject($request)) { $result = $extension; } } return $result; } /** * Validate callback. Determine the modules and themes that the user would like enabled. */ function drush_pm_enable_validate() { $args = pm_parse_arguments(func_get_args()); $extension_info = drush_get_extensions(); $recheck = TRUE; while ($recheck) { $recheck = FALSE; // Classify $args in themes, modules or unknown. $modules = array(); $themes = array(); $download = array(); drush_pm_classify_extensions($args, $modules, $themes, $extension_info); $extensions = array_merge($modules, $themes); $unknown = array_diff($args, $extensions); // If there're unknown extensions, try and download projects // with matching names. if (!empty($unknown)) { $found = array(); foreach ($unknown as $name) { drush_log(dt('!extension was not found.', array('!extension' => $name)), LogLevel::WARNING); $project = drush_pm_enable_find_project_from_extension($name); if (!empty($project)) { $found[] = $project; } } if (!empty($found)) { drush_log(dt("The following projects provide some or all of the extensions not found:\n@list", array('@list' => implode("\n", $found))), LogLevel::OK); if (drush_get_option('resolve-dependencies')) { drush_log(dt("They are being downloaded."), LogLevel::OK); } if ((drush_get_option('resolve-dependencies')) || (drush_confirm("Would you like to download them?"))) { $download = $found; } } } // Discard already enabled and incompatible extensions. foreach ($extensions as $name) { if ($extension_info[$name]->status) { drush_log(dt('!extension is already enabled.', array('!extension' => $name)), LogLevel::OK); } // Check if the extension is compatible with Drupal core and php version. if ($component = drush_extension_check_incompatibility($extension_info[$name])) { drush_set_error('DRUSH_PM_ENABLE_MODULE_INCOMPATIBLE', dt('!name is incompatible with the !component version.', array('!name' => $name, '!component' => $component))); if (drush_extension_get_type($extension_info[$name]) == 'module') { unset($modules[$name]); } else { unset($themes[$name]); } } } if (!empty($modules)) { // Check module dependencies. $dependencies = drush_check_module_dependencies($modules, $extension_info); $unmet_dependencies = array(); foreach ($dependencies as $module => $info) { if (!empty($info['unmet-dependencies'])) { foreach ($info['unmet-dependencies'] as $unmet) { $unmet_project = (!empty($info['dependencies'][$unmet]['project'])) ? $info['dependencies'][$unmet]['project'] : drush_pm_enable_find_project_from_extension($unmet); if (!empty($unmet_project)) { $unmet_dependencies[$module][$unmet_project] = $unmet_project; } } } } if (!empty($unmet_dependencies)) { $msgs = array(); $unmet_project_list = array(); foreach ($unmet_dependencies as $module => $unmet_projects) { $unmet_project_list = array_merge($unmet_project_list, $unmet_projects); $msgs[] = dt("!module requires !unmet-projects", array('!unmet-projects' => implode(', ', $unmet_projects), '!module' => $module)); } drush_log(dt("The following projects have unmet dependencies:\n!list", array('!list' => implode("\n", $msgs))), LogLevel::OK); if (drush_get_option('resolve-dependencies')) { drush_log(dt("They are being downloaded."), LogLevel::OK); } if (drush_get_option('resolve-dependencies') || drush_confirm(dt("Would you like to download them?"))) { $download = array_merge($download, $unmet_project_list); } } } if (!empty($download)) { // Disable DRUSH_AFFIRMATIVE context temporarily. $drush_affirmative = drush_get_context('DRUSH_AFFIRMATIVE'); drush_set_context('DRUSH_AFFIRMATIVE', FALSE); // Invoke a new process to download dependencies. $result = drush_invoke_process('@self', 'pm-download', $download, array(), array('interactive' => TRUE)); // Restore DRUSH_AFFIRMATIVE context. drush_set_context('DRUSH_AFFIRMATIVE', $drush_affirmative); // Refresh module cache after downloading the new modules. if (drush_drupal_major_version() >= 8) { \Drush\Drupal\ExtensionDiscovery::reset(); system_list_reset(); } $extension_info = drush_get_extensions(); $recheck = TRUE; } } if (!empty($modules)) { $all_dependencies = array(); $dependencies_ok = TRUE; foreach ($dependencies as $key => $info) { if (isset($info['error'])) { unset($modules[$key]); $dependencies_ok = drush_set_error($info['error']['code'], $info['error']['message']); } elseif (!empty($info['dependencies'])) { // Make sure we have an assoc array. $dependencies_list = array_keys($info['dependencies']); $assoc = array_combine($dependencies_list, $dependencies_list); $all_dependencies = array_merge($all_dependencies, $assoc); } } if (!$dependencies_ok) { return FALSE; } $modules = array_diff(array_merge($modules, $all_dependencies), drush_module_list()); // Discard modules which doesn't meet requirements. require_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; foreach ($modules as $key => $module) { // Check to see if the module can be installed/enabled (hook_requirements). // See @system_modules_submit if (!drupal_check_module($module)) { unset($modules[$key]); drush_set_error('DRUSH_PM_ENABLE_MODULE_UNMEET_REQUIREMENTS', dt('Module !module doesn\'t meet the requirements to be enabled.', array('!module' => $module))); _drush_log_drupal_messages(); return FALSE; } } } $searchpath = array(); foreach (array_merge($modules, $themes) as $name) { $searchpath[] = drush_extension_get_path($extension_info[$name]); } // Add all modules that passed validation to the drush // list of commandfiles (if they have any). This // will allow these newly-enabled modules to participate // in the pre-pm_enable and post-pm_enable hooks. if (!empty($searchpath)) { _drush_add_commandfiles($searchpath); } drush_set_context('PM_ENABLE_EXTENSION_INFO', $extension_info); drush_set_context('PM_ENABLE_MODULES', $modules); drush_set_context('PM_ENABLE_THEMES', $themes); return TRUE; } /** * Command callback. Enable one or more extensions from downloaded projects. * Note that the modules and themes to be enabled were evaluated during the * pm-enable validate hook, above. */ function drush_pm_enable() { // Get the data built during the validate phase $extension_info = drush_get_context('PM_ENABLE_EXTENSION_INFO'); $modules = drush_get_context('PM_ENABLE_MODULES'); $themes = drush_get_context('PM_ENABLE_THEMES'); // Inform the user which extensions will finally be enabled. $extensions = array_merge($modules, $themes); if (empty($extensions)) { return drush_log(dt('There were no extensions that could be enabled.'), LogLevel::OK); } else { drush_print(dt('The following extensions will be enabled: !extensions', array('!extensions' => implode(', ', $extensions)))); if(!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } // Enable themes. if (!empty($themes)) { drush_theme_enable($themes); } // Enable modules and pass dependency validation in form submit. if (!empty($modules)) { drush_include_engine('drupal', 'environment'); drush_module_enable($modules); } // Inform the user of final status. $result_extensions = drush_get_named_extensions_list($extensions); $problem_extensions = array(); $role = drush_role_get_class(); foreach ($result_extensions as $name => $extension) { if ($extension->status) { drush_log(dt('!extension was enabled successfully.', array('!extension' => $name)), LogLevel::OK); $perms = $role->getModulePerms($name); if (!empty($perms)) { drush_print(dt('!extension defines the following permissions: !perms', array('!extension' => $name, '!perms' => implode(', ', $perms)))); } } else { $problem_extensions[] = $name; } } if (!empty($problem_extensions)) { return drush_set_error('DRUSH_PM_ENABLE_EXTENSION_ISSUE', dt('There was a problem enabling !extension.', array('!extension' => implode(',', $problem_extensions)))); } // Return the list of extensions enabled return $extensions; } /** * Command callback. Disable one or more extensions. */ function drush_pm_disable() { $args = pm_parse_arguments(func_get_args()); drush_include_engine('drupal', 'pm'); _drush_pm_disable($args); } /** * Add extensions that match extension_name*. * * A helper function for commands that take a space separated list of extension * names. It will identify extensions that have been passed in with a * trailing * and add all matching extensions to the array that is returned. * * @param $extensions * An array of extensions, by reference. * @param $extension_info * Optional. An array of extension info as returned by drush_get_extensions(). */ function _drush_pm_expand_extensions(&$extensions, $extension_info = array()) { if (empty($extension_info)) { $extension_info = drush_get_extensions(); } foreach ($extensions as $key => $extension) { if (($wildcard = rtrim($extension, '*')) !== $extension) { foreach (array_keys($extension_info) as $extension_name) { if (substr($extension_name, 0, strlen($wildcard)) == $wildcard) { $extensions[] = $extension_name; } } unset($extensions[$key]); continue; } } } /** * Command callback. Uninstall one or more modules. */ function drush_pm_uninstall() { $args = pm_parse_arguments(func_get_args()); drush_include_engine('drupal', 'pm'); _drush_pm_uninstall($args); } /** * Command callback. Show available releases for given project(s). */ function drush_pm_releases() { $release_info = drush_get_engine('release_info'); // Obtain requests. $requests = pm_parse_arguments(func_get_args(), FALSE); if (!$requests) { $requests = array('drupal'); } // Get installed projects. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) { $projects = drush_get_projects(); } else { $projects = array(); } // Select the filter to apply based on cli options. if (drush_get_option('dev', FALSE)) { $filter = 'dev'; } elseif (drush_get_option('all', FALSE)) { $filter = 'all'; } else { $filter = ''; } $status_url = drush_get_option('source'); $output = array(); foreach ($requests as $request) { $request = pm_parse_request($request, $status_url, $projects); $project_name = $request['name']; $project_release_info = $release_info->get($request); if ($project_release_info) { $version = isset($projects[$project_name]) ? $projects[$project_name]['version'] : NULL; $releases = $project_release_info->filterReleases($filter, $version); foreach ($releases as $key => $release) { $output["${project_name}-${key}"] = array( 'project' => $project_name, 'version' => $release['version'], 'date' => gmdate('Y-M-d', $release['date']), 'status' => implode(', ', $release['release_status']), ) + $release; } } } if (empty($output)) { return drush_log(dt('No valid projects given.'), LogLevel::OK); } return $output; } /** * Command callback. Show release notes for given project(s). */ function drush_pm_releasenotes() { $release_info = drush_get_engine('release_info'); // Obtain requests. if (!$requests = pm_parse_arguments(func_get_args(), FALSE)) { $requests = array('drupal'); } // Get installed projects. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) { $projects = drush_get_projects(); } else { $projects = array(); } $status_url = drush_get_option('source'); $output = ''; foreach($requests as $request) { $request = pm_parse_request($request, $status_url, $projects); $project_release_info = $release_info->get($request); if ($project_release_info) { $version = empty($request['version']) ? NULL : $request['version']; $output .= $project_release_info->getReleaseNotes($version); } } return $output; } /** * Command callback. Refresh update status information. */ function drush_pm_refresh() { $update_status = drush_get_engine('update_status'); drush_print(dt("Refreshing update status information ...")); $update_status->refresh(); drush_print(dt("Done.")); } /** * Command callback. Execute pm-update. */ function drush_pm_update() { // Call pm-updatecode. updatedb will be called in the post-update process. $args = pm_parse_arguments(func_get_args(), FALSE); drush_set_option('check-updatedb', FALSE); return drush_invoke('pm-updatecode', $args); } /** * Post-command callback. * Execute updatedb command after an updatecode - user requested `update`. */ function drush_pm_post_pm_update() { // Use drush_invoke_process to start a subprocess. Cleaner that way. if (drush_get_context('DRUSH_PM_UPDATED', FALSE) !== FALSE) { drush_invoke_process('@self', 'updatedb'); } } /** * Validate callback for updatecode command. Abort if 'backup' directory exists. */ function drush_pm_updatecode_validate() { $path = drush_get_context('DRUSH_DRUPAL_ROOT') . '/backup'; if (is_dir($path) && (realpath(drush_get_option('backup-dir', FALSE)) != $path)) { return drush_set_error('', dt('Backup directory !path found. It\'s a security risk to store backups inside the Drupal tree. Drush now uses by default ~/drush-backups. You need to move !path out of the Drupal tree to proceed. Note: if you know what you\'re doing you can explicitly set --backup-dir to !path and continue.', array('!path' => $path))); } } /** * Post-command callback for updatecode. * * Execute pm-updatecode-postupdate in a backend process to not conflict with * old code already in memory. */ function drush_pm_post_pm_updatecode() { // Skip if updatecode was invoked by pm-update. // This way we avoid being noisy, as updatedb is to be executed. if (drush_get_option('check-updatedb', TRUE)) { if (drush_get_context('DRUSH_PM_UPDATED', FALSE)) { drush_invoke_process('@self', 'pm-updatecode-postupdate'); } } } /** * Command callback. Execute updatecode-postupdate. */ function drush_pm_updatecode_postupdate() { // Clear the cache, since some projects could have moved around. drush_drupal_cache_clear_all(); // Notify of pending database updates. // Make sure the installation API is available require_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; // Load all .install files. drupal_load_updates(); // @see system_requirements(). foreach (drush_module_list() as $module) { $updates = drupal_get_schema_versions($module); if ($updates !== FALSE) { $default = drupal_get_installed_schema_version($module); if (max($updates) > $default) { drush_log(dt("You have pending database updates. Run `drush updatedb` or visit update.php in your browser."), LogLevel::WARNING); break; } } } } /** * Sanitize user provided arguments to several pm commands. * * Return an array of arguments off a space and/or comma separated values. */ function pm_parse_arguments($args, $dashes_to_underscores = TRUE) { $arguments = _convert_csv_to_array($args); foreach ($arguments as $key => $argument) { $argument = ($dashes_to_underscores) ? strtr($argument, '-', '_') : $argument; } return $arguments; } /** * Decompound a version string and returns major, minor, patch and extra parts. * * @see _pm_parse_version_compound() * @see pm_parse_version() * * @param string $version * A version string like X.Y-Z, X.Y.Z-W or a subset. * * @return array * Array with major, patch and extra keys. */ function _pm_parse_version_decompound($version) { $pattern = '/^(\d+)(?:.(\d+))?(?:\.(x|\d+))?(?:-([a-z0-9\.-]*))?(?:\+(\d+)-dev)?$/'; $matches = array(); preg_match($pattern, $version, $matches); $parts = array( 'major' => '', 'minor' => '', 'patch' => '', 'extra' => '', 'offset' => '', ); if (isset($matches[1])) { $parts['major'] = $matches[1]; if (isset($matches[2])) { if (isset($matches[3]) && $matches[3] != '') { $parts['minor'] = $matches[2]; $parts['patch'] = $matches[3]; } else { $parts['patch'] = $matches[2]; } } if (!empty($matches[4])) { $parts['extra'] = $matches[4]; } if (!empty($matches[5])) { $parts['offset'] = $matches[5]; } } return $parts; } /** * Build a version string from an array of major, minor and extra parts. * * @see _pm_parse_version_decompound() * @see pm_parse_version() * * @param array $parts * Array of parts. * * @return string * A Version string. */ function _pm_parse_version_compound($parts) { $project_version = ''; if ($parts['patch'] != '') { $project_version = $parts['major']; if ($parts['minor'] != '') { $project_version = $project_version . '.' . $parts['minor']; } if ($parts['patch'] == 'x') { $project_version = $project_version . '.x-dev'; } else { $project_version = $project_version . '.' . $parts['patch']; if ($parts['extra'] != '') { $project_version = $project_version . '-' . $parts['extra']; } } if ($parts['offset'] != '') { $project_version = $project_version . '+' . $parts['offset'] . '-dev'; } } return $project_version; } /** * Parses a version string and returns its components. * * It parses both core and contrib version strings. * * Core (semantic versioning): * - 8.0.0-beta3+252-dev * - 8.0.0-beta2 * - 8.0.x-dev * - 8.1.x * - 8.0.1 * - 8 * * Core (classic drupal scheme): * - 7.x-dev * - 7.x * - 7.33 * - 7.34+3-dev * - 7 * * Contrib: * - 7.x-1.0-beta1+30-dev * - 7.x-1.0-beta1 * - 7.x-1.0+30-dev * - 7.x-1.0 * - 1.0-beta1 * - 1.0 * - 7.x-1.x * - 7.x-1.x-dev * - 1.x * * @see pm_parse_request() * * @param string $version * A core or project version string. * * @param bool $is_core * Whether this is a core version or a project version. * * @return array * Version string in parts. * Example for a contrib version (ex: 7.x-3.2-beta1): * - version : Fully qualified version string. * - drupal_version : Core compatibility version (ex: 7.x). * - version_major : Major version (ex: 3). * - version_minor : Minor version. Not applicable. Always empty. * - version_patch : Patch version (ex: 2). * - version_extra : Extra version (ex: beta1). * - project_version : Project specific part of the version (ex: 3.2-beta1). * * Example for a core version (ex: 8.1.2-beta2 or 7.0-beta2): * - version : Fully qualified version string. * - drupal_version : Core compatibility version (ex: 8.x). * - version_major : Major version (ex: 8). * - version_minor : Minor version (ex: 1). Empty if not a semver. * - version_patch : Patch version (ex: 2). * - version_extra : Extra version (ex: beta2). * - project_version : Same as 'version'. */ function pm_parse_version($version, $is_core = FALSE) { $core_parts = _pm_parse_version_decompound($version); // If no major version, we have no version at all. Pick a default. $drupal_version_default = drush_drupal_major_version(); if ($core_parts['major'] == '') { $core_parts['major'] = ($drupal_version_default) ? $drupal_version_default : drush_get_option('default-major', 8); } if ($is_core) { $project_version = _pm_parse_version_compound($core_parts); $version_parts = array( 'version' => $project_version, 'drupal_version' => $core_parts['major'] . '.x', 'project_version' => $project_version, 'version_major' => $core_parts['major'], 'version_minor' => $core_parts['minor'], 'version_patch' => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'], 'version_extra' => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'], 'version_offset' => $core_parts['offset'], ); } else { // If something as 7.x-1.0-beta1, the project specific version is // in $version['extra'] and we need to parse it. if (strpbrk($core_parts['extra'], '.-')) { $nocore_parts = _pm_parse_version_decompound($core_parts['extra']); $nocore_parts['offset'] = $core_parts['offset']; $project_version = _pm_parse_version_compound($nocore_parts); $version_parts = array( 'version' => $core_parts['major'] . '.x-' . $project_version, 'drupal_version' => $core_parts['major'] . '.x', 'project_version' => $project_version, 'version_major' => $nocore_parts['major'], 'version_minor' => $core_parts['minor'], 'version_patch' => ($nocore_parts['patch'] == 'x') ? '' : $nocore_parts['patch'], 'version_extra' => ($nocore_parts['patch'] == 'x') ? 'dev' : $nocore_parts['extra'], 'version_offset' => $core_parts['offset'], ); } // At this point we have half a version and must decide if this is a drupal major or a project. else { // If working on a bootstrapped site, core_parts has the project version. if ($drupal_version_default) { $project_version = _pm_parse_version_compound($core_parts); $version = ($project_version) ? $drupal_version_default . '.x-' . $project_version : ''; $version_parts = array( 'version' => $version, 'drupal_version' => $drupal_version_default . '.x', 'project_version' => $project_version, 'version_major' => $core_parts['major'], 'version_minor' => $core_parts['minor'], 'version_patch' => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'], 'version_extra' => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'], 'version_offset' => $core_parts['offset'], ); } // Not working on a bootstrapped site, core_parts is core version. else { $version_parts = array( 'version' => '', 'drupal_version' => $core_parts['major'] . '.x', 'project_version' => '', 'version_major' => '', 'version_minor' => '', 'version_patch' => '', 'version_extra' => '', 'version_offset' => '', ); } } } return $version_parts; } /** * Parse out the project name and version and return as a structured array. * * @see pm_parse_version() * * @param string $request_string * Project name with optional version. Examples: 'ctools-7.x-1.0-beta1' * * @return array * Array with all parts of the request info. */ function pm_parse_request($request_string, $status_url = NULL, &$projects = array()) { // Split $request_string in project name and version. Note that hyphens (-) // are permitted in project names (ex: field-conditional-state). // We use a regex to split the string. The pattern used matches a string // starting with hyphen, followed by one or more numbers, any of the valid // symbols in version strings (.x-) and a catchall for the rest of the // version string. $parts = preg_split('/-(?:([\d+\.x].*))?$/', $request_string, NULL, PREG_SPLIT_DELIM_CAPTURE); if (count($parts) == 1) { // No version in the request string. $project = $request_string; $version = ''; } else { $project = $parts[0]; $version = $parts[1]; } $is_core = ($project == 'drupal'); $request = array( 'name' => $project, ) + pm_parse_version($version, $is_core); // Set the status url if provided or available in project's info file. if ($status_url) { $request['status url'] = $status_url; } elseif (!empty($projects[$project]['status url'])) { $request['status url'] = $projects[$project]['status url']; } return $request; } /** * @defgroup engines Engine types * @{ */ /** * Implementation of hook_drush_engine_type_info(). */ function pm_drush_engine_type_info() { return array( 'package_handler' => array( 'option' => 'package-handler', 'description' => 'Determine how to fetch projects from update service.', 'default' => 'wget', 'options' => array( 'cache' => 'Cache release XML and tarballs or git clones. Git clones use git\'s --reference option. Defaults to 1 for downloads, and 0 for git.', ), ), 'release_info' => array( 'add-options-to-command' => TRUE, ), 'update_status' => array( 'option' => 'update-backend', 'description' => 'Determine how to fetch update status information.', 'default' => 'drush', 'add-options-to-command' => TRUE, 'options' => array( 'update-backend' => 'Backend to obtain available updates.', 'check-disabled' => 'Check for updates of disabled modules and themes.', 'security-only' => 'Only update modules that have security updates available.', ), 'combine-help' => TRUE, ), 'version_control' => array( 'option' => 'version-control', 'default' => 'backup', 'description' => 'Integrate with version control systems.', ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Package handler engine is used by pm-download and * pm-updatecode commands to determine how to download/checkout * new projects and acquire updates to projects. */ function pm_drush_engine_package_handler() { return array( 'wget' => array( 'description' => 'Download project packages using wget or curl.', 'options' => array( 'no-md5' => 'Skip md5 validation of downloads.', ), ), 'git_drupalorg' => array( 'description' => 'Use git.drupal.org to checkout and update projects.', 'options' => array( 'gitusername' => 'Your git username as shown on user/[uid]/edit/git. Typically, this is set this in drushrc.php. Omitting this prevents users from pushing changes back to git.drupal.org.', 'gitsubmodule' => 'Use git submodules for checking out new projects. Existing git checkouts are unaffected, and will continue to (not) use submodules regardless of this setting.', 'gitcheckoutparams' => 'Add options to the `git checkout` command.', 'gitcloneparams' => 'Add options to the `git clone` command.', 'gitfetchparams' => 'Add options to the `git fetch` command.', 'gitpullparams' => 'Add options to the `git pull` command.', 'gitinfofile' => 'Inject version info into each .info file.', ), 'sub-options' => array( 'gitsubmodule' => array( 'gitsubmoduleaddparams' => 'Add options to the `git submodule add` command.', ), ), ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Release info engine is used by several pm commands to obtain * releases info from Drupal's update service or external sources. */ function pm_drush_engine_release_info() { return array( 'updatexml' => array( 'description' => 'Drush release info engine for update.drupal.org and compatible services.', 'options' => array( 'source' => 'The base URL which provides project release history in XML. Defaults to http://updates.drupal.org/release-history.', 'dev' => 'Work with development releases solely.', ), 'sub-options' => array( 'cache' => array( 'cache-duration-releasexml' => 'Expire duration (in seconds) for release XML. Defaults to 86400 (24 hours).', ), 'select' => array( 'all' => 'Shows all available releases instead of a short list of recent releases.', ), ), 'class' => 'Drush\UpdateService\ReleaseInfo', ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Update status engine is used to check available updates for * the projects in a Drupal site. */ function pm_drush_engine_update_status() { return array( 'drupal' => array( 'description' => 'Check available updates with update.module.', 'drupal dependencies' => array('update'), 'class' => 'Drush\UpdateService\StatusInfoDrupal', ), 'drush' => array( 'description' => 'Check available updates without update.module.', 'class' => 'Drush\UpdateService\StatusInfoDrush', ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Integration with VCS in order to easily commit your changes to projects. */ function pm_drush_engine_version_control() { return array( 'backup' => array( 'description' => 'Backup all project files before updates.', 'options' => array( 'no-backup' => 'Do not perform backups. WARNING: Will result in non-core files/dirs being deleted (e.g. .git)', 'backup-dir' => 'Specify a directory to backup projects into. Defaults to drush-backups within the home directory of the user running the command. It is forbidden to specify a directory inside your drupal root.', ), ), 'bzr' => array( 'signature' => 'bzr root %s', 'description' => 'Quickly add/remove/commit your project changes to Bazaar.', 'options' => array( 'bzrsync' => 'Automatically add new files to the Bazaar repository and remove deleted files. Caution.', 'bzrcommit' => 'Automatically commit changes to Bazaar repository. You must also use the --bzrsync option.', ), 'sub-options' => array( 'bzrcommit' => array( 'bzrmessage' => 'Override default commit message which is: Drush automatic commit. Project Command: ', ), ), 'examples' => array( 'drush dl cck --version-control=bzr --bzrsync --bzrcommit' => 'Download the cck project and then add it and commit it to Bazaar.' ), ), 'svn' => array( 'signature' => 'svn info %s', 'description' => 'Quickly add/remove/commit your project changes to Subversion.', 'options' => array( 'svnsync' => 'Automatically add new files to the SVN repository and remove deleted files. Caution.', 'svncommit' => 'Automatically commit changes to SVN repository. You must also using the --svnsync option.', 'svnstatusparams' => "Add options to the 'svn status' command", 'svnaddparams' => 'Add options to the `svn add` command', 'svnremoveparams' => 'Add options to the `svn remove` command', 'svnrevertparams' => 'Add options to the `svn revert` command', 'svncommitparams' => 'Add options to the `svn commit` command', ), 'sub-options' => array( 'svncommit' => array( 'svnmessage' => 'Override default commit message which is: Drush automatic commit: ', ), ), 'examples' => array( 'drush [command] cck --svncommitparams=\"--username joe\"' => 'Commit changes as the user \'joe\' (Quotes are required).' ), ), ); } /** * @} End of "Engine types". */ /** * Interface for version control systems. * We use a simple object layer because we conceivably need more than one * loaded at a time. */ interface drush_version_control { function pre_update(&$project); function rollback($project); function post_update($project); function post_download($project); static function reserved_files(); } /** * A simple factory function that tests for version control systems, in a user * specified order, and returns the one that appears to be appropriate for a * specific directory. */ function drush_pm_include_version_control($directory = '.') { $engine_info = drush_get_engines('version_control'); $version_controls = drush_get_option('version-control', FALSE); // If no version control was given, use a list of defaults. if (!$version_controls) { // Backup engine is the last option. $version_controls = array_reverse(array_keys($engine_info['engines'])); } else { $version_controls = array($version_controls); } // Find the first valid engine in the list, checking signatures if needed. $engine = FALSE; while (!$engine && count($version_controls)) { $version_control = array_shift($version_controls); if (isset($engine_info['engines'][$version_control])) { if (!empty($engine_info['engines'][$version_control]['signature'])) { drush_log(dt('Verifying signature for !vcs version control engine.', array('!vcs' => $version_control)), LogLevel::DEBUG); if (drush_shell_exec($engine_info['engines'][$version_control]['signature'], $directory)) { $engine = $version_control; } } else { $engine = $version_control; } } } if (!$engine) { return drush_set_error('DRUSH_PM_NO_VERSION_CONTROL', dt('No valid version control or backup engine found (the --version-control option was set to "!version-control").', array('!version-control' => $version_control))); } $instance = drush_include_engine('version_control', $engine); return $instance; } /** * Update the locked status of all of the candidate projects * to be updated. * * @param array &$projects * The projects array from pm_updatecode. $project['locked'] will * be set for every file where a persistent lockfile can be found. * The 'lock' and 'unlock' operations are processed first. * @param array $projects_to_lock * A list of projects to create peristent lock files for * @param array $projects_to_unlock * A list of projects to clear the persistent lock on * @param string $lock_message * The reason the project is being locked; stored in the lockfile. * * @return array * A list of projects that are locked. */ function drush_pm_update_lock(&$projects, $projects_to_lock, $projects_to_unlock, $lock_message = NULL) { $locked_result = array(); // Warn about ambiguous lock / unlock values if ($projects_to_lock == array('1')) { $projects_to_lock = array(); drush_log(dt('Ignoring --lock with no value.'), LogLevel::WARNING); } if ($projects_to_unlock == array('1')) { $projects_to_unlock = array(); drush_log(dt('Ignoring --unlock with no value.'), LogLevel::WARNING); } // Log if we are going to lock or unlock anything if (!empty($projects_to_unlock)) { drush_log(dt('Unlocking !projects', array('!projects' => implode(',', $projects_to_unlock))), LogLevel::OK); } if (!empty($projects_to_lock)) { drush_log(dt('Locking !projects', array('!projects' => implode(',', $projects_to_lock))), LogLevel::OK); } $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); foreach ($projects as $name => $project) { $message = NULL; if (isset($project['path'])) { if ($name == 'drupal') { $lockfile = $drupal_root . '/.drush-lock-update'; } else { $lockfile = $drupal_root . '/' . $project['path'] . '/.drush-lock-update'; } // Remove the lock file if the --unlock option was specified if (((in_array($name, $projects_to_unlock)) || (in_array('all', $projects_to_unlock))) && (file_exists($lockfile))) { drush_op('unlink', $lockfile); } // Create the lock file if the --lock option was specified if ((in_array($name, $projects_to_lock)) || (in_array('all', $projects_to_lock))) { drush_op('file_put_contents', $lockfile, $lock_message != NULL ? $lock_message : "Locked via drush."); // Note that the project is locked. This will work even if we are simulated, // or if we get permission denied from the file_put_contents. // If the lock is -not- simulated or transient, then the lock message will be // read from the lock file below. $message = drush_get_context('DRUSH_SIMULATE') ? 'Simulated lock.' : 'Transient lock.'; } // If the persistent lock file exists, then mark the project as locked. if (file_exists($lockfile)) { $message = trim(file_get_contents($lockfile)); } } // If there is a message set, then mark the project as locked. if (isset($message)) { $projects[$name]['locked'] = !empty($message) ? $message : "Locked."; $locked_result[$name] = $project; } } return $locked_result; } /** * Returns the path to the extensions cache file. */ function _drush_pm_extension_cache_file() { return drush_get_context('DRUSH_PER_USER_CONFIGURATION') . "/drush-extension-cache.inc"; } /** * Load the extensions cache. */ function _drush_pm_get_extension_cache() { $extension_cache = array(); $cache_file = _drush_pm_extension_cache_file(); if (file_exists($cache_file)) { include $cache_file; } if (!array_key_exists('extension-map', $extension_cache)) { $extension_cache['extension-map'] = array(); } return $extension_cache; } /** * Lookup an extension in the extensions cache. */ function drush_pm_lookup_extension_in_cache($extension) { $result = NULL; $extension_cache = _drush_pm_get_extension_cache(); if (!empty($extension_cache) && array_key_exists($extension, $extension_cache)) { $result = $extension_cache[$extension]; } return $result; } /** * Persists extensions cache. * * #TODO# not implemented. */ function drush_pm_put_extension_cache($extension_cache) { } /** * Store extensions founds within a project in extensions cache. */ function drush_pm_cache_project_extensions($project, $found) { $extension_cache = _drush_pm_get_extension_cache(); foreach($found as $extension) { // Simple cache does not handle conflicts // We could keep an array of projects, and count // how many times each one has been seen... $extension_cache[$extension] = $project['name']; } drush_pm_put_extension_cache($extension_cache); } /** * Print out all extensions (modules/themes/profiles) found in specified project. * * Find .info.yml files in the project path and identify modules, themes and * profiles. It handles two kind of projects: drupal core/profiles and * modules/themes. * It does nothing with theme engine projects. */ function drush_pm_extensions_in_project($project) { // Mask for drush_scan_directory, to match .info.yml files. $mask = $project['drupal_version'][0] >= 8 ? '/(.*)\.info\.yml$/' : '/(.*)\.info$/'; // Mask for drush_scan_directory, to avoid tests directories. $nomask = array('.', '..', 'CVS', 'tests'); // Drupal core and profiles can contain modules, themes and profiles. if (in_array($project['project_type'], array('core', 'profile'))) { $found = array('profile' => array(), 'theme' => array(), 'module' => array()); // Find all of the .info files foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) { // Extract extension name from filename. $matches = array(); preg_match($mask, $info->basename, $matches); $name = $matches[1]; // Find the project type corresponding the .info file. // (Only drupal >=7.x has .info for .profile) $base = dirname($filename) . '/' . $name; if (is_file($base . '.module')) { $found['module'][] = $name; } else if (is_file($base . '.profile')) { $found['profile'][] = $name; } else { $found['theme'][] = $name; } } // Special case: find profiles for drupal < 7.x (no .info) if ($project['drupal_version'][0] < 7) { foreach (drush_find_profiles($project['full_project_path']) as $filename => $info) { $found['profile'][] = $info->name; } } // Log results. $msg = "Project !project contains:\n"; $args = array('!project' => $project['name']); foreach (array_keys($found) as $type) { if ($count = count($found[$type])) { $msg .= " - !count_$type !type_$type: !found_$type\n"; $args += array("!count_$type" => $count, "!type_$type" => $type, "!found_$type" => implode(', ', $found[$type])); if ($count > 1) { $args["!type_$type"] = $type.'s'; } } } drush_log(dt($msg, $args), LogLevel::SUCCESS); drush_print_pipe(call_user_func_array('array_merge', array_values($found))); } // Modules and themes can only contain other extensions of the same type. elseif (in_array($project['project_type'], array('module', 'theme'))) { $found = array(); foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) { // Extract extension name from filename. $matches = array(); preg_match($mask, $info->basename, $matches); $found[] = $matches[1]; } // If there is only one module / theme in the project, only print out // the message if is different than the project name. if (count($found) == 1) { if ($found[0] != $project['name']) { $msg = "Project !project contains a !type named !found."; } } // If there are multiple modules or themes in the project, list them all. else { $msg = "Project !project contains !count !types: !found."; } if (isset($msg)) { drush_print(dt($msg, array('!project' => $project['name'], '!count' => count($found), '!type' => $project['project_type'], '!found' => implode(', ', $found)))); } drush_print_pipe($found); // Cache results. drush_pm_cache_project_extensions($project, $found); } } /** * Return an array of empty directories. * * Walk a directory and return an array of subdirectories that are empty. Will * return the given directory if it's empty. * If a list of items to exclude is provided, subdirectories will be condidered * empty even if they include any of the items in the list. * * @param string $dir * Path to the directory to work in. * @param array $exclude * Array of files or directory to exclude in the check. * * @return array * A list of directory paths that are empty. A directory is deemed to be empty * if it only contains excluded files or directories. */ function drush_find_empty_directories($dir, $exclude = array()) { // Skip files. if (!is_dir($dir)) { return array(); } $to_exclude = array_merge(array('.', '..'), $exclude); $empty_dirs = array(); $dir_is_empty = TRUE; foreach (scandir($dir) as $file) { // Skip excluded directories. if (in_array($file, $to_exclude)) { continue; } // Recurse into sub-directories to find potentially empty ones. $subdir = $dir . '/' . $file; $empty_dirs += drush_find_empty_directories($subdir, $exclude); // $empty_dir will not contain $subdir, if it is a file or if the // sub-directory is not empty. $subdir is only set if it is empty. if (!isset($empty_dirs[$subdir])) { $dir_is_empty = FALSE; } } if ($dir_is_empty) { $empty_dirs[$dir] = $dir; } return $empty_dirs; } /** * Inject metadata into all .info files for a given project. * * @param string $project_dir * The full path to the root directory of the project to operate on. * @param string $project_name * The project machine name (AKA shortname). * @param string $version * The version string to inject into the .info file(s). * @param int $datestamp * The datestamp of the last commit. * * @return boolean * TRUE on success, FALSE on any failures appending data to .info files. */ function drush_pm_inject_info_file_metadata($project_dir, $project_name, $version, $datestamp) { // `drush_drupal_major_version()` cannot be used here because this may be running // outside of a Drupal context. $yaml_format = substr($version, 0, 1) >= 8; $pattern = preg_quote($yaml_format ? '.info.yml' : '.info'); $info_files = drush_scan_directory($project_dir, '/.*' . $pattern . '$/'); if (!empty($info_files)) { // Construct the string of metadata to append to all the .info files. if ($yaml_format) { $info = _drush_pm_generate_info_yaml_metadata($version, $project_name, $datestamp); } else { $info = _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp); } foreach ($info_files as $info_file) { if (!drush_file_append_data($info_file->filename, $info)) { return FALSE; } } } return TRUE; } /** * Generate version information for `.info` files in ini format. * * Taken with some modifications from: * http://drupalcode.org/project/drupalorg.git/blob/refs/heads/6.x-3.x:/drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php#l192 */ function _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp) { $matches = array(); $extra = ''; if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) { $extra .= "\ncore = \"$matches[1]\""; } if (!drush_get_option('no-gitprojectinfo', FALSE)) { $extra = "\nproject = \"$project_name\""; } $date = date('Y-m-d', $datestamp); $info = <<= 6) { $extra .= "\ncore: '$matches[1]'"; } if (!drush_get_option('no-gitprojectinfo', FALSE)) { $extra = "\nproject: '$project_name'"; } $date = date('Y-m-d', $datestamp); $info = <<