Documentation fixes for omega.drush.inc
[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 cache.
301 cache_clear_all('theme_settings:' . $theme, 'cache');
302
303 // Rebuild the theme data for good measure.
304 drupal_theme_rebuild();
305 system_rebuild_theme_data();
306
307 drush_log(dt('You have successfully reverted the theme settings for the @theme theme.', array('@theme' => $themes[$theme]->info['name'])), 'success');
308 }
309
310 /**
311 * Helper function for printing a list of available themes that the user can
312 * choose from in a drush prompt.
313 *
314 * @return bool|string
315 * The machine-readable name of the chosen theme or FALSE if the operation was
316 * cancelled.
317 */
318 function _omega_drush_theme_choice() {
319 $themes = list_themes();
320 $values = array_values($themes);
321 $options = array();
322 foreach ($values as $key => $info) {
323 $options[$key] = $info->info['name'];
324 }
325
326 if (!$theme = drush_choice($options, dt('Which theme do you want to export the theme settings for?'))) {
327 return FALSE;
328 }
329
330 return $values[$theme]->name;
331 }
332
333 /**
334 * Retrieves all themes that can be used as starterkits for a given base theme.
335 *
336 * @param $base
337 * (Optional) The machine-readable name of a base theme to filter all the
338 * available starterkits for.
339 *
340 * @return array
341 * An array of theme names keyed with the machine-readable name of each theme.
342 */
343 function omega_drush_starterkits($base = NULL) {
344 $options = array();
345
346 // Retrieve all the possible starterkits.
347 foreach (list_themes() as $name => $theme) {
348 $info = $theme->info;
349
350 // A starterkit is always flagged as such in the .info file.
351 if (!empty($info['starterkit']) && !empty($info['base theme']) && (!isset($base) || $info['base theme'] == $base)) {
352 $options[$name] = $info['name'] . (!empty($info['description']) ? ': ' . $info['description'] : '');
353 }
354 }
355
356 return $options;
357 }
358
359 /**
360 * Recursively rewrites (and renames) all files in a given path.
361 *
362 * @param $path
363 * The path to rewrite all files in.
364 * @param $search
365 * The string(s) to look for when replacing the file names and contents. Can
366 * be an array or a string.
367 * @param $replace
368 * The string(s) to replace $search with. Can be an array or a string.
369 *
370 * @return bool
371 * TRUE if the operation succeeded, FALSE otherwise.
372 *
373 * @see omega_drush_replace_contents()
374 * @see str_replace()
375 */
376 function omega_drush_rewrite_recursive($path, $search, $replace) {
377 if ($path !== ($new = str_replace($search, $replace, $path))) {
378 // First, try to rename (move) the file if the name was changed.
379 if (!drush_move_dir($path, $new, TRUE)) {
380 return FALSE;
381 };
382 }
383
384 if (is_dir($new)) {
385 // If the file actually is a directory, proceed with the recursion.
386 $directory = dir($new);
387
388 while (FALSE !== ($read = $directory->read())) {
389 if ($read != '.' && $read != '..' ) {
390 if (!omega_drush_rewrite_recursive($new . '/' . $read, $search, $replace)) {
391 return FALSE;
392 }
393 }
394 }
395
396 $directory->close();
397 }
398 elseif (is_file($new)) {
399 // If it is a file, try to replace its contents.
400 $before = file_get_contents($new);
401 if ($before !== ($after = str_replace($search, $replace, $before))) {
402 if (file_unmanaged_save_data($after, $new, FILE_EXISTS_REPLACE) === FALSE) {
403 return FALSE;
404 }
405 }
406 }
407
408 return TRUE;
409 }
410
411 /**
412 * Recursively builds an .info file structure from an array.
413 *
414 * @param $array
415 * The array to build the .info file from.
416 * @param $prefix
417 * (Optional) Used internally to forward the current prefix (level of nesting)
418 * for the keys.
419 *
420 * @return string
421 * A .info file string.
422 */
423 function omega_drush_build_info_file($array, $prefix = FALSE) {
424 $info = '';
425
426 foreach ($array as $key => $value) {
427 if (is_array($value)) {
428 // This is an array, let's proceed with the next level.
429 $info .= omega_drush_build_info_file($value, (!$prefix ? $key : "{$prefix}[{$key}]"));
430 }
431 else {
432 // Escape all single quotes.
433 $value = str_replace("'", "\'", $value);
434 // Wrap the value in single quotes if it has any trailing or leading
435 // whitespace or it is an empty string from the start.
436 $value = $value === '' || trim($value) != $value ? "'" . $value . "'" : $value;
437 // If the key is numeric remove it entirely (simple brackets are enough in
438 // this case).
439 $key = is_numeric($key) ? '' : $key;
440
441 $info .= $prefix ? ("{$prefix}[" . $key .']') : $key;
442 $info .= ' = ' . $value . "\n";
443 }
444 }
445
446 return $info;
447 }
448
449 /**
450 * Helper function for creating and saving a .info file.
451 *
452 * @param $name
453 * The name of the .info file (equivalent to the machine-readable name of the
454 * theme that the .info file belongs to).
455 * @param $data
456 * The content of the .info file.
457 * @param $destination
458 * (Optional) The path in which the .info file should be saved. Defaults to
459 * the path of the theme as defined by $name.
460 *
461 * @return bool
462 * TRUE if the .info file was successfully saved, FALSE otherwise.
463 */
464 function omega_drush_write_info_file($name, $data, $destination = NULL) {
465 $destination = isset($destination) ? $destination : drupal_get_path('theme', $name);
466
467 if (!empty($destination)) {
468 $data = is_array($data) ? omega_drush_build_info_file($data) : $data;
469 return file_unmanaged_save_data($data, $destination . '/' . $name . '.info', FILE_EXISTS_REPLACE);
470 }
471
472 return FALSE;
473 }
474
475 /**
476 * Creates a subtheme from any given starterkit.
477 *
478 * @param $subtheme
479 * An object containing all the required definitions for creating a subtheme.
480 * The available properties are:
481 *
482 * - 'name': The human-readable name of the subtheme.
483 * - 'machine_name': The machine-readable name of the subtheme.
484 * - 'path': The destination that the subtheme should be placed in relative to
485 * the Drupal omega.
486 * - 'starterkit': The machine-readable name of the starterkit that should be
487 * used for creating the subtheme.
488 * - 'default': (optional) Whether the subtheme should be enabled and set as
489 * the default theme after it has been created.
490 * - 'enable': (ptional) Whether the subtheme should be enabled after it has
491 * been created.
492 *
493 * @return bool
494 * TRUE if the subtheme could be successfully created, FALSE otherwise.
495 */
496 function omega_drush_subtheme_create($subtheme) {
497 // Check whether the destination path does not exist and bail out if it does
498 // so we don't delete any important data by accident.
499 if (is_dir($subtheme->path)) {
500 return drush_set_error('OMEGA_SUBTHEME_PATH', dt('The path @path already exists.', array('@path' => $subtheme->path)));
501 }
502
503 $destination = dirname($subtheme->path);
504 if (!is_writable($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
505 return drush_set_error('OMEGA_SUBTHEME_PATH', dt('The path @path is not writable.', array('@path' => $destination)));
506 }
507
508 $themes = list_themes();
509 // Try to copy the starterkit to the destination path of the new subtheme.
510 $source = drupal_get_path('theme', $subtheme->starterkit);
511 if (!drush_copy_dir($source, $subtheme->path)) {
512 return drush_set_error('OMEGA_STARTERKIT', t('The starterkit @starterkit could not be copied.', array('@starterkit' => $themes[$subtheme->starterkit]->info['name'])));
513 }
514
515 // Recursively rewrite the file names and contents of all the files that are
516 // now in the subtheme's directory to represent the human- and
517 // machine-readable names of the subtheme.
518 $search = array($subtheme->starterkit, $themes[$subtheme->starterkit]->info['name']);
519 $replace = array($subtheme->machine_name, $subtheme->name);
520 omega_drush_rewrite_recursive($subtheme->path, $search, $replace);
521
522 // Parse the contents of the current .info file (provided by the starterkit).
523 $subtheme->info = drupal_parse_info_file($subtheme->path . '/' . $subtheme->machine_name . '.info');
524 // Unset all unneeded (and potentially hindering) properties in the .info file
525 // of the subtheme.
526 unset($subtheme->info['starterkit'], $subtheme->info['hidden'], $subtheme->info['locked'], $subtheme->info['project'], $subtheme->info['datestamp']);
527
528 // Put the name and description, as well as a 'dummy' version for the new
529 // subtheme in place.
530 $subtheme->info['name'] = $subtheme->name;
531 $subtheme->info['description'] = $subtheme->description;
532 $subtheme->info['version'] = '1.x';
533
534 if (!empty($subtheme->basetheme)) {
535 $subtheme->info['base theme'] = $subtheme->basetheme;
536 }
537
538 // Overwrite the existing .info file with the changed values.
539 omega_drush_write_info_file($subtheme->machine_name, $subtheme->info, $subtheme->path);
540
541 // Rebuild the theme caches so that we can do some final tasks.
542 drupal_theme_rebuild();
543 system_rebuild_theme_data();
544
545 if (!empty($subtheme->enable) || !empty($subtheme->default)) {
546 // Enable the subtheme.
547 theme_enable(array($subtheme->machine_name));
548
549 if (!empty($subtheme->default)) {
550 // Make the newly created subtheme the default theme.
551 variable_set('theme_default', $subtheme->machine_name);
552 }
553 }
554
555 return TRUE;
556 }
557
558 /**
559 * Retrieve an array of available sites from the sites.php.
560 *
561 * @return array
562 * An array that contains all the available sites in a multisite environment.
563 */
564 function omega_drush_sites() {
565 $sites = array('all' => 'all');
566
567 // Load the available sites (multisite installation) from the sites.php file
568 // if it exists.
569 if (file_exists(DRUPAL_ROOT . '/sites/sites.php')) {
570 include DRUPAL_ROOT . '/sites/sites.php';
571 }
572
573 // The 'all' destination is always available.
574 return array_keys(array_merge($sites));
575 }
576
577 /**
578 * Helper function for generating a valid machine-readable name for a theme from
579 * any string.
580 *
581 * @param $string
582 * The string to generate the machine-readable name from.
583 *
584 * @return string
585 * The generated machine-readable name.
586 */
587 function _omega_drush_generate_theme_name($string) {
588 // Machine-readable names have to start with a lowercase letter.
589 $string = preg_replace('/^[^a-z]+/', '', strtolower($string));
590 // Machine-readable names may only contain alphanumeric characters and
591 // underscores.
592 $string = preg_replace('/[^a-z0-9_]+/', '_', $string);
593 // Trim all trailing and leading underscores.
594 $string = trim($string, '_');
595
596 $themes = list_themes();
597 if (isset($themes[$string])) {
598 $plain = $string;
599 $counter = 0;
600
601 while (isset($themes[$string])) {
602 // Make sure that the machine-readable name of the theme is unique.
603 $string = $plain . '_' . $counter++;
604 }
605 }
606
607 return $string;
608 }