<?php
// $Id: zenophile.module,v 1.8 2009/08/24 18:44:55 garrettalbright Exp $

/**
 * @file
 * Creates Zen subthemes quickly and easily.
 *
 * Zenophile is a tiny module which allows themers to very easily create Zen
 * subthemes without all the tedious file copying and find-and-replacing
 * required when creating subthemes by hand. With this module, subthemes can be
 * created in a fraction of the time just by entering information into a
 * single-page form and clicking "Submit."
 */

/**
 * Implementation of hook_menu().
 */

function zenophile_menu() {
  return array(
    'admin/build/themes/zenophile' => array(
      'title' => 'Create Zen subtheme',
      'description' => 'Quickly create a Zen subtheme for theming.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array('zenophile_create'),
      'access arguments' => array('create zen theme with zenophile'),
      'type' => MENU_LOCAL_TASK,
    ),
  );
}

/**
 * Implementation of hook_perm().
 */

function zenophile_perm() {
  return array('create zen theme with zenophile');
}

/**
 * Form to create the subtheme. drupal_get_form() callback.
 */

function zenophile_create() {
  // Check for Zen
  if (drupal_get_path('theme', 'STARTERKIT') === '') {
    drupal_set_message(t('The STARTERKIT theme could not be found. Please check that the <a href="!zen">Zen theme</a> is properly installed.', array('!zen' => 'http://drupal.org/project/zen')), 'error');
  }
  else {
    $zen_based = array();
    foreach (list_themes(TRUE) as $theme) {
      if (isset($theme->base_theme) && $theme->base_theme === 'zen') {
        $zen_based[$theme->name] = t('@tname (@tsname)', array('@tname' => $theme->info['name'], '@tsname' => $theme->name));
      }
    }
    return array(
      'parent' => array(
        '#title' => t('Starter theme'),
        '#description' => t('The parent theme for the new theme. If in doubt, select &ldquo;STARTERKIT&rdquo;.'),
        '#type' => 'select',
        '#options' => $zen_based,
        '#default_value' => 'STARTERKIT',
        '#required' => TRUE,
        '#weight' => 0,
      ),
      'sysname' => array(
        '#title' => t('System name'),
        '#description' => t('The machine-compatible name of the new theme. This name may only consist of lowercase letters plus the underscore character.'),
        '#type' => 'textfield',
        '#required' => TRUE,
        '#weight' => 10,
      ),
      'friendly' => array(
        '#title' => t('Human name'),
        '#description' => t('A human-friendly name for the new theme. This name may contain uppercase letters, spaces, punctuation, etc. If left blank, the system name will also be used here.'),
        '#type' => 'textfield',
        '#weight' => 20,
      ),
      'description' => array(
        '#title' => t('Description'),
        '#description' => t('A short description of this theme.'),
        '#type' => 'textfield',
        '#required' => TRUE,
        '#weight' => 30,
      ),
      'site' => array(
        '#title' => t('Site directory'),
        '#description' => t('Which site directory will the new theme to be placed in? If in doubt, select &ldquo;all&rdquo;.'),
        '#type' => 'select',
        '#options' => _zenophile_find_sites(),
        '#default_value' => array('all'),
        '#required' => TRUE,
        '#weight' => 50,
      ),
      'layout_fset' => array(
        '#title' => t('Customize layout'),
        '#type' => 'fieldset',
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#weight' => 60,
        'layout' => array(
          '#title' => t('Layout type'),
          '#description' => t('Fixed layouts are always the same width. Liquid layouts adjust their width to fit the browser window. If in doubt, try a fixed layout.'),
          '#type' => 'radios',
          '#options' => array(
            'fixed' => t('Fixed'),
            'liquid' => t('Liquid'),
          ),
          '#default_value' => 'fixed',
          '#required' => TRUE,
          '#weight' => 0,
        ),
        'sidebars_fset' => array(
          '#title' => t('Adjust sidebar widths'),
          '#type' => 'fieldset',
          '#collapsible' => FALSE,
          '#collapsed' => FALSE,
          '#weight' => 10,
          'desc' => array(
            '#type' => 'item',
/*             '#value' => t('If you wish to change the widths and positions of the page wrapper div (#page) and standard sidebars from their defaults, you may do so using the fields below. Zenophile will do the math for you and add the measurements to a new CSS file named sidebars.css, which will be placed in your new theme&rsquo;s directory and included in the theme&rsquo;s .info file. Note that some parent themes may remove one or both of the standard sidebars and/or add their own additional sidebars.'), */
            '#value' => t('The widths and position of the new themes theme&rsquo;s sidebars and main content area may be altered with the fields below. If the default values are altered, Zenophile will do the math and add the rules to a new CSS file named &ldquo;sidebars.css&rdquo; which will be added to the new theme. The default values provided assume the STARTERKIT starter theme is being used. Note that some starter themes may add or remove sidebars; you may have varying success with starter themes other than STARTERKIT.'),
            '#weight' => 0,
          ),
          'page' => array(
            '#title' => t('Page wrapper width (#page)'),
            '#type' => 'textfield',
            '#size' => 4,
            '#field_suffix' => 'px',
            '#default_value' => '960',
            '#description' => t('This value is ignored if your theme will have a liquid layout.'),
            '#weight' => 10,
          ),
          'sidebar-left' => array(
            '#title' => t('Left sidebar width (#sidebar-left)'),
            '#type' => 'textfield',
            '#size' => 4,
            '#field_suffix' => 'px',
            '#default_value' => '200',
            '#weight' => 20,
          ),
          'sidebar-right' => array(
            '#title' => t('Right sidebar width (#sidebar-right)'),
            '#type' => 'textfield',
            '#size' => 4,
            '#field_suffix' => 'px',
            '#default_value' => '200',
            '#weight' => 30,
          ),
          'sidebar-pos' => array(
            '#title' => t('Sidebar positioning'),
            '#type' => 'radios',
            '#options' => array(
              'normal' => t('Sidebars on their respective sides (left, main, right)'),
              'left' => t('Both sidebars on left (left, right, main)'),
              'right' => t('Both sidebars on right (main, left, right)'),
            ),
            '#default_value' => 'normal',
            '#weight' => 40,
          ),
        ),
      ),
/*
      'tweaks_fset' => array(
        '#title' => t('Other tweaks'),
        '#type' => 'fieldset',
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#weight' => 80,
        'fresh' => array(
          '#title' => t('Create fresh CSS file'),
          '#description' => t('If checked, Zenophile will create a new empty CSS file and add it to the theme via its .info file. Some themers may prefer to start with a fresh empty CSS file rather than adapting the pre-created CSS files which will be copied over from the parent theme directory.'),
          '#type' => 'checkbox',
          '#default_value' => TRUE,
          '#weight' => 60,
        ),
      ),
*/
      // TODO: Regions, mixins?
      'fresh' => array(
        '#title' => t('Create fresh CSS file'),
        '#description' => t('If checked, Zenophile will add a blank CSS file named &ldquo;[theme_name]-fresh.css&rdquo; to the new theme. Some themers prefer to start with a blank CSS file rather than adapt the pre-created CSS files which will be copied over from the parent theme directory.'),
        '#type' => 'checkbox',
        '#default_value' => TRUE,
        '#weight' => 70,
      ),
      'submit' => array(
        '#type' => 'submit',
        '#value' => t('Submit'),
        '#weight' => 1000,
      ),
    );
  }
}

