You are here

patterns.module in Patterns 5

Enables extremely simple adding/removing features to your site with minimal to no configuration

File

patterns.module
View source
<?php

/**
 * @file
 * Enables extremely simple adding/removing features to your site with minimal to no configuration
 */

/**
 * @todo:
 * @ Enable pattern configurations
 * @ **done**Enable actions to see ids created/updated from other actions inside the pattern (tokens?)
 * @ **semi-done** Reset patterns
 * @ Enable components to analyze the current pattern for better validation
 * @ Allow module version restricting
 * @ Put in functionality to auto-download modules (and the correct version)
 * @ Enable backups before running patterns and reverting back to those backups
 * @ Implement a progress meter
 * @ Handle default values better to allow for absolute minimal actions
 * @ Enable interactive actions by allowing patterns to resume from a saved position
 * @ Implement an export feature for all available form_ids
 * @ Allow resuming failed patterns
 * @ In the pattern details, list any sub-patterns directly on the patterns listing page
 */

/**
 * Implementation of hook_perm().
 */
function patterns_perm() {
  return array(
    'administer patterns',
  );
}

/**
 * Implementation of hook_menu().
 */
function patterns_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/build/patterns',
      'description' => t('Administer patterns available for your site'),
      'access' => user_access('administer patterns'),
      'title' => t('Patterns'),
      'callback' => 'patterns_list',
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/list',
      'title' => t('List'),
      'callback' => 'patterns_list',
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => -10,
    );

    //     $items[] = array('path' => 'admin/build/patterns/settings',
    //       'title' => t('Settings'),
    //       'callback' => 'drupal_get_form',
    //       'callback arguments' => array('patterns_settings'),
    //       'type' => MENU_LOCAL_TASK,
    //       'weight' => 10
    //     );
    $items[] = array(
      'path' => 'admin/build/patterns/configure',
      'title' => t('Configure Pattern'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'patterns_configure_pattern',
      ),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/edit',
      'title' => t('Edit Pattern'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'patterns_edit',
      ),
      'type' => MENU_CALLBACK,
    );

    //     $items[] = array('path' => 'admin/build/patterns/info',
    //       'title' => t('Pattern Details'),
    //       'callback' => 'patterns_info',
    //       'type' => MENU_CALLBACK
    //     );
    $items[] = array(
      'path' => 'admin/build/patterns/enable',
      'access' => user_access('administer patterns'),
      'title' => t('Enable Pattern'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'patterns_enable_pattern',
      ),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/disable',
      'access' => user_access('administer patterns'),
      'title' => t('Disable Pattern'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'patterns_disable_pattern',
      ),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/modules',
      'access' => user_access('administer patterns'),
      'title' => t('Pattern Modules'),
      'callback' => 'patterns_modules_page',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/revert',
      'access' => user_access('administer patterns'),
      'title' => t('Revert Pattern'),
      'callback' => 'patterns_revert',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/import',
      'access' => user_access('administer patterns'),
      'title' => t('Import'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'patterns_import_source',
      ),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/import/xmltext',
      'access' => user_access('administer patterns'),
      'title' => t('Import via XML Source'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/import/xmlfile',
      'access' => user_access('administer patterns'),
      'title' => t('Import via XML File'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'patterns_import_file',
      ),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/build/patterns/import/xmlurl',
      'access' => user_access('administer patterns'),
      'title' => t('Import via XML URL'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'patterns_import_url',
      ),
      'type' => MENU_LOCAL_TASK,
    );

    //     $items[] = array('path' => 'admin/build/patterns/import/server',
    //       'access' => user_access('administer patterns'),
    //       'title' => t('Import via Patterns Server'),
    //       'callback' => 'drupal_get_form',
    //       'callback arguments' => array('patterns_import_server'),
    //       'type' => MENU_LOCAL_TASK
    //     );
  }
  return $items;
}

/**
 * Display the import pattern form
 */
