56463907b7233bf06c6d7d7946a49d9a65314aac
[yaffs-website] / vendor / drush / drush / commands / pm / pm.drush.inc
1 <?php
2
3 /**
4  * @file
5  *  The drush Project Manager
6  *
7  * Terminology:
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).
13  */
14
15 use Drush\Log\LogLevel;
16
17 /**
18  * @defgroup update_status_constants Update Status Constants
19  * @{
20  * Represents update status of projects.
21  *
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.
27  */
28
29 /**
30  * Project is missing security update(s).
31  *
32  * Maps UPDATE_NOT_SECURE.
33  */
34 const DRUSH_UPDATESTATUS_NOT_SECURE = 1;
35
36 /**
37  * Current release has been unpublished and is no longer available.
38  *
39  * Maps UPDATE_REVOKED.
40  */
41 const DRUSH_UPDATESTATUS_REVOKED = 2;
42
43 /**
44  * Current release is no longer supported by the project maintainer.
45  *
46  * Maps UPDATE_NOT_SUPPORTED.
47  */
48 const DRUSH_UPDATESTATUS_NOT_SUPPORTED = 3;
49
50 /**
51  * Project has a new release available, but it is not a security release.
52  *
53  * Maps UPDATE_NOT_CURRENT.
54  */
55 const DRUSH_UPDATESTATUS_NOT_CURRENT = 4;
56
57 /**
58  * Project is up to date.
59  *
60  * Maps UPDATE_CURRENT.
61  */
62 const DRUSH_UPDATESTATUS_CURRENT = 5;
63
64 /**
65  * Project's status cannot be checked.
66  *
67  * Maps UPDATE_NOT_CHECKED.
68  */
69 const DRUSH_UPDATESTATUS_NOT_CHECKED = -1;
70
71 /**
72  * No available update data was found for project.
73  *
74  * Maps UPDATE_UNKNOWN.
75  */
76 const DRUSH_UPDATESTATUS_UNKNOWN = -2;
77
78 /**
79  * There was a failure fetching available update data for this project.
80  *
81  * Maps UPDATE_NOT_FETCHED.
82  */
83 const DRUSH_UPDATESTATUS_NOT_FETCHED = -3;
84
85 /**
86  * We need to (re)fetch available update data for this project.
87  *
88  * Maps UPDATE_FETCH_PENDING.
89  */
90 const DRUSH_UPDATESTATUS_FETCH_PENDING = -4;
91
92 /**
93  * Project was not packaged by drupal.org.
94  */
95 const DRUSH_UPDATESTATUS_PROJECT_NOT_PACKAGED = 101;
96
97 /**
98  * Requested project is not updateable.
99  */
100 const DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_UPDATEABLE = 102;
101
102 /**
103  * Requested project not found.
104  */
105 const DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_FOUND = 103;
106
107 /**
108  * Requested version not found.
109  */
110 const DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_FOUND = 104;
111
112 /**
113  * Requested version available.
114  */
115 const DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_CURRENT = 105;
116
117 /**
118  * Requested version already installed.
119  */
120 const DRUSH_UPDATESTATUS_REQUESTED_VERSION_CURRENT = 106;
121
122 /**
123  * @} End of "defgroup update_status_constants".
124  */
125
126 /**
127  * Implementation of hook_drush_help().
128  */
129 function pm_drush_help($section) {
130   switch ($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).");
144       }
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.");
146       return $message;
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.
151
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')));
157   }
158 }
159
160 /**
161  * Implementation of hook_drush_command().
162  */
163 function pm_drush_command() {
164   $update_options = array(
165     'lock' => 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',
168     ),
169   );
170   $update_suboptions = array(
171     'lock' => 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',
175       ),
176       'unlock' => array(
177         'description' => 'Remove the persistent lock from the specified projects so that they may be updated again.',
178         'example-value' => 'foo,bar',
179       ),
180     ),
181   );
182
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.',
187     ),
188     'options' => array(
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).',
191     ),
192     'aliases' => array('en'),
193     'engines' => array(
194       'release_info' => array(
195         'add-options-to-command' => FALSE,
196       ),
197     ),
198   );
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.',
203     ),
204     'aliases' => array('dis'),
205     'engines' => array(
206       'version_control',
207       'package_handler',
208       'release_info' => array(
209         'add-options-to-command' => FALSE,
210       ),
211     ),
212   );
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.',
217     ),
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',
226         'type' => 'Type',
227         'title' => 'Title',
228         'description' => 'Description',
229         'version' => 'Version',
230         'date' => 'Date',
231         'package' => 'Package',
232         'core' => 'Core',
233         'php' => 'PHP',
234         'status' => 'Status',
235         'path' => 'Path',
236         'schema_version' => 'Schema version',
237         'files' => 'Files',
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',
249       ),
250       'output-data-type' => 'format-table',
251     ),
252   );
253
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.',
258     ),
259     'options' => array(
260       'drush' => 'Optional. Only incude projects that have one or more Drush commands.',
261       'status' => array(
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',
264       ),
265     ),
266     'outputformat' => array(
267       'default' => 'key-value-list',
268       'pipe-format' => 'json',
269       'field-labels' => array(
270         'label'      => 'Name',
271         'type'       => 'Type',
272         'version'    => 'Version',
273         'status'     => 'Status',
274         'extensions' => 'Extensions',
275         'drush'      => 'Drush Commands',
276         'datestamp'  => 'Datestamp',
277         'path'       => 'Path',
278       ),
279       'fields-default' => array('label', 'type', 'version', 'status', 'extensions', 'drush', 'datestamp', 'path'),
280       'fields-pipe' => array('label'),
281       'output-data-type' => 'format-table',
282     ),
283     'aliases' => array('pmpi'),
284   );
285
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',
290   //   );
291   $items['pm-uninstall'] = array(
292     'description' => 'Uninstall one or more modules and their dependent modules.',
293     'arguments' => array(
294       'modules' => 'A list of modules.',
295     ),
296     'aliases' => array('pmu'),
297   );
298   $items['pm-list'] = array(
299     'description' => 'Show a list of available extensions (modules and themes).',
300     'callback arguments' => array(array(), FALSE),
301     'options' => array(
302       'type' => array(
303         'description' => 'Filter by extension type. Choices: module, theme.',
304         'example-value' => 'module',
305       ),
306       'status' => array(
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',
309       ),
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.',
313     ),
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',
319     ),
320     'aliases' => array('pml'),
321   );
322   $items['pm-refresh'] = array(
323     'description' => 'Refresh update status information.',
324     'engines' => array(
325       'update_status' => array(
326         'add-options-to-command' => FALSE,
327       ),
328     ),
329     'aliases' => array('rf'),
330   );
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.',
335     ),
336     'options' => array(
337       'pipe' => 'Return a list of the projects with any extensions enabled that need updating, one project per line.',
338     ) + $update_options,
339     'sub-options' => $update_suboptions,
340     'engines' => array(
341       'update_status',
342     ),
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',
350     ),
351     'aliases' => array('ups'),
352   );
353   $items['pm-updatecode'] = array(
354     'description' => 'Update Drupal core and contrib projects to latest recommended releases.',
355     'examples' => array(
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.',
358     ),
359     'arguments' => array(
360       'projects' => 'Optional. A list of installed projects to update.',
361     ),
362     'options' => array(
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.',
366     ) + $update_options,
367     'sub-options' => $update_suboptions,
368     'aliases' => array('upc'),
369     'topics' => array('docs-policy'),
370     'engines' => array(
371       'version_control',
372       'package_handler',
373       'release_info' => array(
374         'add-options-to-command' => FALSE,
375       ),
376       'update_status',
377     ),
378   );
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'),
384   );
385   $items['pm-updatecode-postupdate'] = array(
386     'description' => 'Notify of pending db updates.',
387     'hidden' => TRUE,
388   );
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\'',
393     ),
394     'options' => array(
395       'html' => dt('Display release notes in HTML rather than plain text.'),
396     ),
397     'examples' => array(
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.',
401     ),
402     'aliases' => array('rln'),
403     'bootstrap' => DRUSH_BOOTSTRAP_MAX,
404     'engines' => array(
405       'release_info',
406     ),
407   );
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\'',
412     ),
413     'examples' => array(
414       'drush pm-releases cck zen' => 'View releases for cck and Zen projects for your Drupal version.',
415     ),
416     'options' => array(
417       'default-major' => 'Show releases compatible with the specified major version of Drupal.',
418     ),
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',
427         'date' => 'Date',
428         'status' => 'Status',
429         'release_link' => 'Release link',
430         'download_link' => 'Download link',
431       ),
432       'fields-default' => array('project', 'version', 'date', 'status'),
433       'fields-pipe' => array('project', 'version', 'date', 'status'),
434       'output-data-type' => 'format-table',
435     ),
436     'engines' => array(
437       'release_info',
438     ),
439   );
440   $items['pm-download'] = array(
441     'description' => 'Download projects from drupal.org or other sources.',
442     'examples' => array(
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.',
452     ),
453     'arguments' => array(
454       'projects' => 'A comma delimited list of drupal.org project names, with optional version. Defaults to \'drupal\'',
455     ),
456     'options' => array(
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',
460       ),
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.',
463       'variant' => array(
464         'description' => "Only useful for install profiles. Possible values: 'full', 'projects', 'profile-only'.",
465         'example-value' => 'full',
466       ),
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',
472       ),
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.',
475     ),
476     'bootstrap' => DRUSH_BOOTSTRAP_MAX,
477     'aliases' => array('dl'),
478     'engines' => array(
479       'version_control',
480       'package_handler',
481       'release_info',
482     ),
483   );
484   return $items;
485 }
486
487 /**
488  * @defgroup extensions Extensions management.
489  * @{
490  * Functions to manage extensions.
491  */
492
493 /**
494  * Command argument complete callback.
495  */
496 function pm_pm_enable_complete() {
497   return pm_complete_extensions();
498 }
499
500 /**
501  * Command argument complete callback.
502  */
503 function pm_pm_disable_complete() {
504   return pm_complete_extensions();
505 }
506
507 /**
508  * Command argument complete callback.
509  */
510 function pm_pm_uninstall_complete() {
511   return pm_complete_extensions();
512 }
513
514 /**
515  * Command argument complete callback.
516  */
517 function pm_pm_info_complete() {
518   return pm_complete_extensions();
519 }
520
521 /**
522  * Command argument complete callback.
523  */
524 function pm_pm_releasenotes_complete() {
525   return pm_complete_projects();
526 }
527
528 /**
529  * Command argument complete callback.
530  */
531 function pm_pm_releases_complete() {
532   return pm_complete_projects();
533 }
534
535 /**
536  * Command argument complete callback.
537  */
538 function pm_pm_updatecode_complete() {
539   return pm_complete_projects();
540 }
541
542 /**
543  * Command argument complete callback.
544  */
545 function pm_pm_update_complete() {
546   return pm_complete_projects();
547 }
548
549 /**
550  * List extensions for completion.
551  *
552  * @return
553  *  Array of available extensions.
554  */
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));
559   }
560 }
561
562 /**
563  * List projects for completion.
564  *
565  * @return
566  *  Array of installed projects.
567  */
568 function pm_complete_projects() {
569   if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) {
570     return array('values' => array_keys(drush_get_projects()));
571   }
572 }
573
574 /**
575  * Sort callback function for sorting extensions.
576  *
577  * It will sort first by type, second by package and third by name.
578  */
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') {
583     return -1;
584   }
585   if ($a_type == 'theme' && $b_type == 'module') {
586     return 1;
587   }
588   $cmp = strcasecmp($a->info['package'], $b->info['package']);
589   if ($cmp == 0) {
590     $cmp = strcasecmp($a->info['name'], $b->info['name']);
591   }
592   return $cmp;
593 }
594
595 /**
596  * Calculate an extension status based on current status and schema version.
597  *
598  * @param $extension
599  *   Object of a single extension info.
600  *
601  * @return
602  *   String describing extension status. Values: enabled|disabled|not installed
603  */
604 function drush_get_extension_status($extension) {
605   if ((drush_extension_get_type($extension) == 'module') && ($extension->schema_version == -1)) {
606     $status = "not installed";
607   }
608   else {
609     $status = ($extension->status == 1)?'enabled':'disabled';
610   }
611
612   return $status;
613 }
614
615 /**
616  * Classify extensions as modules, themes or unknown.
617  *
618  * @param $extensions
619  *   Array of extension names, by reference.
620  * @param $modules
621  *   Empty array to be filled with modules in the provided extension list.
622  * @param $themes
623  *   Empty array to be filled with themes in the provided extension list.
624  */
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])) {
629       continue;
630     }
631     $type = drush_extension_get_type($extension_info[$extension]);
632     if ($type == 'module') {
633       $modules[$extension] = $extension;
634     }
635     else if ($type == 'theme') {
636       $themes[$extension] = $extension;
637     }
638   }
639 }
640
641 /**
642  * Obtain an array of installed projects off the extensions available.
643  *
644  * A project is considered to be 'enabled' when any of its extensions is
645  * enabled.
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';
650  *
651  * @param array $extensions
652  *   Array of extensions as returned by drush_get_extensions().
653  *
654  * @return
655  *   Array of installed projects with info of version, status and provided
656  * extensions.
657  */
658 function drush_get_projects(&$extensions = NULL) {
659   if (!isset($extensions)) {
660     $extensions = drush_get_extensions();
661   }
662   $projects = array(
663     'drupal' => array(
664       'label'      => 'Drupal',
665       'version'    => drush_drupal_version(),
666       'type'       => 'core',
667       'extensions' => array(),
668     )
669   );
670   if (isset($extensions['system']->info['datestamp'])) {
671     $projects['drupal']['datestamp'] = $extensions['system']->info['datestamp'];
672   }
673   foreach ($extensions as $extension) {
674     $extension_name = drush_extension_get_name($extension);
675     $extension_path = drush_extension_get_path($extension);
676
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
680     //      is not installed.
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())) {
684         $project = 'drupal';
685       }
686       else {
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);
690         }
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);
694         }
695         continue;
696       }
697     }
698     else {
699       $project = $extension->info['project'];
700     }
701
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(),
715       );
716       if (isset($extension->info['datestamp'])) {
717         $projects[$project]['datestamp'] = $extension->info['datestamp'];
718       }
719       if (isset($extension->info['project status url'])) {
720         $projects[$project]['status url'] = $extension->info['project status url'];
721       }
722     }
723     else {
724       // If any of the extensions is enabled, consider the project is enabled.
725       if ($extension->status != 0) {
726         $projects[$project]['status'] = $extension->status;
727       }
728     }
729     $projects[$project]['extensions'][] = drush_extension_get_name($extension);
730   }
731
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') {
737       continue;
738     }
739
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 . ')';
748       }
749       else {
750         // Make a list of all of the extensions in this project
751         // that do not depend on any other extension in this
752         // project.
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;
760               }
761             }
762           }
763           if ($has_project_dependency === FALSE) {
764             $candidates[] = $extensions[$e]->info['name'];
765           }
766         }
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 . ')';
770         }
771       }
772     }
773
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);
782     }
783     else {
784       $projects[$name]['path'] = $path;
785     }
786   }
787
788   return $projects;
789 }
790
791 /**
792  * Helper function to find the common path for a list of extensions in the aim to obtain the project name.
793  *
794  * @param $project_type
795  *  Type of project we're trying to find. Valid values: module, theme.
796  * @param $extensions
797  *  Array of extension names.
798  */
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);
805   }
806
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);
813       if (!$path2) {
814         drush_log(dt('Unknown path for !extension !type.', array('!extension' => $extension, '!type' => $project_type)), LogLevel::DEBUG);
815         continue;
816       }
817       // Option 1: same path.
818       if ($path == $path2) {
819         continue;
820       }
821       // Option 2: $path is a prefix of $path2.
822       if (strpos($path2, $path) === 0) {
823         continue;
824       }
825       // Option 3: $path2 is a prefix of $path.
826       if (strpos($path, $path2) === 0) {
827         $path = $path2;
828         continue;
829       }
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.
834       do {
835         $path = dirname($path);
836         if (strpos($path2, $path) === 0) {
837           break;
838         }
839       } while ($path != '.');
840     }
841   }
842
843   return $path;
844 }
845
846 /**
847  * @} End of "defgroup extensions".
848  */
849
850 /**
851  * Command callback. Show a list of extensions with type and status.
852  */
853 function drush_pm_list() {
854   //--package
855   $package_filter = array();
856   $package = strtolower(drush_get_option('package'));
857   if (!empty($package)) {
858     $package_filter = explode(',', $package);
859   }
860   if (!empty($package_filter) && (count($package_filter) == 1)) {
861     drush_hide_output_fields('package');
862   }
863
864   //--type
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);
869   }
870   else {
871     $type_filter = $all_types;
872   }
873
874   if (count($type_filter) == 1) {
875     drush_hide_output_fields('type');
876   }
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)));
880     }
881   }
882
883   //--status
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);
888   }
889   else {
890     $status_filter = $all_status;
891   }
892   if (count($status_filter) == 1) {
893     drush_hide_output_fields('status');
894   }
895
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)));
899   }
900   }
901
902   $result = array();
903   $extension_info = drush_get_extensions(FALSE);
904   uasort($extension_info, '_drush_pm_sort_extensions');
905
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]);
910       continue;
911     }
912     $status = drush_get_extension_status($extension);
913     if (!in_array($status, $status_filter)) {
914       unset($extension_info[$key]);
915       continue;
916     }
917
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]);
922         continue;
923       }
924     }
925
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]);
930         continue;
931       }
932     }
933
934     // Filter by package.
935     if (!empty($package_filter)) {
936       if (!in_array(strtolower($extension->info['package']), $package_filter)) {
937         unset($extension_info[$key]);
938         continue;
939       }
940     }
941
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'];
948
949     $result[$key] = $row;
950     unset($row);
951   }
952   // In Drush-5, we used to return $extension_info here.
953   return $result;
954 }
955
956 /**
957  * Helper function for pm-enable.
958  */
959 function drush_pm_enable_find_project_from_extension($extension) {
960   $result =  drush_pm_lookup_extension_in_cache($extension);
961
962   if (!isset($result)) {
963     $release_info = drush_get_engine('release_info');
964
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;
970     }
971   }
972
973   return $result;
974 }
975
976 /**
977  * Validate callback. Determine the modules and themes that the user would like enabled.
978  */
979 function drush_pm_enable_validate() {
980   $args = pm_parse_arguments(func_get_args());
981
982   $extension_info = drush_get_extensions();
983
984   $recheck = TRUE;
985   $last_download = NULL;
986   while ($recheck) {
987     $recheck = FALSE;
988
989     // Classify $args in themes, modules or unknown.
990     $modules = array();
991     $themes = array();
992     $download = array();
993     drush_pm_classify_extensions($args, $modules, $themes, $extension_info);
994     $extensions = array_merge($modules, $themes);
995     $unknown = array_diff($args, $extensions);
996
997     // If there're unknown extensions, try and download projects
998     // with matching names.
999     if (!empty($unknown)) {
1000       $found = array();
1001       foreach ($unknown as $name) {
1002         drush_log(dt('!extension was not found.', array('!extension' => $name)), LogLevel::WARNING);
1003         $project = drush_pm_enable_find_project_from_extension($name);
1004         if (!empty($project)) {
1005           $found[] = $project;
1006         }
1007       }
1008       if (!empty($found)) {
1009         // Prevent from looping if last download failed.
1010         if ($found === $last_download) {
1011           drush_log(dt("Unable to download some or all of the extensions."), LogLevel::WARNING);
1012           break;
1013         }
1014         drush_log(dt("The following projects provide some or all of the extensions not found:\n@list", array('@list' => implode("\n", $found))), LogLevel::OK);
1015         if (drush_get_option('resolve-dependencies')) {
1016           drush_log(dt("They are being downloaded."), LogLevel::OK);
1017         }
1018         if ((drush_get_option('resolve-dependencies')) || (drush_confirm("Would you like to download them?"))) {
1019           $download = $found;
1020         }
1021       }
1022     }
1023
1024     // Discard already enabled and incompatible extensions.
1025     foreach ($extensions as $name) {
1026       if ($extension_info[$name]->status) {
1027         drush_log(dt('!extension is already enabled.', array('!extension' => $name)), LogLevel::OK);
1028       }
1029       // Check if the extension is compatible with Drupal core and php version.
1030       if ($component = drush_extension_check_incompatibility($extension_info[$name])) {
1031         drush_set_error('DRUSH_PM_ENABLE_MODULE_INCOMPATIBLE', dt('!name is incompatible with the !component version.', array('!name' => $name, '!component' => $component)));
1032         if (drush_extension_get_type($extension_info[$name]) == 'module') {
1033           unset($modules[$name]);
1034         }
1035         else {
1036           unset($themes[$name]);
1037         }
1038       }
1039     }
1040
1041     if (!empty($modules)) {
1042       // Check module dependencies.
1043       $dependencies = drush_check_module_dependencies($modules, $extension_info);
1044       $unmet_dependencies = array();
1045       foreach ($dependencies as $module => $info) {
1046         if (!empty($info['unmet-dependencies'])) {
1047           foreach ($info['unmet-dependencies'] as $unmet) {
1048             $unmet_project = (!empty($info['dependencies'][$unmet]['project'])) ? $info['dependencies'][$unmet]['project'] : drush_pm_enable_find_project_from_extension($unmet);
1049             if (!empty($unmet_project)) {
1050               $unmet_dependencies[$module][$unmet_project] = $unmet_project;
1051             }
1052           }
1053         }
1054       }
1055       if (!empty($unmet_dependencies)) {
1056         $msgs = array();
1057         $unmet_project_list = array();
1058         foreach ($unmet_dependencies as $module => $unmet_projects) {
1059           $unmet_project_list = array_merge($unmet_project_list, $unmet_projects);
1060           $msgs[] = dt("!module requires !unmet-projects", array('!unmet-projects' => implode(', ', $unmet_projects), '!module' => $module));
1061         }
1062         $found = array_merge($download, $unmet_project_list);
1063         // Prevent from looping if last download failed.
1064         if ($found === $last_download) {
1065           drush_log(dt("Unable to download some or all of the extensions."), LogLevel::WARNING);
1066           break;
1067         }
1068         drush_log(dt("The following projects have unmet dependencies:\n!list", array('!list' => implode("\n", $msgs))), LogLevel::OK);
1069         if (drush_get_option('resolve-dependencies')) {
1070           drush_log(dt("They are being downloaded."), LogLevel::OK);
1071         }
1072         if (drush_get_option('resolve-dependencies') || drush_confirm(dt("Would you like to download them?"))) {
1073           $download = $found;
1074         }
1075       }
1076     }
1077
1078     if (!empty($download)) {
1079       // Disable DRUSH_AFFIRMATIVE context temporarily.
1080       $drush_affirmative = drush_get_context('DRUSH_AFFIRMATIVE');
1081       drush_set_context('DRUSH_AFFIRMATIVE', FALSE);
1082       // Invoke a new process to download dependencies.
1083       $result = drush_invoke_process('@self', 'pm-download', $download, array(), array('interactive' => TRUE));
1084       // Restore DRUSH_AFFIRMATIVE context.
1085       drush_set_context('DRUSH_AFFIRMATIVE', $drush_affirmative);
1086       // Refresh module cache after downloading the new modules.
1087       if (drush_drupal_major_version() >= 8) {
1088         \Drush\Drupal\ExtensionDiscovery::reset();
1089         system_list_reset();
1090       }
1091       $extension_info = drush_get_extensions();
1092       $last_download = $download;
1093       $recheck = TRUE;
1094     }
1095   }
1096
1097   if (!empty($modules)) {
1098     $all_dependencies = array();
1099     $dependencies_ok = TRUE;
1100     foreach ($dependencies as $key => $info) {
1101       if (isset($info['error'])) {
1102         unset($modules[$key]);
1103         $dependencies_ok = drush_set_error($info['error']['code'], $info['error']['message']);
1104       }
1105       elseif (!empty($info['dependencies'])) {
1106         // Make sure we have an assoc array.
1107         $dependencies_list = array_keys($info['dependencies']);
1108         $assoc = array_combine($dependencies_list, $dependencies_list);
1109         $all_dependencies = array_merge($all_dependencies, $assoc);
1110       }
1111     }
1112     if (!$dependencies_ok) {
1113       return FALSE;
1114     }
1115     $modules = array_diff(array_merge($modules, $all_dependencies), drush_module_list());
1116     // Discard modules which doesn't meet requirements.
1117     require_once DRUSH_DRUPAL_CORE . '/includes/install.inc';
1118     foreach ($modules as $key => $module) {
1119       // Check to see if the module can be installed/enabled (hook_requirements).
1120       // See @system_modules_submit
1121       if (!drupal_check_module($module)) {
1122         unset($modules[$key]);
1123         drush_set_error('DRUSH_PM_ENABLE_MODULE_UNMEET_REQUIREMENTS', dt('Module !module doesn\'t meet the requirements to be enabled.', array('!module' => $module)));
1124         _drush_log_drupal_messages();
1125         return FALSE;
1126       }
1127     }
1128   }
1129
1130   $searchpath = array();
1131   foreach (array_merge($modules, $themes) as $name) {
1132     $searchpath[] = drush_extension_get_path($extension_info[$name]);
1133   }
1134   // Add all modules that passed validation to the drush
1135   // list of commandfiles (if they have any).  This
1136   // will allow these newly-enabled modules to participate
1137   // in the pre-pm_enable and post-pm_enable hooks.
1138   if (!empty($searchpath)) {
1139     _drush_add_commandfiles($searchpath);
1140   }
1141
1142   drush_set_context('PM_ENABLE_EXTENSION_INFO', $extension_info);
1143   drush_set_context('PM_ENABLE_MODULES', $modules);
1144   drush_set_context('PM_ENABLE_THEMES', $themes);
1145
1146   return TRUE;
1147 }
1148
1149 /**
1150  * Command callback. Enable one or more extensions from downloaded projects.
1151  * Note that the modules and themes to be enabled were evaluated during the
1152  * pm-enable validate hook, above.
1153  */
1154 function drush_pm_enable() {
1155   // Get the data built during the validate phase
1156   $extension_info = drush_get_context('PM_ENABLE_EXTENSION_INFO');
1157   $modules = drush_get_context('PM_ENABLE_MODULES');
1158   $themes = drush_get_context('PM_ENABLE_THEMES');
1159
1160   // Inform the user which extensions will finally be enabled.
1161   $extensions = array_merge($modules, $themes);
1162   if (empty($extensions)) {
1163     return drush_log(dt('There were no extensions that could be enabled.'), LogLevel::OK);
1164   }
1165   else {
1166     drush_print(dt('The following extensions will be enabled: !extensions', array('!extensions' => implode(', ', $extensions))));
1167     if(!drush_confirm(dt('Do you really want to continue?'))) {
1168       return drush_user_abort();
1169     }
1170   }
1171
1172   // Enable themes.
1173   if (!empty($themes)) {
1174     drush_theme_enable($themes);
1175   }
1176
1177   // Enable modules and pass dependency validation in form submit.
1178   if (!empty($modules)) {
1179     drush_include_engine('drupal', 'environment');
1180     drush_module_enable($modules);
1181   }
1182
1183   // Inform the user of final status.
1184   $result_extensions = drush_get_named_extensions_list($extensions);
1185   $problem_extensions = array();
1186   $role = drush_role_get_class();
1187   foreach ($result_extensions as $name => $extension) {
1188     if ($extension->status) {
1189       drush_log(dt('!extension was enabled successfully.', array('!extension' => $name)), LogLevel::OK);
1190       $perms = $role->getModulePerms($name);
1191       if (!empty($perms)) {
1192         drush_print(dt('!extension defines the following permissions: !perms', array('!extension' => $name, '!perms' => implode(', ', $perms))));
1193       }
1194     }
1195     else {
1196       $problem_extensions[] = $name;
1197     }
1198   }
1199   if (!empty($problem_extensions)) {
1200     return drush_set_error('DRUSH_PM_ENABLE_EXTENSION_ISSUE', dt('There was a problem enabling !extension.', array('!extension' => implode(',', $problem_extensions))));
1201   }
1202   // Return the list of extensions enabled
1203   return $extensions;
1204 }
1205
1206 /**
1207  * Command callback. Disable one or more extensions.
1208  */
1209 function drush_pm_disable() {
1210   $args = pm_parse_arguments(func_get_args());
1211   drush_include_engine('drupal', 'pm');
1212   _drush_pm_disable($args);
1213 }
1214
1215 /**
1216  * Add extensions that match extension_name*.
1217  *
1218  * A helper function for commands that take a space separated list of extension
1219  * names. It will identify extensions that have been passed in with a
1220  * trailing * and add all matching extensions to the array that is returned.
1221  *
1222  * @param $extensions
1223  *   An array of extensions, by reference.
1224  * @param $extension_info
1225  *   Optional. An array of extension info as returned by drush_get_extensions().
1226  */
1227 function _drush_pm_expand_extensions(&$extensions, $extension_info = array()) {
1228   if (empty($extension_info)) {
1229     $extension_info = drush_get_extensions();
1230   }
1231   foreach ($extensions as $key => $extension) {
1232     if (($wildcard = rtrim($extension, '*')) !== $extension) {
1233       foreach (array_keys($extension_info) as $extension_name) {
1234         if (substr($extension_name, 0, strlen($wildcard)) == $wildcard) {
1235           $extensions[] = $extension_name;
1236         }
1237       }
1238       unset($extensions[$key]);
1239       continue;
1240     }
1241   }
1242 }
1243
1244 /**
1245  * Command callback. Uninstall one or more modules.
1246  */
1247 function drush_pm_uninstall() {
1248   $args = pm_parse_arguments(func_get_args());
1249   drush_include_engine('drupal', 'pm');
1250   _drush_pm_uninstall($args);
1251 }
1252
1253 /**
1254  * Command callback. Show available releases for given project(s).
1255  */
1256 function drush_pm_releases() {
1257   $release_info = drush_get_engine('release_info');
1258
1259   // Obtain requests.
1260   $requests = pm_parse_arguments(func_get_args(), FALSE);
1261   if (!$requests) {
1262     $requests = array('drupal');
1263   }
1264
1265   // Get installed projects.
1266   if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) {
1267     $projects = drush_get_projects();
1268   }
1269   else {
1270     $projects = array();
1271   }
1272
1273   // Select the filter to apply based on cli options.
1274   if (drush_get_option('dev', FALSE)) {
1275     $filter = 'dev';
1276   }
1277   elseif (drush_get_option('all', FALSE)) {
1278     $filter = 'all';
1279   }
1280   else {
1281     $filter = '';
1282   }
1283
1284   $status_url = drush_get_option('source');
1285
1286   $output = array();
1287   foreach ($requests as $request) {
1288     $request = pm_parse_request($request, $status_url, $projects);
1289     $project_name = $request['name'];
1290     $project_release_info = $release_info->get($request);
1291     if ($project_release_info) {
1292       $version = isset($projects[$project_name]) ? $projects[$project_name]['version'] : NULL;
1293       $releases = $project_release_info->filterReleases($filter, $version);
1294       foreach ($releases as $key => $release) {
1295         $output["${project_name}-${key}"] = array(
1296           'project' => $project_name,
1297           'version' => $release['version'],
1298           'date' => gmdate('Y-M-d', $release['date']),
1299           'status' => implode(', ', $release['release_status']),
1300         ) + $release;
1301       }
1302     }
1303   }
1304   if (empty($output)) {
1305     return drush_log(dt('No valid projects given.'), LogLevel::OK);
1306   }
1307
1308   return $output;
1309 }
1310
1311 /**
1312  * Command callback. Show release notes for given project(s).
1313  */
1314 function drush_pm_releasenotes() {
1315   $release_info = drush_get_engine('release_info');
1316
1317   // Obtain requests.
1318   if (!$requests = pm_parse_arguments(func_get_args(), FALSE)) {
1319     $requests = array('drupal');
1320   }
1321
1322   // Get installed projects.
1323   if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) {
1324     $projects = drush_get_projects();
1325   }
1326   else {
1327     $projects = array();
1328   }
1329
1330   $status_url = drush_get_option('source');
1331
1332   $output = '';
1333   foreach($requests as $request) {
1334     $request = pm_parse_request($request, $status_url, $projects);
1335     $project_release_info = $release_info->get($request);
1336     if ($project_release_info) {
1337       $version = empty($request['version']) ? NULL : $request['version'];
1338       $output .= $project_release_info->getReleaseNotes($version);
1339     }
1340   }
1341   return $output;
1342 }
1343
1344 /**
1345  * Command callback. Refresh update status information.
1346  */
1347 function drush_pm_refresh() {
1348   $update_status = drush_get_engine('update_status');
1349   drush_print(dt("Refreshing update status information ..."));
1350   $update_status->refresh();
1351   drush_print(dt("Done."));
1352 }
1353
1354 /**
1355  * Command callback. Execute pm-update.
1356  */
1357 function drush_pm_update() {
1358   // Call pm-updatecode.  updatedb will be called in the post-update process.
1359   $args = pm_parse_arguments(func_get_args(), FALSE);
1360   drush_set_option('check-updatedb', FALSE);
1361   return drush_invoke('pm-updatecode', $args);
1362 }
1363
1364 /**
1365  * Post-command callback.
1366  * Execute updatedb command after an updatecode - user requested `update`.
1367  */
1368 function drush_pm_post_pm_update() {
1369   // Use drush_invoke_process to start a subprocess. Cleaner that way.
1370   if (drush_get_context('DRUSH_PM_UPDATED', FALSE) !== FALSE) {
1371     drush_invoke_process('@self', 'updatedb');
1372   }
1373 }
1374
1375 /**
1376  * Validate callback for updatecode command. Abort if 'backup' directory exists.
1377  */
1378 function drush_pm_updatecode_validate() {
1379   $path = drush_get_context('DRUSH_DRUPAL_ROOT') . '/backup';
1380   if (is_dir($path) && (realpath(drush_get_option('backup-dir', FALSE)) != $path)) {
1381     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)));
1382   }
1383 }
1384
1385 /**
1386  * Post-command callback for updatecode.
1387  *
1388  * Execute pm-updatecode-postupdate in a backend process to not conflict with
1389  * old code already in memory.
1390  */
1391 function drush_pm_post_pm_updatecode() {
1392   // Skip if updatecode was invoked by pm-update.
1393   // This way we avoid being noisy, as updatedb is to be executed.
1394   if (drush_get_option('check-updatedb', TRUE)) {
1395     if (drush_get_context('DRUSH_PM_UPDATED', FALSE)) {
1396       drush_invoke_process('@self', 'pm-updatecode-postupdate');
1397     }
1398   }
1399 }
1400
1401 /**
1402  * Command callback. Execute updatecode-postupdate.
1403  */
1404 function drush_pm_updatecode_postupdate() {
1405   // Clear the cache, since some projects could have moved around.
1406   drush_drupal_cache_clear_all();
1407
1408   // Notify of pending database updates.
1409   // Make sure the installation API is available
1410   require_once DRUSH_DRUPAL_CORE . '/includes/install.inc';
1411
1412   // Load all .install files.
1413   drupal_load_updates();
1414
1415   // @see system_requirements().
1416   foreach (drush_module_list() as $module) {
1417     $updates = drupal_get_schema_versions($module);
1418     if ($updates !== FALSE) {
1419       $default = drupal_get_installed_schema_version($module);
1420       if (max($updates) > $default) {
1421         drush_log(dt("You have pending database updates. Run `drush updatedb` or visit update.php in your browser."), LogLevel::WARNING);
1422         break;
1423       }
1424     }
1425   }
1426 }
1427
1428 /**
1429  * Sanitize user provided arguments to several pm commands.
1430  *
1431  * Return an array of arguments off a space and/or comma separated values.
1432  */
1433 function pm_parse_arguments($args, $dashes_to_underscores = TRUE) {
1434   $arguments = _convert_csv_to_array($args);
1435   foreach ($arguments as $key => $argument) {
1436     $argument = ($dashes_to_underscores) ? strtr($argument, '-', '_') : $argument;
1437   }
1438   return $arguments;
1439 }
1440
1441 /**
1442  * Decompound a version string and returns major, minor, patch and extra parts.
1443  *
1444  * @see _pm_parse_version_compound()
1445  * @see pm_parse_version()
1446  *
1447  * @param string $version
1448  *    A version string like X.Y-Z, X.Y.Z-W or a subset.
1449  *
1450  * @return array
1451  *   Array with major, patch and extra keys.
1452  */
1453 function _pm_parse_version_decompound($version) {
1454   $pattern = '/^(\d+)(?:.(\d+))?(?:\.(x|\d+))?(?:-([a-z0-9\.-]*))?(?:\+(\d+)-dev)?$/';
1455
1456   $matches = array();
1457   preg_match($pattern, $version, $matches);
1458
1459   $parts = array(
1460     'major'  => '',
1461     'minor'  => '',
1462     'patch'  => '',
1463     'extra'  => '',
1464     'offset' => '',
1465   );
1466   if (isset($matches[1])) {
1467     $parts['major'] = $matches[1];
1468     if (isset($matches[2])) {
1469       if (isset($matches[3]) && $matches[3] != '') {
1470         $parts['minor'] = $matches[2];
1471         $parts['patch'] = $matches[3];
1472       }
1473       else {
1474         $parts['patch'] = $matches[2];
1475       }
1476     }
1477     if (!empty($matches[4])) {
1478       $parts['extra'] = $matches[4];
1479     }
1480     if (!empty($matches[5])) {
1481       $parts['offset'] = $matches[5];
1482     }
1483   }
1484
1485   return $parts;
1486 }
1487
1488 /**
1489  * Build a version string from an array of major, minor and extra parts.
1490  *
1491  * @see _pm_parse_version_decompound()
1492  * @see pm_parse_version()
1493  *
1494  * @param array $parts
1495  *    Array of parts.
1496  *
1497  * @return string
1498  *   A Version string.
1499  */
1500 function _pm_parse_version_compound($parts) {
1501   $project_version = '';
1502   if ($parts['patch'] != '') {
1503     $project_version = $parts['major'];
1504     if ($parts['minor'] != '') {
1505       $project_version = $project_version . '.' . $parts['minor'];
1506     }
1507     if ($parts['patch'] == 'x') {
1508       $project_version = $project_version . '.x-dev';
1509     }
1510     else {
1511       $project_version = $project_version . '.' . $parts['patch'];
1512       if ($parts['extra'] != '') {
1513         $project_version = $project_version . '-' . $parts['extra'];
1514       }
1515     }
1516     if ($parts['offset'] != '') {
1517       $project_version = $project_version . '+' . $parts['offset'] . '-dev';
1518     }
1519   }
1520
1521   return $project_version;
1522 }
1523
1524 /**
1525  * Parses a version string and returns its components.
1526  *
1527  * It parses both core and contrib version strings.
1528  *
1529  * Core (semantic versioning):
1530  *   - 8.0.0-beta3+252-dev
1531  *   - 8.0.0-beta2
1532  *   - 8.0.x-dev
1533  *   - 8.1.x
1534  *   - 8.0.1
1535  *   - 8
1536  *
1537  * Core (classic drupal scheme):
1538  *   - 7.x-dev
1539  *   - 7.x
1540  *   - 7.33
1541  *   - 7.34+3-dev
1542  *   - 7
1543  *
1544  * Contrib:
1545  *   - 7.x-1.0-beta1+30-dev
1546  *   - 7.x-1.0-beta1
1547  *   - 7.x-1.0+30-dev
1548  *   - 7.x-1.0
1549  *   - 1.0-beta1
1550  *   - 1.0
1551  *   - 7.x-1.x
1552  *   - 7.x-1.x-dev
1553  *   - 1.x
1554  *
1555  * @see pm_parse_request()
1556  *
1557  * @param string $version
1558  *   A core or project version string.
1559  *
1560  * @param bool $is_core
1561  *   Whether this is a core version or a project version.
1562  *
1563  * @return array
1564  *   Version string in parts.
1565  *   Example for a contrib version (ex: 7.x-3.2-beta1):
1566  *     - version         : Fully qualified version string.
1567  *     - drupal_version  : Core compatibility version (ex: 7.x).
1568  *     - version_major   : Major version (ex: 3).
1569  *     - version_minor   : Minor version. Not applicable. Always empty.
1570  *     - version_patch   : Patch version (ex: 2).
1571  *     - version_extra   : Extra version (ex: beta1).
1572  *     - project_version : Project specific part of the version (ex: 3.2-beta1).
1573  *
1574  *   Example for a core version (ex: 8.1.2-beta2 or 7.0-beta2):
1575  *     - version         : Fully qualified version string.
1576  *     - drupal_version  : Core compatibility version (ex: 8.x).
1577  *     - version_major   : Major version (ex: 8).
1578  *     - version_minor   : Minor version (ex: 1). Empty if not a semver.
1579  *     - version_patch   : Patch version (ex: 2).
1580  *     - version_extra   : Extra version (ex: beta2).
1581  *     - project_version : Same as 'version'.
1582  */
1583 function pm_parse_version($version, $is_core = FALSE) {
1584   $core_parts = _pm_parse_version_decompound($version);
1585
1586   // If no major version, we have no version at all. Pick a default.
1587   $drupal_version_default = drush_drupal_major_version();
1588   if ($core_parts['major'] == '') {
1589     $core_parts['major'] = ($drupal_version_default) ? $drupal_version_default : drush_get_option('default-major', 8);
1590   }
1591
1592   if ($is_core) {
1593     $project_version = _pm_parse_version_compound($core_parts);
1594     $version_parts = array(
1595       'version' => $project_version,
1596       'drupal_version'  => $core_parts['major'] . '.x',
1597       'project_version' => $project_version,
1598       'version_major'   => $core_parts['major'],
1599       'version_minor'   => $core_parts['minor'],
1600       'version_patch'   => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'],
1601       'version_extra'   => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'],
1602       'version_offset'  => $core_parts['offset'],
1603     );
1604   }
1605   else {
1606     // If something as 7.x-1.0-beta1, the project specific version is
1607     // in $version['extra'] and we need to parse it.
1608     if (strpbrk($core_parts['extra'], '.-')) {
1609       $nocore_parts = _pm_parse_version_decompound($core_parts['extra']);
1610       $nocore_parts['offset'] = $core_parts['offset'];
1611       $project_version = _pm_parse_version_compound($nocore_parts);
1612       $version_parts = array(
1613         'version' => $core_parts['major'] . '.x-' . $project_version,
1614         'drupal_version'  => $core_parts['major'] . '.x',
1615         'project_version' => $project_version,
1616         'version_major'   => $nocore_parts['major'],
1617         'version_minor'   => $core_parts['minor'],
1618         'version_patch'   => ($nocore_parts['patch'] == 'x') ? '' : $nocore_parts['patch'],
1619         'version_extra'   => ($nocore_parts['patch'] == 'x') ? 'dev' : $nocore_parts['extra'],
1620         'version_offset'  => $core_parts['offset'],
1621       );
1622     }
1623     // At this point we have half a version and must decide if this is a drupal major or a project.
1624     else {
1625       // If working on a bootstrapped site, core_parts has the project version.
1626       if ($drupal_version_default) {
1627         $project_version = _pm_parse_version_compound($core_parts);
1628         $version = ($project_version) ? $drupal_version_default . '.x-' . $project_version : '';
1629         $version_parts = array(
1630           'version'         => $version,
1631           'drupal_version'  => $drupal_version_default . '.x',
1632           'project_version' => $project_version,
1633           'version_major'   => $core_parts['major'],
1634           'version_minor'   => $core_parts['minor'],
1635           'version_patch'   => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'],
1636           'version_extra'   => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'],
1637           'version_offset'  => $core_parts['offset'],
1638         );
1639       }
1640       // Not working on a bootstrapped site, core_parts is core version.
1641       else {
1642         $version_parts = array(
1643           'version' => '',
1644           'drupal_version'  => $core_parts['major'] . '.x',
1645           'project_version' => '',
1646           'version_major'   => '',
1647           'version_minor'   => '',
1648           'version_patch'   => '',
1649           'version_extra'   => '',
1650           'version_offset'  => '',
1651         );
1652       }
1653     }
1654   }
1655
1656   return $version_parts;
1657 }
1658
1659 /**
1660  * Parse out the project name and version and return as a structured array.
1661  *
1662  * @see pm_parse_version()
1663  *
1664  * @param string $request_string
1665  *   Project name with optional version. Examples: 'ctools-7.x-1.0-beta1'
1666  *
1667  * @return array
1668  *   Array with all parts of the request info.
1669  */
1670 function pm_parse_request($request_string, $status_url = NULL, &$projects = array()) {
1671   // Split $request_string in project name and version. Note that hyphens (-)
1672   // are permitted in project names (ex: field-conditional-state).
1673   // We use a regex to split the string. The pattern used matches a string
1674   // starting with hyphen, followed by one or more numbers, any of the valid
1675   // symbols in version strings (.x-) and a catchall for the rest of the
1676   // version string.
1677   $parts = preg_split('/-(?:([\d+\.x].*))?$/', $request_string, NULL, PREG_SPLIT_DELIM_CAPTURE);
1678
1679   if (count($parts) == 1) {
1680     // No version in the request string.
1681     $project = $request_string;
1682     $version = '';
1683   }
1684   else {
1685     $project = $parts[0];
1686     $version = $parts[1];
1687   }
1688
1689   $is_core = ($project == 'drupal');
1690   $request = array(
1691     'name' => $project,
1692   ) + pm_parse_version($version, $is_core);
1693
1694   // Set the status url if provided or available in project's info file.
1695   if ($status_url) {
1696     $request['status url'] = $status_url;
1697   }
1698   elseif (!empty($projects[$project]['status url'])) {
1699     $request['status url'] = $projects[$project]['status url'];
1700   }
1701
1702   return $request;
1703 }
1704
1705 /**
1706  * @defgroup engines Engine types
1707  * @{
1708  */
1709
1710 /**
1711  * Implementation of hook_drush_engine_type_info().
1712  */
1713 function pm_drush_engine_type_info() {
1714   return array(
1715     'package_handler' => array(
1716       'option' => 'package-handler',
1717       'description' => 'Determine how to fetch projects from update service.',
1718       'default' => 'wget',
1719       'options' => array(
1720         '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.',
1721       ),
1722     ),
1723     'release_info' => array(
1724       'add-options-to-command' => TRUE,
1725     ),
1726     'update_status' => array(
1727       'option' => 'update-backend',
1728       'description' => 'Determine how to fetch update status information.',
1729       'default' => 'drush',
1730       'add-options-to-command' => TRUE,
1731       'options' => array(
1732         'update-backend' => 'Backend to obtain available updates.',
1733         'check-disabled' => 'Check for updates of disabled modules and themes.',
1734         'security-only'  => 'Only update modules that have security updates available.',
1735       ),
1736       'combine-help' => TRUE,
1737     ),
1738     'version_control' => array(
1739       'option' => 'version-control',
1740       'default' => 'backup',
1741       'description' => 'Integrate with version control systems.',
1742     ),
1743   );
1744 }
1745
1746 /**
1747  * Implements hook_drush_engine_ENGINE_TYPE().
1748  *
1749  * Package handler engine is used by pm-download and
1750  * pm-updatecode commands to determine how to download/checkout
1751  * new projects and acquire updates to projects.
1752  */
1753 function pm_drush_engine_package_handler() {
1754   return array(
1755     'wget' => array(
1756       'description' => 'Download project packages using wget or curl.',
1757       'options' => array(
1758         'no-md5' => 'Skip md5 validation of downloads.',
1759       ),
1760     ),
1761     'git_drupalorg' => array(
1762       'description' => 'Use git.drupal.org to checkout and update projects.',
1763       'options' => array(
1764         '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.',
1765         '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.',
1766         'gitcheckoutparams' => 'Add options to the `git checkout` command.',
1767         'gitcloneparams' => 'Add options to the `git clone` command.',
1768         'gitfetchparams' => 'Add options to the `git fetch` command.',
1769         'gitpullparams' => 'Add options to the `git pull` command.',
1770         'gitinfofile' => 'Inject version info into each .info file.',
1771       ),
1772       'sub-options' => array(
1773         'gitsubmodule' => array(
1774           'gitsubmoduleaddparams' => 'Add options to the `git submodule add` command.',
1775         ),
1776       ),
1777     ),
1778   );
1779 }
1780
1781 /**
1782  * Implements hook_drush_engine_ENGINE_TYPE().
1783  *
1784  * Release info engine is used by several pm commands to obtain
1785  * releases info from Drupal's update service or external sources.
1786  */
1787 function pm_drush_engine_release_info() {
1788   return array(
1789     'updatexml' => array(
1790       'description' => 'Drush release info engine for update.drupal.org and compatible services.',
1791       'options' => array(
1792         'source' => 'The base URL which provides project release history in XML. Defaults to http://updates.drupal.org/release-history.',
1793         'dev' => 'Work with development releases solely.',
1794       ),
1795       'sub-options' => array(
1796         'cache' => array(
1797           'cache-duration-releasexml' => 'Expire duration (in seconds) for release XML. Defaults to 86400 (24 hours).',
1798         ),
1799         'select' => array(
1800           'all' => 'Shows all available releases instead of a short list of recent releases.',
1801         ),
1802       ),
1803       'class' => 'Drush\UpdateService\ReleaseInfo',
1804     ),
1805   );
1806 }
1807
1808 /**
1809  * Implements hook_drush_engine_ENGINE_TYPE().
1810  *
1811  * Update status engine is used to check available updates for
1812  * the projects in a Drupal site.
1813  */
1814 function pm_drush_engine_update_status() {
1815   return array(
1816     'drupal' => array(
1817       'description' => 'Check available updates with update.module.',
1818       'drupal dependencies' => array('update'),
1819       'class' => 'Drush\UpdateService\StatusInfoDrupal',
1820     ),
1821     'drush' => array(
1822       'description' => 'Check available updates without update.module.',
1823       'class' => 'Drush\UpdateService\StatusInfoDrush',
1824     ),
1825   );
1826 }
1827
1828 /**
1829  * Implements hook_drush_engine_ENGINE_TYPE().
1830  *
1831  * Integration with VCS in order to easily commit your changes to projects.
1832  */
1833 function pm_drush_engine_version_control() {
1834   return array(
1835     'backup' => array(
1836       'description' => 'Backup all project files before updates.',
1837       'options' => array(
1838         'no-backup' => 'Do not perform backups. WARNING: Will result in non-core files/dirs being deleted (e.g. .git)',
1839         '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.',
1840       ),
1841     ),
1842     'bzr' => array(
1843       'signature' => 'bzr root %s',
1844       'description' => 'Quickly add/remove/commit your project changes to Bazaar.',
1845       'options' => array(
1846         'bzrsync' => 'Automatically add new files to the Bazaar repository and remove deleted files. Caution.',
1847         'bzrcommit' => 'Automatically commit changes to Bazaar repository. You must also use the --bzrsync option.',
1848       ),
1849       'sub-options' => array(
1850         'bzrcommit' => array(
1851           'bzrmessage' => 'Override default commit message which is: Drush automatic commit. Project <name> <type> Command: <the drush command line used>',
1852         ),
1853       ),
1854       'examples' => array(
1855         'drush dl cck --version-control=bzr --bzrsync --bzrcommit' =>  'Download the cck project and then add it and commit it to Bazaar.'
1856       ),
1857     ),
1858     'svn' => array(
1859       'signature' => 'svn info %s',
1860       'description' => 'Quickly add/remove/commit your project changes to Subversion.',
1861       'options' => array(
1862         'svnsync' => 'Automatically add new files to the SVN repository and remove deleted files. Caution.',
1863         'svncommit' => 'Automatically commit changes to SVN repository. You must also using the --svnsync option.',
1864         'svnstatusparams' => "Add options to the 'svn status' command",
1865         'svnaddparams' => 'Add options to the `svn add` command',
1866         'svnremoveparams' => 'Add options to the `svn remove` command',
1867         'svnrevertparams' => 'Add options to the `svn revert` command',
1868         'svncommitparams' => 'Add options to the `svn commit` command',
1869       ),
1870       'sub-options' => array(
1871         'svncommit' => array(
1872          'svnmessage' => 'Override default commit message which is: Drush automatic commit: <the drush command line used>',
1873         ),
1874       ),
1875       'examples' => array(
1876         'drush [command] cck --svncommitparams=\"--username joe\"' =>  'Commit changes as the user \'joe\' (Quotes are required).'
1877       ),
1878     ),
1879   );
1880 }
1881
1882 /**
1883  * @} End of "Engine types".
1884  */
1885
1886 /**
1887  * Interface for version control systems.
1888  * We use a simple object layer because we conceivably need more than one
1889  * loaded at a time.
1890  */
1891 interface drush_version_control {
1892   function pre_update(&$project);
1893   function rollback($project);
1894   function post_update($project);
1895   function post_download($project);
1896   static function reserved_files();
1897 }
1898
1899 /**
1900  * A simple factory function that tests for version control systems, in a user
1901  * specified order, and returns the one that appears to be appropriate for a
1902  * specific directory.
1903  */
1904 function drush_pm_include_version_control($directory = '.') {
1905   $engine_info = drush_get_engines('version_control');
1906   $version_controls = drush_get_option('version-control', FALSE);
1907   // If no version control was given, use a list of defaults.
1908   if (!$version_controls) {
1909     // Backup engine is the last option.
1910     $version_controls = array_reverse(array_keys($engine_info['engines']));
1911   }
1912   else {
1913     $version_controls = array($version_controls);
1914   }
1915
1916   // Find the first valid engine in the list, checking signatures if needed.
1917   $engine = FALSE;
1918   while (!$engine && count($version_controls)) {
1919     $version_control = array_shift($version_controls);
1920     if (isset($engine_info['engines'][$version_control])) {
1921       if (!empty($engine_info['engines'][$version_control]['signature'])) {
1922         drush_log(dt('Verifying signature for !vcs version control engine.', array('!vcs' => $version_control)), LogLevel::DEBUG);
1923         if (drush_shell_exec($engine_info['engines'][$version_control]['signature'], $directory)) {
1924           $engine = $version_control;
1925         }
1926       }
1927       else {
1928         $engine = $version_control;
1929       }
1930     }
1931   }
1932   if (!$engine) {
1933     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)));
1934   }
1935
1936   $instance = drush_include_engine('version_control', $engine);
1937   return $instance;
1938 }
1939
1940 /**
1941  * Update the locked status of all of the candidate projects
1942  * to be updated.
1943  *
1944  * @param array &$projects
1945  *   The projects array from pm_updatecode.  $project['locked'] will
1946  *   be set for every file where a persistent lockfile can be found.
1947  *   The 'lock' and 'unlock' operations are processed first.
1948  * @param array $projects_to_lock
1949  *   A list of projects to create peristent lock files for
1950  * @param array $projects_to_unlock
1951  *   A list of projects to clear the persistent lock on
1952  * @param string $lock_message
1953  *   The reason the project is being locked; stored in the lockfile.
1954  *
1955  * @return array
1956  *   A list of projects that are locked.
1957  */
1958 function drush_pm_update_lock(&$projects, $projects_to_lock, $projects_to_unlock, $lock_message = NULL) {
1959   $locked_result = array();
1960
1961   // Warn about ambiguous lock / unlock values
1962   if ($projects_to_lock == array('1')) {
1963     $projects_to_lock = array();
1964     drush_log(dt('Ignoring --lock with no value.'), LogLevel::WARNING);
1965   }
1966   if ($projects_to_unlock == array('1')) {
1967     $projects_to_unlock = array();
1968     drush_log(dt('Ignoring --unlock with no value.'), LogLevel::WARNING);
1969   }
1970
1971   // Log if we are going to lock or unlock anything
1972   if (!empty($projects_to_unlock)) {
1973     drush_log(dt('Unlocking !projects', array('!projects' => implode(',', $projects_to_unlock))), LogLevel::OK);
1974   }
1975   if (!empty($projects_to_lock)) {
1976     drush_log(dt('Locking !projects', array('!projects' => implode(',', $projects_to_lock))), LogLevel::OK);
1977   }
1978
1979   $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT');
1980   foreach ($projects as $name => $project) {
1981     $message = NULL;
1982     if (isset($project['path'])) {
1983       if ($name == 'drupal') {
1984         $lockfile = $drupal_root . '/.drush-lock-update';
1985       }
1986       else {
1987         $lockfile = $drupal_root . '/' . $project['path'] . '/.drush-lock-update';
1988       }
1989
1990       // Remove the lock file if the --unlock option was specified
1991       if (((in_array($name, $projects_to_unlock)) || (in_array('all', $projects_to_unlock))) && (file_exists($lockfile))) {
1992         drush_op('unlink', $lockfile);
1993       }
1994
1995       // Create the lock file if the --lock option was specified
1996       if ((in_array($name, $projects_to_lock)) || (in_array('all', $projects_to_lock))) {
1997         drush_op('file_put_contents', $lockfile, $lock_message != NULL ? $lock_message : "Locked via drush.");
1998         // Note that the project is locked.  This will work even if we are simulated,
1999         // or if we get permission denied from the file_put_contents.
2000         // If the lock is -not- simulated or transient, then the lock message will be
2001         // read from the lock file below.
2002         $message = drush_get_context('DRUSH_SIMULATE') ? 'Simulated lock.' : 'Transient lock.';
2003       }
2004
2005       // If the persistent lock file exists, then mark the project as locked.
2006       if (file_exists($lockfile)) {
2007         $message = trim(file_get_contents($lockfile));
2008       }
2009     }
2010
2011     // If there is a message set, then mark the project as locked.
2012     if (isset($message)) {
2013       $projects[$name]['locked'] = !empty($message) ? $message : "Locked.";
2014       $locked_result[$name] = $project;
2015     }
2016   }
2017
2018   return $locked_result;
2019 }
2020
2021 /**
2022  * Returns the path to the extensions cache file.
2023  */
2024 function _drush_pm_extension_cache_file() {
2025   return drush_get_context('DRUSH_PER_USER_CONFIGURATION') . "/drush-extension-cache.inc";
2026 }
2027
2028 /**
2029  * Load the extensions cache.
2030  */
2031 function _drush_pm_get_extension_cache() {
2032   $extension_cache = array();
2033   $cache_file = _drush_pm_extension_cache_file();
2034
2035   if (file_exists($cache_file)) {
2036     include $cache_file;
2037   }
2038   if (!array_key_exists('extension-map', $extension_cache)) {
2039     $extension_cache['extension-map'] = array();
2040   }
2041   return $extension_cache;
2042 }
2043
2044 /**
2045  * Lookup an extension in the extensions cache.
2046  */
2047 function drush_pm_lookup_extension_in_cache($extension) {
2048   $result = NULL;
2049   $extension_cache = _drush_pm_get_extension_cache();
2050   if (!empty($extension_cache) && array_key_exists($extension, $extension_cache)) {
2051     $result = $extension_cache[$extension];
2052   }
2053   return $result;
2054 }
2055
2056 /**
2057  * Persists extensions cache.
2058  *
2059  * #TODO# not implemented.
2060  */
2061 function drush_pm_put_extension_cache($extension_cache) {
2062 }
2063
2064 /**
2065  * Store extensions founds within a project in extensions cache.
2066  */
2067 function drush_pm_cache_project_extensions($project, $found) {
2068   $extension_cache = _drush_pm_get_extension_cache();
2069   foreach($found as $extension) {
2070     // Simple cache does not handle conflicts
2071     // We could keep an array of projects, and count
2072     // how many times each one has been seen...
2073     $extension_cache[$extension] = $project['name'];
2074   }
2075   drush_pm_put_extension_cache($extension_cache);
2076 }
2077
2078 /**
2079  * Print out all extensions (modules/themes/profiles) found in specified project.
2080  *
2081  * Find .info.yml files in the project path and identify modules, themes and
2082  * profiles. It handles two kind of projects: drupal core/profiles and
2083  * modules/themes.
2084  * It does nothing with theme engine projects.
2085  */
2086 function drush_pm_extensions_in_project($project) {
2087   // Mask for drush_scan_directory, to match .info.yml files.
2088   $mask = $project['drupal_version'][0] >= 8 ? '/(.*)\.info\.yml$/' : '/(.*)\.info$/';
2089
2090   // Mask for drush_scan_directory, to avoid tests directories.
2091   $nomask = array('.', '..', 'CVS', 'tests');
2092
2093   // Drupal core and profiles can contain modules, themes and profiles.
2094   if (in_array($project['project_type'], array('core', 'profile'))) {
2095     $found = array('profile' => array(), 'theme' => array(), 'module' => array());
2096     // Find all of the .info files
2097     foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) {
2098       // Extract extension name from filename.
2099       $matches = array();
2100       preg_match($mask, $info->basename, $matches);
2101       $name = $matches[1];
2102
2103       // Find the project type corresponding the .info file.
2104      // (Only drupal >=7.x has .info for .profile)
2105       $base = dirname($filename) . '/' . $name;
2106       if (is_file($base . '.module')) {
2107         $found['module'][] = $name;
2108       }
2109       else if (is_file($base . '.profile')) {
2110         $found['profile'][] = $name;
2111       }
2112       else {
2113         $found['theme'][] = $name;
2114       }
2115     }
2116     // Special case: find profiles for drupal < 7.x (no .info)
2117     if ($project['drupal_version'][0] < 7) {
2118       foreach (drush_find_profiles($project['full_project_path']) as $filename => $info) {
2119         $found['profile'][] = $info->name;
2120       }
2121     }
2122     // Log results.
2123     $msg = "Project !project contains:\n";
2124     $args = array('!project' => $project['name']);
2125     foreach (array_keys($found) as $type) {
2126       if ($count = count($found[$type])) {
2127         $msg .= " - !count_$type !type_$type: !found_$type\n";
2128         $args += array("!count_$type" => $count, "!type_$type" => $type, "!found_$type" => implode(', ', $found[$type]));
2129         if ($count > 1) {
2130           $args["!type_$type"] = $type.'s';
2131         }
2132       }
2133     }
2134     drush_log(dt($msg, $args), LogLevel::SUCCESS);
2135     drush_print_pipe(call_user_func_array('array_merge', array_values($found)));
2136   }
2137   // Modules and themes can only contain other extensions of the same type.
2138   elseif (in_array($project['project_type'], array('module', 'theme'))) {
2139     $found = array();
2140     foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) {
2141       // Extract extension name from filename.
2142       $matches = array();
2143       preg_match($mask, $info->basename, $matches);
2144       $found[] = $matches[1];
2145     }
2146     // If there is only one module / theme in the project, only print out
2147     // the message if is different than the project name.
2148     if (count($found) == 1) {
2149       if ($found[0] != $project['name']) {
2150         $msg = "Project !project contains a !type named !found.";
2151       }
2152     }
2153     // If there are multiple modules or themes in the project, list them all.
2154     else {
2155       $msg = "Project !project contains !count !types: !found.";
2156     }
2157     if (isset($msg)) {
2158       drush_print(dt($msg, array('!project' => $project['name'], '!count' => count($found), '!type' => $project['project_type'], '!found' => implode(', ', $found))));
2159     }
2160     drush_print_pipe($found);
2161     // Cache results.
2162     drush_pm_cache_project_extensions($project, $found);
2163   }
2164 }
2165
2166 /**
2167  * Return an array of empty directories.
2168  *
2169  * Walk a directory and return an array of subdirectories that are empty. Will
2170  * return the given directory if it's empty.
2171  * If a list of items to exclude is provided, subdirectories will be condidered
2172  * empty even if they include any of the items in the list.
2173  *
2174  * @param string $dir
2175  *   Path to the directory to work in.
2176  * @param array $exclude
2177  *   Array of files or directory to exclude in the check.
2178  *
2179  * @return array
2180  *   A list of directory paths that are empty. A directory is deemed to be empty
2181  *   if it only contains excluded files or directories.
2182  */
2183 function drush_find_empty_directories($dir, $exclude = array()) {
2184   // Skip files.
2185   if (!is_dir($dir)) {
2186     return array();
2187   }
2188   $to_exclude = array_merge(array('.', '..'), $exclude);
2189   $empty_dirs = array();
2190   $dir_is_empty = TRUE;
2191   foreach (scandir($dir) as $file) {
2192     // Skip excluded directories.
2193     if (in_array($file, $to_exclude)) {
2194       continue;
2195     }
2196     // Recurse into sub-directories to find potentially empty ones.
2197     $subdir = $dir . '/' . $file;
2198     $empty_dirs += drush_find_empty_directories($subdir, $exclude);
2199     // $empty_dir will not contain $subdir, if it is a file or if the
2200     // sub-directory is not empty. $subdir is only set if it is empty.
2201     if (!isset($empty_dirs[$subdir])) {
2202       $dir_is_empty = FALSE;
2203     }
2204   }
2205
2206   if ($dir_is_empty) {
2207     $empty_dirs[$dir] = $dir;
2208   }
2209   return $empty_dirs;
2210 }
2211
2212 /**
2213  * Inject metadata into all .info files for a given project.
2214  *
2215  * @param string $project_dir
2216  *   The full path to the root directory of the project to operate on.
2217  * @param string $project_name
2218  *   The project machine name (AKA shortname).
2219  * @param string $version
2220  *   The version string to inject into the .info file(s).
2221  * @param int $datestamp
2222  *   The datestamp of the last commit.
2223  *
2224  * @return boolean
2225  *   TRUE on success, FALSE on any failures appending data to .info files.
2226  */
2227 function drush_pm_inject_info_file_metadata($project_dir, $project_name, $version, $datestamp) {
2228   // `drush_drupal_major_version()` cannot be used here because this may be running
2229   // outside of a Drupal context.
2230   $yaml_format = substr($version, 0, 1) >= 8;
2231   $pattern = preg_quote($yaml_format ? '.info.yml' : '.info');
2232   $info_files = drush_scan_directory($project_dir, '/.*' . $pattern . '$/');
2233   if (!empty($info_files)) {
2234     // Construct the string of metadata to append to all the .info files.
2235     if ($yaml_format) {
2236       $info = _drush_pm_generate_info_yaml_metadata($version, $project_name, $datestamp);
2237     }
2238     else {
2239       $info = _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp);
2240     }
2241     foreach ($info_files as $info_file) {
2242       if (!drush_file_append_data($info_file->filename, $info)) {
2243         return FALSE;
2244       }
2245     }
2246   }
2247   return TRUE;
2248 }
2249
2250 /**
2251  * Generate version information for `.info` files in ini format.
2252  *
2253  * Taken with some modifications from:
2254  * http://drupalcode.org/project/drupalorg.git/blob/refs/heads/6.x-3.x:/drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php#l192
2255  */
2256 function _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp) {
2257   $matches = array();
2258   $extra = '';
2259   if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) {
2260     $extra .= "\ncore = \"$matches[1]\"";
2261   }
2262   if (!drush_get_option('no-gitprojectinfo', FALSE)) {
2263     $extra = "\nproject = \"$project_name\"";
2264   }
2265   $date = date('Y-m-d', $datestamp);
2266   $info = <<<METADATA
2267
2268 ; Information added by drush on {$date}
2269 version = "{$version}"{$extra}
2270 datestamp = "{$datestamp}"
2271 METADATA;
2272   return $info;
2273 }
2274
2275 /**
2276  * Generate version information for `.info` files in YAML format.
2277  */
2278 function _drush_pm_generate_info_yaml_metadata($version, $project_name, $datestamp) {
2279   $matches = array();
2280   $extra = '';
2281   if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) {
2282     $extra .= "\ncore: '$matches[1]'";
2283   }
2284   if (!drush_get_option('no-gitprojectinfo', FALSE)) {
2285     $extra = "\nproject: '$project_name'";
2286   }
2287   $date = date('Y-m-d', $datestamp);
2288   $info = <<<METADATA
2289
2290 # Information added by drush on {$date}
2291 version: '{$version}'{$extra}
2292 datestamp: {$datestamp}
2293 METADATA;
2294   return $info;
2295 }