/**
 * Validate function for zenophile_create().
 */

function zenophile_create_validate($form, &$form_state) {
  // Check that the system name of the theme is valid
  if (preg_match('/[^a-z_]/', $form_state['values']['sysname'])) {
    form_set_error('sysname', t('The <em>System name</em> may only consist of lowercase letters and the underscore character.'));
  }
  if (drupal_get_path('theme', $form_state['values']['sysname'])) {
    form_set_error('sysname', t('A theme with this <em>System name</em> already exists. Cowardly refusing to create another one.'));
  }
  if (in_array($form_state['values']['sysname'], array('layout', 'print', 'sidebars'))) {
    // drupal6-reference and html-elements should also be excluded, but the
    // regex above will catch those since they have hyphens.
    form_set_error('sysname', t('That <em>System name</em> value cannot be used. Zenophile will need to create @sysname.css to continue, but that filename is reserved for another important Zen CSS file. Please choose a different <em>System name</em> value.', array('@sysname' => $form_state['values']['sysname'])));
  }
  // Test if we can make these directories
  $site_dir = 'sites/' . $form_state['values']['site'];
  $themes_dir = $site_dir . '/themes';
  if (!file_exists($themes_dir) && !mkdir($themes_dir, 0755)) {
    form_set_error('site', t('The <em>themes</em> directory for the %site site directory does not exist, and it could not be created automatically. This is likely a permissions problem. Check that the web server has permissions to write to the %site directory, or create the %themes directory manually and try again.', array('%site' => $site_dir, '%themes' => $themes_dir)), 'error');
  }
  else {
    $dir = "{$themes_dir}/{$form_state['values']['sysname']}";
    // Make the theme directory
    if (file_exists($dir)) {
      // This theoretically should have been caught by the validate function
      // above, but it's possible that there's a directory in this site's
      // themes directory which is not a proper theme… or it's a regular file.
      form_set_error('sysname', t('That <em>System name</em> value cannot be used with that <em>Site directory</em> value. Zenophile wants to create and use the directory %dir, but a file or directory with that name already exists.', array('%dir' => $dir)));
      
    }
    elseif (!mkdir($dir, 0755)) {
      form_set_error('sysname', t('The directory %dir could not be created. This is likely a permissions problem. Check that the web server has permissions to write to the %themes directory.', array('%dir' => $dir, '%themes' => $themes_dir)));
    }
  }
}

