5 * The drush Project Manager
8 * - Request: a requested project (string or keyed array), with a name and (optionally) version.
9 * - Project: a drupal.org project (i.e drupal.org/project/*), such as cck or zen.
10 * - Extension: a drupal.org module, theme or profile.
11 * - Version: a requested version, such as 1.0 or 1.x-dev.
12 * - Release: a specific release of a project, with associated metadata (from the drupal.org update service).
15 use Drush\Log\LogLevel;
18 * @defgroup update_status_constants Update Status Constants
20 * Represents update status of projects.
22 * The first set is a mapping of some constants declared in update.module.
23 * We only declare the ones we're interested in.
24 * The rest of the constants are used by pm-updatestatus to represent
25 * a status when the user asked for updates to specific versions or
26 * other circumstances not managed by Drupal.
30 * Project is missing security update(s).
32 * Maps UPDATE_NOT_SECURE.
34 const DRUSH_UPDATESTATUS_NOT_SECURE = 1;
37 * Current release has been unpublished and is no longer available.
39 * Maps UPDATE_REVOKED.
41 const DRUSH_UPDATESTATUS_REVOKED = 2;
44 * Current release is no longer supported by the project maintainer.
46 * Maps UPDATE_NOT_SUPPORTED.
48 const DRUSH_UPDATESTATUS_NOT_SUPPORTED = 3;
51 * Project has a new release available, but it is not a security release.
53 * Maps UPDATE_NOT_CURRENT.
55 const DRUSH_UPDATESTATUS_NOT_CURRENT = 4;
58 * Project is up to date.
60 * Maps UPDATE_CURRENT.
62 const DRUSH_UPDATESTATUS_CURRENT = 5;
65 * Project's status cannot be checked.
67 * Maps UPDATE_NOT_CHECKED.
69 const DRUSH_UPDATESTATUS_NOT_CHECKED = -1;
72 * No available update data was found for project.
74 * Maps UPDATE_UNKNOWN.
76 const DRUSH_UPDATESTATUS_UNKNOWN = -2;
79 * There was a failure fetching available update data for this project.
81 * Maps UPDATE_NOT_FETCHED.
83 const DRUSH_UPDATESTATUS_NOT_FETCHED = -3;
86 * We need to (re)fetch available update data for this project.
88 * Maps UPDATE_FETCH_PENDING.
90 const DRUSH_UPDATESTATUS_FETCH_PENDING = -4;
93 * Project was not packaged by drupal.org.
95 const DRUSH_UPDATESTATUS_PROJECT_NOT_PACKAGED = 101;
98 * Requested project is not updateable.
100 const DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_UPDATEABLE = 102;
103 * Requested project not found.
105 const DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_FOUND = 103;
108 * Requested version not found.
110 const DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_FOUND = 104;
113 * Requested version available.
115 const DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_CURRENT = 105;
118 * Requested version already installed.
120 const DRUSH_UPDATESTATUS_REQUESTED_VERSION_CURRENT = 106;
123 * @} End of "defgroup update_status_constants".
127 * Implementation of hook_drush_help().
129 function pm_drush_help($section) {
131 case 'meta:pm:title':
132 return dt('Project manager commands');
133 case 'meta:pm:summary':
134 return dt('Download, enable, examine and update your modules and themes.');
135 case 'drush:pm-enable':
136 return dt('Enable one or more extensions (modules or themes). Enable dependant extensions as well.');
137 case 'drush:pm-disable':
138 return dt('Disable one or more extensions (modules or themes). Disable dependant extensions as well.');
139 case 'drush:pm-updatecode':
140 case 'drush:pm-update':
141 $message = dt("Display available update information for Drupal core and all enabled projects and allow updating to latest recommended releases.");
142 if ($section == 'drush:pm-update') {
143 $message .= ' '.dt("Also apply any database updates required (same as pm-updatecode + updatedb).");
145 $message .= ' '.dt("Note: The user is asked to confirm before the actual update. Backups are performed unless directory is already under version control. Updated projects can potentially break your site. It is NOT recommended to update production sites without prior testing.");
147 case 'drush:pm-updatecode-postupdate':
148 return dt("This is a helper command needed by updatecode. It is used to check for db updates in a backend process after code updated have been performed. We need to run this task in a separate process to not conflict with old code already in memory.");
149 case 'drush:pm-download':
150 return dt("Download Drupal core or projects from drupal.org (Drupal core, modules, themes or profiles) and other sources. It will automatically figure out which project version you want based on its recommended release, or you may specify a particular version.
152 If no --destination is provided, then destination depends on the project type:
153 - Profiles will be downloaded to profiles/ in your Drupal root.
154 - Modules and themes will be downloaded to the site specific directory (sites/example.com/modules|themes) if available, or to the site wide directory otherwise.
155 - If you're downloading drupal core or you are not running the command within a bootstrapped drupal site, the default location is the current directory.
156 - Drush commands will be relocated to @site_wide_location (if available) or ~/.drush. Relocation is determined once the project is downloaded by examining its content. Note you can provide your own function in a commandfile to determine the relocation of any project.", array('@site_wide_location' => drush_get_context('DRUSH_SITE_WIDE_COMMANDFILES')));
161 * Implementation of hook_drush_command().
163 function pm_drush_command() {
164 $update_options = array(
166 '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.',
167 'example-value' => 'foo,bar',
170 $update_suboptions = array(
172 'lock-message' => array(
173 'description' => 'A brief message explaining why a project is being locked; displayed during pm-updatecode. Optional.',
174 'example-value' => 'message',
177 'description' => 'Remove the persistent lock from the specified projects so that they may be updated again.',
178 'example-value' => 'foo,bar',
183 $items['pm-enable'] = array(
184 'description' => 'Enable one or more extensions (modules or themes).',
185 'arguments' => array(
186 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to enable all matches.',
189 'resolve-dependencies' => 'Attempt to download any missing dependencies. At the moment, only works when the module name is the same as the project name.',
190 'skip' => 'Skip automatic downloading of libraries (c.f. devel).',
192 'aliases' => array('en'),
194 'release_info' => array(
195 'add-options-to-command' => FALSE,
199 $items['pm-disable'] = array(
200 'description' => 'Disable one or more extensions (modules or themes).',
201 'arguments' => array(
202 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to disable multiple matches.',
204 'aliases' => array('dis'),
208 'release_info' => array(
209 'add-options-to-command' => FALSE,
213 $items['pm-info'] = array(
214 'description' => 'Show detailed info for one or more extensions (modules or themes).',
215 'arguments' => array(
216 '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.',
218 'aliases' => array('pmi'),
219 'outputformat' => array(
220 'default' => 'key-value-list',
221 'pipe-format' => 'json',
222 'formatted-filter' => '_drush_pm_info_format_table_data',
223 'field-labels' => array(
224 'extension' => 'Extension',
225 'project' => 'Project',
228 'description' => 'Description',
229 'version' => 'Version',
231 'package' => 'Package',
234 'status' => 'Status',
236 'schema_version' => 'Schema version',
238 'requires' => 'Requires',
239 'required_by' => 'Required by',
240 'permissions' => 'Permissions',
241 'config' => 'Configure',
242 'engine' => 'Engine',
243 'base_theme' => 'Base theme',
244 'regions' => 'Regions',
245 'features' => 'Features',
246 'stylesheets' => 'Stylesheets',
247 // 'media_' . $media => 'Media '. $media for each $info->info['stylesheets'] as $media => $files
248 'scripts' => 'Scripts',
250 'output-data-type' => 'format-table',
254 $items['pm-projectinfo'] = array(
255 'description' => 'Show a report of available projects and their extensions.',
256 'arguments' => array(
257 'projects' => 'Optional. A list of installed projects to show.',
260 'drush' => 'Optional. Only incude projects that have one or more Drush commands.',
262 'description' => 'Filter by project status. Choices: enabled, disabled. A project is considered enabled when at least one of its extensions is enabled.',
263 'example-value' => 'enabled',
266 'outputformat' => array(
267 'default' => 'key-value-list',
268 'pipe-format' => 'json',
269 'field-labels' => array(
272 'version' => 'Version',
273 'status' => 'Status',
274 'extensions' => 'Extensions',
275 'drush' => 'Drush Commands',
276 'datestamp' => 'Datestamp',
279 'fields-default' => array('label', 'type', 'version', 'status', 'extensions', 'drush', 'datestamp', 'path'),
280 'fields-pipe' => array('label'),
281 'output-data-type' => 'format-table',
283 'aliases' => array('pmpi'),
286 // Install command is reserved for the download and enable of projects including dependencies.
287 // @see http://drupal.org/node/112692 for more information.
288 // $items['install'] = array(
289 // 'description' => 'Download and enable one or more modules',
291 $items['pm-uninstall'] = array(
292 'description' => 'Uninstall one or more modules.',
293 'arguments' => array(
294 'modules' => 'A list of modules.',
296 'aliases' => array('pmu'),
298 $items['pm-list'] = array(
299 'description' => 'Show a list of available extensions (modules and themes).',
300 'callback arguments' => array(array(), FALSE),
303 'description' => 'Filter by extension type. Choices: module, theme.',
304 'example-value' => 'module',
307 '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").',
308 'example-value' => 'disabled',
310 'package' => 'Filter by project packages. You can use multiple comma separated values. (i.e. --package="Core - required,Other").',
311 'core' => 'Filter out extensions that are not in drupal core.',
312 'no-core' => 'Filter out extensions that are provided by drupal core.',
314 'outputformat' => array(
315 'default' => 'table',
316 'pipe-format' => 'list',
317 'field-labels' => array('package' => 'Package', 'name' => 'Name', 'type' => 'Type', 'status' => 'Status', 'version' => 'Version'),
318 'output-data-type' => 'format-table',
320 'aliases' => array('pml'),
322 $items['pm-refresh'] = array(
323 'description' => 'Refresh update status information.',
325 'update_status' => array(
326 'add-options-to-command' => FALSE,
329 'aliases' => array('rf'),
331 $items['pm-updatestatus'] = array(
332 'description' => 'Show a report of available minor updates to Drupal core and contrib projects.',
333 'arguments' => array(
334 'projects' => 'Optional. A list of installed projects to show.',
337 'pipe' => 'Return a list of the projects with any extensions enabled that need updating, one project per line.',
339 'sub-options' => $update_suboptions,
343 'outputformat' => array(
344 'default' => 'table',
345 'pipe-format' => 'list',
346 'field-labels' => array('name' => 'Short Name', 'label' => 'Name', 'existing_version' => 'Installed Version', 'status' => 'Status', 'status_msg' => 'Message', 'candidate_version' => 'Proposed version'),
347 'fields-default' => array('label', 'existing_version', 'candidate_version', 'status_msg' ),
348 'fields-pipe' => array('name', 'existing_version', 'candidate_version', 'status_msg'),
349 'output-data-type' => 'format-table',
351 'aliases' => array('ups'),
353 $items['pm-updatecode'] = array(
354 'description' => 'Update Drupal core and contrib projects to latest recommended releases.',
356 'drush pm-updatecode --no-core' => 'Update contrib projects, but skip core.',
357 '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.',
359 'arguments' => array(
360 'projects' => 'Optional. A list of installed projects to update.',
363 'notes' => 'Show release notes for each project to be updated.',
364 'no-core' => 'Only update modules and skip the core update.',
365 'check-updatedb' => 'Check to see if an updatedb is needed after updating the code. Default is on; use --check-updatedb=0 to disable.',
367 'sub-options' => $update_suboptions,
368 'aliases' => array('upc'),
369 'topics' => array('docs-policy'),
373 'release_info' => array(
374 'add-options-to-command' => FALSE,
379 // Merge all items from above.
380 $items['pm-update'] = array(
381 'description' => 'Update Drupal core and contrib projects and apply any pending database updates (Same as pm-updatecode + updatedb).',
382 'aliases' => array('up'),
383 'allow-additional-options' => array('pm-updatecode', 'updatedb'),
385 $items['pm-updatecode-postupdate'] = array(
386 'description' => 'Notify of pending db updates.',
389 $items['pm-releasenotes'] = array(
390 'description' => 'Print release notes for given projects.',
391 'arguments' => array(
392 'projects' => 'A list of project names, with optional version. Defaults to \'drupal\'',
395 'html' => dt('Display release notes in HTML rather than plain text.'),
398 'drush rln cck' => 'Prints the release notes for the recommended version of CCK project.',
399 'drush rln token-1.13' => 'View release notes of a specfic version of the Token project for my version of Drupal.',
400 'drush rln pathauto zen' => 'View release notes for the recommended version of Pathauto and Zen projects.',
402 'aliases' => array('rln'),
403 'bootstrap' => DRUSH_BOOTSTRAP_MAX,
408 $items['pm-releases'] = array(
409 'description' => 'Print release information for given projects.',
410 'arguments' => array(
411 'projects' => 'A list of drupal.org project names. Defaults to \'drupal\'',
414 'drush pm-releases cck zen' => 'View releases for cck and Zen projects for your Drupal version.',
417 'default-major' => 'Show releases compatible with the specified major version of Drupal.',
419 'aliases' => array('rl'),
420 'bootstrap' => DRUSH_BOOTSTRAP_MAX,
421 'outputformat' => array(
422 'default' => 'table',
423 'pipe-format' => 'csv',
424 'field-labels' => array(
425 'project' => 'Project',
426 'version' => 'Release',
428 'status' => 'Status',
429 'release_link' => 'Release link',
430 'download_link' => 'Download link',
432 'fields-default' => array('project', 'version', 'date', 'status'),
433 'fields-pipe' => array('project', 'version', 'date', 'status'),
434 'output-data-type' => 'format-table',
440 $items['pm-download'] = array(
441 'description' => 'Download projects from drupal.org or other sources.',
443 'drush dl drupal' => 'Download latest recommended release of Drupal core.',
444 'drush dl drupal-7.x' => 'Download latest 7.x development version of Drupal core.',
445 'drush dl drupal-6' => 'Download latest recommended release of Drupal 6.x.',
446 'drush dl cck zen' => 'Download latest versions of CCK and Zen projects.',
447 'drush dl og-1.3' => 'Download a specfic version of Organic groups module for my version of Drupal.',
448 'drush dl diff-6.x-2.x' => 'Download a specific development branch of diff module for a specific Drupal version.',
449 'drush dl views --select' => 'Show a list of recent releases of the views project, prompt for which one to download.',
450 'drush dl webform --dev' => 'Download the latest dev release of webform.',
451 'drush dl webform --cache' => 'Download webform. Fetch and populate the download cache as needed.',
453 'arguments' => array(
454 'projects' => 'A comma delimited list of drupal.org project names, with optional version. Defaults to \'drupal\'',
457 'destination' => array(
458 '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).',
459 'example-value' => 'path',
461 '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.',
462 'notes' => 'Show release notes after each project is downloaded.',
464 'description' => "Only useful for install profiles. Possible values: 'full', 'projects', 'profile-only'.",
465 'example-value' => 'full',
467 'select' => "Select the version to download interactively from a list of available releases.",
468 'drupal-project-rename' => 'Alternate name for "drupal-x.y" directory when downloading Drupal project. Defaults to "drupal".',
469 'default-major' => array(
470 'description' => 'Specify the default major version of modules to download when there is no bootstrapped Drupal site. Defaults to "8".',
471 'example-value' => '7',
473 'skip' => 'Skip automatic downloading of libraries (c.f. devel).',
474 'pipe' => 'Returns a list of the names of the extensions (modules and themes) contained in the downloaded projects.',
476 'bootstrap' => DRUSH_BOOTSTRAP_MAX,
477 'aliases' => array('dl'),
488 * @defgroup extensions Extensions management.
490 * Functions to manage extensions.
494 * Command argument complete callback.
496 function pm_pm_enable_complete() {
497 return pm_complete_extensions();
501 * Command argument complete callback.
503 function pm_pm_disable_complete() {
504 return pm_complete_extensions();
508 * Command argument complete callback.
510 function pm_pm_uninstall_complete() {
511 return pm_complete_extensions();
515 * Command argument complete callback.
517 function pm_pm_info_complete() {
518 return pm_complete_extensions();
522 * Command argument complete callback.
524 function pm_pm_releasenotes_complete() {
525 return pm_complete_projects();
529 * Command argument complete callback.
531 function pm_pm_releases_complete() {
532 return pm_complete_projects();
536 * Command argument complete callback.
538 function pm_pm_updatecode_complete() {
539 return pm_complete_projects();
543 * Command argument complete callback.
545 function pm_pm_update_complete() {
546 return pm_complete_projects();
550 * List extensions for completion.
553 * Array of available extensions.
555 function pm_complete_extensions() {
556 if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) {
557 $extension_info = drush_get_extensions(FALSE);
558 return array('values' => array_keys($extension_info));
563 * List projects for completion.
566 * Array of installed projects.
568 function pm_complete_projects() {
569 if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) {
570 return array('values' => array_keys(drush_get_projects()));
575 * Sort callback function for sorting extensions.
577 * It will sort first by type, second by package and third by name.
579 function _drush_pm_sort_extensions($a, $b) {
580 $a_type = drush_extension_get_type($a);
581 $b_type = drush_extension_get_type($b);
582 if ($a_type == 'module' && $b_type == 'theme') {
585 if ($a_type == 'theme' && $b_type == 'module') {
588 $cmp = strcasecmp($a->info['package'], $b->info['package']);
590 $cmp = strcasecmp($a->info['name'], $b->info['name']);
596 * Calculate an extension status based on current status and schema version.
599 * Object of a single extension info.
602 * String describing extension status. Values: enabled|disabled|not installed
604 function drush_get_extension_status($extension) {
605 if ((drush_extension_get_type($extension) == 'module') && ($extension->schema_version == -1)) {
606 $status = "not installed";
609 $status = ($extension->status == 1)?'enabled':'disabled';
616 * Classify extensions as modules, themes or unknown.
619 * Array of extension names, by reference.
621 * Empty array to be filled with modules in the provided extension list.
623 * Empty array to be filled with themes in the provided extension list.
625 function drush_pm_classify_extensions(&$extensions, &$modules, &$themes, $extension_info) {
626 _drush_pm_expand_extensions($extensions, $extension_info);
627 foreach ($extensions as $extension) {
628 if (!isset($extension_info[$extension])) {
631 $type = drush_extension_get_type($extension_info[$extension]);
632 if ($type == 'module') {
633 $modules[$extension] = $extension;
635 else if ($type == 'theme') {
636 $themes[$extension] = $extension;
642 * Obtain an array of installed projects off the extensions available.
644 * A project is considered to be 'enabled' when any of its extensions is
646 * If any extension lacks project information and it is found that the
647 * extension was obtained from drupal.org's cvs or git repositories, a new
648 * 'vcs' attribute will be set on the extension. Example:
649 * $extensions[name]->vcs = 'cvs';
651 * @param array $extensions
652 * Array of extensions as returned by drush_get_extensions().
655 * Array of installed projects with info of version, status and provided
658 function drush_get_projects(&$extensions = NULL) {
659 if (!isset($extensions)) {
660 $extensions = drush_get_extensions();
665 'version' => drush_drupal_version(),
667 'extensions' => array(),
670 if (isset($extensions['system']->info['datestamp'])) {
671 $projects['drupal']['datestamp'] = $extensions['system']->info['datestamp'];
673 foreach ($extensions as $extension) {
674 $extension_name = drush_extension_get_name($extension);
675 $extension_path = drush_extension_get_path($extension);
677 // Obtain the project name. It is not available in this cases:
678 // 1. the extension is part of drupal core.
679 // 2. the project was checked out from CVS/git and cvs_deploy/git_deploy
681 // 3. it is not a project hosted in drupal.org.
682 if (empty($extension->info['project'])) {
683 if (isset($extension->info['version']) && ($extension->info['version'] == drush_drupal_version())) {
687 if (is_dir($extension_path . '/CVS') && (!drush_module_exists('cvs_deploy'))) {
688 $extension->vcs = 'cvs';
689 drush_log(dt('Extension !extension is fetched from cvs. Ignoring.', array('!extension' => $extension_name)), LogLevel::DEBUG);
691 elseif (is_dir($extension_path . '/.git') && (!drush_module_exists('git_deploy'))) {
692 $extension->vcs = 'git';
693 drush_log(dt('Extension !extension is fetched from git. Ignoring.', array('!extension' => $extension_name)), LogLevel::DEBUG);
699 $project = $extension->info['project'];
702 // Create/update the project in $projects with the project data.
703 if (!isset($projects[$project])) {
704 $projects[$project] = array(
705 // If there's an extension with matching name, pick its label.
706 // Otherwise use just the project name. We avoid $extension->label
707 // for the project label because the extension's label may have
708 // no direct relation with the project name. For example,
709 // "Text (text)" or "Number (number)" for the CCK project.
710 'label' => isset($extensions[$project]) ? $extensions[$project]->label : $project,
711 'type' => drush_extension_get_type($extension),
712 'version' => $extension->info['version'],
713 'status' => $extension->status,
714 'extensions' => array(),
716 if (isset($extension->info['datestamp'])) {
717 $projects[$project]['datestamp'] = $extension->info['datestamp'];
719 if (isset($extension->info['project status url'])) {
720 $projects[$project]['status url'] = $extension->info['project status url'];
724 // If any of the extensions is enabled, consider the project is enabled.
725 if ($extension->status != 0) {
726 $projects[$project]['status'] = $extension->status;
729 $projects[$project]['extensions'][] = drush_extension_get_name($extension);
732 // Obtain each project's path and try to provide a better label for ones
733 // with machine name.
734 $reserved = array('modules', 'sites', 'themes');
735 foreach ($projects as $name => $project) {
736 if ($name == 'drupal') {
740 // If this project has no human label, see if we can find
741 // one "main" extension whose label we could use.
742 if ($project['label'] == $name) {
743 // If there is only one extension, construct a label based on
744 // the extension name.
745 if (count($project['extensions']) == 1) {
746 $extension = $extensions[$project['extensions'][0]];
747 $projects[$name]['label'] = $extension->info['name'] . ' (' . $name . ')';
750 // Make a list of all of the extensions in this project
751 // that do not depend on any other extension in this
753 $candidates = array();
754 foreach ($project['extensions'] as $e) {
755 $has_project_dependency = FALSE;
756 if (isset($extensions[$e]->info['dependencies']) && is_array($extensions[$e]->info['dependencies'])) {
757 foreach ($extensions[$e]->info['dependencies'] as $dependent) {
758 if (in_array($dependent, $project['extensions'])) {
759 $has_project_dependency = TRUE;
763 if ($has_project_dependency === FALSE) {
764 $candidates[] = $extensions[$e]->info['name'];
767 // If only one of the modules is a candidate, use its name in the label
768 if (count($candidates) == 1) {
769 $projects[$name]['label'] = reset($candidates) . ' (' . $name . ')';
774 drush_log(dt('Obtaining !project project path.', array('!project' => $name)), LogLevel::DEBUG);
775 $path = _drush_pm_find_common_path($project['type'], $project['extensions']);
776 // Prevent from setting a reserved path. For example it may happen in a case
777 // where a module and a theme are declared as part of a same project.
778 // There's a special case, a project called "sites", this is the reason for
779 // the second condition here.
780 if ($path == '.' || (in_array(basename($path), $reserved) && !in_array($name, $reserved))) {
781 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);
784 $projects[$name]['path'] = $path;
792 * Helper function to find the common path for a list of extensions in the aim to obtain the project name.
794 * @param $project_type
795 * Type of project we're trying to find. Valid values: module, theme.
797 * Array of extension names.
799 function _drush_pm_find_common_path($project_type, $extensions) {
800 // Select the first path as the candidate to be the common prefix.
801 $extension = array_pop($extensions);
802 while (!($path = drupal_get_path($project_type, $extension))) {
803 drush_log(dt('Unknown path for !extension !type.', array('!extension' => $extension, '!type' => $project_type)), LogLevel::WARNING);
804 $extension = array_pop($extensions);
807 // If there's only one extension we are done. Otherwise, we need to find
808 // the common prefix for all of them.
809 if (count($extensions) > 0) {
810 // Iterate over the other projects.
811 while($extension = array_pop($extensions)) {
812 $path2 = drupal_get_path($project_type, $extension);
814 drush_log(dt('Unknown path for !extension !type.', array('!extension' => $extension, '!type' => $project_type)), LogLevel::DEBUG);
817 // Option 1: same path.
818 if ($path == $path2) {
821 // Option 2: $path is a prefix of $path2.
822 if (strpos($path2, $path) === 0) {
825 // Option 3: $path2 is a prefix of $path.
826 if (strpos($path, $path2) === 0) {
830 // Option 4: no one is a prefix of the other. Find the common
831 // prefix by iteratively strip the rigthtmost piece of $path.
832 // We will iterate until a prefix is found or path = '.', that on the
833 // other hand is a condition theorically impossible to reach.
835 $path = dirname($path);
836 if (strpos($path2, $path) === 0) {
839 } while ($path != '.');
847 * @} End of "defgroup extensions".
851 * Command callback. Show a list of extensions with type and status.
853 function drush_pm_list() {
855 $package_filter = array();
856 $package = strtolower(drush_get_option('package'));
857 if (!empty($package)) {
858 $package_filter = explode(',', $package);
860 if (!empty($package_filter) && (count($package_filter) == 1)) {
861 drush_hide_output_fields('package');
865 $all_types = array('module', 'theme');
866 $type_filter = strtolower(drush_get_option('type'));
867 if (!empty($type_filter)) {
868 $type_filter = explode(',', $type_filter);
871 $type_filter = $all_types;
874 if (count($type_filter) == 1) {
875 drush_hide_output_fields('type');
877 foreach ($type_filter as $type) {
878 if (!in_array($type, $all_types)) { //TODO: this kind of check can be implemented drush-wide
879 return drush_set_error('DRUSH_PM_INVALID_PROJECT_TYPE', dt('!type is not a valid project type.', array('!type' => $type)));
884 $all_status = array('enabled', 'disabled', 'not installed');
885 $status_filter = strtolower(drush_get_option('status'));
886 if (!empty($status_filter)) {
887 $status_filter = explode(',', $status_filter);
890 $status_filter = $all_status;
892 if (count($status_filter) == 1) {
893 drush_hide_output_fields('status');
896 foreach ($status_filter as $status) {
897 if (!in_array($status, $all_status)) { //TODO: this kind of check can be implemented drush-wide
898 return drush_set_error('DRUSH_PM_INVALID_PROJECT_STATUS', dt('!status is not a valid project status.', array('!status' => $status)));
903 $extension_info = drush_get_extensions(FALSE);
904 uasort($extension_info, '_drush_pm_sort_extensions');
906 $major_version = drush_drupal_major_version();
907 foreach ($extension_info as $key => $extension) {
908 if (!in_array(drush_extension_get_type($extension), $type_filter)) {
909 unset($extension_info[$key]);
912 $status = drush_get_extension_status($extension);
913 if (!in_array($status, $status_filter)) {
914 unset($extension_info[$key]);
918 // Filter out core if --no-core specified.
919 if (drush_get_option('no-core', FALSE)) {
920 if ((($major_version >= 8) && ($extension->origin == 'core')) || (($major_version <= 7) && (strpos($extension->info['package'], 'Core') === 0))) {
921 unset($extension_info[$key]);
926 // Filter out non-core if --core specified.
927 if (drush_get_option('core', FALSE)) {
928 if ((($major_version >= 8) && ($extension->origin != 'core')) || (($major_version <= 7) && (strpos($extension->info['package'], 'Core') !== 0))) {
929 unset($extension_info[$key]);
934 // Filter by package.
935 if (!empty($package_filter)) {
936 if (!in_array(strtolower($extension->info['package']), $package_filter)) {
937 unset($extension_info[$key]);
942 $row['package'] = $extension->info['package'];
943 $row['name'] = $extension->label;
944 $row['type'] = ucfirst(drush_extension_get_type($extension));
945 $row['status'] = ucfirst($status);
946 // Suppress notice when version is not present.
947 $row['version'] = @$extension->info['version'];
949 $result[$key] = $row;
952 // In Drush-5, we used to return $extension_info here.
957 * Helper function for pm-enable.
959 function drush_pm_enable_find_project_from_extension($extension) {
960 $result = drush_pm_lookup_extension_in_cache($extension);
962 if (!isset($result)) {
963 $release_info = drush_get_engine('release_info');
965 // If we can find info on a project that has the same name
966 // as the requested extension, then we'll call that a match.
967 $request = pm_parse_request($extension);
968 if ($release_info->checkProject($request)) {
969 $result = $extension;
977 * Validate callback. Determine the modules and themes that the user would like enabled.
979 function drush_pm_enable_validate() {
980 $args = pm_parse_arguments(func_get_args());
982 $extension_info = drush_get_extensions();
988 // Classify $args in themes, modules or unknown.
992 drush_pm_classify_extensions($args, $modules, $themes, $extension_info);
993 $extensions = array_merge($modules, $themes);
994 $unknown = array_diff($args, $extensions);
996 // If there're unknown extensions, try and download projects
997 // with matching names.
998 if (!empty($unknown)) {
1000 foreach ($unknown as $name) {
1001 drush_log(dt('!extension was not found.', array('!extension' => $name)), LogLevel::WARNING);
1002 $project = drush_pm_enable_find_project_from_extension($name);
1003 if (!empty($project)) {
1004 $found[] = $project;
1007 if (!empty($found)) {
1008 drush_log(dt("The following projects provide some or all of the extensions not found:\n@list", array('@list' => implode("\n", $found))), LogLevel::OK);
1009 if (drush_get_option('resolve-dependencies')) {
1010 drush_log(dt("They are being downloaded."), LogLevel::OK);
1012 if ((drush_get_option('resolve-dependencies')) || (drush_confirm("Would you like to download them?"))) {
1018 // Discard already enabled and incompatible extensions.
1019 foreach ($extensions as $name) {
1020 if ($extension_info[$name]->status) {
1021 drush_log(dt('!extension is already enabled.', array('!extension' => $name)), LogLevel::OK);
1023 // Check if the extension is compatible with Drupal core and php version.
1024 if ($component = drush_extension_check_incompatibility($extension_info[$name])) {
1025 drush_set_error('DRUSH_PM_ENABLE_MODULE_INCOMPATIBLE', dt('!name is incompatible with the !component version.', array('!name' => $name, '!component' => $component)));
1026 if (drush_extension_get_type($extension_info[$name]) == 'module') {
1027 unset($modules[$name]);
1030 unset($themes[$name]);
1035 if (!empty($modules)) {
1036 // Check module dependencies.
1037 $dependencies = drush_check_module_dependencies($modules, $extension_info);
1038 $unmet_dependencies = array();
1039 foreach ($dependencies as $module => $info) {
1040 if (!empty($info['unmet-dependencies'])) {
1041 foreach ($info['unmet-dependencies'] as $unmet) {
1042 $unmet_project = (!empty($info['dependencies'][$unmet]['project'])) ? $info['dependencies'][$unmet]['project'] : drush_pm_enable_find_project_from_extension($unmet);
1043 if (!empty($unmet_project)) {
1044 $unmet_dependencies[$module][$unmet_project] = $unmet_project;
1049 if (!empty($unmet_dependencies)) {
1051 $unmet_project_list = array();
1052 foreach ($unmet_dependencies as $module => $unmet_projects) {
1053 $unmet_project_list = array_merge($unmet_project_list, $unmet_projects);
1054 $msgs[] = dt("!module requires !unmet-projects", array('!unmet-projects' => implode(', ', $unmet_projects), '!module' => $module));
1056 drush_log(dt("The following projects have unmet dependencies:\n!list", array('!list' => implode("\n", $msgs))), LogLevel::OK);
1057 if (drush_get_option('resolve-dependencies')) {
1058 drush_log(dt("They are being downloaded."), LogLevel::OK);
1060 if (drush_get_option('resolve-dependencies') || drush_confirm(dt("Would you like to download them?"))) {
1061 $download = array_merge($download, $unmet_project_list);
1066 if (!empty($download)) {
1067 // Disable DRUSH_AFFIRMATIVE context temporarily.
1068 $drush_affirmative = drush_get_context('DRUSH_AFFIRMATIVE');
1069 drush_set_context('DRUSH_AFFIRMATIVE', FALSE);
1070 // Invoke a new process to download dependencies.
1071 $result = drush_invoke_process('@self', 'pm-download', $download, array(), array('interactive' => TRUE));
1072 // Restore DRUSH_AFFIRMATIVE context.
1073 drush_set_context('DRUSH_AFFIRMATIVE', $drush_affirmative);
1074 // Refresh module cache after downloading the new modules.
1075 if (drush_drupal_major_version() >= 8) {
1076 \Drush\Drupal\ExtensionDiscovery::reset();
1077 system_list_reset();
1079 $extension_info = drush_get_extensions();
1084 if (!empty($modules)) {
1085 $all_dependencies = array();
1086 $dependencies_ok = TRUE;
1087 foreach ($dependencies as $key => $info) {
1088 if (isset($info['error'])) {
1089 unset($modules[$key]);
1090 $dependencies_ok = drush_set_error($info['error']['code'], $info['error']['message']);
1092 elseif (!empty($info['dependencies'])) {
1093 // Make sure we have an assoc array.
1094 $dependencies_list = array_keys($info['dependencies']);
1095 $assoc = array_combine($dependencies_list, $dependencies_list);
1096 $all_dependencies = array_merge($all_dependencies, $assoc);
1099 if (!$dependencies_ok) {
1102 $modules = array_diff(array_merge($modules, $all_dependencies), drush_module_list());
1103 // Discard modules which doesn't meet requirements.
1104 require_once DRUSH_DRUPAL_CORE . '/includes/install.inc';
1105 foreach ($modules as $key => $module) {
1106 // Check to see if the module can be installed/enabled (hook_requirements).
1107 // See @system_modules_submit
1108 if (!drupal_check_module($module)) {
1109 unset($modules[$key]);
1110 drush_set_error('DRUSH_PM_ENABLE_MODULE_UNMEET_REQUIREMENTS', dt('Module !module doesn\'t meet the requirements to be enabled.', array('!module' => $module)));
1111 _drush_log_drupal_messages();
1117 $searchpath = array();
1118 foreach (array_merge($modules, $themes) as $name) {
1119 $searchpath[] = drush_extension_get_path($extension_info[$name]);
1121 // Add all modules that passed validation to the drush
1122 // list of commandfiles (if they have any). This
1123 // will allow these newly-enabled modules to participate
1124 // in the pre-pm_enable and post-pm_enable hooks.
1125 if (!empty($searchpath)) {
1126 _drush_add_commandfiles($searchpath);
1129 drush_set_context('PM_ENABLE_EXTENSION_INFO', $extension_info);
1130 drush_set_context('PM_ENABLE_MODULES', $modules);
1131 drush_set_context('PM_ENABLE_THEMES', $themes);
1137 * Command callback. Enable one or more extensions from downloaded projects.
1138 * Note that the modules and themes to be enabled were evaluated during the
1139 * pm-enable validate hook, above.
1141 function drush_pm_enable() {
1142 // Get the data built during the validate phase
1143 $extension_info = drush_get_context('PM_ENABLE_EXTENSION_INFO');
1144 $modules = drush_get_context('PM_ENABLE_MODULES');
1145 $themes = drush_get_context('PM_ENABLE_THEMES');
1147 // Inform the user which extensions will finally be enabled.
1148 $extensions = array_merge($modules, $themes);
1149 if (empty($extensions)) {
1150 return drush_log(dt('There were no extensions that could be enabled.'), LogLevel::OK);
1153 drush_print(dt('The following extensions will be enabled: !extensions', array('!extensions' => implode(', ', $extensions))));
1154 if(!drush_confirm(dt('Do you really want to continue?'))) {
1155 return drush_user_abort();
1160 if (!empty($themes)) {
1161 drush_theme_enable($themes);
1164 // Enable modules and pass dependency validation in form submit.
1165 if (!empty($modules)) {
1166 drush_include_engine('drupal', 'environment');
1167 drush_module_enable($modules);
1170 // Inform the user of final status.
1171 $result_extensions = drush_get_named_extensions_list($extensions);
1172 $problem_extensions = array();
1173 $role = drush_role_get_class();
1174 foreach ($result_extensions as $name => $extension) {
1175 if ($extension->status) {
1176 drush_log(dt('!extension was enabled successfully.', array('!extension' => $name)), LogLevel::OK);
1177 $perms = $role->getModulePerms($name);
1178 if (!empty($perms)) {
1179 drush_print(dt('!extension defines the following permissions: !perms', array('!extension' => $name, '!perms' => implode(', ', $perms))));
1183 $problem_extensions[] = $name;
1186 if (!empty($problem_extensions)) {
1187 return drush_set_error('DRUSH_PM_ENABLE_EXTENSION_ISSUE', dt('There was a problem enabling !extension.', array('!extension' => implode(',', $problem_extensions))));
1189 // Return the list of extensions enabled
1194 * Command callback. Disable one or more extensions.
1196 function drush_pm_disable() {
1197 $args = pm_parse_arguments(func_get_args());
1198 drush_include_engine('drupal', 'pm');
1199 _drush_pm_disable($args);
1203 * Add extensions that match extension_name*.
1205 * A helper function for commands that take a space separated list of extension
1206 * names. It will identify extensions that have been passed in with a
1207 * trailing * and add all matching extensions to the array that is returned.
1209 * @param $extensions
1210 * An array of extensions, by reference.
1211 * @param $extension_info
1212 * Optional. An array of extension info as returned by drush_get_extensions().
1214 function _drush_pm_expand_extensions(&$extensions, $extension_info = array()) {
1215 if (empty($extension_info)) {
1216 $extension_info = drush_get_extensions();
1218 foreach ($extensions as $key => $extension) {
1219 if (($wildcard = rtrim($extension, '*')) !== $extension) {
1220 foreach (array_keys($extension_info) as $extension_name) {
1221 if (substr($extension_name, 0, strlen($wildcard)) == $wildcard) {
1222 $extensions[] = $extension_name;
1225 unset($extensions[$key]);
1232 * Command callback. Uninstall one or more modules.
1234 function drush_pm_uninstall() {
1235 $args = pm_parse_arguments(func_get_args());
1236 drush_include_engine('drupal', 'pm');
1237 _drush_pm_uninstall($args);
1241 * Command callback. Show available releases for given project(s).
1243 function drush_pm_releases() {
1244 $release_info = drush_get_engine('release_info');
1247 $requests = pm_parse_arguments(func_get_args(), FALSE);
1249 $requests = array('drupal');
1252 // Get installed projects.
1253 if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) {
1254 $projects = drush_get_projects();
1257 $projects = array();
1260 // Select the filter to apply based on cli options.
1261 if (drush_get_option('dev', FALSE)) {
1264 elseif (drush_get_option('all', FALSE)) {
1271 $status_url = drush_get_option('source');
1274 foreach ($requests as $request) {
1275 $request = pm_parse_request($request, $status_url, $projects);
1276 $project_name = $request['name'];
1277 $project_release_info = $release_info->get($request);
1278 if ($project_release_info) {
1279 $version = isset($projects[$project_name]) ? $projects[$project_name]['version'] : NULL;
1280 $releases = $project_release_info->filterReleases($filter, $version);
1281 foreach ($releases as $key => $release) {
1282 $output["${project_name}-${key}"] = array(
1283 'project' => $project_name,
1284 'version' => $release['version'],
1285 'date' => gmdate('Y-M-d', $release['date']),
1286 'status' => implode(', ', $release['release_status']),
1291 if (empty($output)) {
1292 return drush_log(dt('No valid projects given.'), LogLevel::OK);
1299 * Command callback. Show release notes for given project(s).
1301 function drush_pm_releasenotes() {
1302 $release_info = drush_get_engine('release_info');
1305 if (!$requests = pm_parse_arguments(func_get_args(), FALSE)) {
1306 $requests = array('drupal');
1309 // Get installed projects.
1310 if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) {
1311 $projects = drush_get_projects();
1314 $projects = array();
1317 $status_url = drush_get_option('source');
1320 foreach($requests as $request) {
1321 $request = pm_parse_request($request, $status_url, $projects);
1322 $project_release_info = $release_info->get($request);
1323 if ($project_release_info) {
1324 $version = empty($request['version']) ? NULL : $request['version'];
1325 $output .= $project_release_info->getReleaseNotes($version);
1332 * Command callback. Refresh update status information.
1334 function drush_pm_refresh() {
1335 $update_status = drush_get_engine('update_status');
1336 drush_print(dt("Refreshing update status information ..."));
1337 $update_status->refresh();
1338 drush_print(dt("Done."));
1342 * Command callback. Execute pm-update.
1344 function drush_pm_update() {
1345 // Call pm-updatecode. updatedb will be called in the post-update process.
1346 $args = pm_parse_arguments(func_get_args(), FALSE);
1347 drush_set_option('check-updatedb', FALSE);
1348 return drush_invoke('pm-updatecode', $args);
1352 * Post-command callback.
1353 * Execute updatedb command after an updatecode - user requested `update`.
1355 function drush_pm_post_pm_update() {
1356 // Use drush_invoke_process to start a subprocess. Cleaner that way.
1357 if (drush_get_context('DRUSH_PM_UPDATED', FALSE) !== FALSE) {
1358 drush_invoke_process('@self', 'updatedb');
1363 * Validate callback for updatecode command. Abort if 'backup' directory exists.
1365 function drush_pm_updatecode_validate() {
1366 $path = drush_get_context('DRUSH_DRUPAL_ROOT') . '/backup';
1367 if (is_dir($path) && (realpath(drush_get_option('backup-dir', FALSE)) != $path)) {
1368 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)));
1373 * Post-command callback for updatecode.
1375 * Execute pm-updatecode-postupdate in a backend process to not conflict with
1376 * old code already in memory.
1378 function drush_pm_post_pm_updatecode() {
1379 // Skip if updatecode was invoked by pm-update.
1380 // This way we avoid being noisy, as updatedb is to be executed.
1381 if (drush_get_option('check-updatedb', TRUE)) {
1382 if (drush_get_context('DRUSH_PM_UPDATED', FALSE)) {
1383 drush_invoke_process('@self', 'pm-updatecode-postupdate');
1389 * Command callback. Execute updatecode-postupdate.
1391 function drush_pm_updatecode_postupdate() {
1392 // Clear the cache, since some projects could have moved around.
1393 drush_drupal_cache_clear_all();
1395 // Notify of pending database updates.
1396 // Make sure the installation API is available
1397 require_once DRUSH_DRUPAL_CORE . '/includes/install.inc';
1399 // Load all .install files.
1400 drupal_load_updates();
1402 // @see system_requirements().
1403 foreach (drush_module_list() as $module) {
1404 $updates = drupal_get_schema_versions($module);
1405 if ($updates !== FALSE) {
1406 $default = drupal_get_installed_schema_version($module);
1407 if (max($updates) > $default) {
1408 drush_log(dt("You have pending database updates. Run `drush updatedb` or visit update.php in your browser."), LogLevel::WARNING);
1416 * Sanitize user provided arguments to several pm commands.
1418 * Return an array of arguments off a space and/or comma separated values.
1420 function pm_parse_arguments($args, $dashes_to_underscores = TRUE) {
1421 $arguments = _convert_csv_to_array($args);
1422 foreach ($arguments as $key => $argument) {
1423 $argument = ($dashes_to_underscores) ? strtr($argument, '-', '_') : $argument;
1429 * Decompound a version string and returns major, minor, patch and extra parts.
1431 * @see _pm_parse_version_compound()
1432 * @see pm_parse_version()
1434 * @param string $version
1435 * A version string like X.Y-Z, X.Y.Z-W or a subset.
1438 * Array with major, patch and extra keys.
1440 function _pm_parse_version_decompound($version) {
1441 $pattern = '/^(\d+)(?:.(\d+))?(?:\.(x|\d+))?(?:-([a-z0-9\.-]*))?(?:\+(\d+)-dev)?$/';
1444 preg_match($pattern, $version, $matches);
1453 if (isset($matches[1])) {
1454 $parts['major'] = $matches[1];
1455 if (isset($matches[2])) {
1456 if (isset($matches[3]) && $matches[3] != '') {
1457 $parts['minor'] = $matches[2];
1458 $parts['patch'] = $matches[3];
1461 $parts['patch'] = $matches[2];
1464 if (!empty($matches[4])) {
1465 $parts['extra'] = $matches[4];
1467 if (!empty($matches[5])) {
1468 $parts['offset'] = $matches[5];
1476 * Build a version string from an array of major, minor and extra parts.
1478 * @see _pm_parse_version_decompound()
1479 * @see pm_parse_version()
1481 * @param array $parts
1487 function _pm_parse_version_compound($parts) {
1488 $project_version = '';
1489 if ($parts['patch'] != '') {
1490 $project_version = $parts['major'];
1491 if ($parts['minor'] != '') {
1492 $project_version = $project_version . '.' . $parts['minor'];
1494 if ($parts['patch'] == 'x') {
1495 $project_version = $project_version . '.x-dev';
1498 $project_version = $project_version . '.' . $parts['patch'];
1499 if ($parts['extra'] != '') {
1500 $project_version = $project_version . '-' . $parts['extra'];
1503 if ($parts['offset'] != '') {
1504 $project_version = $project_version . '+' . $parts['offset'] . '-dev';
1508 return $project_version;
1512 * Parses a version string and returns its components.
1514 * It parses both core and contrib version strings.
1516 * Core (semantic versioning):
1517 * - 8.0.0-beta3+252-dev
1524 * Core (classic drupal scheme):
1532 * - 7.x-1.0-beta1+30-dev
1542 * @see pm_parse_request()
1544 * @param string $version
1545 * A core or project version string.
1547 * @param bool $is_core
1548 * Whether this is a core version or a project version.
1551 * Version string in parts.
1552 * Example for a contrib version (ex: 7.x-3.2-beta1):
1553 * - version : Fully qualified version string.
1554 * - drupal_version : Core compatibility version (ex: 7.x).
1555 * - version_major : Major version (ex: 3).
1556 * - version_minor : Minor version. Not applicable. Always empty.
1557 * - version_patch : Patch version (ex: 2).
1558 * - version_extra : Extra version (ex: beta1).
1559 * - project_version : Project specific part of the version (ex: 3.2-beta1).
1561 * Example for a core version (ex: 8.1.2-beta2 or 7.0-beta2):
1562 * - version : Fully qualified version string.
1563 * - drupal_version : Core compatibility version (ex: 8.x).
1564 * - version_major : Major version (ex: 8).
1565 * - version_minor : Minor version (ex: 1). Empty if not a semver.
1566 * - version_patch : Patch version (ex: 2).
1567 * - version_extra : Extra version (ex: beta2).
1568 * - project_version : Same as 'version'.
1570 function pm_parse_version($version, $is_core = FALSE) {
1571 $core_parts = _pm_parse_version_decompound($version);
1573 // If no major version, we have no version at all. Pick a default.
1574 $drupal_version_default = drush_drupal_major_version();
1575 if ($core_parts['major'] == '') {
1576 $core_parts['major'] = ($drupal_version_default) ? $drupal_version_default : drush_get_option('default-major', 8);
1580 $project_version = _pm_parse_version_compound($core_parts);
1581 $version_parts = array(
1582 'version' => $project_version,
1583 'drupal_version' => $core_parts['major'] . '.x',
1584 'project_version' => $project_version,
1585 'version_major' => $core_parts['major'],
1586 'version_minor' => $core_parts['minor'],
1587 'version_patch' => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'],
1588 'version_extra' => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'],
1589 'version_offset' => $core_parts['offset'],
1593 // If something as 7.x-1.0-beta1, the project specific version is
1594 // in $version['extra'] and we need to parse it.
1595 if (strpbrk($core_parts['extra'], '.-')) {
1596 $nocore_parts = _pm_parse_version_decompound($core_parts['extra']);
1597 $nocore_parts['offset'] = $core_parts['offset'];
1598 $project_version = _pm_parse_version_compound($nocore_parts);
1599 $version_parts = array(
1600 'version' => $core_parts['major'] . '.x-' . $project_version,
1601 'drupal_version' => $core_parts['major'] . '.x',
1602 'project_version' => $project_version,
1603 'version_major' => $nocore_parts['major'],
1604 'version_minor' => $core_parts['minor'],
1605 'version_patch' => ($nocore_parts['patch'] == 'x') ? '' : $nocore_parts['patch'],
1606 'version_extra' => ($nocore_parts['patch'] == 'x') ? 'dev' : $nocore_parts['extra'],
1607 'version_offset' => $core_parts['offset'],
1610 // At this point we have half a version and must decide if this is a drupal major or a project.
1612 // If working on a bootstrapped site, core_parts has the project version.
1613 if ($drupal_version_default) {
1614 $project_version = _pm_parse_version_compound($core_parts);
1615 $version = ($project_version) ? $drupal_version_default . '.x-' . $project_version : '';
1616 $version_parts = array(
1617 'version' => $version,
1618 'drupal_version' => $drupal_version_default . '.x',
1619 'project_version' => $project_version,
1620 'version_major' => $core_parts['major'],
1621 'version_minor' => $core_parts['minor'],
1622 'version_patch' => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'],
1623 'version_extra' => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'],
1624 'version_offset' => $core_parts['offset'],
1627 // Not working on a bootstrapped site, core_parts is core version.
1629 $version_parts = array(
1631 'drupal_version' => $core_parts['major'] . '.x',
1632 'project_version' => '',
1633 'version_major' => '',
1634 'version_minor' => '',
1635 'version_patch' => '',
1636 'version_extra' => '',
1637 'version_offset' => '',
1643 return $version_parts;
1647 * Parse out the project name and version and return as a structured array.
1649 * @see pm_parse_version()
1651 * @param string $request_string
1652 * Project name with optional version. Examples: 'ctools-7.x-1.0-beta1'
1655 * Array with all parts of the request info.
1657 function pm_parse_request($request_string, $status_url = NULL, &$projects = array()) {
1658 // Split $request_string in project name and version. Note that hyphens (-)
1659 // are permitted in project names (ex: field-conditional-state).
1660 // We use a regex to split the string. The pattern used matches a string
1661 // starting with hyphen, followed by one or more numbers, any of the valid
1662 // symbols in version strings (.x-) and a catchall for the rest of the
1664 $parts = preg_split('/-(?:([\d+\.x].*))?$/', $request_string, NULL, PREG_SPLIT_DELIM_CAPTURE);
1666 if (count($parts) == 1) {
1667 // No version in the request string.
1668 $project = $request_string;
1672 $project = $parts[0];
1673 $version = $parts[1];
1676 $is_core = ($project == 'drupal');
1679 ) + pm_parse_version($version, $is_core);
1681 // Set the status url if provided or available in project's info file.
1683 $request['status url'] = $status_url;
1685 elseif (!empty($projects[$project]['status url'])) {
1686 $request['status url'] = $projects[$project]['status url'];
1693 * @defgroup engines Engine types
1698 * Implementation of hook_drush_engine_type_info().
1700 function pm_drush_engine_type_info() {
1702 'package_handler' => array(
1703 'option' => 'package-handler',
1704 'description' => 'Determine how to fetch projects from update service.',
1705 'default' => 'wget',
1707 '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.',
1710 'release_info' => array(
1711 'add-options-to-command' => TRUE,
1713 'update_status' => array(
1714 'option' => 'update-backend',
1715 'description' => 'Determine how to fetch update status information.',
1716 'default' => 'drush',
1717 'add-options-to-command' => TRUE,
1719 'update-backend' => 'Backend to obtain available updates.',
1720 'check-disabled' => 'Check for updates of disabled modules and themes.',
1721 'security-only' => 'Only update modules that have security updates available.',
1723 'combine-help' => TRUE,
1725 'version_control' => array(
1726 'option' => 'version-control',
1727 'default' => 'backup',
1728 'description' => 'Integrate with version control systems.',
1734 * Implements hook_drush_engine_ENGINE_TYPE().
1736 * Package handler engine is used by pm-download and
1737 * pm-updatecode commands to determine how to download/checkout
1738 * new projects and acquire updates to projects.
1740 function pm_drush_engine_package_handler() {
1743 'description' => 'Download project packages using wget or curl.',
1745 'no-md5' => 'Skip md5 validation of downloads.',
1748 'git_drupalorg' => array(
1749 'description' => 'Use git.drupal.org to checkout and update projects.',
1751 '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.',
1752 '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.',
1753 'gitcheckoutparams' => 'Add options to the `git checkout` command.',
1754 'gitcloneparams' => 'Add options to the `git clone` command.',
1755 'gitfetchparams' => 'Add options to the `git fetch` command.',
1756 'gitpullparams' => 'Add options to the `git pull` command.',
1757 'gitinfofile' => 'Inject version info into each .info file.',
1759 'sub-options' => array(
1760 'gitsubmodule' => array(
1761 'gitsubmoduleaddparams' => 'Add options to the `git submodule add` command.',
1769 * Implements hook_drush_engine_ENGINE_TYPE().
1771 * Release info engine is used by several pm commands to obtain
1772 * releases info from Drupal's update service or external sources.
1774 function pm_drush_engine_release_info() {
1776 'updatexml' => array(
1777 'description' => 'Drush release info engine for update.drupal.org and compatible services.',
1779 'source' => 'The base URL which provides project release history in XML. Defaults to http://updates.drupal.org/release-history.',
1780 'dev' => 'Work with development releases solely.',
1782 'sub-options' => array(
1784 'cache-duration-releasexml' => 'Expire duration (in seconds) for release XML. Defaults to 86400 (24 hours).',
1787 'all' => 'Shows all available releases instead of a short list of recent releases.',
1790 'class' => 'Drush\UpdateService\ReleaseInfo',
1796 * Implements hook_drush_engine_ENGINE_TYPE().
1798 * Update status engine is used to check available updates for
1799 * the projects in a Drupal site.
1801 function pm_drush_engine_update_status() {
1804 'description' => 'Check available updates with update.module.',
1805 'drupal dependencies' => array('update'),
1806 'class' => 'Drush\UpdateService\StatusInfoDrupal',
1809 'description' => 'Check available updates without update.module.',
1810 'class' => 'Drush\UpdateService\StatusInfoDrush',
1816 * Implements hook_drush_engine_ENGINE_TYPE().
1818 * Integration with VCS in order to easily commit your changes to projects.
1820 function pm_drush_engine_version_control() {
1823 'description' => 'Backup all project files before updates.',
1825 'no-backup' => 'Do not perform backups. WARNING: Will result in non-core files/dirs being deleted (e.g. .git)',
1826 '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.',
1830 'signature' => 'bzr root %s',
1831 'description' => 'Quickly add/remove/commit your project changes to Bazaar.',
1833 'bzrsync' => 'Automatically add new files to the Bazaar repository and remove deleted files. Caution.',
1834 'bzrcommit' => 'Automatically commit changes to Bazaar repository. You must also use the --bzrsync option.',
1836 'sub-options' => array(
1837 'bzrcommit' => array(
1838 'bzrmessage' => 'Override default commit message which is: Drush automatic commit. Project <name> <type> Command: <the drush command line used>',
1841 'examples' => array(
1842 'drush dl cck --version-control=bzr --bzrsync --bzrcommit' => 'Download the cck project and then add it and commit it to Bazaar.'
1846 'signature' => 'svn info %s',
1847 'description' => 'Quickly add/remove/commit your project changes to Subversion.',
1849 'svnsync' => 'Automatically add new files to the SVN repository and remove deleted files. Caution.',
1850 'svncommit' => 'Automatically commit changes to SVN repository. You must also using the --svnsync option.',
1851 'svnstatusparams' => "Add options to the 'svn status' command",
1852 'svnaddparams' => 'Add options to the `svn add` command',
1853 'svnremoveparams' => 'Add options to the `svn remove` command',
1854 'svnrevertparams' => 'Add options to the `svn revert` command',
1855 'svncommitparams' => 'Add options to the `svn commit` command',
1857 'sub-options' => array(
1858 'svncommit' => array(
1859 'svnmessage' => 'Override default commit message which is: Drush automatic commit: <the drush command line used>',
1862 'examples' => array(
1863 'drush [command] cck --svncommitparams=\"--username joe\"' => 'Commit changes as the user \'joe\' (Quotes are required).'
1870 * @} End of "Engine types".
1874 * Interface for version control systems.
1875 * We use a simple object layer because we conceivably need more than one
1878 interface drush_version_control {
1879 function pre_update(&$project);
1880 function rollback($project);
1881 function post_update($project);
1882 function post_download($project);
1883 static function reserved_files();
1887 * A simple factory function that tests for version control systems, in a user
1888 * specified order, and returns the one that appears to be appropriate for a
1889 * specific directory.
1891 function drush_pm_include_version_control($directory = '.') {
1892 $engine_info = drush_get_engines('version_control');
1893 $version_controls = drush_get_option('version-control', FALSE);
1894 // If no version control was given, use a list of defaults.
1895 if (!$version_controls) {
1896 // Backup engine is the last option.
1897 $version_controls = array_reverse(array_keys($engine_info['engines']));
1900 $version_controls = array($version_controls);
1903 // Find the first valid engine in the list, checking signatures if needed.
1905 while (!$engine && count($version_controls)) {
1906 $version_control = array_shift($version_controls);
1907 if (isset($engine_info['engines'][$version_control])) {
1908 if (!empty($engine_info['engines'][$version_control]['signature'])) {
1909 drush_log(dt('Verifying signature for !vcs version control engine.', array('!vcs' => $version_control)), LogLevel::DEBUG);
1910 if (drush_shell_exec($engine_info['engines'][$version_control]['signature'], $directory)) {
1911 $engine = $version_control;
1915 $engine = $version_control;
1920 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)));
1923 $instance = drush_include_engine('version_control', $engine);
1928 * Update the locked status of all of the candidate projects
1931 * @param array &$projects
1932 * The projects array from pm_updatecode. $project['locked'] will
1933 * be set for every file where a persistent lockfile can be found.
1934 * The 'lock' and 'unlock' operations are processed first.
1935 * @param array $projects_to_lock
1936 * A list of projects to create peristent lock files for
1937 * @param array $projects_to_unlock
1938 * A list of projects to clear the persistent lock on
1939 * @param string $lock_message
1940 * The reason the project is being locked; stored in the lockfile.
1943 * A list of projects that are locked.
1945 function drush_pm_update_lock(&$projects, $projects_to_lock, $projects_to_unlock, $lock_message = NULL) {
1946 $locked_result = array();
1948 // Warn about ambiguous lock / unlock values
1949 if ($projects_to_lock == array('1')) {
1950 $projects_to_lock = array();
1951 drush_log(dt('Ignoring --lock with no value.'), LogLevel::WARNING);
1953 if ($projects_to_unlock == array('1')) {
1954 $projects_to_unlock = array();
1955 drush_log(dt('Ignoring --unlock with no value.'), LogLevel::WARNING);
1958 // Log if we are going to lock or unlock anything
1959 if (!empty($projects_to_unlock)) {
1960 drush_log(dt('Unlocking !projects', array('!projects' => implode(',', $projects_to_unlock))), LogLevel::OK);
1962 if (!empty($projects_to_lock)) {
1963 drush_log(dt('Locking !projects', array('!projects' => implode(',', $projects_to_lock))), LogLevel::OK);
1966 $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT');
1967 foreach ($projects as $name => $project) {
1969 if (isset($project['path'])) {
1970 if ($name == 'drupal') {
1971 $lockfile = $drupal_root . '/.drush-lock-update';
1974 $lockfile = $drupal_root . '/' . $project['path'] . '/.drush-lock-update';
1977 // Remove the lock file if the --unlock option was specified
1978 if (((in_array($name, $projects_to_unlock)) || (in_array('all', $projects_to_unlock))) && (file_exists($lockfile))) {
1979 drush_op('unlink', $lockfile);
1982 // Create the lock file if the --lock option was specified
1983 if ((in_array($name, $projects_to_lock)) || (in_array('all', $projects_to_lock))) {
1984 drush_op('file_put_contents', $lockfile, $lock_message != NULL ? $lock_message : "Locked via drush.");
1985 // Note that the project is locked. This will work even if we are simulated,
1986 // or if we get permission denied from the file_put_contents.
1987 // If the lock is -not- simulated or transient, then the lock message will be
1988 // read from the lock file below.
1989 $message = drush_get_context('DRUSH_SIMULATE') ? 'Simulated lock.' : 'Transient lock.';
1992 // If the persistent lock file exists, then mark the project as locked.
1993 if (file_exists($lockfile)) {
1994 $message = trim(file_get_contents($lockfile));
1998 // If there is a message set, then mark the project as locked.
1999 if (isset($message)) {
2000 $projects[$name]['locked'] = !empty($message) ? $message : "Locked.";
2001 $locked_result[$name] = $project;
2005 return $locked_result;
2009 * Returns the path to the extensions cache file.
2011 function _drush_pm_extension_cache_file() {
2012 return drush_get_context('DRUSH_PER_USER_CONFIGURATION') . "/drush-extension-cache.inc";
2016 * Load the extensions cache.
2018 function _drush_pm_get_extension_cache() {
2019 $extension_cache = array();
2020 $cache_file = _drush_pm_extension_cache_file();
2022 if (file_exists($cache_file)) {
2023 include $cache_file;
2025 if (!array_key_exists('extension-map', $extension_cache)) {
2026 $extension_cache['extension-map'] = array();
2028 return $extension_cache;
2032 * Lookup an extension in the extensions cache.
2034 function drush_pm_lookup_extension_in_cache($extension) {
2036 $extension_cache = _drush_pm_get_extension_cache();
2037 if (!empty($extension_cache) && array_key_exists($extension, $extension_cache)) {
2038 $result = $extension_cache[$extension];
2044 * Persists extensions cache.
2046 * #TODO# not implemented.
2048 function drush_pm_put_extension_cache($extension_cache) {
2052 * Store extensions founds within a project in extensions cache.
2054 function drush_pm_cache_project_extensions($project, $found) {
2055 $extension_cache = _drush_pm_get_extension_cache();
2056 foreach($found as $extension) {
2057 // Simple cache does not handle conflicts
2058 // We could keep an array of projects, and count
2059 // how many times each one has been seen...
2060 $extension_cache[$extension] = $project['name'];
2062 drush_pm_put_extension_cache($extension_cache);
2066 * Print out all extensions (modules/themes/profiles) found in specified project.
2068 * Find .info.yml files in the project path and identify modules, themes and
2069 * profiles. It handles two kind of projects: drupal core/profiles and
2071 * It does nothing with theme engine projects.
2073 function drush_pm_extensions_in_project($project) {
2074 // Mask for drush_scan_directory, to match .info.yml files.
2075 $mask = $project['drupal_version'][0] >= 8 ? '/(.*)\.info\.yml$/' : '/(.*)\.info$/';
2077 // Mask for drush_scan_directory, to avoid tests directories.
2078 $nomask = array('.', '..', 'CVS', 'tests');
2080 // Drupal core and profiles can contain modules, themes and profiles.
2081 if (in_array($project['project_type'], array('core', 'profile'))) {
2082 $found = array('profile' => array(), 'theme' => array(), 'module' => array());
2083 // Find all of the .info files
2084 foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) {
2085 // Extract extension name from filename.
2087 preg_match($mask, $info->basename, $matches);
2088 $name = $matches[1];
2090 // Find the project type corresponding the .info file.
2091 // (Only drupal >=7.x has .info for .profile)
2092 $base = dirname($filename) . '/' . $name;
2093 if (is_file($base . '.module')) {
2094 $found['module'][] = $name;
2096 else if (is_file($base . '.profile')) {
2097 $found['profile'][] = $name;
2100 $found['theme'][] = $name;
2103 // Special case: find profiles for drupal < 7.x (no .info)
2104 if ($project['drupal_version'][0] < 7) {
2105 foreach (drush_find_profiles($project['full_project_path']) as $filename => $info) {
2106 $found['profile'][] = $info->name;
2110 $msg = "Project !project contains:\n";
2111 $args = array('!project' => $project['name']);
2112 foreach (array_keys($found) as $type) {
2113 if ($count = count($found[$type])) {
2114 $msg .= " - !count_$type !type_$type: !found_$type\n";
2115 $args += array("!count_$type" => $count, "!type_$type" => $type, "!found_$type" => implode(', ', $found[$type]));
2117 $args["!type_$type"] = $type.'s';
2121 drush_log(dt($msg, $args), LogLevel::SUCCESS);
2122 drush_print_pipe(call_user_func_array('array_merge', array_values($found)));
2124 // Modules and themes can only contain other extensions of the same type.
2125 elseif (in_array($project['project_type'], array('module', 'theme'))) {
2127 foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) {
2128 // Extract extension name from filename.
2130 preg_match($mask, $info->basename, $matches);
2131 $found[] = $matches[1];
2133 // If there is only one module / theme in the project, only print out
2134 // the message if is different than the project name.
2135 if (count($found) == 1) {
2136 if ($found[0] != $project['name']) {
2137 $msg = "Project !project contains a !type named !found.";
2140 // If there are multiple modules or themes in the project, list them all.
2142 $msg = "Project !project contains !count !types: !found.";
2145 drush_print(dt($msg, array('!project' => $project['name'], '!count' => count($found), '!type' => $project['project_type'], '!found' => implode(', ', $found))));
2147 drush_print_pipe($found);
2149 drush_pm_cache_project_extensions($project, $found);
2154 * Return an array of empty directories.
2156 * Walk a directory and return an array of subdirectories that are empty. Will
2157 * return the given directory if it's empty.
2158 * If a list of items to exclude is provided, subdirectories will be condidered
2159 * empty even if they include any of the items in the list.
2161 * @param string $dir
2162 * Path to the directory to work in.
2163 * @param array $exclude
2164 * Array of files or directory to exclude in the check.
2167 * A list of directory paths that are empty. A directory is deemed to be empty
2168 * if it only contains excluded files or directories.
2170 function drush_find_empty_directories($dir, $exclude = array()) {
2172 if (!is_dir($dir)) {
2175 $to_exclude = array_merge(array('.', '..'), $exclude);
2176 $empty_dirs = array();
2177 $dir_is_empty = TRUE;
2178 foreach (scandir($dir) as $file) {
2179 // Skip excluded directories.
2180 if (in_array($file, $to_exclude)) {
2183 // Recurse into sub-directories to find potentially empty ones.
2184 $subdir = $dir . '/' . $file;
2185 $empty_dirs += drush_find_empty_directories($subdir, $exclude);
2186 // $empty_dir will not contain $subdir, if it is a file or if the
2187 // sub-directory is not empty. $subdir is only set if it is empty.
2188 if (!isset($empty_dirs[$subdir])) {
2189 $dir_is_empty = FALSE;
2193 if ($dir_is_empty) {
2194 $empty_dirs[$dir] = $dir;
2200 * Inject metadata into all .info files for a given project.
2202 * @param string $project_dir
2203 * The full path to the root directory of the project to operate on.
2204 * @param string $project_name
2205 * The project machine name (AKA shortname).
2206 * @param string $version
2207 * The version string to inject into the .info file(s).
2208 * @param int $datestamp
2209 * The datestamp of the last commit.
2212 * TRUE on success, FALSE on any failures appending data to .info files.
2214 function drush_pm_inject_info_file_metadata($project_dir, $project_name, $version, $datestamp) {
2215 // `drush_drupal_major_version()` cannot be used here because this may be running
2216 // outside of a Drupal context.
2217 $yaml_format = substr($version, 0, 1) >= 8;
2218 $pattern = preg_quote($yaml_format ? '.info.yml' : '.info');
2219 $info_files = drush_scan_directory($project_dir, '/.*' . $pattern . '$/');
2220 if (!empty($info_files)) {
2221 // Construct the string of metadata to append to all the .info files.
2223 $info = _drush_pm_generate_info_yaml_metadata($version, $project_name, $datestamp);
2226 $info = _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp);
2228 foreach ($info_files as $info_file) {
2229 if (!drush_file_append_data($info_file->filename, $info)) {
2238 * Generate version information for `.info` files in ini format.
2240 * Taken with some modifications from:
2241 * http://drupalcode.org/project/drupalorg.git/blob/refs/heads/6.x-3.x:/drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php#l192
2243 function _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp) {
2246 if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) {
2247 $extra .= "\ncore = \"$matches[1]\"";
2249 if (!drush_get_option('no-gitprojectinfo', FALSE)) {
2250 $extra = "\nproject = \"$project_name\"";
2252 $date = date('Y-m-d', $datestamp);
2255 ; Information added by drush on {$date}
2256 version = "{$version}"{$extra}
2257 datestamp = "{$datestamp}"
2263 * Generate version information for `.info` files in YAML format.
2265 function _drush_pm_generate_info_yaml_metadata($version, $project_name, $datestamp) {
2268 if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) {
2269 $extra .= "\ncore: '$matches[1]'";
2271 if (!drush_get_option('no-gitprojectinfo', FALSE)) {
2272 $extra = "\nproject: '$project_name'";
2274 $date = date('Y-m-d', $datestamp);
2277 # Information added by drush on {$date}
2278 version: '{$version}'{$extra}
2279 datestamp: {$datestamp}