make.utilities.inc

  1. 8.0.x commands/make/make.utilities.inc
  2. 6.x commands/make/make.utilities.inc
  3. 7.x commands/make/make.utilities.inc
  4. 5.x commands/make/make.utilities.inc
  5. master commands/make/make.utilities.inc

General utility functions for Drush Make.

Functions

Namesort descending Description
make_apply_defaults Apply any defaults.
make_clean_tmp Removes the temporary build directory. On failed builds, this is handled by drush_register_file_for_deletion().
make_error Logs an error unless the --force-complete command line option is specified.
make_get_data Get data based on the source.
make_md5 Calculate a cksum on each file in the build, and md5 the resulting hashes.
make_normalize_info Expand shorthand elements, so that we have an associative array.
make_parse_info_file Helper function to parse a makefile and prune projects.
make_prepare_install Prepare a Drupal installation, copying default.settings.php to settings.php.
make_prune_info_file Remove entries in the info file in accordance with the options passed in. Entries are either explicitly 'allowed' (with the $include_only parameter) in which case all *other* entries will be excluded.
make_safe_path Checks an attribute's path to ensure it's not maliciously crafted.
make_tar @todo drush_archive_dump() also makes a tar. Consolidate?
make_tmp Find, and possibly create, a temporary directory.
make_validate_info_file Validate the make file.
make_valid_url Verify the syntax of the given URL.
_get_working_copy_option Gather any working copy options.
_make_determine_format Given data from stdin, determine format.
_make_is_override_allowed Check if makefile overrides are allowed
_make_parse_info_file Parse makefile recursively.

File

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