/**
 * Submit function for zenophile_create().
 */

function zenophile_create_submit($form, &$form_state) {
  $dir = "sites/{$form_state['values']['site']}/themes/{$form_state['values']['sysname']}";
  $parent_dir = drupal_get_path('theme', $form_state['values']['parent']);
  $zen_dir = drupal_get_path('theme', 'zen');
  $parent_info = $form_state['values']['parent'] . '.info';
  $parent_css = $form_state['values']['parent'] . '.css';
  $path_part = "{$dir}/{$form_state['values']['sysname']}";


  $h = opendir($parent_dir);  
  while (($file = readdir($h)) !== FALSE) {
    // We're not just using _zenophile_rcopy() here because we want to
    // exclude some files.
    $fpath = "{$parent_dir}/{$file}";
    if ($file[0] !== '.' && $file !== $parent_info && $file !== 'template.php' && $file !== 'theme-settings.php') {
      // Copy and rename the parent CSS file
      if ($file === $parent_css) {
        copy($fpath, "{$dir}/{$form_state['values']['sysname']}.css");
      }
      else {
        _zenophile_rcopy($fpath, "{$dir}/{$file}");
      }
    }
  }
  // Now take care of STARTERKIT.info. Step 2.
  // Load the info file into a string.
  $info = file_get_contents("{$parent_dir}/{$parent_info}");
  // Reset the \$Id\$ string
  $info = preg_replace('/^; \$Id.*\$$/m', '; \$Id\$', $info, 1);
  // Build replacement arrays. We definitely want to replace the name and
  // description.
  $from = array(
    "/{$form_state['values']['parent']}/",
    '/^name\s*=.*/m',
    '/^description\s*=.*/m',
  );
  $to = array(
    $form_state['values']['sysname'],
    'name        = ' . ($form_state['values']['friendly'] === '' ? $form_state['values']['sysname'] : $form_state['values']['friendly']),
    'description = ' . $form_state['values']['description'],
  );
  
  // Do we also want to add the fresh stylesheet?
  if ($form_state['values']['fresh']) {
    $from[] = '/^stylesheets\[all\]\[\]\s*=\s*zen\.css$/m';
    $to[] = "stylesheets[all][]   = zen.css\n\n  ; Specifying a nice clean stylesheet\nstylesheets[all][] = {$form_state['values']['sysname']}-fresh.css";
    // Make the blank stylesheet file
    touch($path_part . '-fresh.css');
  }
  
  // Do we want to do sidebars.css?
  if ($form_state['values']['sidebar-left'] !== '200' || $form_state['values']['sidebar-right'] !== '200' || $form_state['values']['page'] !== '960' || $form_state['values']['sidebar-pos'] !== 'normal') {
    $page = intval($form_state['values']['page']);
    $ls = intval($form_state['values']['sidebar-left']);
    $rs = intval($form_state['values']['sidebar-right']);
    $ls_content_width = $page - $ls;
    $rs_content_width = $page - $rs;
    $both_sidebars_width = $ls + $rs;
    $both_content_width = $page - $both_sidebars_width;
    if ($form_state['values']['sidebar-pos'] === 'left') {
      if ($form_state['values']['layout'] === 'fixed') {
        // All of these huge godawful blobs need to go in separate files or
        // something. But we still need to do string replacement on them…
        // Hmm.
        $sidebars = <<<END
/* \$Id\$ */

/* Fixed layout; both sidebars on left */

#page, .no-sidebars #content {
  width: {$page}px;
}

#sidebar-left {
  width: {$ls}px;
  margin-right: -{$ls}px;
}

#sidebar-right {
  width: {$rs}px;
}

.two-sidebars #sidebar-right {
  margin-left: {$ls}px;
  margin-right: -{$both_sidebars_width}px;
}

.sidebar-right #sidebar-right {
  margin-left: 0px;
  margin-right: -{$rs}px;
}

#sidebar-right-inner {
  margin-left: 0px;
  margin-right: 20px;
}

