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 ascending Description
_make_parse_info_file Parse makefile recursively.
_make_is_override_allowed Check if makefile overrides are allowed
_make_determine_format Given data from stdin, determine format.
_get_working_copy_option Gather any working copy options.
make_valid_url Verify the syntax of the given URL.
make_validate_info_file Validate the make file.
make_tmp Find, and possibly create, a temporary directory.
make_tar @todo drush_archive_dump() also makes a tar. Consolidate?
make_safe_path Checks an attribute's path to ensure it's not maliciously crafted.
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_prepare_install Prepare a Drupal installation, copying default.settings.php to settings.php.
make_parse_info_file Helper function to parse a makefile and prune projects.
make_normalize_info Expand shorthand elements, so that we have an associative array.
make_md5 Calculate a cksum on each file in the build, and md5 the resulting hashes.
make_get_data Get data based on the source.
make_error Logs an error unless the --force-complete command line option is specified.
make_clean_tmp Removes the temporary build directory. On failed builds, this is handled by drush_register_file_for_deletion().
make_apply_defaults Apply any defaults.

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