Version 1
[yaffs-website] / vendor / drush / drush / commands / make / make.utilities.inc
1 <?php
2 /**
3  * @file
4  * General utility functions for Drush Make.
5  */
6
7 use Drush\Log\LogLevel;
8 use Drush\Make\Parser\ParserIni;
9 use Drush\Make\Parser\ParserYaml;
10
11 /**
12  * Helper function to parse a makefile and prune projects.
13  */
14 function make_parse_info_file($makefile) {
15   $info = _make_parse_info_file($makefile);
16
17   // Support making just a portion of a make file.
18   $include_only = array(
19     'projects' => array_filter(drush_get_option_list('projects')),
20     'libraries' => array_filter(drush_get_option_list('libraries')),
21   );
22   $info = make_prune_info_file($info, $include_only);
23
24   if ($info === FALSE || ($info = make_validate_info_file($info)) === FALSE) {
25     return FALSE;
26   }
27
28   return $info;
29 }
30
31 /**
32  * Parse makefile recursively.
33  */
34 function _make_parse_info_file($makefile, $element = 'includes') {
35   if (!($data = make_get_data($makefile))) {
36     return drush_set_error('MAKE_INVALID_MAKE_FILE', dt('Invalid or empty make file: !makefile', array('!makefile' => $makefile)));
37   }
38
39   // $info['format'] will specify the determined format.
40   $info = _make_determine_format($data);
41
42   // Set any allowed options.
43   if (!empty($info['options'])) {
44     foreach ($info['options'] as $key => $value) {
45       if (_make_is_override_allowed($key)) {
46         // n.b. 'custom' context has lower priority than 'cli', so
47         // options entered on the command line will "mask" makefile options.
48         drush_set_option($key, $value, 'custom');
49       }
50     }
51   }
52
53   // Include any makefiles specified on the command line.
54   if ($include_makefiles = drush_get_option_list('includes', FALSE)) {
55     drush_unset_option('includes'); // Avoid infinite loop.
56     $info['includes'] = is_array($info['includes']) ? $info['includes'] : array();
57     foreach ($include_makefiles as $include_make) {
58       if (!array_search($include_make, $info['includes'])) {
59         $info['includes'][] = $include_make;
60       }
61     }
62   }
63
64   // Override elements with values from makefiles specified on the command line.
65   if ($overrides = drush_get_option_list('overrides', FALSE)) {
66     drush_unset_option('overrides'); // Avoid infinite loop.
67     $info['overrides'] = is_array($info['overrides']) ? $info['overrides'] : array();
68     foreach ($overrides as $override) {
69       if (!array_search($override, $info['overrides'])) {
70         $info['overrides'][] = $override;
71       }
72     }
73   }
74
75   $info = _make_merge_includes_recursively($info, $makefile);
76   $info = _make_merge_includes_recursively($info, $makefile, 'overrides');
77
78   return $info;
79 }
80
81 /**
82  * Helper function to merge includes recursively.
83  */
84 function _make_merge_includes_recursively($info, $makefile, $element = 'includes') {
85   if (!empty($info[$element])) {
86     if (is_array($info[$element])) {
87       $includes = array();
88       foreach ($info[$element] as $key => $include) {
89         if (!empty($include)) {
90           if (!$include_makefile = _make_get_include_path($include, $makefile)) {
91             return make_error('BUILD_ERROR', dt("Cannot determine include file location: !include", array('!include' => $include)));
92           }
93
94           if ($element == 'overrides') {
95             $info = array_replace_recursive($info, _make_parse_info_file($include_makefile, $element));
96           }
97           else {
98             $info = array_replace_recursive(_make_parse_info_file($include_makefile), $info);
99           }
100           unset($info[$element][$key]);
101           // Move core back to the top of the list, where
102           // make_generate_from_makefile() expects it.
103           if (!empty($info['projects'])) {
104             array_reverse($info['projects']);
105           }
106         }
107       }
108     }
109   }
110   // Ensure $info['projects'] is an associative array, so that we can merge
111   // includes properly.
112   make_normalize_info($info);
113
114   return $info;
115 }
116
117 /**
118  * Helper function to determine the proper path for an include makefile.
119  */
120 function _make_get_include_path($include, $makefile) {
121   if (is_array($include) && $include['download']['type'] = 'git') {
122     $tmp_dir = make_tmp();
123     make_download_git($include['makefile'], $include['download']['type'], $include['download'], $tmp_dir);
124     $include_makefile = $tmp_dir . '/' . $include['makefile'];
125   }
126   elseif (is_string($include)) {
127     $include_path = dirname($makefile);
128     if (make_valid_url($include, TRUE)) {
129       $include_makefile = $include;
130     }
131     elseif (file_exists($include_path . '/' . $include)) {
132       $include_makefile = $include_path . '/' . $include;
133     }
134     elseif (file_exists($include)) {
135       $include_makefile = $include;
136     }
137     else {
138       return make_error('BUILD_ERROR', dt("Include file missing: !include", array('!include' => $include)));
139     }
140   }
141   else {
142     return FALSE;
143   }
144   return $include_makefile;
145 }
146
147 /**
148  * Expand shorthand elements, so that we have an associative array.
149  */
150 function make_normalize_info(&$info) {
151   if (isset($info['projects'])) {
152     foreach($info['projects'] as $key => $project) {
153       if (is_numeric($key) && is_string($project)) {
154         unset($info['projects'][$key]);
155         $info['projects'][$project] = array(
156           'version' => '',
157         );
158       }
159       if (is_string($key) && is_numeric($project)) {
160         $info['projects'][$key] = array(
161           'version' => $project,
162         );
163       }
164     }
165   }
166 }
167
168 /**
169  * Remove entries in the info file in accordance with the options passed in.
170  * Entries are either explicitly 'allowed' (with the $include_only parameter) in
171  * which case all *other* entries will be excluded.
172  *
173  * @param array $info
174  *   A parsed info file.
175  *
176  * @param array $include_only
177  *   (Optional) Array keyed by entry type (e.g. 'libraries') against an array of
178  *   allowed keys for that type. The special value '*' means 'all entries of
179  *   this type'. If this parameter is omitted, no entries will be excluded.
180  *
181  * @return array
182  *   The $info array, pruned if necessary.
183  */
184 function make_prune_info_file($info, $include_only = array()) {
185
186   // We may get passed FALSE in some cases.
187   // Also we cannot prune an empty array, so no point in this code running!
188   if (empty($info)) {
189     return $info;
190   }
191
192   // We will accrue an explanation of our activities here.
193   $msg = array();
194   $msg['scope'] = dt("Drush make restricted to the following entries:");
195
196   $pruned = FALSE;
197
198   if (count(array_filter($include_only))) {
199     $pruned = TRUE;
200     foreach ($include_only as $type => $keys) {
201
202       if (!isset($info[$type])) {
203         continue;
204       }
205       // For translating
206       // dt("Projects");
207       // dt("Libraries");
208       $type_title = dt(ucfirst($type));
209
210       // Handle the special '*' value.
211       if (in_array('*', $keys)) {
212         $msg[$type] = dt("!entry_type: <All>", array('!entry_type' => $type_title));
213       }
214
215       // Handle a (possibly empty) array of keys to include/exclude.
216       else {
217         $info[$type] = array_intersect_key($info[$type], array_fill_keys($keys, 1));
218         unset($msg[$type]);
219         if (!empty($info[$type])) {
220           $msg[$type] = dt("!entry_type: !make_entries", array('!entry_type' => $type_title, '!make_entries' => implode(', ', array_keys($info[$type]))));
221         }
222       }
223     }
224   }
225
226   if ($pruned) {
227     // Make it clear to the user what's going on.
228     drush_log(implode("\n", $msg), LogLevel::OK);
229
230     // Throw an error if these restrictions reduced the make to nothing.
231     if (empty($info['projects']) && empty($info['libraries'])) {
232       // This error mentions the options explicitly to make it as clear as
233       // possible to the user why this error has occurred.
234       make_error('BUILD_ERROR', dt("All projects and libraries have been excluded. Review the 'projects' and 'libraries' options."));
235     }
236   }
237
238   return $info;
239 }
240
241 /**
242  * Validate the make file.
243  */
244 function make_validate_info_file($info) {
245   // Assume no errors to start.
246   $errors = FALSE;
247
248   if (empty($info['core'])) {
249     make_error('BUILD_ERROR', dt("The 'core' attribute is required"));
250     $errors = TRUE;
251   }
252   // Standardize on core.
253   elseif (preg_match('/^(\d+)(\.(x|(\d+)(-[a-z0-9]+)?))?$/', $info['core'], $matches)) {
254     // An exact version of core has been specified, so pass that to an
255     // internal variable for storage.
256     if (isset($matches[4])) {
257       $info['core_release'] = $info['core'];
258     }
259     // Format the core attribute consistently.
260     $info['core'] = $matches[1] . '.x';
261   }
262   else {
263     make_error('BUILD_ERROR', dt("The 'core' attribute !core has an incorrect format.", array('!core' => $info['core'])));
264     $errors = TRUE;
265   }
266
267   if (!isset($info['api'])) {
268     $info['api'] = MAKE_API;
269     drush_log(dt("You need to specify an API version of two in your makefile:\napi = !api", array("!api" => MAKE_API)), LogLevel::WARNING);
270   }
271   elseif ($info['api'] != MAKE_API) {
272     make_error('BUILD_ERROR', dt("The specified API attribute is incompatible with this version of Drush Make."));
273     $errors = TRUE;
274   }
275
276   $names = array();
277
278   // Process projects.
279   if (isset($info['projects'])) {
280     if (!is_array($info['projects'])) {
281       make_error('BUILD_ERROR', dt("'projects' attribute must be an array."));
282       $errors = TRUE;
283     }
284     else {
285       // Filter out entries that have been forcibly removed via [foo] = FALSE.
286       $info['projects'] = array_filter($info['projects']);
287
288       foreach ($info['projects'] as $project => $project_data) {
289         // Project has an attributes array.
290         if (is_string($project) && is_array($project_data)) {
291           if (in_array($project, $names)) {
292             make_error('BUILD_ERROR', dt("Project !project defined twice (remove the first projects[] = !project).", array('!project' => $project)));
293             $errors = TRUE;
294           }
295           $names[] = $project;
296           foreach ($project_data as $attribute => $value) {
297             // Prevent malicious attempts to access other areas of the
298             // filesystem.
299             if (in_array($attribute, array('subdir', 'directory_name', 'contrib_destination')) && !make_safe_path($value)) {
300               $args = array(
301                 '!path' => $value,
302                 '!attribute' => $attribute,
303                 '!project' => $project,
304               );
305               make_error('BUILD_ERROR', dt("Illegal path !path for '!attribute' attribute in project !project.", $args));
306               $errors = TRUE;
307             }
308           }
309         }
310         // Cover if there is no project info, it's just a project name.
311         elseif (is_numeric($project) && is_string($project_data)) {
312           if (in_array($project_data, $names)) {
313             make_error('BUILD_ERROR', dt("Project !project defined twice (remove the first projects[] = !project).", array('!project' => $project_data)));
314             $errors = TRUE;
315           }
316           $names[] = $project_data;
317           unset($info['projects'][$project]);
318           $info['projects'][$project_data] = array();
319         }
320         // Convert shorthand project version style to array format.
321         elseif (is_string($project_data)) {
322           if (in_array($project, $names)) {
323             make_error('BUILD_ERROR', dt("Project !project defined twice (remove the first projects[] = !project).", array('!project' => $project)));
324             $errors = TRUE;
325           }
326           $names[] = $project;
327           $info['projects'][$project] = array('version' => $project_data);
328         }
329         else {
330           make_error('BUILD_ERROR', dt('Project !project incorrectly specified.', array('!project' => $project)));
331           $errors = TRUE;
332         }
333       }
334     }
335   }
336   if (isset($info['libraries'])) {
337     if (!is_array($info['libraries'])) {
338       make_error('BUILD_ERROR', dt("'libraries' attribute must be an array."));
339       $errors = TRUE;
340     }
341     else {
342       // Filter out entries that have been forcibly removed via [foo] = FALSE.
343       $info['libraries'] = array_filter($info['libraries']);
344
345       foreach ($info['libraries'] as $library => $library_data) {
346         if (is_array($library_data)) {
347           foreach ($library_data as $attribute => $value) {
348             // Unset disallowed attributes.
349             if (in_array($attribute, array('contrib_destination'))) {
350               unset($info['libraries'][$library][$attribute]);
351             }
352             // Prevent malicious attempts to access other areas of the
353             // filesystem.
354             elseif (in_array($attribute, array('contrib_destination', 'directory_name')) && !make_safe_path($value)) {
355               $args = array(
356                 '!path' => $value,
357                 '!attribute' => $attribute,
358                 '!library' => $library,
359               );
360               make_error('BUILD_ERROR', dt("Illegal path !path for '!attribute' attribute in library !library.", $args));
361               $errors = TRUE;
362             }
363           }
364         }
365       }
366     }
367   }
368
369   // Convert shorthand project/library download style to array format.
370   foreach (array('projects', 'libraries') as $type) {
371     if (isset($info[$type]) && is_array($info[$type])) {
372       foreach ($info[$type] as $name => $item) {
373         if (!empty($item['download']) && is_string($item['download'])) {
374           $info[$type][$name]['download'] = array('url' => $item['download']);
375         }
376       }
377     }
378   }
379
380   // Apply defaults after projects[] array has been expanded, but prior to
381   // external validation.
382   make_apply_defaults($info);
383
384   foreach (drush_command_implements('make_validate_info') as $module) {
385     $function = $module . '_make_validate_info';
386     $return = $function($info);
387     if ($return) {
388       $info = $return;
389     }
390     else {
391       $errors = TRUE;
392     }
393   }
394
395   if ($errors) {
396     return FALSE;
397   }
398   return $info;
399 }
400
401 /**
402  * Verify the syntax of the given URL.
403  *
404  * Copied verbatim from includes/common.inc
405  *
406  * @see valid_url
407  */
408 function make_valid_url($url, $absolute = FALSE) {
409   if ($absolute) {
410     return (bool) preg_match("
411       /^                                                      # Start at the beginning of the text
412       (?:ftp|https?):\/\/                                     # Look for ftp, http, or https schemes
413       (?:                                                     # Userinfo (optional) which is typically
414         (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)*      # a username or a username and password
415         (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@          # combination
416       )?
417       (?:
418         (?:[a-z0-9\-\.]|%[0-9a-f]{2})+                        # A domain name or a IPv4 address
419         |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\])         # or a well formed IPv6 address
420       )
421       (?::[0-9]+)?                                            # Server port number (optional)
422       (?:[\/|\?]
423         (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})   # The path and query (optional)
424       *)?
425     $/xi", $url);
426   }
427   else {
428     return (bool) preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
429   }
430 }
431
432 /**
433  * Find, and possibly create, a temporary directory.
434  *
435  * @param boolean $set
436  *   Must be TRUE to create a directory.
437  * @param string $directory
438  *   Pass in a directory to use. This is required if using any
439  *   concurrent operations.
440  *
441  * @todo Merge with drush_tempdir().
442  */
443 function make_tmp($set = TRUE, $directory = NULL) {
444   static $tmp_dir;
445
446   if (isset($directory) && !isset($tmp_dir)) {
447     $tmp_dir = $directory;
448   }
449
450   if (!isset($tmp_dir) && $set) {
451     $tmp_dir = drush_find_tmp();
452     if (strrpos($tmp_dir, '/') == strlen($tmp_dir) - 1) {
453       $tmp_dir .= 'make_tmp_' . time() . '_' . uniqid();
454     }
455     else {
456       $tmp_dir .= '/make_tmp_' . time() . '_' . uniqid();
457     }
458     if (!drush_get_option('no-clean', FALSE)) {
459       drush_register_file_for_deletion($tmp_dir);
460     }
461     if (file_exists($tmp_dir)) {
462       return make_tmp(TRUE);
463     }
464     // Create the directory.
465     drush_mkdir($tmp_dir);
466   }
467   return $tmp_dir;
468 }
469
470 /**
471  * Removes the temporary build directory. On failed builds, this is handled by
472  * drush_register_file_for_deletion().
473  */
474 function make_clean_tmp() {
475   if (!($tmp_dir = make_tmp(FALSE))) {
476     return;
477   }
478   if (!drush_get_option('no-clean', FALSE)) {
479     drush_delete_dir($tmp_dir);
480   }
481   else {
482     drush_log(dt('Temporary directory: !dir', array('!dir' => $tmp_dir)), LogLevel::OK);
483   }
484 }
485
486 /**
487  * Prepare a Drupal installation, copying default.settings.php to settings.php.
488  */
489 function make_prepare_install($build_path) {
490   $default = make_tmp() . '/__build__/sites/default';
491   drush_copy_dir($default . DIRECTORY_SEPARATOR . 'default.settings.php', $default . DIRECTORY_SEPARATOR . 'settings.php', FILE_EXISTS_OVERWRITE);
492   drush_mkdir($default . '/files');
493   chmod($default . DIRECTORY_SEPARATOR . 'settings.php', 0666);
494   chmod($default . DIRECTORY_SEPARATOR . 'files', 0777);
495 }
496
497 /**
498  * Calculate a cksum on each file in the build, and md5 the resulting hashes.
499  */
500 function make_md5() {
501   return drush_dir_md5(make_tmp());
502 }
503
504 /**
505  * @todo drush_archive_dump() also makes a tar. Consolidate?
506  */
507 function make_tar($build_path) {
508   $tmp_path = make_tmp();
509
510   drush_mkdir(dirname($build_path));
511   $filename = basename($build_path);
512   $dirname = basename($build_path, '.tar.gz');
513   // Move the build directory to a more human-friendly name, so that tar will
514   // use it instead.
515   drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . '__build__', $tmp_path . DIRECTORY_SEPARATOR . $dirname, TRUE);
516   // Only move the tar file to it's final location if it's been built
517   // successfully.
518   if (drush_shell_exec("%s -C %s -Pczf %s %s", drush_get_tar_executable(), $tmp_path, $tmp_path . '/' . $filename, $dirname)) {
519     drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . $filename, $build_path, TRUE);
520   };
521   // Move the build directory back to it's original location for consistency.
522   drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . $dirname, $tmp_path . DIRECTORY_SEPARATOR . '__build__');
523 }
524
525 /**
526  * Logs an error unless the --force-complete command line option is specified.
527  */
528 function make_error($error_code, $message) {
529   if (drush_get_option('force-complete')) {
530     drush_log("$error_code: $message -- build forced", LogLevel::WARNING);
531   }
532   else {
533     return drush_set_error($error_code, $message);
534   }
535 }
536
537 /**
538  * Checks an attribute's path to ensure it's not maliciously crafted.
539  *
540  * @param string $path
541  *   The path to check.
542  */
543 function make_safe_path($path) {
544   return !preg_match("+^/|^\.\.|/\.\./+", $path);
545 }
546 /**
547  * Get data based on the source.
548  *
549  * This is a helper function to abstract the retrieval of data, so that it can
550  * come from files, STDIN, etc.  Currently supports filepath and STDIN.
551  *
552  * @param string $data_source
553  *   The path to a file, or '-' for STDIN.
554  *
555  * @return string
556  *   The raw data as a string.
557  */
558 function make_get_data($data_source) {
559   if ($data_source == '-') {
560     // See http://drupal.org/node/499758 before changing this.
561     $stdin = fopen('php://stdin', 'r');
562     $data = '';
563     $has_input = FALSE;
564
565     while ($line = fgets($stdin)) {
566       $has_input = TRUE;
567       $data .= $line;
568     }
569
570     if ($has_input) {
571       return $data;
572     }
573     return FALSE;
574   }
575   // Local file.
576   elseif (!strpos($data_source, '://')) {
577     $data = file_get_contents($data_source);
578   }
579   // Remote file.
580   else {
581     $file = _make_download_file($data_source);
582     $data = file_get_contents($file);
583     drush_op('unlink', $file);
584   }
585   return $data;
586 }
587
588 /**
589  * Apply any defaults.
590  *
591  * @param array &$info
592  *   A parsed make array.
593  */
594 function make_apply_defaults(&$info) {
595   if (isset($info['defaults'])) {
596     $defaults = $info['defaults'];
597
598     foreach ($defaults as $type => $default_data) {
599       if (isset($info[$type])) {
600         foreach ($info[$type] as $project => $data) {
601           $info[$type][$project] = _drush_array_overlay_recursive($default_data, $info[$type][$project]);
602         }
603       }
604       else {
605         drush_log(dt("Unknown attribute '@type' in defaults array", array('@type' => $type)), LogLevel::WARNING);
606       }
607     }
608   }
609 }
610
611 /**
612  * Check if makefile overrides are allowed
613  *
614  * @param array $option
615  *   The option to check.
616  */
617 function _make_is_override_allowed ($option) {
618   $allow_override = drush_get_option('allow-override', 'all');
619
620   if ($allow_override == 'all') {
621     $allow_override = array();
622   }
623   elseif (!is_array($allow_override)) {
624     $allow_override = _convert_csv_to_array($allow_override);
625   }
626
627   if ((empty($allow_override)) || ((in_array($option, $allow_override)) && (!in_array('none', $allow_override)))) {
628     return TRUE;
629   }
630   drush_log(dt("'!option' not allowed; use --allow-override=!option or --allow-override=all to permit", array("!option" => $option)), LogLevel::WARNING);
631   return FALSE;
632 }
633
634 /**
635  * Gather any working copy options.
636  *
637  * @param array $download
638  *   The download array.
639  */
640 function _get_working_copy_option($download) {
641   $wc = '';
642
643   if (_make_is_override_allowed('working-copy') && isset ($download['working-copy'])) {
644     $wc = $download['working-copy'];
645   }
646   else {
647     $wc = drush_get_option('working-copy');
648   }
649   return $wc;
650 }
651
652 /**
653  * Given data from stdin, determine format.
654  *
655  * @return array|bool
656  *   Returns parsed data if it matches any known format.
657  */
658 function _make_determine_format($data) {
659   // Most .make files will have a `core` attribute. Use this to determine
660   // the format.
661   if (preg_match('/^\s*core:/m', $data)) {
662     $parsed = ParserYaml::parse($data);
663     $parsed['format'] = 'yaml';
664     return $parsed;
665   }
666   elseif (preg_match('/^\s*core\s*=/m', $data)) {
667     $parsed = ParserIni::parse($data);
668     $parsed['format'] = 'ini';
669     return $parsed;
670   }
671
672   // If the .make file did not have a core attribute, it is being included
673   // by another .make file. Test YAML first to avoid segmentation faults from
674   // preg_match in INI parser.
675   $yaml_parse_exception = FALSE;
676   try {
677     if ($parsed = ParserYaml::parse($data)) {
678       $parsed['format'] = 'yaml';
679       return $parsed;
680     }
681   }
682   catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
683     // Note that an exception was thrown, and display after .ini parsing.
684     $yaml_parse_exception = $e;
685   }
686
687   // Try INI format.
688   if ($parsed = ParserIni::parse($data)) {
689     $parsed['format'] = 'ini';
690     return $parsed;
691   }
692
693   if ($yaml_parse_exception) {
694     throw $e;
695   }
696
697   return drush_set_error('MAKE_STDIN_ERROR', dt('Unknown make file format'));
698 }