patterns.module

Tracking 5.x-1.x branch
  1. drupal
    1. 5 contributions/patterns/patterns.module

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

Functions & methods

NameDescription
patterns_disable_pattern
patterns_disable_pattern_submit
patterns_editMenu callback to edit a patterns data
patterns_edit_submitSubmit edits to the pattern
patterns_edit_validateValidate pattern modifications (make sure proper XML)
patterns_enable_pattern
patterns_enable_pattern_submit
patterns_execute_actionExecute an action
patterns_execute_pattern
patterns_executing
patterns_exit
patterns_form_alter
patterns_form_helper
patterns_from_sourceCreate a pattern from an XML data source
patterns_get_pattern
patterns_get_patterns
patterns_import_fileDisplay the import pattern file form
patterns_import_sourceDisplay the import pattern form
patterns_import_submit
patterns_import_urlDisplay the import pattern url form
patterns_import_validate
patterns_init
patterns_invoke
patterns_list
patterns_load
patterns_load_xml
patterns_menuImplementation of hook_menu().
patterns_modulesReturn a list of modules for a pattern
patterns_modules_pageList the modules used by a particular pattern
patterns_permImplementation of hook_perm().
patterns_process_modules
patterns_rearrange_data
patterns_revertMenu callback to undo a patterns update changes
patterns_save_pattern
patterns_token_valuesImplementation of hook_token_values()
theme_patterns_form_helper
theme_patterns_form_helper_menu
_patterns_check_file_dirCheck if a .htaccess file exists to prevent downloads of pattern files
_patterns_modify_valueFunction callback
_patterns_parse_tagRecurse through the values of a parsed xml file to create a multi-dimensional representation of the data.
_patterns_rearrange_data
_patterns_recurse_tokensRecurse 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_tokensArray walk callback to replace tokens inside form values

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Enables extremely simple adding/removing features to your site with minimal to no configuration
  5. */
  6. /**
  7. * @todo:
  8. * @ Enable pattern configurations
  9. * @ **done**Enable actions to see ids created/updated from other actions inside the pattern (tokens?)
  10. * @ **semi-done** Reset patterns
  11. * @ Enable components to analyze the current pattern for better validation
  12. * @ Allow module version restricting
  13. * @ Put in functionality to auto-download modules (and the correct version)
  14. * @ Enable backups before running patterns and reverting back to those backups
  15. * @ Implement a progress meter
  16. * @ Handle default values better to allow for absolute minimal actions
  17. * @ Enable interactive actions by allowing patterns to resume from a saved position
  18. * @ Implement an export feature for all available form_ids
  19. * @ Allow resuming failed patterns
  20. * @ In the pattern details, list any sub-patterns directly on the patterns listing page
  21. */
  22. /**
  23. * Implementation of hook_perm().
  24. */
  25. function patterns_perm() {
  26. return array('administer patterns');
  27. }
  28. /**
  29. * Implementation of hook_menu().
  30. */
  31. function patterns_menu($may_cache) {
  32. $items = array();
  33. if ($may_cache) {
  34. $items[] = array('path' => 'admin/build/patterns',
  35. 'description' => t('Administer patterns available for your site'),
  36. 'access' => user_access('administer patterns'),
  37. 'title' => t('Patterns'),
  38. 'callback' => 'patterns_list',
  39. 'type' => MENU_NORMAL_ITEM
  40. );
  41. $items[] = array('path' => 'admin/build/patterns/list',
  42. 'title' => t('List'),
  43. 'callback' => 'patterns_list',
  44. 'type' => MENU_DEFAULT_LOCAL_TASK,
  45. 'weight' => -10
  46. );
  47. // $items[] = array('path' => 'admin/build/patterns/settings',
  48. // 'title' => t('Settings'),
  49. // 'callback' => 'drupal_get_form',
  50. // 'callback arguments' => array('patterns_settings'),
  51. // 'type' => MENU_LOCAL_TASK,
  52. // 'weight' => 10
  53. // );
  54. $items[] = array('path' => 'admin/build/patterns/configure',
  55. 'title' => t('Configure Pattern'),
  56. 'callback' => 'drupal_get_form',
  57. 'callback arguments' => array('patterns_configure_pattern'),
  58. 'type' => MENU_CALLBACK
  59. );
  60. $items[] = array('path' => 'admin/build/patterns/edit',
  61. 'title' => t('Edit Pattern'),
  62. 'callback' => 'drupal_get_form',
  63. 'callback arguments' => array('patterns_edit'),
  64. 'type' => MENU_CALLBACK
  65. );
  66. // $items[] = array('path' => 'admin/build/patterns/info',
  67. // 'title' => t('Pattern Details'),
  68. // 'callback' => 'patterns_info',
  69. // 'type' => MENU_CALLBACK
  70. // );
  71. $items[] = array('path' => 'admin/build/patterns/enable',
  72. 'access' => user_access('administer patterns'),
  73. 'title' => t('Enable Pattern'),
  74. 'callback' => 'drupal_get_form',
  75. 'callback arguments' => array('patterns_enable_pattern'),
  76. 'type' => MENU_CALLBACK
  77. );
  78. $items[] = array('path' => 'admin/build/patterns/disable',
  79. 'access' => user_access('administer patterns'),
  80. 'title' => t('Disable Pattern'),
  81. 'callback' => 'drupal_get_form',
  82. 'callback arguments' => array('patterns_disable_pattern'),
  83. 'type' => MENU_CALLBACK
  84. );
  85. $items[] = array('path' => 'admin/build/patterns/modules',
  86. 'access' => user_access('administer patterns'),
  87. 'title' => t('Pattern Modules'),
  88. 'callback' => 'patterns_modules_page',
  89. 'type' => MENU_CALLBACK
  90. );
  91. $items[] = array('path' => 'admin/build/patterns/revert',
  92. 'access' => user_access('administer patterns'),
  93. 'title' => t('Revert Pattern'),
  94. 'callback' => 'patterns_revert',
  95. 'type' => MENU_CALLBACK
  96. );
  97. $items[] = array('path' => 'admin/build/patterns/import',
  98. 'access' => user_access('administer patterns'),
  99. 'title' => t('Import'),
  100. 'callback' => 'drupal_get_form',
  101. 'callback arguments' => array('patterns_import_source'),
  102. 'type' => MENU_LOCAL_TASK
  103. );
  104. $items[] = array('path' => 'admin/build/patterns/import/xmltext',
  105. 'access' => user_access('administer patterns'),
  106. 'title' => t('Import via XML Source'),
  107. 'type' => MENU_DEFAULT_LOCAL_TASK
  108. );
  109. $items[] = array('path' => 'admin/build/patterns/import/xmlfile',
  110. 'access' => user_access('administer patterns'),
  111. 'title' => t('Import via XML File'),
  112. 'callback' => 'drupal_get_form',
  113. 'callback arguments' => array('patterns_import_file'),
  114. 'type' => MENU_LOCAL_TASK
  115. );
  116. $items[] = array('path' => 'admin/build/patterns/import/xmlurl',
  117. 'access' => user_access('administer patterns'),
  118. 'title' => t('Import via XML URL'),
  119. 'callback' => 'drupal_get_form',
  120. 'callback arguments' => array('patterns_import_url'),
  121. 'type' => MENU_LOCAL_TASK
  122. );
  123. // $items[] = array('path' => 'admin/build/patterns/import/server',
  124. // 'access' => user_access('administer patterns'),
  125. // 'title' => t('Import via Patterns Server'),
  126. // 'callback' => 'drupal_get_form',
  127. // 'callback arguments' => array('patterns_import_server'),
  128. // 'type' => MENU_LOCAL_TASK
  129. // );
  130. }
  131. return $items;
  132. }
  133. /**
  134. * Display the import pattern form
  135. */
  136. function patterns_import_source() {
  137. $form['xmlname'] = array(
  138. '#type' => 'textfield',
  139. '#title' => t('Pattern Identifier'),
  140. '#description' => t('Machine readable name for the pattern. The actual title should be included in the pattern itself.'),
  141. '#required' => true
  142. );
  143. $form['xmlsource'] = array(
  144. '#type' => 'textarea',
  145. '#rows' => 15,
  146. '#title' => t('Enter Pattern XML'),
  147. '#description' => t('Imported patterns are not executed until you run them manually. You can leave off the <?xml> tag at the top.')
  148. );
  149. $form['submit'] = array(
  150. '#type' => 'submit',
  151. '#value' => t('Submit')
  152. );
  153. $form['#base'] = 'patterns_import';
  154. return $form;
  155. }
  156. /**
  157. * Display the pattern settings form
  158. */
  159. // function patterns_settings() {
  160. // drupal_set_message('This feature not implemented yet.');
  161. // }
  162. /**
  163. * Display the import pattern from server form
  164. */
  165. // function patterns_import_server() {
  166. // drupal_set_message('This feature not implemented yet.');
  167. // }
  168. /**
  169. * Display the import pattern file form
  170. */
  171. function patterns_import_file() {
  172. $form['#attributes']['enctype'] = 'multipart/form-data';
  173. $form['xmlfile'] = array(
  174. '#type' => 'file',
  175. '#title' => t('Upload XML Pattern File'),
  176. '#description' => t('Imported patterns are not executed until you run them manually.'),
  177. '#size' => 48
  178. );
  179. $form['submit'] = array(
  180. '#type' => 'submit',
  181. '#value' => t('Submit')
  182. );
  183. $form['#base'] = 'patterns_import';
  184. return $form;
  185. }
  186. /**
  187. * Display the import pattern url form
  188. */
  189. function patterns_import_url() {
  190. $form['xmlurl'] = array(
  191. '#type' => 'textfield',
  192. '#title' => t('Specify an XML url'),
  193. '#description' => t('Import a pattern from a remote URL. Imported patterns are not executed until you run them manually.'),
  194. '#size' => 48
  195. );
  196. $form['submit'] = array(
  197. '#type' => 'submit',
  198. '#value' => t('Submit')
  199. );
  200. $form['#base'] = 'patterns_import';
  201. return $form;
  202. }
  203. function patterns_import_validate($form_id, $values) {
  204. global $form_values;
  205. if ($file = file_check_upload('xmlfile')) {
  206. $form_values['xmlsource'] = file_get_contents($file->filepath);
  207. }
  208. else if (isset($values['xmlfile'])) {
  209. form_set_error('xmlfile', t('Error uploading XML file.'));
  210. }
  211. else if ($values['xmlurl']) {
  212. if (!ini_get('allow_url_fopen')) {
  213. form_set_error('xmlsource', t('allow_url_fopen must be enabled in your php configuration in order to use this feature.'));
  214. return;
  215. }
  216. if (!($form_values['xmlsource'] = file_get_contents($values['xmlurl']))) {
  217. form_set_error('xmlsource', t('Failed to retreive the pattern specified.'));
  218. return;
  219. }
  220. $form_values['xmlname'] = preg_replace('/\.[^\.]*$/', '', basename($form_values['xmlurl']));
  221. }
  222. if (strpos($form_values['xmlsource'], '<?xml') !== 0) {
  223. $form_values['xmlsource'] = '<?xml version="1.0" encoding="ISO-8859-1"?>' . $form_values['xmlsource'];
  224. }
  225. if ($form_values['xmlname'] && preg_match('/[^a-zA-Z0-9_]/', $form_values['xmlname'])) {
  226. form_set_error('xmlname', t('You can only include letters, numbers, and underscores in the pattern identifier.'));
  227. }
  228. else if ($form_values['xmlname'] && preg_match('/^_/', $form_values['xmlname'])) {
  229. form_set_error('xmlname', t('You cannot start the pattern identifier with an underscore.'));
  230. }
  231. $parse = xml_parser_create();
  232. xml_parse_into_struct($parse, $form_values['xmlsource'], $vals, $index);
  233. // Check that the xml was properly parsed and also that the
  234. // root <pattern> tag and also an <info> tag were used.
  235. if (!$vals || $vals[0]['tag'] != 'PATTERN' || $vals[1]['tag'] != 'INFO') {
  236. form_set_error('xmlsource', t('Error parsing the XML, please check your syntax and try again.'));
  237. }
  238. }
  239. function patterns_import_submit($form_id, $form_values) {
  240. if ($file = file_check_upload('xmlfile')) {
  241. // Currently nothing is happening with files saved here....
  242. if (file_check_directory(file_create_path(variable_get('patterns_save_xml', 'patterns')))) {
  243. file_save_upload('xmlfile', variable_get('patterns_save_xml', 'patterns'));
  244. }
  245. else {
  246. drupal_set_message(t('Pattern was registered but the file was not saved on the server because of improperly setup files directory.'));
  247. }
  248. }
  249. else if ($form_values['xmlsource']) {
  250. file_save_data($form_values['xmlsource'], variable_get('patterns_save_xml', 'patterns') .'/'. $form_values['xmlname'] .'.xml', FILE_EXISTS_REPLACE);
  251. }
  252. // Reload patterns
  253. patterns_get_patterns(true);
  254. return 'admin/build/patterns';
  255. }
  256. function patterns_list() {
  257. drupal_add_css(drupal_get_path('module', 'patterns') .'/patterns.css');
  258. drupal_add_js(drupal_get_path('module', 'patterns') .'/patterns.js');
  259. patterns_load();
  260. $patterns = patterns_get_patterns();
  261. if (empty($patterns)) return t('No patterns available.');
  262. // $header = array(t('Title'), t('Status'), t('Version'), t('Public'), t('Actions'));
  263. $header = array(t('Title'), t('Status'), t('Version'), t('Actions'));
  264. // List all patterns
  265. $rows = array();
  266. foreach($patterns as $pid => $pattern) {
  267. $actions = array();
  268. if (!$pattern->status) {
  269. $actions[] = l(t('Run'), 'admin/build/patterns/enable/'. $pid);
  270. }
  271. else if ($pattern->enabled >= $pattern->updated) {
  272. $actions[] = l(t('Re-Run'), 'admin/build/patterns/enable/'. $pid);
  273. }
  274. else {
  275. $actions[] = l(t('Run Update'), 'admin/build/patterns/enable/'. $pid);
  276. }
  277. $actions[] = l(t('Edit'), 'admin/build/patterns/edit/'. $pid);
  278. $actions = implode('&nbsp;&nbsp;', $actions);
  279. $cells = array();
  280. // $title = l($pattern->title, 'admin/build/patterns/info/'. $pid, array('class' => 'pattern-title', 'id' => 'pid-'. $pid));
  281. $title = '<span id="pid-'. $pid .'" class="pattern-title">'. $pattern->title .'</span>';
  282. // $view_more = '<div>'. t('Clik on pattern title to see more details.') .'</div>';
  283. $info = array();
  284. $info[] = t('Author: ') . $pattern->info['author'];
  285. $info[] = t('Email: ') . $pattern->info['author_email'];
  286. $info[] = t('Web: ') . $pattern->info['author_website'];
  287. $author = theme('item_list', $info);
  288. $title .= '<div id="pid-'. $pid .'-info" class="pattern-info">'. $author . $pattern->description . $view_more .'</div>';
  289. $cells[] = $title;
  290. $cells[] = $pattern->status ? t('Enabled') : t('Disabled');
  291. $cells[] = $pattern->info['version'];
  292. // $cells[] = $pattern->public ? t('Yes') : t('No');
  293. $cells[] = $actions;
  294. $category = $pattern->info['category'] ? $pattern->info['category'] : t('Other');
  295. $rows[$category][] = $cells;
  296. }
  297. ksort($rows);
  298. foreach ($rows as $title => $category) {
  299. $fieldset = array(
  300. '#title' => t($title),
  301. '#collapsible' => TRUE,
  302. '#collapsed' => FALSE,
  303. '#value' => theme('table', $header, $category),
  304. );
  305. $output .= theme('fieldset', $fieldset);
  306. }
  307. return $output;
  308. }
  309. /**
  310. * Menu callback to undo a patterns update changes
  311. */
  312. function patterns_revert($pid) {
  313. if ($name = db_result(db_query('SELECT name FROM {patterns} WHERE pid = "%d"', $pid))) {
  314. $path = file_create_path(variable_get('patterns_save_xml', 'patterns') .'/enabled/'. $name .'.xml');
  315. $new = db_result(db_query('SELECT file FROM {patterns} WHERE pid = "%d"'));
  316. }
  317. else {
  318. drupal_set_message(t('The pattern you specified does not exist.'), 'error');
  319. drupal_goto('admin/build/patterns');
  320. }
  321. if (file_exists($path)) {
  322. if (file_move($path, $new, FILE_EXISTS_REPLACE)) {
  323. drupal_set_message(t('This pattern was reverted to the state it was at when it was enabled.'));
  324. drupal_goto();
  325. }
  326. }
  327. else {
  328. drupal_set_message(t('The patterns enabled-state was not saved properly, therefore it cannot be reverted.'), 'error');
  329. }
  330. drupal_goto('admin/build/patterns');
  331. }
  332. /**
  333. * Menu callback to display patterns details page
  334. */
  335. // function patterns_info($pid = null) {
  336. // if (!is_numeric($pid)) {
  337. // drupal_set_message(t('You must specify a pattern.'));
  338. // return;
  339. // }
  340. //
  341. // $pattern = patterns_get_pattern($pid);
  342. //
  343. // $output = '';
  344. // return $output;
  345. // }
  346. /**
  347. * Menu callback to edit a patterns data
  348. */
  349. function patterns_edit($pid = null) {
  350. if (!is_numeric($pid)) {
  351. drupal_set_message(t('You must specify a pattern to edit.'));
  352. return;
  353. }
  354. $pattern = patterns_get_pattern($pid);
  355. // TODO: Turn php into xml here.
  356. // For now just allow modifying the original xml, which
  357. // means the modification cannot be further modified
  358. if (!$pattern->file) {
  359. drupal_set_message(t('This pattern does not seem to have an XML source file to base the modifications off of.'), 'error');
  360. return;
  361. }
  362. $xml = file_get_contents($pattern->file);
  363. $form['name'] = array(
  364. '#type' => 'value',
  365. '#value' => $pattern->name
  366. );
  367. $form['pid'] = array(
  368. '#type' => 'value',
  369. '#value' => $pattern->pid
  370. );
  371. if ($pattern->enabled <= $pattern->updated) {
  372. $form['revert'] = array(
  373. '#type' => 'markup',
  374. '#value' => l(t('Undo update changes to the state when you enabled the pattern.'), 'admin/build/patterns/revert/'. $pid, array(), drupal_get_destination())
  375. );
  376. }
  377. $form['xml'] = array(
  378. '#type' => 'textarea',
  379. '#title' => t('Pattern\'s XML'),
  380. '#rows' => 25,
  381. '#default_value' => $xml
  382. );
  383. $form['submit'] = array(
  384. '#type' => 'submit',
  385. '#value' => t('Submit')
  386. );
  387. return $form;
  388. }
  389. /**
  390. * Validate pattern modifications (make sure proper XML)
  391. */
  392. function patterns_edit_validate($form_id, $form_values) {
  393. // Do validations....
  394. $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
  395. if (!file_check_directory($path, true)) {
  396. form_set_error('form_token', t('Unable to create @path to save the new pattern to.', array('@path' => $path)));
  397. }
  398. }
  399. /**
  400. * Submit edits to the pattern
  401. */
  402. function patterns_edit_submit($form_id, $form_values) {
  403. // If this is an enabled pattern, make sure the enabled pattern is saved in its current state
  404. if ($file = db_result(db_query('SELECT file FROM {patterns} WHERE status = 1 AND name = "%s"', $form_values['name']))) {
  405. $dir = file_directory_path() .'/'. variable_get('patterns_save_xml', 'patterns') .'/enabled';
  406. file_check_directory($dir, true);
  407. $path = $dir .'/'. $form_values['name'] .'.xml';
  408. if (!file_exists($path)) {
  409. file_copy($file, $path, FILE_EXISTS_ERROR);
  410. }
  411. }
  412. // Save the new pattern into the pattern files dir.
  413. $path = file_directory_path() .'/'. variable_get('patterns_save_xml', 'patterns') .'/'. $form_values['name'] .'.xml';
  414. file_save_data($form_values['xml'], $path, FILE_EXISTS_REPLACE);
  415. $old = db_result(db_query('SELECT file FROM {patterns} WHERE name = "%s"', $form_values['name']));
  416. // Load and save pattern
  417. if ($pattern = patterns_load_xml($path)) {
  418. if ($old) {
  419. db_query('UPDATE {patterns} SET file = "%s", updated = "%s" WHERE pid = "%d"', $path, time(), $form_values['pid']);
  420. }
  421. patterns_save_pattern($pattern, $path);
  422. }
  423. drupal_set_message(t('%name was saved.', array('%name' => $form_values['name'])));
  424. return 'admin/build/patterns';
  425. }
  426. /**
  427. * List the modules used by a particular pattern
  428. */
  429. function patterns_modules_page($pid) {
  430. $pattern = patterns_get_pattern($pid);
  431. drupal_set_title($pattern->title .' '. t('(Pattern Modules)'));
  432. $modules = patterns_modules($pattern);
  433. $modules_info = module_rebuild_cache();
  434. $modules_list = module_list();
  435. // Get module name, whether its to be disabled or enabled,
  436. // whether the module is available or not, and whether it is
  437. // currently enabled or not
  438. foreach($modules as $module) {
  439. $row = array();
  440. $delete = is_array($module) ? $module['delete'] : false;
  441. $module = is_array($module) ? $module['value'] : $module;
  442. $available = array_key_exists($module, $modules_info);
  443. $enabled = array_key_exists($module, $modules_list);
  444. $row[] = $module;
  445. $row[] = $available ? t('Yes') : '<span class="alert">'. t('No') .'</span>';
  446. $row[] = $enabled ? t('Yes') : '<span class="alert">'. t('No') .'</span>';
  447. $row[] = $delete ? t('Delete') : t('Enable');
  448. $rows[] = $row;
  449. if (!$available) {
  450. $not_available = true;
  451. }
  452. }
  453. if ($not_available) {
  454. drupal_set_message(t('Some modules are not availalble, please download them before running this pattern.'), 'error');
  455. }
  456. else {
  457. 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))));
  458. }
  459. return theme('table', array(t('Name'), t('Available'), t('Enabled'), t('Pattern Action')), $rows, t('Modules used for this pattern'));
  460. }
  461. function patterns_load() {
  462. static $loaded = false;
  463. if ($loaded) {
  464. return;
  465. }
  466. require_once drupal_get_path('module', 'patterns') .'/patterns.inc';
  467. foreach(file_scan_directory(drupal_get_path('module', 'patterns') .'/components', '.\.inc') as $file) {
  468. include_once $file->filename;
  469. }
  470. $loaded = true;
  471. }
  472. function patterns_get_patterns($reset = true) {
  473. patterns_load();
  474. if ($reset || !variable_get('patterns_loaded', false)) {
  475. // Get a listing of enabled patterns
  476. $enabled = array();
  477. $result = db_query('SELECT file FROM {patterns} WHERE status = 1');
  478. while ($pattern = db_fetch_object($result)) {
  479. $enabled[] = $result->file;
  480. }
  481. $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
  482. $priority = array();
  483. // Get uploaded patterns first
  484. if (file_check_directory($path)) {
  485. // Check if .htaccess file exists in path, if not insert it
  486. _patterns_check_file_dir();
  487. foreach(file_scan_directory($path, '.\.xml') as $file) {
  488. // Don't save enabled pattern backups
  489. if (strpos($file->filename, $path .'/enabled/') === 0) {
  490. continue;
  491. }
  492. // Set priority so these patterns won't get over-written
  493. $priority[] = $file->name;
  494. // Can't update existing patterns that are enabled
  495. if (in_array($file->filename, $enabled)) {
  496. continue;
  497. }
  498. // Load and save pattern
  499. if ($pattern = patterns_load_xml($file->filename)) {
  500. patterns_save_pattern($pattern, $file->filename);
  501. }
  502. }
  503. }
  504. // Get per-site patterns next
  505. foreach(file_scan_directory(conf_path() .'/patterns', '.\.xml') as $file) {
  506. // Can't update existing patterns that are enabled
  507. if (in_array($file->filename, $enabled) || in_array($file->name, $priority)) {
  508. continue;
  509. }
  510. $priority[] = $file->name;
  511. // Load and save pattern
  512. if ($pattern = patterns_load_xml($file->filename)) {
  513. patterns_save_pattern($pattern, $file->filename);
  514. }
  515. }
  516. // Get profile patterns next
  517. global $profile;
  518. foreach(file_scan_directory('profiles/'. $profile .'/patterns', '.\.xml') as $file) {
  519. // Can't update existing patterns that are enabled
  520. if (in_array($file->filename, $enabled) || in_array($file->name, $priority)) {
  521. continue;
  522. }
  523. $priority[] = $file->name;
  524. // Load and save pattern
  525. if ($pattern = patterns_load_xml($file->filename)) {
  526. patterns_save_pattern($pattern, $file->filename);
  527. }
  528. }
  529. // Last get the default patterns
  530. foreach(file_scan_directory(drupal_get_path('module', 'patterns') .'/patterns', '.\.xml') as $file) {
  531. // Can't update existing patterns that are enabled
  532. if (in_array($file->filename, $enabled) || in_array($file->name, $priority)) {
  533. continue;
  534. }
  535. $pattern = patterns_load_xml($file->filename);
  536. // Load and save pattern
  537. if ($pattern = patterns_load_xml($file->filename)) {
  538. patterns_save_pattern($pattern, $file->filename);
  539. }
  540. }
  541. variable_set('patterns_loaded', time());
  542. }
  543. $result = db_query('SELECT * FROM {patterns}');
  544. while ($pattern = db_fetch_object($result)) {
  545. $patterns[$pattern->pid] = $pattern;
  546. $data = unserialize($patterns[$pattern->pid]->pattern);
  547. $patterns[$pattern->pid]->pattern = $data;
  548. foreach ($data as $key => $section) {
  549. if ($data[$key]['tag'] == 'info') {
  550. $patterns[$pattern->pid]->info = array();
  551. array_shift($section);
  552. foreach($section as $property) {
  553. $patterns[$pattern->pid]->info[$property['tag']] = $property['value'];
  554. }
  555. }
  556. }
  557. }
  558. return $patterns;
  559. }
  560. function patterns_save_pattern($pattern, $xmlpath = '') {
  561. $name = basename($xmlpath, '.xml');
  562. foreach($pattern[0] as $index => $value) {
  563. if (is_numeric($index)) {
  564. switch($value['tag']) {
  565. case 'title':
  566. $title = $value['value'];
  567. break;
  568. case 'description':
  569. $description = $value['value'];
  570. break;
  571. case 'author':
  572. $author = $value['value'];
  573. break;
  574. }
  575. }
  576. }
  577. if ($pid = db_result(db_query('SELECT pid FROM {patterns} WHERE name = "%s"', $name))) {
  578. $updated = db_result(db_query('SELECT updated FROM {patterns} WHERE pid = "%d"', $pid));
  579. if (($new_updated = filemtime(file_create_path($xmlpath))) > $updated) {
  580. 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);
  581. }
  582. else {
  583. db_query('UPDATE {patterns} SET pattern = "%s", title = "%s", file = "%s", description = "%s" WHERE pid = %d', serialize($pattern), $title, $xmlpath, $description, $pid);
  584. }
  585. }
  586. else {
  587. $pid = db_next_id('{patterns}_pid');
  588. db_query('INSERT INTO {patterns} VALUES(%d, "%s", 0, "%s", "%s", 0, "%s", "%s", "%s")', $pid, $name, $xmlpath, time(), $title, $description, serialize($pattern));
  589. }
  590. }
  591. function patterns_get_pattern($id) {
  592. if (is_numeric($id)) {
  593. $pattern = db_fetch_object(db_query('SELECT * FROM {patterns} WHERE pid = "%d"', $id));
  594. }
  595. else if (is_string($id)) {
  596. $pattern = db_fetch_object(db_query('SELECT * FROM {patterns} WHERE name = "%s"', $id));
  597. }
  598. // Get the actual data. Data is stored in serialized form in the db.
  599. $pattern->pattern = unserialize($pattern->pattern);
  600. // Rearrange the data in a nice way for each component.
  601. // Make sure actions are processed differently so order is preserved.
  602. $pattern->pattern = patterns_rearrange_data($pattern->pattern);
  603. return $pattern;
  604. }
  605. function patterns_load_xml($path) {
  606. if (!file_exists($path)) {
  607. return;
  608. }
  609. $xml = file_get_contents($path);
  610. $pattern = patterns_from_source($xml);
  611. return $pattern;
  612. }
  613. /**
  614. * Create a pattern from an XML data source
  615. */
  616. function patterns_from_source($xml) {
  617. $parse = xml_parser_create();
  618. xml_parse_into_struct($parse, $xml, $vals, $index);
  619. // Create a multi-dimensional array representing the XML structure
  620. $pattern = current(_patterns_parse_tag($vals, 0));
  621. return $pattern;
  622. }
  623. /**
  624. * Recurse through the values of a parsed xml file to create a
  625. * multi-dimensional representation of the data.
  626. */
  627. function _patterns_parse_tag($data, $index) {
  628. $pattern = array();
  629. while ($current = $data[$index]) {
  630. $type = $current['type'];
  631. foreach((array)$current['attributes'] as $key => $value) {
  632. $current[strtolower($key)] = $value;
  633. }
  634. $current['tag'] = strtolower($current['tag']);
  635. unset($current['type'], $current['level'], $current['attributes']);
  636. if (!trim($current['value']) && $current['value'] != "0") {
  637. unset($current['value']);
  638. }
  639. switch($type) {
  640. case 'open':
  641. $index++;
  642. $current += _patterns_parse_tag($data, &$index);
  643. $pattern[] = $current;
  644. break;
  645. case 'close':
  646. $index++;
  647. return $pattern;
  648. break;
  649. case 'complete':
  650. // In order to support more complex/non-standard features we can use serialized data
  651. if ($current['attributes']['serialized']) {
  652. $value = unserialize($current['value']);
  653. if (isset($value)) {
  654. $current['value'] = $value;
  655. }
  656. }
  657. // This enables tags like <blog /> to turn into array('blog' => 'blog')
  658. // which is useful for checkbox/select type forms
  659. if (!isset($current['value'])) {
  660. $current['value'] = $current['tag'];
  661. }
  662. $pattern[] = $current;
  663. break;
  664. }
  665. $index++;
  666. }
  667. return $pattern;
  668. }
  669. function patterns_disable_pattern($pid) {
  670. $form['pid'] = array(
  671. '#type' => 'value',
  672. '#value' => $pid
  673. );
  674. $pattern = patterns_get_pattern($pid);
  675. return confirm_form($form, t('Proceed with disabling pattern %pattern?', array('%pattern' => $pattern->title)), 'admin/build/patterns', '');
  676. }
  677. function patterns_enable_pattern($pid) {
  678. $form['pid'] = array(
  679. '#type' => 'value',
  680. '#value' => $pid
  681. );
  682. $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.');
  683. $pattern = patterns_get_pattern($pid);
  684. return confirm_form($form, t('Proceed with running pattern %pattern?', array('%pattern' => $pattern->title)), 'admin/build/patterns', $disclaimer);
  685. }
  686. function patterns_disable_pattern_submit($form_id, $form_values) {
  687. $pid = $form_values['pid'];
  688. $pattern = patterns_get_pattern($pid);
  689. if (patterns_execute_pattern($pattern, true, true)) {
  690. return 'admin/build/patterns';
  691. }
  692. }
  693. function patterns_enable_pattern_submit($form_id, $form_values) {
  694. $pid = $form_values['pid'];
  695. patterns_load();
  696. $pattern = patterns_get_pattern($pid);
  697. if (patterns_execute_pattern($pattern, false, true)) {
  698. return 'admin/build/patterns';
  699. }
  700. }
  701. function patterns_execute_pattern($pattern, $reverse = false, $interact = false, $jump = null) {
  702. patterns_load();
  703. set_time_limit(0);
  704. if (!is_object($pattern)) {
  705. $pattern = patterns_get_pattern($pattern);
  706. if (!$pattern) {
  707. return false;
  708. }
  709. }
  710. $errors = $action_list = $identifiers = array();
  711. $error = true;
  712. // Pattern info
  713. $status = $pattern->status;
  714. $title = $pattern->title;
  715. $pid = $pattern->pid;
  716. // From here on out just need the actual pattern data
  717. $pattern = $pattern->pattern;
  718. // Split the pattern up into modules and actions. Submit modules as its
  719. // own pattern programattically.
  720. $modules = $actions = array();
  721. for($i=0;$tag=$pattern[$i];$i++) {
  722. if ($tag['tag'] == 'modules') {
  723. $modules = $tag;
  724. }
  725. else if ($tag['tag'] == 'actions') {
  726. $actions = $tag;
  727. unset($actions['tag']);
  728. }
  729. }
  730. // If there are no actions or modules, most likely the pattern
  731. // was not created correctly.
  732. if (empty($actions) && empty($modules)) {
  733. drupal_set_message(t('Could not recognize pattern %title, aborting.', array('%title' => $title)), 'error');
  734. return true;
  735. }
  736. if ($modules && (!$interact || ($interact && !$jump))) {
  737. // Make the modules look like a normal pattern so they can be executed
  738. // on their own.
  739. $obj = new stdClass();
  740. $obj->title = t('Enable/disable %title pattern modules', array('%title' => $title));
  741. $obj->status = $status;
  742. $obj->pattern = array(array('tag' => 'actions', $modules));
  743. $modules = $obj;
  744. // Modules need to be enabled first so the rest of the pattern
  745. // can proceed smoothly.
  746. if (!$reverse) {
  747. $error = patterns_execute_pattern($modules, $reverse, $interact);
  748. module_rebuild_cache();
  749. }
  750. }
  751. // Keep a list of what modules handle what tags
  752. $tag_modules = patterns_invoke($empty, 'tag modules');
  753. // If an interactive pattern needs resuming, remove the
  754. // already executed actions
  755. if ($interact && $jump > 0) {
  756. array_splice($actions, 0, $jump);
  757. }
  758. // Prepare actions for validation/processing
  759. foreach($actions as $key => $data) {
  760. patterns_invoke($actions[$key], 'prepare');
  761. }
  762. // Reverse a pattern for disabling
  763. if ($reverse && $status) {
  764. $actions = array_reverse($actions);
  765. foreach($actions as $key => $data) {
  766. $continue = patterns_invoke($data, 'reverse');
  767. if ($continue === false) {
  768. drupal_set_message(t('[Error] Disabling of this pattern is not supported at this time.'));
  769. return false;
  770. }
  771. $actions[$key] = $data;
  772. }
  773. }
  774. // Pre validate tags with their appropriate components
  775. foreach($actions as $key => $data) {
  776. if (!array_key_exists($data['tag'], $tag_modules)) {
  777. $errors[$data['tag']][] = t('Invalid Pattern: <%tag> is not a valid tag', array('%tag' => $data['tag']));
  778. }
  779. else {
  780. $error = patterns_invoke($data, 'pre-validate');
  781. if ($error) {
  782. $errors[$data['tag']][] = t('Invalid Pattern: !msg', array('!msg' => $error));
  783. }
  784. }
  785. }
  786. if (count($errors)) {
  787. foreach($errors as $error) {
  788. drupal_set_message(implode('<br>', $error), 'error');
  789. }
  790. return;
  791. }
  792. // Build and execute a list of actions
  793. foreach($actions as $key => $data) {
  794. // Prepare actions for processing, ensure smooth pattern executions, and return form ids for execution
  795. $return = patterns_invoke($data, 'form_id');
  796. // If prepare removed the data, dont continue with this action
  797. if (!$data || !$return) {
  798. continue;
  799. }
  800. if (is_string($return)) {
  801. $form_ids = array($return);
  802. }
  803. else if ($return) {
  804. $form_ids = $return;
  805. }
  806. // Build the action
  807. foreach($form_ids as $form_id) {
  808. $clone = $data;
  809. $error = patterns_invoke($clone, 'validate', $form_id);
  810. if ($error) {
  811. if (is_array($error)) {
  812. foreach($error as $msg) {
  813. drupal_set_message($msg, 'error');
  814. }
  815. $errors[$clone['tag']] = t('Broken Pattern: %msg', array('%msg' => implode('<br>', $error)));
  816. }
  817. else {
  818. drupal_set_message($error, 'error');
  819. $errors[$clone['tag']] = t('Broken Pattern: %msg', array('%msg' => $error));
  820. }
  821. return;
  822. }
  823. // If tokens are enabled, apply tokens to the action values
  824. // before processing
  825. if (module_exists('token')) {
  826. _patterns_recurse_tokens($clone, $identifiers);
  827. //array_walk($clone, '_patterns_replace_tokens', $identifiers);
  828. }
  829. // Get the form data for the action
  830. $values = patterns_invoke($clone, 'build', $form_id);
  831. // Dont execute the action if a string was returned, indicating the pattern component
  832. // most likely handled the action on its own and this is the message to display.
  833. if (is_string($values)) {
  834. drupal_set_message($values);
  835. }
  836. else {
  837. // Get any extra parameters required for the action
  838. $params = patterns_invoke($clone, 'params', $form_id, $values);
  839. if (isset($params) && !is_array($params)) {
  840. $params = array($params);
  841. }
  842. // Execute action
  843. patterns_execute_action($form_id, $values, $params);
  844. if (form_get_errors()) {
  845. $descriptions = patterns_invoke($clone, 'actions');
  846. drupal_set_message(t('An error occured running action #%num (%action)', array('%num' => $key+1, '%action' => $descriptions[$form_id])), 'error');
  847. $error = true;
  848. break;
  849. }
  850. }
  851. patterns_invoke($clone, 'cleanup', $form_id);
  852. // Clear the cache in case it causes problems
  853. cache_clear_all();
  854. }
  855. if ($error) {
  856. break;
  857. }
  858. // Get any primary identifiers from the action for further actions to take advantage of
  859. $id = null;
  860. $id = patterns_invoke($clone, 'identifier', $form_id);
  861. if (isset($id)) {
  862. $identifiers[$key+1] = $id;
  863. }
  864. }
  865. if (empty($errors)) {
  866. if ($reverse) {
  867. if ($modules) {
  868. // Modules need to be disabled last so the rest of the pattern
  869. // can reverse itself properly
  870. $error = patterns_execute_pattern($modules, $reverse, $interact);
  871. }
  872. // Mark pattern as disabled
  873. if ($pid) {
  874. db_query('UPDATE {patterns} SET status = 0 WHERE pid = %d', $pid);
  875. }
  876. drupal_set_message(t('Pattern reversed successfully.'));
  877. }
  878. else {
  879. // Mark pattern as enabled
  880. if ($pid) {
  881. db_query('UPDATE {patterns} SET status = 1, enabled = "%s" WHERE pid = %d', time(), $pid);
  882. }
  883. drupal_set_message(t('Pattern ran successfully.'));
  884. }
  885. }
  886. return !$error;
  887. }
  888. /**
  889. * Execute an action
  890. */
  891. function patterns_execute_action($form_id, $values, $params) {
  892. // Make sure we always have a clear cache for everything
  893. $result = db_query('SHOW TABLES LIKE "cache_%"');
  894. while ($table = db_fetch_array($result)) {
  895. $table = current($table);
  896. cache_clear_all(null, $table);
  897. }
  898. $args = array($form_id, $values);
  899. if (is_array($params)) {
  900. $args = array_merge($args, $params);
  901. }
  902. patterns_executing(true);
  903. //$form = call_user_func_array('drupal_retrieve_form', $args);
  904. //$form['#post'] = $values;
  905. //$return = drupal_process_form($form_id, $form);
  906. $return = call_user_func_array('drupal_execute', $args);
  907. patterns_executing(false);
  908. return $return;
  909. }
  910. function patterns_executing($b = null) {
  911. static $executing = false;
  912. if (is_bool($b)) {
  913. $executing = $b;
  914. }
  915. return $executing;
  916. }
  917. function patterns_rearrange_data($pattern) {
  918. foreach($pattern as $key => $value) {
  919. if (is_string($key)) {
  920. unset($pattern[$key]);
  921. }
  922. else {
  923. if ($value['tag'] == 'actions') {
  924. $pattern[$key] = patterns_rearrange_data($value);
  925. $pattern[$key]['tag'] = 'actions';
  926. }
  927. else {
  928. $pattern[$key] = _patterns_rearrange_data($value);
  929. }
  930. }
  931. }
  932. return $pattern;
  933. }
  934. /**
  935. * Return a list of modules for a pattern
  936. */
  937. function patterns_modules($pattern) {
  938. $pattern = $pattern->pattern;
  939. for($i=0;$tag=$pattern[$i];$i++) {
  940. if ($tag['tag'] == 'modules') {
  941. unset($tag['tag']);
  942. $modules = $tag;
  943. break;
  944. }
  945. }
  946. return $modules;
  947. }
  948. function patterns_process_modules($modules, $op = 'enable') {
  949. // Enable at the beginning of the pattern. Disable at the end.
  950. for($i=0;$module=$modules[$i];$i++) {
  951. if (($op == 'enable' && $module['delete']) || ($op == 'disable' && !$module['delete'])) {
  952. unset($modules[$i]);
  953. }
  954. }
  955. patterns_invoke($empty, 'tag modules');
  956. $error = patterns_invoke($modules, 'pre-validate');
  957. // Error validating modules
  958. if ($error) {
  959. return $error;
  960. }
  961. patterns_invoke($modules, 'prepare');
  962. }
  963. function patterns_invoke(&$data, $op, $form_id = null, $a = null) {
  964. static $tag_modules;
  965. if (!is_array($tag_modules) || $op == 'tag modules') {
  966. // Get a list of tags and their modules
  967. foreach(module_implements('patterns') as $module) {
  968. $tags = module_invoke($module, 'patterns', 'tags');
  969. foreach($tags as $tag => $value) {
  970. if (is_array($value)) {
  971. $tag_modules[$tag] = $module;
  972. }
  973. else {
  974. $tag_modules[$value] = $module;
  975. }
  976. }
  977. }
  978. }
  979. // If you just want the modules list
  980. if ($op == 'tag modules') {
  981. return $tag_modules;
  982. }
  983. $tag = $data['tag'];
  984. unset($data['tag']);
  985. $module = $tag_modules[$tag];
  986. $func = $module .'_patterns';
  987. if (function_exists($func)) {
  988. if ($form_id) {
  989. $return = $func($op, $form_id, $data, $a);
  990. }
  991. else {
  992. $return = $func($op, $tag, $data, $a);
  993. }
  994. }
  995. $data['tag'] = $tag;
  996. return $return;
  997. }
  998. function _patterns_rearrange_data($data, $parent = '') {
  999. $numeric = array();
  1000. $count=0;
  1001. foreach($data as $key => $value) {
  1002. if ($value['value'] == 'false') {
  1003. $value['value'] = false;
  1004. }
  1005. else if ($value['value'] == 'true') {
  1006. $value['value'] = true;
  1007. }
  1008. if (is_numeric($key) && is_array($value) && count($value) == 2 && isset($value['tag']) && isset($value['value'])) {
  1009. unset($data[$key]);
  1010. if (isset($data[$value['tag']])) {
  1011. $numeric[] = $value['tag'];
  1012. $data[$count++] = $data[$value['tag']];
  1013. $data[$count++] = $value['value'];
  1014. unset($data[$value['tag']]);
  1015. }
  1016. else if (in_array($value['tag'], $numeric)) {
  1017. $data[$count++] = $value['value'];
  1018. }
  1019. else {
  1020. $data[$value['tag']] = $value['value'];
  1021. }
  1022. }
  1023. else if (is_numeric($key)) {
  1024. $tag = $value['tag'];
  1025. unset($value['tag']);
  1026. $data[$tag][] = _patterns_rearrange_data($value, $tag);
  1027. unset($data[$key]);
  1028. }
  1029. }
  1030. foreach($data as $key => $value) {
  1031. if (is_array($value) && count($value) == 1 && $value[0]) {
  1032. $data[$key] = $data[$key][0];
  1033. }
  1034. }
  1035. return $data;
  1036. }
  1037. function patterns_form_alter($form_id, &$form) {
  1038. if (user_access('administer patterns') && variable_get('patterns_form_helper', true)) {
  1039. $form['#after_build'][] = 'patterns_form_helper';
  1040. }
  1041. }
  1042. function patterns_form_helper($form) {
  1043. global $form_submitted;
  1044. // static $form_id, $form_values;
  1045. $args = func_get_args();
  1046. if (!$form_id && $form_submitted) {
  1047. $form_values = $args[1];
  1048. $form_id = $form_values['form_id'];
  1049. $_SESSION['patterns_form_helper'] = array('form_id' => $form_id, 'form_values' => $form_values);
  1050. }
  1051. return $form;
  1052. }
  1053. function patterns_init() {
  1054. if (variable_get('patterns_form_helper', true) && $_SESSION['patterns_form_helper']) {
  1055. drupal_add_css(drupal_get_path('module', 'patterns') .'/patterns.css');
  1056. drupal_add_js(drupal_get_path('module', 'patterns') .'/patterns.js');
  1057. }
  1058. }
  1059. function patterns_exit($destination = null) {
  1060. if (variable_get('patterns_form_helper', true) && $_SESSION['patterns_form_helper'] && !$destination) {
  1061. print theme('patterns_form_helper', $_SESSION['patterns_form_helper']['form_id'], $_SESSION['patterns_form_helper']['form_values']);
  1062. //unset($_SESSION['patterns_form_helper']);
  1063. }
  1064. }
  1065. /**
  1066. * Implementation of hook_token_values()
  1067. *
  1068. * @If these get implementated directly into token.module, this should be removed
  1069. */
  1070. function patterns_token_values($type, $object = NULL, $options = array()) {
  1071. if ($type == 'global') {
  1072. $path = conf_path();
  1073. $tokens['confpath'] = $path;
  1074. return $tokens;
  1075. }
  1076. }
  1077. /**
  1078. * Function callback
  1079. */
  1080. function _patterns_modify_value(&$form) {
  1081. foreach($form as $key => $value) {
  1082. if (is_array($value) && isset($value['#type']) && $value['#type'] == 'value') {
  1083. $form[$key]['#default_value'] = $value['#value'];
  1084. unset($form[$key]['#value']);
  1085. }
  1086. else if (is_array($value)) {
  1087. _patterns_modify_value($form[$key]);
  1088. }
  1089. }
  1090. }
  1091. /**
  1092. * Recurse an array and replace with tokens
  1093. * @ This is used instead of array_walk_recursive because
  1094. * of some strange issues with token_get_values failing.
  1095. */
  1096. function _patterns_recurse_tokens(&$object, $identifiers) {
  1097. foreach($object as $key => $value) {
  1098. if (is_array($value)) {
  1099. _patterns_recurse_tokens($object[$key], $identifiers);
  1100. }
  1101. else if (is_scalar($value)) {
  1102. $old_key = $key;
  1103. _patterns_replace_tokens($object[$key], $key, $identifiers);
  1104. // The key was changed, change it
  1105. if ($old_key != $key) {
  1106. $keys = array_keys($object);
  1107. $keys[array_search($old_key, $keys)] = $key;
  1108. $object = array_combine($keys, array_values($object));
  1109. }
  1110. }
  1111. }
  1112. }
  1113. /**
  1114. * Array walk callback to replace tokens inside form values
  1115. */
  1116. function _patterns_replace_tokens(&$a, &$b, $identifiers = array()) {
  1117. static $count = 0;
  1118. // Replace IDs with identifiers from the current executing pattern
  1119. if (preg_match('/@([0-9]+)@/', $a, $match)) {
  1120. $a = str_replace($match[0], $identifiers[$match[1]], $a);
  1121. }
  1122. if (preg_match('/__([0-9]+)__/', $b, $match)) {
  1123. $b = str_replace($match[0], $identifiers[$match[1]], $a);
  1124. }
  1125. // Replace tokens
  1126. $a = token_replace($a, 'global', NULL, '@', '@');
  1127. $b = token_replace($b, 'global', NULL, '__', '__');
  1128. }
  1129. /**
  1130. * Check if a .htaccess file exists to prevent downloads of pattern files
  1131. */
  1132. function _patterns_check_file_dir() {
  1133. return false;
  1134. $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
  1135. if (!is_file($path .'/.htaccess')) {
  1136. $content = '# Prevent downloading site patterns
  1137. <FilesMatch "\.xml$">
  1138. Order allow,deny
  1139. </FilesMatch>
  1140. ';
  1141. file_save_data($content, $path .'/.htaccess');
  1142. }
  1143. }
  1144. function theme_patterns_form_helper_menu($forms) {
  1145. $output = '<ul class="patterns-form-menu">';
  1146. foreach ($forms as $form_id => $values) {
  1147. $output .= '<li class="patterns-form-menu-item">'. $form_id .'</li>';
  1148. }
  1149. $output .= '</li>';
  1150. return $output;
  1151. }
  1152. function theme_patterns_form_helper($form_id, $values) {
  1153. $output = '<div class="patterns-form" id="patterns-form-'. $form_id .'">';
  1154. $output .= '<div class="patterns-form-title">'. t('Form values for %key', array('%key' => $form_id)) .'</div>';
  1155. foreach($values as $key => $value) {
  1156. $output .= '<div class="patterns-form-item"><div class="patterns-form-key">'. $key .' => </div>';
  1157. $output .= '<div class="patterns-form-value">'. print_r($value, true) .'</div></div>';
  1158. }
  1159. $output .= '</div>';
  1160. return $output;
  1161. }