.no-sidebars #content, .one-sidebar #content, .two-sidebars #content {
  margin-right: -{$page}px;
}

.sidebar-left #content {
  margin-left: {$ls}px;
  width: {$ls_content_width}px;
}

.sidebar-right #content {
  margin-left: {$rs}px;
  width: {$rs_content_width}px;
}

.two-sidebars #content {
  margin-left: {$both_sidebars_width}px;
  width: {$both_content_width}px;
}
END;
      }
      else {
        $sidebars = <<<END
/* \$Id\$ */

/* Liquid layout; both sidebars on left */

#sidebar-left {
  width: {$ls}px;
  margin-right: -{$ls}px;
}

#sidebar-right {
  float: left;
  width: {$rs}px;
}

.two-sidebars #sidebar-right {
  margin-left: {$ls}px;
  margin-right: -{$both_sidebars_width}px;
}

.sidebar-right #sidebar-right {
  margin-left: 0px;
  margin-right: -{$rs}px;
}

#sidebar-right-inner {
  margin-left: 0px;
  margin-right: 20px;
}

.sidebar-left #content-inner {
  padding-left: {$ls}px;
}

.sidebar-right #content-inner {
  padding-left: {$rs}px;
  padding-right: 0px;
}

.two-sidebars #content-inner {
  padding-left: {$both_sidebars_width}px;
  padding-right: 0px;
}
END;
      }
    }
    elseif ($form_state['values']['sidebar-pos'] === 'right') {
      if ($form_state['values']['layout'] === 'fixed') {
              $sidebars = <<< END
/* \$Id\$ */

/* Fixed layout; both sidebars on right */

#page, .no-sidebars #content {
  width: {$page}px;
}

#sidebar-left {
  width: {$ls}px;
}

#sidebar-right {
  width: {$rs}px;
  margin-left: {$rs_content_width}px;
  margin-right: -{$page}px;
}

.two-sidebars #sidebar-left {
  margin-left: {$both_content_width}px;
  margin-right: -{$rs_content_width}px;
}

.two-sidebars #sidebar-right {
  margin-right: -{$page}px;
}

.sidebar-left #sidebar-left {
  margin-left: {$ls_content_width}px;
  margin-right: -{$page}px;
}

#sidebar-left-inner {
  margin-left: 20px;
  margin-right: 0px;
}

.sidebar-right #sidebar-right {
  margin-left: {$rs_content_width}px;
}

.no-siebars #content, .one-sidebar #content, .two-sidebars #content {
  margin-left: 0px;
}

.sidebar-left #content {
  width: {$ls_content_width}px;
  margin-right: -{$ls_content_width}px;
}

.sidebar-right #content {
  width: {$rs_content_width}px;
  margin-right: -{$rs_content_width}px;
}

.two-sidebars #content {
  width: {$both_content_width}px;
  margin-right: -{$both_content_width}px;
}
END;
      }
      else {
        $sidebars = <<< END
/* \$Id\$ */

/* Liquid layout; both sidebars on right */

#sidebar-left {
  width: {$ls}px;
  float: right;
  margin-right: 0px;
}

#sidebar-right {
  width: {$rs}px;
}

#sidebar-left-inner {
  margin-left: 20px;
  margin-right: 0px;
}

.sidebar-left #content-inner {
  padding-left: 0px;
  padding-right: {$ls}px;
}

.sidebar-right #content-inner {
  padding-right: {$rs}px;
}

.two-sidebars #sidebar-left {
  margin-right: {$rs}px;
}

.two-sidebars #sidebar-right {
  margin-right: -{$both_sidebars_width}px;
}

.two-sidebars #content-inner {
  padding-left: 0px;
  padding-right: {$both_sidebars_width}px;
}
END;
      }
    }
    else {
      if ($form_state['values']['layout'] === 'fixed') {
        $sidebars = <<<END
/* \$Id\$ */

/* Fixed layout; standard sidebar placement */

#page, .no-sidebars #content {
  width: {$page}px;
}

#sidebar-left {
  width: {$ls}px;
  margin-right: -{$ls}px;
}

#sidebar-right {
  width: {$rs}px;
  margin-left: {$rs_content_width}px;
  margin-right: -{$page}px;
}

#content {
  margin-right: -{$page}px;
}

.sidebar-left #content {
  width: {$ls_content_width}px;
  margin-left: {$ls}px;
  margin-right: -{$page}px;
}

.sidebar-right #content {
  width: {$rs_content_width}px;
  margin-right: -{$rs_content_width}px;
}