function patterns_import_source() {
  $form['xmlname'] = array(
    '#type' => 'textfield',
    '#title' => t('Pattern Identifier'),
    '#description' => t('Machine readable name for the pattern. The actual title should be included in the pattern itself.'),
    '#required' => true,
  );
  $form['xmlsource'] = array(
    '#type' => 'textarea',
    '#rows' => 15,
    '#title' => t('Enter Pattern XML'),
    '#description' => t('Imported patterns are not executed until you run them manually. You can leave off the <?xml> tag at the top.'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  $form['#base'] = 'patterns_import';
  return $form;
}

/**
 * Display the pattern settings form
 */

// function patterns_settings() {
//   drupal_set_message('This feature not implemented yet.');
// }

/**
 * Display the import pattern from server form
 */

// function patterns_import_server() {
//   drupal_set_message('This feature not implemented yet.');
// }

/**
 * Display the import pattern file form
 */
function patterns_import_file() {
  $form['#attributes']['enctype'] = 'multipart/form-data';
  $form['xmlfile'] = array(
    '#type' => 'file',
    '#title' => t('Upload XML Pattern File'),
    '#description' => t('Imported patterns are not executed until you run them manually.'),
    '#size' => 48,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  $form['#base'] = 'patterns_import';
  return $form;
}

/**
 * Display the import pattern url form
 */
function patterns_import_url() {
  $form['xmlurl'] = array(
    '#type' => 'textfield',
    '#title' => t('Specify an XML url'),
    '#description' => t('Import a pattern from a remote URL. Imported patterns are not executed until you run them manually.'),
    '#size' => 48,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  $form['#base'] = 'patterns_import';
  return $form;
}
function patterns_import_validate($form_id, $values) {
  global $form_values;
  if ($file = file_check_upload('xmlfile')) {
    $form_values['xmlsource'] = file_get_contents($file->filepath);
  }
  else {
    if (isset($values['xmlfile'])) {
      form_set_error('xmlfile', t('Error uploading XML file.'));
    }
    else {
      if ($values['xmlurl']) {
        if (!ini_get('allow_url_fopen')) {
          form_set_error('xmlsource', t('allow_url_fopen must be enabled in your php configuration in order to use this feature.'));
          return;
        }
        if (!($form_values['xmlsource'] = file_get_contents($values['xmlurl']))) {
          form_set_error('xmlsource', t('Failed to retreive the pattern specified.'));
          return;
        }
        $form_values['xmlname'] = preg_replace('/\\.[^\\.]*$/', '', basename($form_values['xmlurl']));
      }
    }
  }
  if (strpos($form_values['xmlsource'], '<?xml') !== 0) {
    $form_values['xmlsource'] = '<?xml version="1.0" encoding="ISO-8859-1"?>' . $form_values['xmlsource'];
  }
  if ($form_values['xmlname'] && preg_match('/[^a-zA-Z0-9_]/', $form_values['xmlname'])) {
    form_set_error('xmlname', t('You can only include letters, numbers, and underscores in the pattern identifier.'));
  }
  else {
    if ($form_values['xmlname'] && preg_match('/^_/', $form_values['xmlname'])) {
      form_set_error('xmlname', t('You cannot start the pattern identifier with an underscore.'));
    }
  }
  $parse = xml_parser_create();
  xml_parse_into_struct($parse, $form_values['xmlsource'], $vals, $index);

  // Check that the xml was properly parsed and also that the
  // root <pattern> tag and also an <info> tag were used.
  if (!$vals || $vals[0]['tag'] != 'PATTERN' || $vals[1]['tag'] != 'INFO') {
    form_set_error('xmlsource', t('Error parsing the XML, please check your syntax and try again.'));
  }
}
function patterns_import_submit($form_id, $form_values) {
  if ($file = file_check_upload('xmlfile')) {

    // Currently nothing is happening with files saved here....
    if (file_check_directory(file_create_path(variable_get('patterns_save_xml', 'patterns')))) {
      file_save_upload('xmlfile', variable_get('patterns_save_xml', 'patterns'));
    }
    else {
      drupal_set_message(t('Pattern was registered but the file was not saved on the server because of improperly setup files directory.'));
    }
  }
  else {
    if ($form_values['xmlsource']) {
      file_save_data($form_values['xmlsource'], variable_get('patterns_save_xml', 'patterns') . '/' . $form_values['xmlname'] . '.xml', FILE_EXISTS_REPLACE);
    }
  }

  // Reload patterns
  patterns_get_patterns(true);
  return 'admin/build/patterns';
}
function patterns_list() {
  drupal_add_css(drupal_get_path('module', 'patterns') . '/patterns.css');
  drupal_add_js(drupal_get_path('module', 'patterns') . '/patterns.js');
  patterns_load();
  $patterns = patterns_get_patterns();
  if (empty($patterns)) {
    return t('No patterns available.');
  }

  //   $header = array(t('Title'), t('Status'), t('Version'), t('Public'), t('Actions'));
  $header = array(
    t('Title'),
    t('Status'),
    t('Version'),
    t('Actions'),
  );

  // List all patterns
  $rows = array();
  foreach ($patterns as $pid => $pattern) {
    $actions = array();
    if (!$pattern->status) {
      $actions[] = l(t('Run'), 'admin/build/patterns/enable/' . $pid);
    }
    else {
      if ($pattern->enabled >= $pattern->updated) {
        $actions[] = l(t('Re-Run'), 'admin/build/patterns/enable/' . $pid);
      }
      else {
        $actions[] = l(t('Run Update'), 'admin/build/patterns/enable/' . $pid);
      }
    }
    $actions[] = l(t('Edit'), 'admin/build/patterns/edit/' . $pid);
    $actions = implode('&nbsp;&nbsp;', $actions);
    $cells = array();

    //     $title = l($pattern->title, 'admin/build/patterns/info/'. $pid, array('class' => 'pattern-title', 'id' => 'pid-'. $pid));
    $title = '<span id="pid-' . $pid . '" class="pattern-title">' . $pattern->title . '</span>';

    //     $view_more = '<div>'. t('Clik on pattern title to see more details.') .'</div>';
    $info = array();
    $info[] = t('Author: ') . $pattern->info['author'];
    $info[] = t('Email: ') . $pattern->info['author_email'];
    $info[] = t('Web: ') . $pattern->info['author_website'];
    $author = theme('item_list', $info);
    $title .= '<div id="pid-' . $pid . '-info" class="pattern-info">' . $author . $pattern->description . $view_more . '</div>';
    $cells[] = $title;
    $cells[] = $pattern->status ? t('Enabled') : t('Disabled');
    $cells[] = $pattern->info['version'];

    //     $cells[] = $pattern->public ?  t('Yes') : t('No');
    $cells[] = $actions;
    $category = $pattern->info['category'] ? $pattern->info['category'] : t('Other');
    $rows[$category][] = $cells;
  }
  ksort($rows);
  foreach ($rows as $title => $category) {
    $fieldset = array(
      '#title' => t($title),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => theme('table', $header, $category),
    );
    $output .= theme('fieldset', $fieldset);
  }
  return $output;
}

/**
 * Menu callback to undo a patterns update changes
 */
function patterns_revert($pid) {
  if ($name = db_result(db_query('SELECT name FROM {patterns} WHERE pid = "%d"', $pid))) {
    $path = file_create_path(variable_get('patterns_save_xml', 'patterns') . '/enabled/' . $name . '.xml');
    $new = db_result(db_query('SELECT file FROM {patterns} WHERE pid = "%d"'));
  }
  else {
    drupal_set_message(t('The pattern you specified does not exist.'), 'error');
    drupal_goto('admin/build/patterns');
  }
  if (file_exists($path)) {
    if (file_move($path, $new, FILE_EXISTS_REPLACE)) {
      drupal_set_message(t('This pattern was reverted to the state it was at when it was enabled.'));
      drupal_goto();
    }
  }
  else {
    drupal_set_message(t('The patterns enabled-state was not saved properly, therefore it cannot be reverted.'), 'error');
  }
  drupal_goto('admin/build/patterns');
}

/**
 * Menu callback to display patterns details page
 */

// function patterns_info($pid = null) {
//   if (!is_numeric($pid)) {
//     drupal_set_message(t('You must specify a pattern.'));
//     return;
//   }
//
//   $pattern = patterns_get_pattern($pid);
//
//   $output = '';
//   return $output;
// }

/**
 * Menu callback to edit a patterns data
 */
function patterns_edit($pid = null) {
  if (!is_numeric($pid)) {
    drupal_set_message(t('You must specify a pattern to edit.'));
    return;
  }
  $pattern = patterns_get_pattern($pid);

  // TODO: Turn php into xml here.
  // For now just allow modifying the original xml, which
  // means the modification cannot be further modified
  if (!$pattern->file) {
    drupal_set_message(t('This pattern does not seem to have an XML source file to base the modifications off of.'), 'error');
    return;
  }
  $xml = file_get_contents($pattern->file);
  $form['name'] = array(
    '#type' => 'value',
    '#value' => $pattern->name,
  );
  $form['pid'] = array(
    '#type' => 'value',
    '#value' => $pattern->pid,
  );
  if ($pattern->enabled <= $pattern->updated) {
    $form['revert'] = array(
      '#type' => 'markup',
      '#value' => l(t('Undo update changes to the state when you enabled the pattern.'), 'admin/build/patterns/revert/' . $pid, array(), drupal_get_destination()),
    );
  }
  $form['xml'] = array(
    '#type' => 'textarea',
    '#title' => t('Pattern\'s XML'),
    '#rows' => 25,
    '#default_value' => $xml,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

/**
 * Validate pattern modifications (make sure proper XML)
 */
function patterns_edit_validate($form_id, $form_values) {

  // Do validations....
  $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
  if (!file_check_directory($path, true)) {
    form_set_error('form_token', t('Unable to create @path to save the new pattern to.', array(
      '@path' => $path,
    )));
  }
}

/**
 * Submit edits to the pattern
 */
function patterns_edit_submit($form_id, $form_values) {

  // If this is an enabled pattern, make sure the enabled pattern is saved in its current state
  if ($file = db_result(db_query('SELECT file FROM {patterns} WHERE status = 1 AND name = "%s"', $form_values['name']))) {
    $dir = file_directory_path() . '/' . variable_get('patterns_save_xml', 'patterns') . '/enabled';
    file_check_directory($dir, true);
    $path = $dir . '/' . $form_values['name'] . '.xml';
    if (!file_exists($path)) {
      file_copy($file, $path, FILE_EXISTS_ERROR);
    }
  }

  // Save the new pattern into the pattern files dir.
  $path = file_directory_path() . '/' . variable_get('patterns_save_xml', 'patterns') . '/' . $form_values['name'] . '.xml';
  file_save_data($form_values['xml'], $path, FILE_EXISTS_REPLACE);
  $old = db_result(db_query('SELECT file FROM {patterns} WHERE name = "%s"', $form_values['name']));

  // Load and save pattern
  if ($pattern = patterns_load_xml($path)) {
    if ($old) {
      db_query('UPDATE {patterns} SET file = "%s", updated = "%s" WHERE pid = "%d"', $path, time(), $form_values['pid']);
    }
    patterns_save_pattern($pattern, $path);
  }
  drupal_set_message(t('%name was saved.', array(
    '%name' => $form_values['name'],
  )));
  return 'admin/build/patterns';
}

/**
 * List the modules used by a particular pattern
 */
function patterns_modules_page($pid) {
  $pattern = patterns_get_pattern($pid);
  drupal_set_title($pattern->title . ' ' . t('(Pattern Modules)'));
  $modules = patterns_modules($pattern);
  $modules_info = module_rebuild_cache();
  $modules_list = module_list();

  // Get module name, whether its to be disabled or enabled,
  // whether the module is available or not, and whether it is
  // currently enabled or not
  foreach ($modules as $module) {
    $row = array();
    $delete = is_array($module) ? $module['delete'] : false;
    $module = is_array($module) ? $module['value'] : $module;
    $available = array_key_exists($module, $modules_info);
    $enabled = array_key_exists($module, $modules_list);
    $row[] = $module;
    $row[] = $available ? t('Yes') : '<span class="alert">' . t('No') . '</span>';
    $row[] = $enabled ? t('Yes') : '<span class="alert">' . t('No') . '</span>';
    $row[] = $delete ? t('Delete') : t('Enable');
    $rows[] = $row;
    if (!$available) {
      $not_available = true;
    }
  }
  if ($not_available) {
    drupal_set_message(t('Some modules are not availalble, please download them before running this pattern.'), 'error');
  }
  else {
    drupal_set_message(t('All modules required by this module are available. Click !here to run this pattern.', array(
      '!here' => l(t('here'), 'admin/build/patterns/enable/' . $pid),
    )));
  }
  return theme('table', array(
    t('Name'),
    t('Available'),
    t('Enabled'),
    t('Pattern Action'),
  ), $rows, t('Modules used for this pattern'));
}
function patterns_load() {
  static $loaded = false;
  if ($loaded) {
    return;
  }
  require_once drupal_get_path('module', 'patterns') . '/patterns.inc';
  foreach (file_scan_directory(drupal_get_path('module', 'patterns') . '/components', '.\\.inc') as $file) {
    include_once $file->filename;
  }
  $loaded = true;
}
function patterns_get_patterns($reset = true) {
  patterns_load();
  if ($reset || !variable_get('patterns_loaded', false)) {

    // Get a listing of enabled patterns
    $enabled = array();
    $result = db_query('SELECT file FROM {patterns} WHERE status = 1');
    while ($pattern = db_fetch_object($result)) {
      $enabled[] = $result->file;
    }
    $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
    $priority = array();

    // Get uploaded patterns first
    if (file_check_directory($path)) {

      // Check if .htaccess file exists in path, if not insert it
      _patterns_check_file_dir();
      foreach (file_scan_directory($path, '.\\.xml') as $file) {

        // Don't save enabled pattern backups
        if (strpos($file->filename, $path . '/enabled/') === 0) {
          continue;
        }

        // Set priority so these patterns won't get over-written
        $priority[] = $file->name;

        // Can't update existing patterns that are enabled
        if (in_array($file->filename, $enabled)) {
          continue;
        }

        // Load and save pattern
        if ($pattern = patterns_load_xml($file->filename)) {
          patterns_save_pattern($pattern, $file->filename);
        }
      }
    }

    // Get per-site patterns next
    foreach (file_scan_directory(conf_path() . '/patterns', '.\\.xml') as $file) {

      // Can't update existing patterns that are enabled
      if (in_array($file->filename, $enabled) || in_array($file->name, $priority)) {
        continue;
      }
      $priority[] = $file->name;

      // Load and save pattern
      if ($pattern = patterns_load_xml($file->filename)) {
        patterns_save_pattern($pattern, $file->filename);
      }
    }

    // Get profile patterns next
    global $profile;
    foreach (file_scan_directory('profiles/' . $profile . '/patterns', '.\\.xml') as $file) {

      // Can't update existing patterns that are enabled
      if (in_array($file->filename, $enabled) || in_array($file->name, $priority)) {
        continue;
      }
      $priority[] = $file->name;

      // Load and save pattern
      if ($pattern = patterns_load_xml($file->filename)) {
        patterns_save_pattern($pattern, $file->filename);
      }
    }

    // Last get the default patterns
    foreach (file_scan_directory(drupal_get_path('module', 'patterns') . '/patterns', '.\\.xml') as $file) {

      // Can't update existing patterns that are enabled
      if (in_array($file->filename, $enabled) || in_array($file->name, $priority)) {
        continue;
      }
      $pattern = patterns_load_xml($file->filename);

      // Load and save pattern
      if ($pattern = patterns_load_xml($file->filename)) {
        patterns_save_pattern($pattern, $file->filename);
      }
    }
    variable_set('patterns_loaded', time());
  }
  $result = db_query('SELECT * FROM {patterns}');
  while ($pattern = db_fetch_object($result)) {
    $patterns[$pattern->pid] = $pattern;
    $data = unserialize($patterns[$pattern->pid]->pattern);
    $patterns[$pattern->pid]->pattern = $data;
    foreach ($data as $key => $section) {
      if ($data[$key]['tag'] == 'info') {
        $patterns[$pattern->pid]->info = array();
        array_shift($section);
        foreach ($section as $property) {
          $patterns[$pattern->pid]->info[$property['tag']] = $property['value'];
        }
      }
    }
  }
  return $patterns;
}
function patterns_save_pattern($pattern, $xmlpath = '') {
  $name = basename($xmlpath, '.xml');
  foreach ($pattern[0] as $index => $value) {
    if (is_numeric($index)) {
      switch ($value['tag']) {
        case 'title':
          $title = $value['value'];
          break;
        case 'description':
          $description = $value['value'];
          break;
        case 'author':
          $author = $value['value'];
          break;
      }
    }
  }
  if ($pid = db_result(db_query('SELECT pid FROM {patterns} WHERE name = "%s"', $name))) {
    $updated = db_result(db_query('SELECT updated FROM {patterns} WHERE pid = "%d"', $pid));
    if (($new_updated = filemtime(file_create_path($xmlpath))) > $updated) {
      db_query('UPDATE {patterns} SET pattern = "%s", title = "%s", file = "%s", updated = "%s", description = "%s" WHERE pid = %d', serialize($pattern), $title, $xmlpath, $new_updated, $description, $pid);
    }
    else {
      db_query('UPDATE {patterns} SET pattern = "%s", title = "%s", file = "%s", description = "%s" WHERE pid = %d', serialize($pattern), $title, $xmlpath, $description, $pid);
    }
  }
  else {
    $pid = db_next_id('{patterns}_pid');
    db_query('INSERT INTO {patterns} VALUES(%d, "%s", 0, "%s", "%s", 0, "%s", "%s", "%s")', $pid, $name, $xmlpath, time(), $title, $description, serialize($pattern));
  }
}
function patterns_get_pattern($id) {
  if (is_numeric($id)) {
    $pattern = db_fetch_object(db_query('SELECT * FROM {patterns} WHERE pid = "%d"', $id));
  }
  else {
    if (is_string($id)) {
      $pattern = db_fetch_object(db_query('SELECT * FROM {patterns} WHERE name = "%s"', $id));
    }
  }

  // Get the actual data. Data is stored in serialized form in the db.
  $pattern->pattern = unserialize($pattern->pattern);

  // Rearrange the data in a nice way for each component.
  // Make sure actions are processed differently so order is preserved.
  $pattern->pattern = patterns_rearrange_data($pattern->pattern);
  return $pattern;
}
function patterns_load_xml($path) {
  if (!file_exists($path)) {
    return;
  }
  $xml = file_get_contents($path);
  $pattern = patterns_from_source($xml);
  return $pattern;
}

/**
 * Create a pattern from an XML data source
 */
function patterns_from_source($xml) {
  $parse = xml_parser_create();
  xml_parse_into_struct($parse, $xml, $vals, $index);

  // Create a multi-dimensional array representing the XML structure
  $pattern = current(_patterns_parse_tag($vals, 0));
  return $pattern;
}

/**
 * Recurse through the values of a parsed xml file to create a
 * multi-dimensional representation of the data.
 */
function _patterns_parse_tag($data, $index) {
  $pattern = array();
  while ($current = $data[$index]) {
    $type = $current['type'];
    foreach ((array) $current['attributes'] as $key => $value) {
      $current[strtolower($key)] = $value;
    }
    $current['tag'] = strtolower($current['tag']);
    unset($current['type'], $current['level'], $current['attributes']);
    if (!trim($current['value']) && $current['value'] != "0") {
      unset($current['value']);
    }
    switch ($type) {
      case 'open':
        $index++;
        $current += _patterns_parse_tag($data, &$index);
        $pattern[] = $current;
        break;
      case 'close':
        $index++;
        return $pattern;
        break;
      case 'complete':

        // In order to support more complex/non-standard features we can use serialized data
        if ($current['attributes']['serialized']) {
          $value = unserialize($current['value']);
          if (isset($value)) {
            $current['value'] = $value;
          }
        }

        // This enables tags like <blog /> to turn into array('blog' => 'blog')
        // which is useful for checkbox/select type forms
        if (!isset($current['value'])) {
          $current['value'] = $current['tag'];
        }
        $pattern[] = $current;
        break;
    }
    $index++;
  }
  return $pattern;
}
function patterns_disable_pattern($pid) {
  $form['pid'] = array(
    '#type' => 'value',
    '#value' => $pid,
  );
  $pattern = patterns_get_pattern($pid);
  return confirm_form($form, t('Proceed with disabling pattern %pattern?', array(
    '%pattern' => $pattern->title,
  )), 'admin/build/patterns', '');
}
function patterns_enable_pattern($pid) {
  $form['pid'] = array(
    '#type' => 'value',
    '#value' => $pid,
  );
  $disclaimer = t('Please be sure to backup your site before running a pattern. Patterns are not guaranteed to be reversable in case they do not execute well or if unforseen side effects occur.');
  $pattern = patterns_get_pattern($pid);
  return confirm_form($form, t('Proceed with running pattern %pattern?', array(
    '%pattern' => $pattern->title,
  )), 'admin/build/patterns', $disclaimer);
}
function patterns_disable_pattern_submit($form_id, $form_values) {
  $pid = $form_values['pid'];
  $pattern = patterns_get_pattern($pid);
  if (patterns_execute_pattern($pattern, true, true)) {
    return 'admin/build/patterns';
  }
}
function patterns_enable_pattern_submit($form_id, $form_values) {
  $pid = $form_values['pid'];
  patterns_load();
  $pattern = patterns_get_pattern($pid);
  if (patterns_execute_pattern($pattern, false, true)) {
    return 'admin/build/patterns';
  }
}
function patterns_execute_pattern($pattern, $reverse = false, $interact = false, $jump = null) {
  patterns_load();
  set_time_limit(0);
  if (!is_object($pattern)) {
    $pattern = patterns_get_pattern($pattern);
    if (!$pattern) {
      return false;
    }
  }
  $errors = $action_list = $identifiers = array();
  $error = true;

  // Pattern info
  $status = $pattern->status;
  $title = $pattern->title;
  $pid = $pattern->pid;

  // From here on out just need the actual pattern data
  $pattern = $pattern->pattern;

  // Split the pattern up into modules and actions. Submit modules as its
  // own pattern programattically.
  $modules = $actions = array();
  for ($i = 0; $tag = $pattern[$i]; $i++) {
    if ($tag['tag'] == 'modules') {
      $modules = $tag;
    }
    else {
      if ($tag['tag'] == 'actions') {
        $actions = $tag;
        unset($actions['tag']);
      }
    }
  }

  // If there are no actions or modules, most likely the pattern
  // was not created correctly.
  if (empty($actions) && empty($modules)) {
    drupal_set_message(t('Could not recognize pattern %title, aborting.', array(
      '%title' => $title,
    )), 'error');
    return true;
  }
  if ($modules && (!$interact || $interact && !$jump)) {

    // Make the modules look like a normal pattern so they can be executed
    // on their own.
    $obj = new stdClass();
    $obj->title = t('Enable/disable %title pattern modules', array(
      '%title' => $title,
    ));
    $obj->status = $status;
    $obj->pattern = array(
      array(
        'tag' => 'actions',
        $modules,
      ),
    );
    $modules = $obj;

    // Modules need to be enabled first so the rest of the pattern
    // can proceed smoothly.
    if (!$reverse) {
      $error = patterns_execute_pattern($modules, $reverse, $interact);
      module_rebuild_cache();
    }
  }

  // Keep a list of what modules handle what tags
  $tag_modules = patterns_invoke($empty, 'tag modules');

  // If an interactive pattern needs resuming, remove the
  // already executed actions
  if ($interact && $jump > 0) {
    array_splice($actions, 0, $jump);
  }

  // Prepare actions for validation/processing
  foreach ($actions as $key => $data) {
    patterns_invoke($actions[$key], 'prepare');
  }

  // Reverse a pattern for disabling
  if ($reverse && $status) {
    $actions = array_reverse($actions);
    foreach ($actions as $key => $data) {
      $continue = patterns_invoke($data, 'reverse');
      if ($continue === false) {
        drupal_set_message(t('[Error] Disabling of this pattern is not supported at this time.'));
        return false;
      }
      $actions[$key] = $data;
    }
  }

  // Pre validate tags with their appropriate components
  foreach ($actions as $key => $data) {
    if (!array_key_exists($data['tag'], $tag_modules)) {
      $errors[$data['tag']][] = t('Invalid Pattern: <%tag> is not a valid tag', array(
        '%tag' => $data['tag'],
      ));
    }
    else {
      $error = patterns_invoke($data, 'pre-validate');
      if ($error) {
        $errors[$data['tag']][] = t('Invalid Pattern: !msg', array(
          '!msg' => $error,
        ));
      }
    }
  }
  if (count($errors)) {
    foreach ($errors as $error) {
      drupal_set_message(implode('<br>', $error), 'error');
    }
    return;
  }

  // Build and execute a list of actions
  foreach ($actions as $key => $data) {

    // Prepare actions for processing, ensure smooth pattern executions, and return form ids for execution
    $return = patterns_invoke($data, 'form_id');

    // If prepare removed the data, dont continue with this action
    if (!$data || !$return) {
      continue;
    }
    if (is_string($return)) {
      $form_ids = array(
        $return,
      );
    }
    else {
      if ($return) {
        $form_ids = $return;
      }
    }

    // Build the action
    foreach ($form_ids as $form_id) {
      $clone = $data;
      $error = patterns_invoke($clone, 'validate', $form_id);
      if ($error) {
        if (is_array($error)) {
          foreach ($error as $msg) {
            drupal_set_message($msg, 'error');
          }
          $errors[$clone['tag']] = t('Broken Pattern: %msg', array(
            '%msg' => implode('<br>', $error),
          ));
        }
        else {
          drupal_set_message($error, 'error');
          $errors[$clone['tag']] = t('Broken Pattern: %msg', array(
            '%msg' => $error,
          ));
        }
        return;
      }

      // If tokens are enabled, apply tokens to the action values
      // before processing
      if (module_exists('token')) {
        _patterns_recurse_tokens($clone, $identifiers);

        //array_walk($clone, '_patterns_replace_tokens', $identifiers);
      }

      // Get the form data for the action
      $values = patterns_invoke($clone, 'build', $form_id);

      // Dont execute the action if a string was returned, indicating the pattern component
      // most likely handled the action on its own and this is the message to display.
      if (is_string($values)) {
        drupal_set_message($values);
      }
      else {

        // Get any extra parameters required for the action
        $params = patterns_invoke($clone, 'params', $form_id, $values);
        if (isset($params) && !is_array($params)) {
          $params = array(
            $params,
          );
        }

        // Execute action
        patterns_execute_action($form_id, $values, $params);
        if (form_get_errors()) {
          $descriptions = patterns_invoke($clone, 'actions');
          drupal_set_message(t('An error occured running action #%num (%action)', array(
            '%num' => $key + 1,
            '%action' => $descriptions[$form_id],
          )), 'error');
          $error = true;
          break;
        }
      }
      patterns_invoke($clone, 'cleanup', $form_id);

      // Clear the cache in case it causes problems
      cache_clear_all();
    }
    if ($error) {
      break;
    }

    // Get any primary identifiers from the action for further actions to take advantage of
    $id = null;
    $id = patterns_invoke($clone, 'identifier', $form_id);
    if (isset($id)) {
      $identifiers[$key + 1] = $id;
    }
  }
  if (empty($errors)) {
    if ($reverse) {
      if ($modules) {

        // Modules need to be disabled last so the rest of the pattern
        // can reverse itself properly
        $error = patterns_execute_pattern($modules, $reverse, $interact);
      }

      // Mark pattern as disabled
      if ($pid) {
        db_query('UPDATE {patterns} SET status = 0 WHERE pid = %d', $pid);
      }
      drupal_set_message(t('Pattern reversed successfully.'));
    }
    else {

      // Mark pattern as enabled
      if ($pid) {
        db_query('UPDATE {patterns} SET status = 1, enabled = "%s" WHERE pid = %d', time(), $pid);
      }
      drupal_set_message(t('Pattern ran successfully.'));
    }
  }
  return !$error;
}

/**
 * Execute an action
 */
function patterns_execute_action($form_id, $values, $params) {

  // Make sure we always have a clear cache for everything
  $result = db_query('SHOW TABLES LIKE "cache_%"');
  while ($table = db_fetch_array($result)) {
    $table = current($table);
    cache_clear_all(null, $table);
  }
  $args = array(
    $form_id,
    $values,
  );
  if (is_array($params)) {
    $args = array_merge($args, $params);
  }
  patterns_executing(true);

  //$form = call_user_func_array('drupal_retrieve_form', $args);

  //$form['#post'] = $values;

  //$return = drupal_process_form($form_id, $form);
  $return = call_user_func_array('drupal_execute', $args);
  patterns_executing(false);
  return $return;
}
function patterns_executing($b = null) {
  static $executing = false;
  if (is_bool($b)) {
    $executing = $b;
  }
  return $executing;
}
function patterns_rearrange_data($pattern) {
  foreach ($pattern as $key => $value) {
    if (is_string($key)) {
      unset($pattern[$key]);
    }
    else {
      if ($value['tag'] == 'actions') {
        $pattern[$key] = patterns_rearrange_data($value);
        $pattern[$key]['tag'] = 'actions';
      }
      else {
        $pattern[$key] = _patterns_rearrange_data($value);
      }
    }
  }
  return $pattern;
}

/**
 * Return a list of modules for a pattern
 */
function patterns_modules($pattern) {
  $pattern = $pattern->pattern;
  for ($i = 0; $tag = $pattern[$i]; $i++) {
    if ($tag['tag'] == 'modules') {
      unset($tag['tag']);
      $modules = $tag;
      break;
    }
  }
  return $modules;
}
function patterns_process_modules($modules, $op = 'enable') {

  // Enable at the beginning of the pattern. Disable at the end.
  for ($i = 0; $module = $modules[$i]; $i++) {
    if ($op == 'enable' && $module['delete'] || $op == 'disable' && !$module['delete']) {
      unset($modules[$i]);
    }
  }
  patterns_invoke($empty, 'tag modules');
  $error = patterns_invoke($modules, 'pre-validate');

  // Error validating modules
  if ($error) {
    return $error;
  }
  patterns_invoke($modules, 'prepare');
}
function patterns_invoke(&$data, $op, $form_id = null, $a = null) {
  static $tag_modules;
  if (!is_array($tag_modules) || $op == 'tag modules') {

    // Get a list of tags and their modules
    foreach (module_implements('patterns') as $module) {
      $tags = module_invoke($module, 'patterns', 'tags');
      foreach ($tags as $tag => $value) {
        if (is_array($value)) {
          $tag_modules[$tag] = $module;
        }
        else {
          $tag_modules[$value] = $module;
        }
      }
    }
  }

  // If you just want the modules list
  if ($op == 'tag modules') {
    return $tag_modules;
  }
  $tag = $data['tag'];
  unset($data['tag']);
  $module = $tag_modules[$tag];
  $func = $module . '_patterns';
  if (function_exists($func)) {
    if ($form_id) {
      $return = $func($op, $form_id, $data, $a);
    }
    else {
      $return = $func($op, $tag, $data, $a);
    }
  }
  $data['tag'] = $tag;
  return $return;
}
function _patterns_rearrange_data($data, $parent = '') {
  $numeric = array();
  $count = 0;
  foreach ($data as $key => $value) {
    if ($value['value'] == 'false') {
      $value['value'] = false;
    }
    else {
      if ($value['value'] == 'true') {
        $value['value'] = true;
      }
    }
    if (is_numeric($key) && is_array($value) && count($value) == 2 && isset($value['tag']) && isset($value['value'])) {
      unset($data[$key]);
      if (isset($data[$value['tag']])) {
        $numeric[] = $value['tag'];
        $data[$count++] = $data[$value['tag']];
        $data[$count++] = $value['value'];
        unset($data[$value['tag']]);
      }
      else {
        if (in_array($value['tag'], $numeric)) {
          $data[$count++] = $value['value'];
        }
        else {
          $data[$value['tag']] = $value['value'];
        }
      }
    }
    else {
      if (is_numeric($key)) {
        $tag = $value['tag'];
        unset($value['tag']);
        $data[$tag][] = _patterns_rearrange_data($value, $tag);
        unset($data[$key]);
      }
    }
  }
  foreach ($data as $key => $value) {
    if (is_array($value) && count($value) == 1 && $value[0]) {
      $data[$key] = $data[$key][0];
    }
  }
  return $data;
}
function patterns_form_alter($form_id, &$form) {
  if (user_access('administer patterns') && variable_get('patterns_form_helper', true)) {
    $form['#after_build'][] = 'patterns_form_helper';
  }
}
function patterns_form_helper($form) {
  global $form_submitted;

  //  static $form_id, $form_values;
  $args = func_get_args();
  if (!$form_id && $form_submitted) {
    $form_values = $args[1];
    $form_id = $form_values['form_id'];
    $_SESSION['patterns_form_helper'] = array(
      'form_id' => $form_id,
      'form_values' => $form_values,
    );
  }
  return $form;
}
function patterns_init() {
  if (variable_get('patterns_form_helper', true) && $_SESSION['patterns_form_helper']) {
    drupal_add_css(drupal_get_path('module', 'patterns') . '/patterns.css');
    drupal_add_js(drupal_get_path('module', 'patterns') . '/patterns.js');
  }
}
function patterns_exit($destination = null) {
  if (variable_get('patterns_form_helper', true) && $_SESSION['patterns_form_helper'] && !$destination) {
    print theme('patterns_form_helper', $_SESSION['patterns_form_helper']['form_id'], $_SESSION['patterns_form_helper']['form_values']);

    //unset($_SESSION['patterns_form_helper']);
  }
}

/**
 * Implementation of hook_token_values()
 * 
 * @If these get implementated directly into token.module, this should be removed
 */
function patterns_token_values($type, $object = NULL, $options = array()) {
  if ($type == 'global') {
    $path = conf_path();
    $tokens['confpath'] = $path;
    return $tokens;
  }
}

/**
 * Function callback
 */
function _patterns_modify_value(&$form) {
  foreach ($form as $key => $value) {
    if (is_array($value) && isset($value['#type']) && $value['#type'] == 'value') {
      $form[$key]['#default_value'] = $value['#value'];
      unset($form[$key]['#value']);
    }
    else {
      if (is_array($value)) {
        _patterns_modify_value($form[$key]);
      }
    }
  }
}

/**
 * Recurse an array and replace with tokens
 * @ This is used instead of array_walk_recursive because
 *   of some strange issues with token_get_values failing.
 */
function _patterns_recurse_tokens(&$object, $identifiers) {
  foreach ($object as $key => $value) {
    if (is_array($value)) {
      _patterns_recurse_tokens($object[$key], $identifiers);
    }
    else {
      if (is_scalar($value)) {
        $old_key = $key;
        _patterns_replace_tokens($object[$key], $key, $identifiers);

        // The key was changed, change it
        if ($old_key != $key) {
          $keys = array_keys($object);
          $keys[array_search($old_key, $keys)] = $key;
          $object = array_combine($keys, array_values($object));
        }
      }
    }
  }
}

/**
 * Array walk callback to replace tokens inside form values
 */
function _patterns_replace_tokens(&$a, &$b, $identifiers = array()) {
  static $count = 0;

  // Replace IDs with identifiers from the current executing pattern
  if (preg_match('/@([0-9]+)@/', $a, $match)) {
    $a = str_replace($match[0], $identifiers[$match[1]], $a);
  }
  if (preg_match('/__([0-9]+)__/', $b, $match)) {
    $b = str_replace($match[0], $identifiers[$match[1]], $a);
  }

  // Replace tokens
  $a = token_replace($a, 'global', NULL, '@', '@');
  $b = token_replace($b, 'global', NULL, '__', '__');
}

/**
 * Check if a .htaccess file exists to prevent downloads of pattern files
 */
function _patterns_check_file_dir() {
  return false;
  $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
  if (!is_file($path . '/.htaccess')) {
    $content = '# Prevent downloading site patterns
<FilesMatch "\\.xml$">
  Order allow,deny
</FilesMatch>
';
    file_save_data($content, $path . '/.htaccess');
  }
}
function theme_patterns_form_helper_menu($forms) {
  $output = '<ul class="patterns-form-menu">';
  foreach ($forms as $form_id => $values) {
    $output .= '<li class="patterns-form-menu-item">' . $form_id . '</li>';
  }
  $output .= '</li>';
  return $output;
}
function theme_patterns_form_helper($form_id, $values) {
  $output = '<div class="patterns-form" id="patterns-form-' . $form_id . '">';
  $output .= '<div class="patterns-form-title">' . t('Form values for %key', array(
    '%key' => $form_id,
  )) . '</div>';
  foreach ($values as $key => $value) {
    $output .= '<div class="patterns-form-item"><div class="patterns-form-key">' . $key . ' => </div>';
    $output .= '<div class="patterns-form-value">' . print_r($value, true) . '</div></div>';
  }
  $output .= '</div>';
  return $output;
}

Functions

Namesort descending Description
patterns_disable_pattern
patterns_disable_pattern_submit
patterns_edit Menu callback to edit a patterns data
patterns_edit_submit Submit edits to the pattern
patterns_edit_validate Validate pattern modifications (make sure proper XML)
patterns_enable_pattern
patterns_enable_pattern_submit
patterns_execute_action Execute an action
patterns_execute_pattern
patterns_executing
patterns_exit
patterns_form_alter
patterns_form_helper
patterns_from_source Create a pattern from an XML data source
patterns_get_pattern
patterns_get_patterns
patterns_import_file Display the import pattern file form
patterns_import_source Display the import pattern form
patterns_import_submit
patterns_import_url Display the import pattern url form
patterns_import_validate
patterns_init
patterns_invoke
patterns_list
patterns_load
patterns_load_xml
patterns_menu Implementation of hook_menu().
patterns_modules Return a list of modules for a pattern
patterns_modules_page List the modules used by a particular pattern
patterns_perm Implementation of hook_perm().
patterns_process_modules
patterns_rearrange_data
patterns_revert Menu callback to undo a patterns update changes
patterns_save_pattern
patterns_token_values Implementation of hook_token_values()
theme_patterns_form_helper
theme_patterns_form_helper_menu
_patterns_check_file_dir Check if a .htaccess file exists to prevent downloads of pattern files
_patterns_modify_value Function callback
_patterns_parse_tag Recurse through the values of a parsed xml file to create a multi-dimensional representation of the data.
_patterns_rearrange_data
_patterns_recurse_tokens Recurse an array and replace with tokens @ This is used instead of array_walk_recursive because of some strange issues with token_get_values failing.
_patterns_replace_tokens Array walk callback to replace tokens inside form values