2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.100 2007/10/02 13:45:13 henoheno Exp $
4 // Copyright (C) 2003-2005, 2007 PukiWiki Developers Team
5 // License: GPL v2 or (at your option) any later version
7 // Issue tracker plugin (See Also bugtrack plugin)
9 define('PLUGIN_TRACKER_USAGE', '#tracker([config[/form][,basepage]])');
10 define('PLUGIN_TRACKER_LIST_USAGE', '#tracker_list([config[/list]][[,base][,field:sort[;field:sort ...][,limit]]])');
12 define('PLUGIN_TRACKER_DEFAULT_CONFIG', 'default');
13 define('PLUGIN_TRACKER_DEFAULT_FORM', 'form');
14 define('PLUGIN_TRACKER_DEFAULT_LIST', 'list');
15 define('PLUGIN_TRACKER_DEFAULT_LIMIT', 0 ); // 0 = Unlimited
16 define('PLUGIN_TRACKER_DEFAULT_ORDER', ''); // Example: '_real'
18 // Allow N columns sorted at a time
19 define('PLUGIN_TRACKER_LIST_SORT_LIMIT', 3);
22 define('PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN','#^SubMenu$|/#'); // 'SubMenu' and using '/'
23 //define('PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN','#(?!)#'); // Nothing excluded
25 // Show error rows (can't capture columns properly)
26 define('PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE', 1);
31 define('PLUGIN_TRACKER_SORT_TYPE_REGULAR', 0);
32 define('PLUGIN_TRACKER_SORT_TYPE_NUMERIC', 1);
33 define('PLUGIN_TRACKER_SORT_TYPE_STRING', 2);
34 //define('PLUGIN_TRACKER_SORT_TYPE_LOCALE_STRING', 5);
35 define('PLUGIN_TRACKER_SORT_TYPE_NATURAL', 6);
36 if (! defined('SORT_NATURAL')) define('SORT_NATURAL', PLUGIN_TRACKER_SORT_TYPE_NATURAL);
39 define('PLUGIN_TRACKER_SORT_ORDER_DESC', 3);
40 define('PLUGIN_TRACKER_SORT_ORDER_ASC', 4);
41 define('PLUGIN_TRACKER_SORT_ORDER_DEFAULT', PLUGIN_TRACKER_SORT_ORDER_ASC);
45 function plugin_tracker_convert()
49 if (PKWK_READONLY) return ''; // Show nothing
51 $base = $refer = isset($vars['page']) ? $vars['page'] : '';
52 $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
53 $form = PLUGIN_TRACKER_DEFAULT_FORM;
55 $args = func_get_args();
58 return PLUGIN_TRACKER_USAGE . '<br />';
62 $arg = get_fullname($args[1], $base);
63 if (is_pagename($arg)) $base = $arg;
68 $arg = explode('/', trim($args[0]), 2);
69 if ($arg[0] != '' ) $config_name = trim($arg[0]);
70 if (isset($arg[1])) $form = trim($arg[1]);
73 unset($args, $argc, $arg);
75 $config = new Config('plugin/tracker/' . $config_name);
76 if (! $config->read()) {
77 return '#tracker: Config \'' . htmlspecialchars($config_name) . '\' not found<br />';
79 $config->config_name = $config_name;
81 $form = $config->page . '/' . $form;
82 $template = plugin_tracker_get_source($form, TRUE);
83 if ($template === FALSE || empty($template)) {
84 return '#tracker: Form \'' . make_pagelink($form) . '\' not found or seems empty<br />';
87 $_form = & new Tracker_form($base, $refer, $config);
88 $_form->initFields(plugin_tracker_field_pickup($template));
89 $_form->initHiddenFields();
90 $fields = $_form->fields;
92 $from = $to = $hidden = array();
93 foreach (array_keys($fields) as $fieldname) {
94 $from[] = '[' . $fieldname . ']';
95 $_to = $fields[$fieldname]->get_tag();
96 if (is_a($fields[$fieldname], 'Tracker_field_hidden')) {
102 unset($fields[$fieldname]);
105 $script = get_script_uri();
106 $template = str_replace($from, $to, convert_html($template));
107 $hidden = implode('<br />' . "\n", $hidden);
109 <form enctype="multipart/form-data" action="$script" method="post">
119 function plugin_tracker_action()
121 global $post, $vars, $now;
123 if (PKWK_READONLY) die_message('PKWK_READONLY prohibits editing');
125 $base = isset($post['_base']) ? $post['_base'] : '';
126 $refer = isset($post['_refer']) ? $post['_refer'] : $base;
127 if (! is_pagename($refer)) {
129 'msg' => 'Cannot write',
130 'body' => 'Page name (' . htmlspecialchars($refer) . ') invalid'
134 // $page name to add will be decided here
136 $name = isset($post['_name']) ? $post['_name'] : '';
137 if (isset($post['_page'])) {
138 $real = $page = $post['_page'];
140 $real = is_pagename($name) ? $name : ++$num;
141 $page = get_fullname('./' . $real, $base);
143 if (! is_pagename($page)) $page = $base;
144 while (is_page($page)) {
146 $page = $base . '/' . $real;
149 // Loading configuration
150 $config_name = isset($post['_config']) ? $post['_config'] : '';
151 $config = new Config('plugin/tracker/' . $config_name);
152 if (! $config->read()) {
153 return '<p>config file \'' . htmlspecialchars($config_name) . '\' not found.</p>';
155 $config->config_name = $config_name;
158 $_post = array_merge($post, $_FILES);
159 $_post['_date'] = $now;
160 $_post['_page'] = $page;
161 $_post['_name'] = $name;
162 $_post['_real'] = $real;
163 // $_post['_refer'] = $_post['refer'];
165 // Creating an empty page, before attaching files
166 pkwk_touch_file(get_filename($page));
168 $from = $to = array();
171 $template_page = $config->page . '/page';
172 $template = plugin_tracker_get_source($template_page);
173 if ($template === FALSE || empty($template)) {
175 'msg' => 'Cannot write',
176 'body' => 'Page template (' . htmlspecialchars($template_page) . ') not exists or seems empty'
180 $form = & new Tracker_form($base, $refer, $config);
181 $form->initFields(plugin_tracker_field_pickup(implode('', $template)));
182 $fields = & $form->fields; // unset()
183 foreach (array_keys($fields) as $field) {
184 $from[] = '[' . $field . ']';
185 $to[] = isset($_post[$field]) ? $fields[$field]->format_value($_post[$field]) : '';
186 unset($fields[$field]);
189 // Repalace every [$field]s (found inside $template) to real values
190 $subject = $escape = array();
191 foreach (array_keys($template) as $linenum) {
192 if (trim($template[$linenum]) == '') continue;
194 // Escape some TextFormattingRules
195 $letter = $template[$linenum][0];
196 if ($letter == '|' || $letter == ':') {
197 $escape['|'][$linenum] = $template[$linenum];
198 } else if ($letter == ',') {
199 $escape[','][$linenum] = $template[$linenum];
201 // TODO: Escape "\n" except multiline-allowed fields
202 $subject[$linenum] = $template[$linenum];
205 foreach (str_replace($from, $to, $subject) as $linenum => $line) {
206 $template[$linenum] = $line;
209 // Escape for some TextFormattingRules
210 foreach(array_keys($escape) as $hint) {
211 $to_e = plugin_tracker_escape($to, $hint);
212 foreach (str_replace($from, $to_e, $escape[$hint]) as $linenum => $line) {
213 $template[$linenum] = $line;
220 // Write $template, without touch
221 page_write($page, join('', $template));
224 header('Location: ' . get_script_uri() . '?' . rawurlencode($page));
228 // Data set of XHTML form or something
237 var $fields = array();
239 var $error = ''; // Error message
241 function Tracker_form($base, $refer, $config)
243 static $id = 0; // Unique id per instance
247 $this->refer = $refer;
248 $this->config = $config;
251 function addField($fieldname, $displayname, $type = 'text', $options = '20', $default = '')
253 // TODO: Return an error
254 if (isset($this->fields[$fieldname])) return TRUE;
256 $class = 'Tracker_field_' . $type;
257 if (! class_exists($class)) {
258 // TODO: Return an error
260 $class = 'Tracker_field_' . $type;
264 $this->fields[$fieldname] = & new $class(
278 function initFields($requests = NULL)
280 if (! isset($this->raw_fields)) {
281 $raw_fields = array();
283 foreach ($this->config->get('fields') as $field) {
284 $fieldname = isset($field[0]) ? $field[0] : '';
285 $raw_fields[$fieldname] = array(
286 'display' => isset($field[1]) ? $field[1] : '',
287 'type' => isset($field[2]) ? $field[2] : '',
288 'options' => isset($field[3]) ? $field[3] : '',
289 'default' => isset($field[4]) ? $field[4] : '',
293 $default = array('options' => '20', 'default' => '');
295 '_date' => 'text', // Post date
296 '_update' => 'date', // Last modified date
297 '_past' => 'past', // Elapsed time (passage)
298 '_page' => 'page', // Page name
299 '_name' => 'text', // Page name specified by poster
300 '_real' => 'real', // Page name (Real)
301 '_refer' => 'page', // Page name refer from this (Page who has forms)
303 '_submit' => 'submit'
304 ) as $fieldname => $type) {
305 if (isset($raw_fields[$fieldname])) continue;
306 $raw_fields[$fieldname] = array(
307 'display' => plugin_tracker_message('btn' . $fieldname),
311 $this->raw_fields = & $raw_fields;
313 $raw_fields = & $this->raw_fields;
316 if ($requests === NULL) {
317 // (The rest of) All, defined order
318 foreach ($raw_fields as $fieldname => $field) {
327 $raw_fields = array();
329 // Part of, specific order
330 if (! is_array($requests)) $requests = array($requests);
331 foreach ($requests as $fieldname) {
332 if (! isset($raw_fields[$fieldname])) continue;
333 $field = $raw_fields[$fieldname];
341 unset($raw_fields[$fieldname]);
348 function initHiddenFields()
350 // Make sure to init $this->raw_fields
351 $this->initFields(array());
354 foreach ($this->raw_fields as $fieldname => $field) {
355 if ($field['type'] == 'hidden') {
356 $fields[] = $fieldname;
360 $this->initFields($fields);
364 // TODO: Why a filter sometimes created so many?
365 // Field classes within a form
370 var $form; // Parent (class Tracker_form)
378 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_REGULAR;
380 function Tracker_field(& $tracker_form, $field)
383 static $id = 0; // Unique id per instance, and per class(extended-class)
387 $this->form = & $tracker_form;
388 $this->name = isset($field[0]) ? $field[0] : '';
389 $this->title = isset($field[1]) ? $field[1] : '';
390 $this->values = isset($field[3]) ? explode(',', $field[3]) : array();
391 $this->default_value = isset($field[4]) ? $field[4] : '';
393 $this->data = isset($post[$this->name]) ? $post[$this->name] : '';
396 // XHTML part inside a form
407 function format_value($value)
412 function format_cell($str)
417 // Compare key for Tracker_list->sort()
418 function get_value($value)
420 return $value; // Default: $value itself
424 class Tracker_field_text extends Tracker_field
426 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
430 return '<input type="text"' .
431 ' name="' . htmlspecialchars($this->name) . '"' .
432 ' size="' . htmlspecialchars($this->values[0]) . '"' .
433 ' value="' . htmlspecialchars($this->default_value) . '" />';
437 class Tracker_field_page extends Tracker_field_text
439 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
441 function format_value($value)
443 $value = strip_bracket($value);
444 if (is_pagename($value)) $value = '[[' . $value . ']]';
445 return parent::format_value($value);
449 class Tracker_field_real extends Tracker_field_text
451 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NATURAL;
454 class Tracker_field_title extends Tracker_field_text
456 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
458 function format_cell($str)
465 class Tracker_field_textarea extends Tracker_field
467 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
472 ' name="' . htmlspecialchars($this->name) . '"' .
473 ' cols="' . htmlspecialchars($this->values[0]) . '"' .
474 ' rows="' . htmlspecialchars($this->values[1]) . '">' .
475 htmlspecialchars($this->default_value) .
479 function format_cell($str)
481 $str = preg_replace('/[\r\n]+/', '', $str);
482 if (! empty($this->values[2]) && strlen($str) > ($this->values[2] + 3)) {
483 $str = mb_substr($str, 0, $this->values[2]) . '...';
489 class Tracker_field_format extends Tracker_field
491 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
492 var $styles = array();
493 var $formats = array();
495 function Tracker_field_format(& $tracker_form, $field)
497 parent::Tracker_field($tracker_form, $field);
499 foreach ($this->form->config->get($this->name) as $option) {
500 list($key, $style, $format) =
501 array_pad(array_map(create_function('$a', 'return trim($a);'), $option), 3, '');
502 if ($style != '') $this->styles[$key] = $style;
503 if ($format != '') $this->formats[$key] = $format;
509 return '<input type="text"' .
510 ' name="' . htmlspecialchars($this->name) . '"' .
511 ' size="' . htmlspecialchars($this->values[0]) . '" />';
514 function get_key($str)
516 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
519 function format_value($str)
521 if (is_array($str)) {
522 return join(', ', array_map(array($this, 'format_value'), $str));
525 $key = $this->get_key($str);
526 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
529 function get_style($str)
531 $key = $this->get_key($str);
532 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
536 class Tracker_field_file extends Tracker_field_format
538 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
542 return '<input type="file"' .
543 ' name="' . htmlspecialchars($this->name) . '"' .
544 ' size="' . htmlspecialchars($this->values[0]) . '" />';
547 function format_value()
549 if (isset($_FILES[$this->name])) {
551 require_once(PLUGIN_DIR . 'attach.inc.php');
553 $base = $this->form->base;
554 $result = attach_upload($_FILES[$this->name], $base);
555 if (isset($result['result']) && $result['result']) {
557 return parent::format_value($base . '/' . $_FILES[$this->name]['name']);
561 // Filename not specified, or Fail to upload
562 return parent::format_value('');
566 class Tracker_field_radio extends Tracker_field_format
568 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
569 var $_options = array();
576 $s_name = htmlspecialchars($this->name);
577 foreach ($this->form->config->get($this->name) as $option) {
579 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
580 $s_option = htmlspecialchars($option[0]);
581 $checked = trim($option[0]) == trim($this->default_value) ? ' checked="checked"' : '';
583 $retval .= '<input type="radio"' .
584 ' name="' . $s_name . '"' .
585 ' id="' . $s_id . '"' .
586 ' value="' . $s_option . '"' .
588 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
594 function get_key($str)
599 function get_value($value)
601 $options = & $this->_options;
604 if (! isset($options[$name])) {
605 $values = array_map('reset', $this->form->config->get($name));
606 $options[$name] = array_flip($values); // array('value0' => 0, 'value1' => 1, ...)
609 return isset($options[$name][$value]) ? $options[$name][$value] : $value;
613 class Tracker_field_select extends Tracker_field_radio
615 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
617 function get_tag($empty = FALSE)
619 $s_name = htmlspecialchars($this->name);
620 $s_size = (isset($this->values[0]) && is_numeric($this->values[0])) ?
621 ' size="' . htmlspecialchars($this->values[0]) . '"' :
623 $s_multiple = (isset($this->values[1]) && strtolower($this->values[1]) == 'multiple') ?
624 ' multiple="multiple"' :
627 $retval = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>' . "\n";
628 if ($empty) $retval .= ' <option value=""></option>' . "\n";
629 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
630 foreach ($this->form->config->get($this->name) as $option) {
631 $s_option = htmlspecialchars($option[0]);
632 $selected = isset($defaults[trim($option[0])]) ? ' selected="selected"' : '';
633 $retval .= ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>' . "\n";
635 $retval .= '</select>';
641 class Tracker_field_checkbox extends Tracker_field_radio
643 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
650 $s_name = htmlspecialchars($this->name);
651 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
652 foreach ($this->form->config->get($this->name) as $option) {
654 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
655 $s_option = htmlspecialchars($option[0]);
656 $checked = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
658 $retval .= '<input type="checkbox"' .
659 ' name="' . $s_name . '[]"' .
660 ' id="' . $s_id . '"' .
661 ' value="' . $s_option . '"' .
663 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
670 class Tracker_field_hidden extends Tracker_field_radio
672 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
676 return '<input type="hidden"' .
677 ' name="' . htmlspecialchars($this->name) . '"' .
678 ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
682 class Tracker_field_submit extends Tracker_field
688 $s_title = htmlspecialchars($this->title);
689 $s_base = htmlspecialchars($form->base);
690 $s_refer = htmlspecialchars($form->refer);
691 $s_config = htmlspecialchars($form->config->config_name);
694 <input type="submit" value="$s_title" />
695 <input type="hidden" name="plugin" value="tracker" />
696 <input type="hidden" name="_refer" value="$s_refer" />
697 <input type="hidden" name="_base" value="$s_base" />
698 <input type="hidden" name="_config" value="$s_config" />
703 class Tracker_field_date extends Tracker_field
705 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
707 function format_cell($timestamp)
709 return format_date($timestamp);
713 class Tracker_field_past extends Tracker_field
715 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
717 function format_cell($timestamp)
719 return get_passage($timestamp, FALSE);
722 function get_value($value)
724 return UTIME - $value;
728 ///////////////////////////////////////////////////////////////////////////
729 // tracker_list plugin
731 function plugin_tracker_list_convert()
735 $base = $refer = isset($vars['page']) ? $vars['page'] : '';
736 $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
737 $list = PLUGIN_TRACKER_DEFAULT_LIST;
738 $limit = PLUGIN_TRACKER_DEFAULT_LIMIT;
739 $order = PLUGIN_TRACKER_DEFAULT_ORDER;
741 $args = func_get_args();
742 $argc = count($args);
744 return PLUGIN_TRACKER_LIST_USAGE . '<br />';
747 case 4: $limit = $args[3]; /*FALLTHROUGH*/
748 case 3: $order = $args[2]; /*FALLTHROUGH*/
750 $arg = get_fullname($args[1], $base);
751 if (is_pagename($arg)) $base = $arg;
755 if ($args[0] != '') {
756 $arg = explode('/', $args[0], 2);
757 if ($arg[0] != '' ) $config_name = $arg[0];
758 if (isset($arg[1])) $list = $arg[1];
761 unset($args, $argc, $arg);
763 return plugin_tracker_list_render($base, $refer, $config_name, $list, $order, $limit);
766 function plugin_tracker_list_action()
770 $base = isset($get['base']) ? $get['base'] : ''; // Base directory to load
772 if (isset($get['refer'])) {
773 $refer = $get['refer']; // Where to #tracker_list
774 if ($base == '') $base = $refer;
779 $config = isset($get['config']) ? $get['config'] : '';
780 $list = isset($get['list']) ? $get['list'] : 'list';
781 $order = isset($get['order']) ? $get['order'] : PLUGIN_TRACKER_DEFAULT_ORDER;
782 $limit = isset($get['limit']) ? $get['limit'] : 0;
784 $s_refer = make_pagelink($refer);
786 'msg' => plugin_tracker_message('msg_list'),
787 'body'=> str_replace('$1', $s_refer, plugin_tracker_message('msg_back')) .
788 plugin_tracker_list_render($base, $refer, $config, $list, $order, $limit)
792 function plugin_tracker_list_render($base, $refer, $config_name, $list, $order_commands = '', $limit = 0)
795 if ($base == '') return '#tracker_list: Base not specified' . '<br />';
797 $refer = trim($refer);
798 if (! is_page($refer)) {
799 return '#tracker_list: Refer page not found: ' . htmlspecialchars($refer) . '<br />';
802 $config_name = trim($config_name);
803 if ($config_name == '') $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
806 if (! is_numeric($limit)) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
807 $limit = intval($limit);
809 $config = new Config('plugin/tracker/' . $config_name);
810 if (! $config->read()) {
811 return '#tracker_list: Config not found: ' . htmlspecialchars($config_name) . '<br />';
813 $config->config_name = $config_name;
814 if (! is_page($config->page . '/' . $list)) {
815 return '#tracker_list: List not found: ' . make_pagelink($config->page . '/' . $list) . '<br />';
818 $list = & new Tracker_list($base, $refer, $config, $list);
819 if ($list->setSortOrder($order_commands) === FALSE) {
820 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
822 $result = $list->toString($limit);
823 if ($result === FALSE) {
824 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
828 return convert_html($result);
841 var $orders = array();
842 var $error = ''; // Error message
845 var $_added = array();
849 var $_the_first_character_of_the_line;
851 function Tracker_list($base, $refer, & $config, $list)
853 $form = & new Tracker_form($base, $refer, $config);
858 // Add multiple pages at a time
861 $base = $this->form->base . '/';
862 $len = strlen($base);
863 $regex = '#^' . preg_quote($base, '#') . '#';
865 // Adding $this->rows
866 foreach (preg_grep($regex, array_values(get_existpages())) as $pagename) {
867 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, substr($pagename, $len))) {
870 if ($this->addRow($pagename) === FALSE) return FALSE;
872 if (empty($this->rows)) {
873 $this->error = 'Pages not found under: ' . $base;
880 // addRow(): Generate regex to load a page
881 function _generate_regex()
883 $template_page = $this->form->config->page . '/page';
884 $fields = $this->form->fields;
887 $pattern_fields = array();
889 $template = plugin_tracker_get_source($template_page, TRUE);
890 if ($template === FALSE || empty($template)) {
891 $this->error = 'Page not found or seems empty: ' . $template_page;
895 // Block-plugins to pseudo fields (#convert => [_block_convert])
896 $template = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $template);
898 // Now, $template = array('*someting*', 'fieldname', '*someting*', 'fieldname', ...)
899 $template = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($template, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
901 // NOTE: if the page has garbages between [field]s, it will fail to be load
902 while (! empty($template)) {
903 // Just ignore these _fixed_ data
904 $pattern[] = preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($template)) . '\\s*)');
905 if (empty($template)) continue;
907 $fieldname = array_shift($template);
908 if (isset($fields[$fieldname])) {
909 $pattern[] = '(.*?)'; // Just capture it
910 $pattern_fields[] = $fieldname; // Capture it as this $filedname
912 $pattern[] = '.*?'; // Just ignore pseudo fields etc
915 $this->pattern = '/' . implode('', $pattern) . '/sS';
916 $this->pattern_fields = $pattern_fields;
922 function addRow($pagename, $rescan = FALSE)
924 if (isset($this->_added[$pagename])) return TRUE;
925 $this->_added[$pagename] = TRUE;
927 $source = plugin_tracker_get_source($pagename, TRUE);
928 if ($source === FALSE) $source = '';
930 // Compat: 'move to [[page]]' (like bugtrack plugin)
932 if (! $rescan && ! empty($source) && preg_match('/move\sto\s(.+)/', $source, $matches)) {
933 $to_page = strip_bracket(trim($matches[1]));
934 if (is_page($to_page)) {
935 unset($source, $matches); // Release
936 return $this->addRow($to_page, TRUE); // Recurse(Rescan) once
941 $filetime = get_filetime($pagename);
943 // column => default data of the cell
944 '_page' => '[[' . $pagename . ']]', // TODO: Redudant column pair [1]
945 '_real' => $pagename, // TODO: Redudant column pair [1]
946 '_update' => $filetime, // TODO: Redudant column pair [2]
947 '_past' => $filetime, // TODO: Redudant column pair [2]
950 // Load / Redefine cell
952 if (preg_match($this->pattern, $source, $matches)) {
953 array_shift($matches); // $matches[0] = all of the captured string
954 foreach ($this->pattern_fields as $key => $fieldname) {
955 $row[$fieldname] = trim($matches[$key]);
956 unset($matches[$key]);
958 $this->rows[] = $row;
959 } else if (PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE) {
960 $this->rows[] = $row; // Error
967 function _order_commands2orders($order_commands = '')
969 $order_commands = trim($order_commands);
970 if ($order_commands == '') return array();
975 foreach (explode(';', $order_commands) as $command) {
976 $command = trim($command);
977 if ($command == '') continue;
979 $arg = explode(':', $command, 2);
980 $fieldname = isset($arg[0]) ? trim($arg[0]) : '';
981 $order = isset($arg[1]) ? trim($arg[1]) : '';
983 $_order = $this->_sortkey_string2define($order);
984 if ($_order === FALSE) {
985 $this->error = 'Invalid sort key: ' . $order;
987 } else if (isset($orders[$fieldname])) {
988 $this->error = 'Sort key already set for: ' . $fieldname;
992 if (PLUGIN_TRACKER_LIST_SORT_LIMIT <= $i) continue;
995 $orders[$fieldname] = $_order;
1001 // Set commands for sort()
1002 function setSortOrder($order_commands = '')
1004 $orders = $this->_order_commands2orders($order_commands);
1005 if ($orders === FALSE) {
1006 $this->orders = array();
1009 $this->orders = $orders;
1013 // sortRows(): Internal sort type => PHP sort define
1014 function _sort_type_dropout($order)
1017 case PLUGIN_TRACKER_SORT_TYPE_REGULAR: return SORT_REGULAR;
1018 case PLUGIN_TRACKER_SORT_TYPE_NUMERIC: return SORT_NUMERIC;
1019 case PLUGIN_TRACKER_SORT_TYPE_STRING: return SORT_STRING;
1020 case PLUGIN_TRACKER_SORT_TYPE_NATURAL: return SORT_NATURAL;
1022 $this->error = 'Invalid sort type';
1027 // sortRows(): Internal sort order => PHP sort define
1028 function _sort_order_dropout($order)
1031 case PLUGIN_TRACKER_SORT_ORDER_ASC: return SORT_ASC;
1032 case PLUGIN_TRACKER_SORT_ORDER_DESC: return SORT_DESC;
1034 $this->error = 'Invalid sort order';
1039 // Sort $this->rows by $this->orders
1042 $fields = $this->form->fields;
1043 $orders = $this->orders;
1045 foreach (array_keys($orders) as $fieldname) {
1046 if (! isset($fields[$fieldname])) {
1047 $this->error = 'No such field: ' . $fieldname;
1052 $params = array(); // Arguments for array_multisort()
1054 foreach ($orders as $fieldname => $order) {
1055 $field = $fields[$fieldname];
1057 $type = $this->_sort_type_dropout($field->sort_type);
1058 if ($type === FALSE) return FALSE;
1060 $order = $this->_sort_order_dropout($order);
1061 if ($order === FALSE) return FALSE;
1064 foreach ($this->rows as $row) {
1065 $column[] = isset($row[$fieldname]) ?
1066 $field->get_value($row[$fieldname]) :
1069 if ($type == SORT_NATURAL) {
1070 natcasesort($column);
1073 foreach (array_keys($column) as $key) {
1074 // Consider the same values there for array_multisort()
1075 if ($last !== $column[$key]) ++$i;
1076 $last = strtolower($column[$key]); // natCASEsort()
1079 ksort($column, SORT_NUMERIC);
1080 $type = SORT_NUMERIC;
1083 // One column set (one-dimensional array, sort type, and sort order)
1084 // for array_multisort()
1085 $params[] = $column;
1090 if (! empty($params) && ! empty($this->rows)) {
1091 $params[] = & $this->rows; // The target
1092 call_user_func_array('array_multisort', $params);
1098 // toString(): Sort key: Define to string (internal var => string)
1099 function _sortkey_define2string($sortkey)
1102 case PLUGIN_TRACKER_SORT_ORDER_ASC: return 'asc';
1103 case PLUGIN_TRACKER_SORT_ORDER_DESC: return 'desc';
1105 $this->error = 'No such define: ' . $sortkey;
1110 // toString(): Sort key: String to define (string => internal var)
1111 function _sortkey_string2define($sortkey)
1113 switch (strtoupper(trim($sortkey))) {
1114 case '': return PLUGIN_TRACKER_SORT_ORDER_DEFAULT; break;
1116 case SORT_ASC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1117 case 'SORT_ASC': /*FALLTHROUGH*/
1118 case 'ASC': return PLUGIN_TRACKER_SORT_ORDER_ASC;
1120 case SORT_DESC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1121 case 'SORT_DESC': /*FALLTHROUGH*/
1122 case 'DESC': return PLUGIN_TRACKER_SORT_ORDER_DESC;
1125 $this->error = 'Invalid sort key: ' . $sortkey;
1130 // toString(): Called within preg_replace_callback()
1131 function _replace_title($matches = array())
1133 $form = $this->form;
1134 $base = $form->base;
1135 $refer = $form->refer;
1136 $fields = $form->fields;
1137 $config_name = $form->config->config_name;
1139 $list = $this->list;
1140 $orders = $this->orders;
1142 $fieldname = isset($matches[1]) ? $matches[1] : '';
1143 if (! isset($fields[$fieldname])) {
1144 // Invalid $fieldname or user's own string or something. Nothing to do
1145 return isset($matches[0]) ? $matches[0] : '';
1147 if ($fieldname == '_name' || $fieldname == '_page') $fieldname = '_real';
1149 // This column seems sorted or not
1150 if (isset($orders[$fieldname])) {
1151 $is_asc = ($orders[$fieldname] == PLUGIN_TRACKER_SORT_ORDER_ASC);
1153 $indexes = array_flip(array_keys($orders));
1154 $index = $indexes[$fieldname] + 1;
1157 $arrow = '&br;' . ($is_asc ? '↑' : '↓') . '(' . $index . ')';
1158 // Allow flip, if this is the first column
1159 if (($index == 1) xor $is_asc) {
1160 $order = PLUGIN_TRACKER_SORT_ORDER_ASC;
1162 $order = PLUGIN_TRACKER_SORT_ORDER_DESC;
1166 $order = PLUGIN_TRACKER_SORT_ORDER_DEFAULT;
1169 // This column will be the first position , if you click
1170 $orders = array($fieldname => $order) + $orders;
1173 foreach ($orders as $_fieldname => $_order) {
1174 if ($_order == PLUGIN_TRACKER_SORT_ORDER_DEFAULT) {
1175 $_orders[] = $_fieldname;
1177 $_orders[] = $_fieldname . ':' . $this->_sortkey_define2string($_order);
1181 $script = get_script_uri();
1182 $r_base = ($refer != $base) ?
1183 '&base=' . rawurlencode($base) : '';
1184 $r_config = ($config_name != PLUGIN_TRACKER_DEFAULT_CONFIG) ?
1185 '&config=' . rawurlencode($config_name) : '';
1186 $r_list = ($list != PLUGIN_TRACKER_DEFAULT_LIST) ?
1187 '&list=' . rawurlencode($list) : '';
1188 $r_order = ! empty($_orders) ?
1189 '&order=' . rawurlencode(join(';', $_orders)) : '';
1193 $fields[$fieldname]->title . $arrow .
1195 $script . '?plugin=tracker_list' .
1196 '&refer=' . rawurlencode($refer) . // Try to show 'page title' properly
1197 $r_base . $r_config . $r_list . $r_order .
1201 // toString(): Called within preg_replace_callback()
1202 function _replace_item($matches = array())
1204 $fields = $this->form->fields;
1206 $tfc = $this->_the_first_character_of_the_line ;
1208 $params = isset($matches[1]) ? explode(',', $matches[1]) : array();
1209 $fieldname = isset($params[0]) ? $params[0] : '';
1210 $stylename = isset($params[1]) ? $params[1] : $fieldname;
1214 if ($fieldname != '') {
1215 if (! isset($row[$fieldname])) {
1216 // Maybe load miss of the page
1217 if (isset($fields[$fieldname])) {
1218 $str = '[page_err]'; // Exactlly
1220 $str = isset($matches[0]) ? $matches[0] : ''; // Nothing to do
1223 $str = $row[$fieldname];
1224 if (isset($fields[$fieldname])) {
1225 $str = $fields[$fieldname]->format_cell($str);
1230 if (isset($fields[$stylename]) && isset($row[$stylename])) {
1231 $_style = $fields[$stylename]->get_style($row[$stylename]);
1232 $str = sprintf($_style, $str);
1235 return plugin_tracker_escape($str, $tfc);
1238 // Output a part of Wiki text
1239 function toString($limit = 0)
1241 $form = & $this->form;
1242 $list = $form->config->page . '/' . $this->list;
1244 $regex = '/\[([^\[\]]+)\]/';
1247 $template = plugin_tracker_get_source($list, TRUE);
1248 if ($template === FALSE || empty($template)) {
1249 $this->error = 'Page not found or seems empty: ' . $template;
1253 // Creating $form->fields just you need
1254 if ($form->initFields('_real') === FALSE ||
1255 $form->initFields(plugin_tracker_field_pickup($template)) === FALSE ||
1256 $form->initFields(array_keys($this->orders)) === FALSE) {
1257 $this->error = $form->error;
1261 // Generate regex for $form->fields
1262 if ($this->_generate_regex() === FALSE) return FALSE;
1264 // Load and sort $this->rows
1265 if ($this->loadRows() === FALSE || $this->sortRows() === FALSE) return FALSE;
1266 $rows = $this->rows;
1269 $count = count($this->rows);
1270 $limit = intval($limit);
1271 if ($limit != 0) $limit = max(1, $limit);
1272 if ($limit != 0 && $count > $limit) {
1273 $source[] = str_replace(
1275 array($count, $limit),
1276 plugin_tracker_message('msg_limit')
1278 $rows = array_slice($this->rows, 0, $limit);
1282 // TODO: How do you feel single/multiple table rows with 'c'(decolation)?
1283 $matches = $t_header = $t_body = $t_footer = array();
1284 $template = plugin_tracker_get_source($list);
1285 if ($template === FALSE) {
1286 $this->error = 'Page not found or seems empty: ' . $list;
1289 foreach ($template as $line) {
1290 if (preg_match('/^\|.+\|([hfc])$/i', $line, $matches)) {
1291 if (strtolower($matches[1]) == 'f') {
1292 $t_footer[] = $line; // Table footer
1294 $t_header[] = $line; // Table header, or decoration
1302 // Header and decolation
1303 foreach($t_header as $line) {
1304 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1308 foreach ($rows as $row) {
1311 foreach ($t_body as $line) {
1312 if (ltrim($line) != '') {
1313 $this->_the_first_character_of_the_line = $line[0];
1314 $line = preg_replace_callback($regex, array(& $this, '_replace_item'), $line);
1321 foreach($t_footer as $line) {
1322 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1326 return implode('', $source);
1330 // Roughly checking listed fields from template
1331 // " [field1] [field2,style1] " => array('fielld', 'field2')
1332 function plugin_tracker_field_pickup($string = '')
1334 if (! is_string($string) || empty($string)) return array();
1336 $fieldnames = array();
1339 preg_match_all('/\[([^\[\]]+)\]/', $string, $matches);
1342 foreach ($matches[1] as $match) {
1343 $params = explode(',', $match, 2);
1344 if (isset($params[0])) {
1345 $fieldnames[$params[0]] = TRUE;
1349 return array_keys($fieldnames);
1352 function plugin_tracker_get_source($page, $join = FALSE)
1354 $source = get_source($page, TRUE, $join);
1355 if ($source === FALSE) return FALSE;
1357 return preg_replace(
1360 '/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', // Remove fixed-heading anchors
1370 // Escape special characters not to break Wiki syntax
1371 function plugin_tracker_escape($string, $syntax_hint = '')
1373 // Default: line-oriented
1374 $from = array("\n", "\r" );
1375 $to = array('&br;', '&br;');
1377 if ($syntax_hint == '|' || $syntax_hint == ':') {
1378 // <table> or <dl> Wiki syntax: Excape '|'
1381 } else if ($syntax_hint == ',') {
1386 return str_replace($from, $to, $string);
1389 function plugin_tracker_message($key)
1391 global $_tracker_messages;
1392 return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';