.two-sidebars #content {
  width: {$both_content_width}px;
  margin-left: {$ls}px;
  margin-right: -{$rs_content_width}px;
}
END;
      }
      else {
        $sidebars = <<<END
/* \$Id\$ */

/* Liquid layout; standard sidebar placement */

#sidebar-left {
  width: {$ls}px;
}

#sidebar-right {
  width: {$rs}px;
}

.sidebar-left #content-inner, .two-sidebars #content-inner {
  padding-left: {$ls}px;
}

.sidebar-right #content-inner, .two-sidebars #content-inner {
  padding-right: {$rs}px;
}
END;
      }
    }
    // Write the CSS file
    file_put_contents($dir . '/sidebars.css', $sidebars);
    // Add replacement for .info file
    $from[] = '/^stylesheets\[all\]\[\]\s*=\s*zen\.css$/m';
    $to[] = "stylesheets[all][]  = zen.css\n  ; Customized sidebar/content widths and positions\nstylesheets[all][]    = sidebars.css";
  }
  // Do replacement and write the info file
  $info = preg_replace($from, $to, $info);
  file_put_contents($path_part . '.info', $info);
  
  // Copy the liquid or fixed stylesheet, zen.css, print.css and
  // html-elements.css. Steps 3 through 6. Only do this if the parent is
  // STARTERKIT - otherwise these should have already been copied.
  if ($form_state['values']['parent'] === 'STARTERKIT') {
    copy("{$zen_dir}/layout-{$form_state['values']['layout']}.css", $dir . '/layout.css');
    copy($zen_dir . '/print.css', $dir . '/print.css');
    copy($zen_dir . '/html-elements.css', $dir . '/html-elements.css');
    // If there is a starter_theme.css file in the directory already,
    // rename it to this_theme.css. Otherwise, copy over zen.css and
    // rename it.
    $parent_css = "{$dir}/{$form_state['values']['parent']}.css";
    if (file_exists($parent_css)) {
      rename($parent_css, "{$dir}/{$form_state['values']['sysname']}.css");
    }
    else {
      copy($zen_dir . '/zen.css', "{$dir}/{$form_state['values']['sysname']}.css");
    }
  }
  
  // Copy template.php and theme-settings.php and replace STARTERKIT.
  // Kind of Step 1 plus Step 7 mixed together.
  $info = file_get_contents($parent_dir . '/template.php');
  $info = str_replace($form_state['values']['parent'], $form_state['values']['sysname'], $info);
  file_put_contents($dir . '/template.php', $info);
  $info = file_get_contents($parent_dir . '/theme-settings.php');
  $info = str_replace($form_state['values']['parent'], $form_state['values']['sysname'], $info);
  file_put_contents($dir . '/theme-settings.php', $info);

  drupal_set_message(t('A new subtheme was successfully created in %dir.', array('%dir' => $dir)));
  
  // Flush the cached theme data so the new subtheme appears in the parent
  // theme list
  system_theme_data();
}

/**
 * List this Drupal installation's site directories.
 *
 * @return An array of directories in the sites directory.
 */

function _zenophile_find_sites() {
  $sites = array();
  if ($h = opendir('sites')) {
    while (($site = readdir($h)) !== FALSE) {
      $sitepath = 'sites/' . $site;
      // Don't allow dot files or links for security reasons (redundancy, too)
      if (is_dir($sitepath) && !is_link($sitepath) && $site{0} !== '.') {
        $sites[] = $site;
      }
    }
    closedir($h);
    return drupal_map_assoc($sites);
  }
  else {
    drupal_set_message(t('The <em>sites</em> directory could not be read.'), 'error');
    return array();
  }
}

/**
 * Copy a file, or recursively copy a directory.
 *
 * Adapted from code created by Aidan Lister:
 * http://aidanlister.com/2004/04/recursively-copying-directories-in-php/
 *
 * @param $source
 *   The source path
 * @param $dest
 *   The destination path
 * @return TRUE or FALSE depending on success.
 */

function _zenophile_rcopy($source, $dest) {

  // Check for symlinks
  if (is_link($source)) {
    return symlink(readlink($source), $dest);
  }

  // Simple copy for a file
  if (is_file($source)) {
    return copy($source, $dest);
  }

  // Make destination directory
  if (!is_dir($dest)) {
    mkdir($dest, 0755);
  }

  // Loop through the folder
  $dir = opendir($source);
  while (($entry = readdir($dir)) !== FALSE) {
    // Skip pointers
    if ($entry[0] !== '.') {
    // Deep copy directories
    _zenophile_rcopy("{$source}/{$entry}", "{$dest}/{$entry}");
    }
  }

  // Clean up
  closedir($dir);
  return TRUE;
}