Renaming extensions cache key.
[project/omega.git] / includes / omega.drush.inc
1 <?php
2
3 /**
4 * @file
5 * Please supply a file description.
6 */
7
8 /**
9 * Implements hook_drush_command().
10 */
11 function omega_drush_command() {
12 $items['omega-subtheme'] = array(
13 'description' => dt('Creates a Omega subtheme.'),
14 'arguments' => array(
15 'name' => dt('The name of your subtheme.'),
16 ),
17 'options' => array(
18 'destination' => dt('The destination of your subtheme. Defaults to "all" (sites/all/themes).'),
19 'machine-name' => dt('The machine-readable name of your subtheme. This will be auto-generated from the human-readable name if omitted.'),
20 'starterkit' => dt('The starterkit that your subtheme should use. Defaults to "omega_starterkit".'),
21 'basetheme' => dt('Overrides the base theme defined in the starterkit.'),
22 'enable' => dt('Automatically enable the subtheme after creation.'),
23 'set-default' => dt('Automatically enable the subtheme after creation and make it the default theme.'),
24 ),
25 'examples' => array(
26 'drush omega-subtheme "My Theme"' => dt('Creates a Omega subtheme called "My Theme".'),
27 'drush omega-subtheme "My Theme" --destination=example.com' => dt('Creates a Omega subtheme called "My Theme" in sites/example.com/themes.'),
28 'drush omega-subtheme "My Theme" --starterkit=my_custom_starterkit' => dt('Uses a custom starterkit to create a Omega subtheme called "My Theme" in sites/all/themes (default).'),
29 ),
30 'aliases' => array('osub'),
31 );
32
33 $items['omega-export'] = array(
34 'description' => dt('Exports the theme settings of a given theme from the database to the .info file.'),
35 'arguments' => array(
36 'theme' => dt('The machine-readable name of the theme to export the theme settings for.'),
37 ),
38 'options' => array(
39 'revert' => dt('Purges the theme settings from the database after exporting them to the .info file.'),
40 ),
41 'examples' => array(
42 'drush omega-export foo' => dt('Exports the theme settings of the "foo" theme to the "foo.info" file in that theme.'),
43 'drush omega-export foo --revert' => dt('Purges the theme settings of the "foo" theme from the database after exporting them to the .info file.'),
44 ),
45 'aliases' => array('oexp'),
46 );
47
48 $items['omega-revert'] = array(
49 'description' => dt('Reverts the theme settings of a given theme by deleting them from the database.'),
50 'arguments' => array(
51 'theme' => dt('The machine-readable name of the theme to revert the theme settings for.'),
52 ),
53 'examples' => array(
54 'drush omega-revert foo' => dt('Reverts the theme settings of the "foo" theme.'),
55 ),
56 'aliases' => array('orev'),
57 );
58
59 $items['omega-layout'] = array(
60 'description' => dt('Generates a layout from the starterkit.'),
61 'arguments' => array(
62 'theme' => dt('The machine-readable name of the theme to create the layout for.'),
63 'layout' => dt('The name of your layout.'),
64 ),
65 'options' => array(
66 'machine-name' => dt('The machine-readable name of the layout. This will be auto-generated from the human-readable name if omitted.'),
67 ),
68 'examples' => array(
69 'drush omega-layout foo bar' => dt('Creates a layout "bar" for the "foo" theme.'),
70 ),
71 'aliases' => array('olay'),
72 );
73
74 return $items;
75 }
76
77 /**
78 * Implements hook_drush_help().
79 */
80 function omega_drush_help($section) {
81 switch ($section) {
82 case 'drush:omega-subtheme':
83 return dt('Generates a subtheme.');
84 case 'drush:omega-export':
85 return dt('Exports the theme settings of a given theme.');
86 case 'drush:omega-revert':
87 return dt('Reverts the theme settings of a given theme.');
88 case 'drush:omega-layout':
89 return dt('Generates a layout.');
90 }
91 }
92
93 /**
94 * Implements drush_hook_COMMAND_validate().
95 */
96 function drush_omega_subtheme_validate($name = NULL) {
97 // Rebuild the theme data so that we can safely check for the existance of
98 // themes by using the information provided by list_themes().
99 system_rebuild_theme_data();
100
101 // Retrieve the option values from the drush command.
102 $destination = drush_get_option('destination', 'sites/all');
103 $starterkit = drush_get_option('starterkit', 'omega_starterkit');
104
105 if ($machine_name = drush_get_option('machine-name')) {
106 // Validate the machine-readable name of the theme.
107 if (!preg_match('/^[a-z][a-z0-9_]*$/', $machine_name)) {
108 return drush_set_error('OMEGA_THEME_NAME_INVALID', dt('The machine name is invalid. It may only contain lowercase numbers, letters and underscores and must start with a letter.'));
109 }
110
111 $themes = list_themes();
112 // Validate that the machine-readable name of the theme is unique.
113 if (isset($themes[$machine_name])) {
114 return drush_set_error('OMEGA_THEME_ALREADY_EXISTS', dt('A theme with that name already exists. The machine-readable name must be unique.'));
115 }
116 }
117
118 // Check if the destination is valid.
119 if (!omega_drush_resolve_destination($destination)) {
120 return drush_set_error('OMEGA_DESITINATION_INVALID', dt('The destination is invalid.'));
121 }
122
123 // Check if the chosen base theme or starterkit exists.
124 if (!array_key_exists($starterkit, omega_drush_starterkits())) {
125 return drush_set_error('OMEGA_STARTERKIT_NOT_EXISTS', dt('There is no valid Omega theme starterkit with the name @starterkit.', array('@starterkit' => $starterkit)));
126 }
127
128 if ($basetheme = drush_get_option('basetheme')) {
129 if (!array_key_exists($basetheme, list_themes()) || !array_key_exists('omega', omega_theme_trail($basetheme))) {
130 return drush_set_error('OMEGA_BASETHEME_INVALID', dt('The base theme @basetheme does not exist or is invalid.', array('@basetheme' => $basetheme)));
131 }
132 }
133 }
134
135 /**
136 * Implements drush_hook_COMMAND().
137 */
138 function drush_omega_subtheme($name = NULL) {
139 if (!isset($name)) {
140 drush_prompt(dt('Please enter the name of the new sub-theme'));
141 }
142
143 $destination = drush_get_option('destination', 'sites/all');
144
145 $subtheme = new stdClass();
146 $subtheme->name = $name;
147 $subtheme->machine_name = drush_get_option('machine-name', _omega_drush_generate_theme_name($name));
148 $subtheme->path = omega_drush_resolve_destination($destination) . '/' . $subtheme->machine_name;
149 $subtheme->starterkit = drush_get_option('starterkit', 'omega_starterkit');
150 $subtheme->default = drush_get_option('set-default') !== NULL;
151 $subtheme->enable = $subtheme->default || drush_get_option('enable') !== NULL;
152 $subtheme->basetheme = drush_get_option('basetheme');
153
154 if (omega_drush_subtheme_create($subtheme)) {
155 drush_log(dt('You have successfully created the theme @theme (@name) in @path.', array(
156 '@theme' => $subtheme->name,
157 '@name' => $subtheme->machine_name,
158 '@path' => dirname($subtheme->path),
159 )), 'success');
160 }
161 }
162
163 /**
164 * Resolves the destination path for a subtheme. This can either be a profile
165 * theme folder, a sites theme folder or a theme.
166 *
167 * @param $destination
168 * A destination string, this can either be a site path ('sites/foo'), a
169 * profile path ('profiles/foo') or a theme path ('themes/foo').
170 *
171 * @return string|bool
172 * The full path to the given destination, FALSE if the destination could not
173 * be resolved.
174 */
175 function omega_drush_resolve_destination($destination) {
176 if (strpos($destination, 'sites/') === 0) {
177 if (in_array(substr($destination, 6), omega_drush_sites())) {
178 return $destination . '/themes';
179 }
180 }
181 elseif (strpos($destination, 'profiles/') === 0) {
182 require_once DRUPAL_ROOT . '/includes/install.core.inc';
183
184 $profile = substr($destination, 9);
185 $profiles = install_find_profiles();
186 if (array_key_exists($profile, $profiles)) {
187 return dirname($profiles[$profile]->uri) . '/themes';
188 }
189 }
190 elseif (strpos($destination, 'themes/') === 0) {
191 $theme = substr($destination, 7);
192
193 if (array_key_exists($theme, list_themes())) {
194 return drupal_get_path('theme', $theme);
195 }
196 }
197
198 return FALSE;
199 }
200
201 /**
202 * Implements drush_hook_COMMAND_validate().
203 */
204 function drush_omega_export_validate($theme = NULL) {
205 $themes = list_themes();
206 // Check if the given theme exists.
207 if (isset($theme) && !isset($themes[$theme])) {
208 return drush_set_error('OMEGA_THEME_NOT_EXISTS', dt('There is no theme with that name.'));
209 }
210 }
211
212 /**
213 * Implements drush_hook_COMMAND().
214 *
215 * Exports the theme settings for the given theme from the database and writes
216 * them into the .info file of that theme.
217 *
218 * @param $theme
219 * (optional) The machine-readable name of a theme.
220 * @return bool
221 * TRUE on success, FALSE on failure.
222 */
223 function drush_omega_export($theme = NULL) {
224 if (!isset($theme) && !$theme = _omega_drush_theme_choice()) {
225 return;
226 }
227
228 $themes = list_themes();
229
230 // Insert the theme settings from the database.
231 if ($settings = variable_get('theme_' . $theme . '_settings')) {
232 foreach (_system_default_theme_features() as $feature) {
233 // Remove the 'toggle_foobar' elements from the theme settings array.
234 unset($settings['toggle_' . $feature]);
235 }
236 }
237 elseif (!drush_confirm(dt('There are no theme settings for @theme stored in the database. Do you want to purge the theme settings from the .info file too?', array('@theme' => $themes[$theme]->info['name'])))) {
238 return;
239 }
240
241 // Parse the current content of the .info file so we can append the settings
242 // from the database.
243 $path = drupal_get_path('theme', $theme) . '/' . $theme . '.info';
244 $data = file_get_contents($path);
245
246 // Remove the old theme settings from the .info file.
247 $data = trim(preg_replace('/^settings\[.*\].*\n?/mi', '', $data));
248
249 // Append the exported theme settings to the .info file if there are any.
250 $data = $settings ? $data . "\n\n" . omega_drush_build_info_file($settings, 'settings') : $data;
251
252 // Write the data to the .info file of the theme.
253 if (file_unmanaged_save_data($data, $path, FILE_EXISTS_REPLACE)) {
254 drush_log(dt('The theme settings for the @theme theme have been exported to the .info file of the theme.', array('@theme' => $themes[$theme]->info['name'])), 'success');
255
256 if (drush_get_option('revert')) {
257 // Revert the theme settings if the 'revert' option is set and they have
258 // been exported successfully. In this case, we invoke the API function
259 // through the drush command to get the print messages displazed.
260 drush_op('drush_omega_revert', $theme);
261 }
262
263 return TRUE;
264 }
265 else {
266 // There was an error while exporting the theme settings.
267 return drush_set_error('OMEGA_EXPORT_ERROR', dt('An error occurred while trying to export the theme settings for the @theme theme.', array('@theme' => $themes[$theme]->info['name'])));
268 }
269 }
270
271 /**
272 * Implements drush_hook_COMMAND_validate().
273 */
274 function drush_omega_revert_validate($theme) {
275 $themes = list_themes();
276 // Check if the given theme exists.
277 if (isset($theme) && !isset($themes[$theme])) {
278 return drush_set_error('OMEGA_THEME_NOT_EXISTS', dt('There is no theme with that name.'));
279 }
280 }
281
282 /**
283 * Implements drush_hook_COMMAND().
284 *
285 * Delete the theme settings that have been stored in the database and thereby
286 * reverts them to the default settings from the .info file.
287 *
288 * @param $theme
289 * The machine-readable name of a theme.
290 */
291 function drush_omega_revert($theme = NULL) {
292 if (!isset($theme) && !$theme = _omega_drush_theme_choice()) {
293 return;
294 }
295
296 $themes = list_themes();
297 // Delete the theme settings variable for the given theme.
298 variable_del('theme_' . $theme . '_settings');
299
300 // Clear the theme settings and extensions cache.
301 cache_clear_all('theme_settings:' . $theme, 'cache');
302 cache_clear_all('omega_extensions:' . $theme, 'cache');
303
304 // Rebuild the theme data for good measure.
305 drupal_theme_rebuild();
306 system_rebuild_theme_data();
307
308 drush_log(dt('You have successfully reverted the theme settings for the @theme theme.', array('@theme' => $themes[$theme]->info['name'])), 'success');
309 }
310
311 /**
312 * Helper function for printing a list of available themes that the user can
313 * choose from in a drush prompt.
314 *
315 * @return bool|string
316 * The machine-readable name of the chosen theme or FALSE if the operation was
317 * cancelled.
318 */
319 function _omega_drush_theme_choice() {
320 $themes = list_themes();
321 $values = array_values($themes);
322 $options = array();
323 foreach ($values as $key => $info) {
324 $options[$key] = $info->info['name'];
325 }
326
327 if (!$theme = drush_choice($options, dt('Which theme do you want to export the theme settings for?'))) {
328 return FALSE;
329 }
330
331 return $values[$theme]->name;
332 }
333
334 /**
335 * Retrieves all themes that can be used as starterkits for a given base theme.
336 *
337 * @param $base
338 * (Optional) The machine-readable name of a base theme to filter all the
339 * available starterkits for.
340 *
341 * @return array
342 * An array of theme names keyed with the machine-readable name of each theme.
343 */
344 function omega_drush_starterkits($base = NULL) {
345 $options = array();
346
347 // Retrieve all the possible starterkits.
348 foreach (list_themes() as $name => $theme) {
349 $info = $theme->info;
350
351 // A starterkit is always flagged as such in the .info file.
352 if (!empty($info['starterkit']) && !empty($info['base theme']) && (!isset($base) || $info['base theme'] == $base)) {
353 $options[$name] = $info['name'] . (!empty($info['description']) ? ': ' . $info['description'] : '');
354 }
355 }
356
357 return $options;
358 }
359
360 /**
361 * Recursively rewrites (and renames) all files in a given path.
362 *
363 * @param $path
364 * The path to rewrite all files in.
365 * @param $search
366 * The string(s) to look for when replacing the file names and contents. Can
367 * be an array or a string.
368 * @param $replace
369 * The string(s) to replace $search with. Can be an array or a string.
370 *
371 * @return bool
372 * TRUE if the operation succeeded, FALSE otherwise.
373 *
374 * @see omega_drush_replace_contents()
375 * @see str_replace()
376 */
377 function omega_drush_rewrite_recursive($path, $search, $replace) {
378 if ($path !== ($new = str_replace($search, $replace, $path))) {
379 // First, try to rename (move) the file if the name was changed.
380 if (!drush_move_dir($path, $new, TRUE)) {
381 return FALSE;
382 };
383 }
384
385 if (is_dir($new)) {
386 // If the file actually is a directory, proceed with the recursion.
387 $directory = dir($new);
388
389 while (FALSE !== ($read = $directory->read())) {
390 if ($read != '.' && $read != '..' ) {
391 if (!omega_drush_rewrite_recursive($new . '/' . $read, $search, $replace)) {
392 return FALSE;
393 }
394 }
395 }
396
397 $directory->close();
398 }
399 elseif (is_file($new)) {
400 // If it is a file, try to replace its contents.
401 $before = file_get_contents($new);
402 if ($before !== ($after = str_replace($search, $replace, $before))) {
403 if (file_unmanaged_save_data($after, $new, FILE_EXISTS_REPLACE) === FALSE) {
404 return FALSE;
405 }
406 }
407 }
408
409 return TRUE;
410 }
411
412 /**
413 * Recursively builds an .info file structure from an array.
414 *
415 * @param $array
416 * The array to build the .info file from.
417 * @param $prefix
418 * (Optional) Used internally to forward the current prefix (level of nesting)
419 * for the keys.
420 *
421 * @return string
422 * A .info file string.
423 */
424 function omega_drush_build_info_file($array, $prefix = FALSE) {
425 $info = '';
426
427 foreach ($array as $key => $value) {
428 if (is_array($value)) {
429 // This is an array, let's proceed with the next level.
430 $info .= omega_drush_build_info_file($value, (!$prefix ? $key : "{$prefix}[{$key}]"));
431 }
432 else {
433 // Escape all single quotes.
434 $value = str_replace("'", "\'", $value);
435 // Wrap the value in single quotes if it has any trailing or leading
436 // whitespace or it is an empty string from the start.
437 $value = $value === '' || trim($value) != $value ? "'" . $value . "'" : $value;
438 // If the key is numeric remove it entirely (simple brackets are enough in
439 // this case).
440 $key = is_numeric($key) ? '' : $key;
441
442 $info .= $prefix ? ("{$prefix}[" . $key .']') : $key;
443 $info .= ' = ' . $value . "\n";
444 }
445 }
446
447 return $info;
448 }
449
450 /**
451 * Helper function for creating and saving a .info file.
452 *
453 * @param $name
454 * The name of the .info file (equivalent to the machine-readable name of the
455 * theme that the .info file belongs to).
456 * @param $data
457 * The content of the .info file.
458 * @param $destination
459 * (Optional) The path in which the .info file should be saved. Defaults to
460 * the path of the theme as defined by $name.
461 *
462 * @return bool
463 * TRUE if the .info file was successfully saved, FALSE otherwise.
464 */
465 function omega_drush_write_info_file($name, $data, $destination = NULL) {
466 $destination = isset($destination) ? $destination : drupal_get_path('theme', $name);
467
468 if (!empty($destination)) {
469 $data = is_array($data) ? omega_drush_build_info_file($data) : $data;
470 return file_unmanaged_save_data($data, $destination . '/' . $name . '.info', FILE_EXISTS_REPLACE);
471 }
472
473 return FALSE;
474 }
475
476 /**
477 * Creates a subtheme from any given starterkit.
478 *
479 * @param $subtheme
480 * An object containing all the required definitions for creating a subtheme.
481 * The available properties are:
482 *
483 * - 'name': The human-readable name of the subtheme.
484 * - 'machine_name': The machine-readable name of the subtheme.
485 * - 'path': The destination that the subtheme should be placed in relative to
486 * the Drupal omega.
487 * - 'starterkit': The machine-readable name of the starterkit that should be
488 * used for creating the subtheme.
489 * - 'default': (optional) Whether the subtheme should be enabled and set as
490 * the default theme after it has been created.
491 * - 'enable': (ptional) Whether the subtheme should be enabled after it has
492 * been created.
493 *
494 * @return bool
495 * TRUE if the subtheme could be successfully created, FALSE otherwise.
496 */
497 function omega_drush_subtheme_create($subtheme) {
498 // Check whether the destination path does not exist and bail out if it does
499 // so we don't delete any important data by accident.
500 if (is_dir($subtheme->path)) {
501 return drush_set_error('OMEGA_SUBTHEME_PATH', dt('The path @path already exists.', array('@path' => $subtheme->path)));
502 }
503
504 $destination = dirname($subtheme->path);
505 if (!is_writable($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
506 return drush_set_error('OMEGA_SUBTHEME_PATH', dt('The path @path is not writable.', array('@path' => $destination)));
507 }
508
509 $themes = list_themes();
510 // Try to copy the starterkit to the destination path of the new subtheme.
511 $source = drupal_get_path('theme', $subtheme->starterkit);
512 if (!drush_copy_dir($source, $subtheme->path)) {
513 return drush_set_error('OMEGA_STARTERKIT', t('The starterkit @starterkit could not be copied.', array('@starterkit' => $themes[$subtheme->starterkit]->info['name'])));
514 }
515
516 // Recursively rewrite the file names and contents of all the files that are
517 // now in the subtheme's directory to represent the human- and
518 // machine-readable names of the subtheme.
519 $search = array($subtheme->starterkit, $themes[$subtheme->starterkit]->info['name']);
520 $replace = array($subtheme->machine_name, $subtheme->name);
521 omega_drush_rewrite_recursive($subtheme->path, $search, $replace);
522
523 // Parse the contents of the current .info file (provided by the starterkit).
524 $subtheme->info = drupal_parse_info_file($subtheme->path . '/' . $subtheme->machine_name . '.info');
525 // Unset all unneeded (and potentially hindering) properties in the .info file
526 // of the subtheme.
527 unset($subtheme->info['starterkit'], $subtheme->info['hidden'], $subtheme->info['locked'], $subtheme->info['project'], $subtheme->info['datestamp']);
528
529 // Put the name and description, as well as a 'dummy' version for the new
530 // subtheme in place.
531 $subtheme->info['name'] = $subtheme->name;
532 $subtheme->info['description'] = $subtheme->description;
533 $subtheme->info['version'] = '1.x';
534
535 if (!empty($subtheme->basetheme)) {
536 $subtheme->info['base theme'] = $subtheme->basetheme;
537 }
538
539 // Overwrite the existing .info file with the changed values.
540 omega_drush_write_info_file($subtheme->machine_name, $subtheme->info, $subtheme->path);
541
542 // Rebuild the theme caches so that we can do some final tasks.
543 drupal_theme_rebuild();
544 system_rebuild_theme_data();
545
546 if (!empty($subtheme->enable) || !empty($subtheme->default)) {
547 // Enable the subtheme.
548 theme_enable(array($subtheme->machine_name));
549
550 if (!empty($subtheme->default)) {
551 // Make the newly created subtheme the default theme.
552 variable_set('theme_default', $subtheme->machine_name);
553 }
554 }
555
556 return TRUE;
557 }
558
559 /**
560 * Retrieve an array of available sites from the sites.php.
561 *
562 * @return array
563 * An array that contains all the available sites in a multisite environment.
564 */
565 function omega_drush_sites() {
566 $return = array('all' => 'all', 'default' => 'default');
567
568 // Load the available sites (multisite installation) from the sites.php file
569 // if it exists.
570 if (file_exists(DRUPAL_ROOT . '/sites/sites.php')) {
571 include DRUPAL_ROOT . '/sites/sites.php';
572
573 if (isset($sites)) {
574 $return = array_merge($return, $sites);
575 }
576 }
577
578 // The 'all' and 'default' destinations are always available.
579 return array_keys($return);
580 }
581
582 /**
583 * Helper function for generating a valid machine-readable name for a theme from
584 * any string.
585 *
586 * @param $string
587 * The string to generate the machine-readable name from.
588 *
589 * @return string
590 * The generated machine-readable name.
591 */
592 function _omega_drush_generate_theme_name($string) {
593 // Machine-readable names have to start with a lowercase letter.
594 $string = preg_replace('/^[^a-z]+/', '', strtolower($string));
595 // Machine-readable names may only contain alphanumeric characters and
596 // underscores.
597 $string = preg_replace('/[^a-z0-9_]+/', '_', $string);
598 // Trim all trailing and leading underscores.
599 $string = trim($string, '_');
600
601 $themes = list_themes();
602 if (isset($themes[$string])) {
603 $plain = $string;
604 $counter = 0;
605
606 while (isset($themes[$string])) {
607 // Make sure that the machine-readable name of the theme is unique.
608 $string = $plain . '_' . $counter++;
609 }
610 }
611
612 return $string;
613 }