basic.drush.inc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <?php
  2. /**
  3. * @file
  4. * Drush hooks and include commands for Basic theme.
  5. */
  6. /**
  7. * Implements hook_drush_command().
  8. */
  9. function basic_drush_command() {
  10. $items['basic-install'] = array(
  11. 'description' => dt('Run through the installation process of the Basic theme.'),
  12. 'options' => array(
  13. 'destination' => array(
  14. 'description' => 'Path to which the theme will be placed. If you\'re providing a relative path, note it is relative to the drupal root.',
  15. 'example-value' => 'themes/',
  16. ),
  17. ),
  18. 'aliases' => array('binstall'),
  19. );
  20. return $items;
  21. }
  22. /**
  23. * Implements hook_drush_help().
  24. */
  25. function basic_drush_help($section) {
  26. switch ($section) {
  27. case 'drush:basic-install':
  28. return dt('Runs through an automated process for installing and renaming your theme to use as your own.');
  29. }
  30. }
  31. /**
  32. * Implements drush_hook_COMMAND().
  33. */
  34. function drush_basic_install() {
  35. drush_print('Checking for npm...');
  36. $npm_check_output = drush_shell_exec('npm version 2>&1');
  37. if (strpos($npm_check_output, 'command not found')) {
  38. drush_print('NPM could not be found. Please install npm (https://nodejs.org/en/) and try again.');
  39. exit();
  40. }
  41. else {
  42. drush_print('npm was found.');
  43. }
  44. // Prompt the user what we're doing.
  45. drush_print('-----------------------------');
  46. drush_print(dt('Welcome to the Basic Install!'));
  47. drush_print('-----------------------------');
  48. drush_print(dt('We will perform the automated tasks for you to use your new theme. We will create a copy of the Basic theme and store it in the standard theme directory or in spescified destination.'));
  49. drush_confirm(dt('Do you wish to continue?'));
  50. // Rebuild the theme data so that we can safely check for the existence of
  51. // themes by using the information provided by list_themes().
  52. $theme_handler = \Drupal::service('theme_handler');
  53. $theme_handler->rebuildThemeData();
  54. // Prompt for a theme name.
  55. $name = drush_prompt(dt('Please enter the name of that you want for your theme'), 'My Theme');
  56. // Try to generate a machine-readable name. If that fails, prompt for one.
  57. if (!$machine_name = drush_basic_generate_theme_name($name)) {
  58. drush_print(dt("Sorry, I couldn't generate a machine-readable name for @name", array(
  59. '@name' => $name,
  60. )));
  61. }
  62. // Prompt for a theme name using the automatically generated default if any.
  63. drush_set_option('machine-name', drush_basic_require_valid_theme_name(dt('Please enter a machine-readable name for your new theme'), $machine_name));
  64. // Try to generate a machine-readable name. If that fails, prompt for one.
  65. if (!$machine_name = drush_get_option('machine-name', drush_basic_generate_theme_name($name))) {
  66. drush_print(dt("Sorry, I couldn't generate a machine-readable name for @name. Please use the '--machine-name' option to specify it manually.", array(
  67. '@name' => $name,
  68. )));
  69. }
  70. $origin_path = $theme_handler->getTheme('basic')->getPath();
  71. $temporary = \Drupal::config('system.file')->get('path.temporary');
  72. $temp_path = $temporary . '/' . $machine_name;
  73. $destination_path = drush_basic_get_destination($machine_name);
  74. // Check whether the destination path exist and bail out if it does,
  75. // so we don't delete any important data by accident.
  76. if (file_exists($destination_path)) {
  77. return drush_set_error('BASIC_COPY_PATH', dt('The path !path already exists.', array(
  78. '!path' => $destination_path,
  79. )));
  80. }
  81. if (is_dir($temp_path)) {
  82. basic_rm_dir($temp_path);
  83. }
  84. // Copy into temp directory.
  85. if (!basic_copy_dir($origin_path, $temp_path)) {
  86. return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Failed to copy !origin_path to !temp_path.', array(
  87. '!origin_path' => $origin_path,
  88. '!temp_path' => $temp_path,
  89. )));
  90. }
  91. // Recursively rewrite the file names and contents of all the files that are
  92. // now in the subtheme's directory to represent the human- and
  93. // machine-readable names of the subtheme.
  94. $search = array(
  95. 'basic' => $machine_name,
  96. 'Basic' => $name,
  97. );
  98. if (!drush_basic_rewrite_recursive($temp_path, array_keys($search), array_values($search))) {
  99. return drush_set_error('BASIC_COPY_GENERATE_THEME', dt('Failed to rewrite files and contents while generating theme.'));
  100. }
  101. // Copy from temp folder into new theme's folder.
  102. if (!basic_copy_dir($temp_path, $destination_path)) {
  103. return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Failed to copy !temp_path to !destination_path.', array(
  104. '!temp_path' => $temp_path,
  105. '!destination_path' => $destination_path,
  106. )));
  107. }
  108. // Run npm install on the new theme folder.
  109. drush_print(dt("Building node dependencies for the theme @theme. This could take a couple minutes.", array(
  110. '@theme' => $machine_name,
  111. )));
  112. drush_op('chdir', DRUPAL_ROOT . '/' . $destination_path);
  113. drush_shell_exec('npm install');
  114. drush_print(dt("Node dependancies have been successfully built."));
  115. // Notify user of successful installation.
  116. drush_print(dt("------------------------------------------------------------------"));
  117. drush_print(dt("The new theme, !theme, has been successfully derived from Basic and placed in !destination.", array(
  118. '!theme' => $machine_name,
  119. '!destination' => $destination_path,
  120. )));
  121. drush_print(dt("------------------------------------------------------------------"));
  122. }
  123. /**
  124. * Generates a valid machine-readable name for a theme from any string.
  125. *
  126. * @param string $string
  127. * The string to generate the machine-readable name from.
  128. *
  129. * @return string
  130. * The generated machine-readable name.
  131. */
  132. function drush_basic_generate_theme_name($string) {
  133. // Lowercase to start.
  134. $string = strtolower($string);
  135. // Machine-readable names have to start with a lowercase letter.
  136. // $string = preg_replace('/^[^a-z]+/', '', strtolower($string));
  137. // Machine-readable names may only contain alphanumeric characters and
  138. // underscores.
  139. $string = preg_replace('/[^a-z0-9_]+/', '_', $string);
  140. // Trim all trailing and leading underscores.
  141. $string = trim($string, '_');
  142. // Get list of current themes.
  143. $theme_handler = \Drupal::service('theme_handler');
  144. $themes = $theme_handler->listInfo();
  145. if (isset($themes[$string])) {
  146. $plain = $string;
  147. $counter = 0;
  148. while (isset($themes[$string])) {
  149. // Make sure that the machine-readable name of the theme is unique.
  150. $string = $plain . '_' . $counter++;
  151. }
  152. }
  153. return $string;
  154. }
  155. /**
  156. * Helper function that continuously prompts for a valid machine-readable name.
  157. *
  158. * @param string $message
  159. * The message that should be displayed.
  160. * @param string $default
  161. * (Optional) The default theme name. Defaults to NULL.
  162. *
  163. * @return string
  164. * A valid, unique machine-readable name.
  165. */
  166. function drush_basic_require_valid_theme_name($message, $default = NULL) {
  167. while (TRUE) {
  168. // Keep prompting for a machine-name until we get an acceptable value.
  169. $prompt = drush_prompt($message, $default);
  170. if (!preg_match('/^[a-z][a-z0-9_]*$/', $prompt)) {
  171. drush_print('The machine-readable name is invalid. It may only contain lowercase numbers, letters and underscores and must start with a letter.');
  172. }
  173. else {
  174. // Get list of current themes.
  175. $theme_handler = \Drupal::service('theme_handler');
  176. $themes = $theme_handler->listInfo();
  177. // Validate that the machine-readable name of the theme is unique.
  178. if (isset($themes[$prompt])) {
  179. drush_print(dt('A theme with the name @name already exists. The machine-readable name must be unique.', array(
  180. '@name' => $prompt,
  181. )));
  182. }
  183. else {
  184. // The given machine-readable name is valid. Let's proceed.
  185. return $prompt;
  186. }
  187. }
  188. }
  189. }
  190. /**
  191. * Helper function to get proper destination for the new theme.
  192. *
  193. * @param string $machine_name
  194. * A valid, unique machine-readable name for the new theme.
  195. *
  196. * @return string
  197. * The path to the destination where the new theme should be placed.
  198. */
  199. function drush_basic_get_destination($machine_name) {
  200. // Find proper destination if no destination option is provided.
  201. if ($destination = drush_get_option('destination')) {
  202. $destination_path = drush_trim_path($destination) . '/' . $machine_name;
  203. }
  204. else {
  205. $site_root = drush_get_context('DRUSH_DRUPAL_SITE_ROOT');
  206. if ($site_root == 'sites/default') {
  207. $destination_path = 'themes/' . $machine_name;
  208. }
  209. else {
  210. $destination_path = $site_root . '/themes/' . $machine_name;
  211. }
  212. }
  213. $destination_path = drush_normalize_path($destination_path);
  214. return $destination_path;
  215. }
  216. /**
  217. * Recursively rewrites (and renames) all files in a given path.
  218. *
  219. * @param string $path
  220. * The path to rewrite all files in.
  221. * @param string|array $search
  222. * The string(s) to look for when replacing the file names and contents. Can
  223. * be an array or a string.
  224. * @param string|array $replace
  225. * The string(s) to replace $search with. Can be an array or a string.
  226. *
  227. * @return bool
  228. * TRUE if the operation succeeded, FALSE otherwise.
  229. *
  230. * @see omega_drush_replace_contents()
  231. * @see str_replace()
  232. */
  233. function drush_basic_rewrite_recursive($path, $search, $replace) {
  234. if (!is_dir($path)) {
  235. return drush_set_error('INVALID_PATH', dt('The given path !path is not a directory.', array(
  236. '!path' => $path,
  237. )));
  238. }
  239. // If the file actually is a directory, proceed with the recursion.
  240. $directory = new DirectoryIterator($path);
  241. foreach ($directory as $item) {
  242. if ($item->isDot()) {
  243. // Do not process '..' and '.'.
  244. continue;
  245. }
  246. // Retrieve the path of the current item.
  247. $pathname = $item->getPathname();
  248. if ($item->isDir() && !drush_basic_rewrite_recursive($pathname, $search, $replace)) {
  249. return FALSE;
  250. }
  251. elseif ($item->isFile()) {
  252. // If it is a file, try to replace its contents.
  253. $content = file_get_contents($pathname);
  254. // Nothing to replace in empty files.
  255. if (empty($content)) {
  256. unlink($pathname);
  257. continue;
  258. }
  259. if (($changed_content = str_replace($search, $replace, $content)) === NULL) {
  260. return drush_set_error('REWRITE_FAILURE', dt('There was an error while trying to rewrite !path (!search to !replace)', array(
  261. '!path' => $pathname,
  262. '!search' => $search,
  263. '!replace' => $replace,
  264. )));
  265. }
  266. if ($content !== $changed_content) {
  267. file_put_contents($pathname, $changed_content);
  268. }
  269. }
  270. }
  271. return TRUE;
  272. }
  273. /**
  274. * Copy $src to $dest.
  275. *
  276. * @param string $src
  277. * The directory to copy.
  278. * @param string $dest
  279. * The destination to copy the source to, including the new name of
  280. * the directory. To copy directory "a" from "/b" to "/c", then
  281. * $src = "/b/a" and $dest = "/c/a". To copy "a" to "/c" and rename
  282. * it to "d", then $dest = "/c/d".
  283. * @param int $overwrite
  284. * Action to take if destination already exists.
  285. * - FILE_EXISTS_OVERWRITE - completely removes existing directory.
  286. * - FILE_EXISTS_ABORT - aborts the operation.
  287. * - FILE_EXISTS_MERGE - Leaves existing files and directories in place.
  288. *
  289. * @return bool
  290. * TRUE on success, FALSE on failure.
  291. */
  292. function basic_copy_dir($src, $dest, $overwrite = FILE_EXISTS_ABORT) {
  293. // Preflight based on $overwrite if $dest exists.
  294. if (file_exists($dest)) {
  295. if ($overwrite === FILE_EXISTS_OVERWRITE) {
  296. drush_op('drush_delete_dir', $dest, TRUE);
  297. }
  298. elseif ($overwrite === FILE_EXISTS_ABORT) {
  299. return drush_set_error('DRUSH_DESTINATION_EXISTS', dt('Destination directory !dest already exists.', array('!dest' => $dest)));
  300. }
  301. elseif ($overwrite === FILE_EXISTS_MERGE) {
  302. // $overwrite flag may indicate we should merge instead.
  303. drush_log(dt('Merging existing !dest directory', array('!dest' => $dest)));
  304. }
  305. }
  306. // $src readable?
  307. if (!is_readable($src)) {
  308. return drush_set_error('DRUSH_SOURCE_NOT_EXISTS', dt('Source directory !src is not readable or does not exist.', array('!src' => $src)));
  309. }
  310. // $dest writable?
  311. if (!is_writable(dirname($dest))) {
  312. return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Destination directory !dest is not writable.', array('!dest' => dirname($dest))));
  313. }
  314. // Try to do a recursive copy.
  315. if (@drush_op('_basic_recursive_copy', $src, $dest)) {
  316. return TRUE;
  317. }
  318. return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Unable to copy !src to !dest.', array('!src' => $src, '!dest' => $dest)));
  319. }
  320. /**
  321. * Internal function called by basic_copy_dir; do not use directly.
  322. */
  323. function _basic_recursive_copy($src, $destination) {
  324. static $machine_name;
  325. if (!isset($machine_name)) {
  326. $machine_name = drush_get_option('machine-name');
  327. }
  328. // All subdirectories and contents.
  329. $ignore_directories = array(
  330. 'node_modules',
  331. );
  332. $no_mask = '/^' . implode('|', $ignore_directories) . '$/';
  333. if (is_dir($src)) {
  334. if (!drush_mkdir($destination, TRUE)) {
  335. return FALSE;
  336. }
  337. $dir_handle = opendir($src);
  338. while ($file = readdir($dir_handle)) {
  339. if (($file[0] != '.' || $file == '.gitignore') && !preg_match($no_mask, $file)) {
  340. if (_basic_recursive_copy("$src/$file", "$destination/$file") !== TRUE) {
  341. return FALSE;
  342. }
  343. }
  344. }
  345. closedir($dir_handle);
  346. }
  347. elseif (is_link($src)) {
  348. symlink(readlink($src), $destination);
  349. }
  350. elseif (!copy($src, $destination)) {
  351. return FALSE;
  352. }
  353. // Rename files with basic.
  354. if (strpos($destination, 'basic') !== FALSE) {
  355. $renamed_destination = str_replace('basic', $machine_name, $destination);
  356. rename($destination, $renamed_destination);
  357. }
  358. // Preserve file modification time.
  359. touch($destination, filemtime($src));
  360. // Preserve execute permission.
  361. if (!is_link($src) && !drush_is_windows()) {
  362. // Get execute bits of $src.
  363. $execperms = fileperms($src) & 0111;
  364. // Apply execute permissions if any.
  365. if ($execperms > 0) {
  366. $perms = fileperms($destination) | $execperms;
  367. chmod($destination, $perms);
  368. }
  369. }
  370. return TRUE;
  371. }
  372. /**
  373. * Recursively removes all files within the provided directory.
  374. *
  375. * USE WITH CAUTION.
  376. *
  377. * @param string $dir
  378. * File path for the directory you want to delete.
  379. */
  380. function basic_rm_dir($dir) {
  381. if (is_dir($dir)) {
  382. $objects = scandir($dir);
  383. foreach ($objects as $object) {
  384. if ($object != "." && $object != "..") {
  385. if (is_dir($dir . "/" . $object)) {
  386. basic_rm_dir($dir . "/" . $object);
  387. }
  388. else {
  389. unlink($dir . "/" . $object);
  390. }
  391. }
  392. }
  393. rmdir($dir);
  394. }
  395. }