2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.102 2007/10/03 15:18:15 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
231 var $id; // Unique id per instance
238 var $fields = array();
240 var $error = ''; // Error message
242 function Tracker_form($base, $refer, $config)
248 $this->refer = $refer;
249 $this->config = $config;
252 function addField($fieldname, $displayname, $type = 'text', $options = '20', $default = '')
254 // TODO: Return an error
255 if (isset($this->fields[$fieldname])) return TRUE;
257 $class = 'Tracker_field_' . $type;
258 if (! class_exists($class)) {
259 // TODO: Return an error
261 $class = 'Tracker_field_' . $type;
265 $this->fields[$fieldname] = & new $class(
279 function initFields($requests = NULL)
281 if (! isset($this->raw_fields)) {
282 $raw_fields = array();
284 foreach ($this->config->get('fields') as $field) {
285 $fieldname = isset($field[0]) ? $field[0] : '';
286 $raw_fields[$fieldname] = array(
287 'display' => isset($field[1]) ? $field[1] : '',
288 'type' => isset($field[2]) ? $field[2] : '',
289 'options' => isset($field[3]) ? $field[3] : '',
290 'default' => isset($field[4]) ? $field[4] : '',
294 $default = array('options' => '20', 'default' => '');
296 '_date' => 'text', // Post date
297 '_update' => 'date', // Last modified date
298 '_past' => 'past', // Elapsed time (passage)
299 '_page' => 'page', // Page name
300 '_name' => 'text', // Page name specified by poster
301 '_real' => 'real', // Page name (Real)
302 '_refer' => 'page', // Page name refer from this (Page who has forms)
304 '_submit' => 'submit'
305 ) as $fieldname => $type) {
306 if (isset($raw_fields[$fieldname])) continue;
307 $raw_fields[$fieldname] = array(
308 'display' => plugin_tracker_message('btn' . $fieldname),
312 $this->raw_fields = & $raw_fields;
314 $raw_fields = & $this->raw_fields;
317 if ($requests === NULL) {
318 // (The rest of) All, defined order
319 foreach ($raw_fields as $fieldname => $field) {
328 $raw_fields = array();
330 // Part of, specific order
331 if (! is_array($requests)) $requests = array($requests);
332 foreach ($requests as $fieldname) {
333 if (! isset($raw_fields[$fieldname])) continue;
334 $field = $raw_fields[$fieldname];
342 unset($raw_fields[$fieldname]);
349 function initHiddenFields()
351 // Make sure to init $this->raw_fields
352 $this->initFields(array());
355 foreach ($this->raw_fields as $fieldname => $field) {
356 if ($field['type'] == 'hidden') {
357 $fields[] = $fieldname;
361 $this->initFields($fields);
365 // TODO: Why a filter sometimes created so many?
366 // Field classes within a form
369 var $id; // Unique id per instance, and per class(extended-class)
371 var $form; // Parent (class Tracker_form)
380 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_REGULAR;
382 function Tracker_field(& $tracker_form, $field)
389 $this->form = & $tracker_form;
390 $this->name = isset($field[0]) ? $field[0] : '';
391 $this->title = isset($field[1]) ? $field[1] : '';
392 $this->values = isset($field[3]) ? explode(',', $field[3]) : array();
393 $this->default_value = isset($field[4]) ? $field[4] : '';
395 $this->data = isset($post[$this->name]) ? $post[$this->name] : '';
398 // XHTML part inside a form
409 function format_value($value)
414 function format_cell($str)
419 // Compare key for Tracker_list->sort()
420 function get_value($value)
422 return $value; // Default: $value itself
426 class Tracker_field_text extends Tracker_field
428 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
432 return '<input type="text"' .
433 ' name="' . htmlspecialchars($this->name) . '"' .
434 ' size="' . htmlspecialchars($this->values[0]) . '"' .
435 ' value="' . htmlspecialchars($this->default_value) . '" />';
439 // Special type: The page names
440 class Tracker_field_page extends Tracker_field_text
442 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
444 function format_value($value)
446 $value = strip_bracket($value);
447 if (is_pagename($value)) $value = '[[' . $value . ']]';
448 return parent::format_value($value);
451 function format_cell($value)
453 return '[[' . $value . ']]';
457 // Special type : Real(Raw) value of page name
458 class Tracker_field_real extends Tracker_field_text
460 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NATURAL;
463 class Tracker_field_title extends Tracker_field_text
465 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
467 function format_cell($str)
474 class Tracker_field_textarea extends Tracker_field
476 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
481 ' name="' . htmlspecialchars($this->name) . '"' .
482 ' cols="' . htmlspecialchars($this->values[0]) . '"' .
483 ' rows="' . htmlspecialchars($this->values[1]) . '">' .
484 htmlspecialchars($this->default_value) .
488 function format_cell($str)
490 $str = preg_replace('/[\r\n]+/', '', $str);
491 if (! empty($this->values[2]) && strlen($str) > ($this->values[2] + 3)) {
492 $str = mb_substr($str, 0, $this->values[2]) . '...';
498 // Text with formatting if trim($cell) != ''
499 // See also: http://home.arino.jp/?tracker.inc.php%2F41
500 class Tracker_field_format extends Tracker_field
502 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
504 var $styles = array();
505 var $formats = array();
507 function Tracker_field_format(& $tracker_form, $field)
509 parent::Tracker_field($tracker_form, $field);
510 foreach ($this->form->config->get($this->name) as $option) {
511 list($key, $style, $format) = array_pad(array_map('trim', $option), 3, '');
512 if ($style != '') $this->styles[$key] = $style;
513 if ($format != '') $this->formats[$key] = $format;
519 return '<input type="text"' .
520 ' name="' . htmlspecialchars($this->name) . '"' .
521 ' size="' . htmlspecialchars($this->values[0]) . '" />';
524 function get_key($str)
526 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
529 function format_value($str)
531 if (is_array($str)) {
532 return join(', ', array_map(array($this, 'format_value'), $str));
535 $key = $this->get_key($str);
536 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
539 function get_style($str)
541 $key = $this->get_key($str);
542 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
546 class Tracker_field_file extends Tracker_field_format
548 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
552 return '<input type="file"' .
553 ' name="' . htmlspecialchars($this->name) . '"' .
554 ' size="' . htmlspecialchars($this->values[0]) . '" />';
557 function format_value()
559 if (isset($_FILES[$this->name])) {
561 require_once(PLUGIN_DIR . 'attach.inc.php');
563 $base = $this->form->base;
564 $result = attach_upload($_FILES[$this->name], $base);
565 if (isset($result['result']) && $result['result']) {
567 return parent::format_value($base . '/' . $_FILES[$this->name]['name']);
571 // Filename not specified, or Fail to upload
572 return parent::format_value('');
576 class Tracker_field_radio extends Tracker_field_format
578 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
579 var $_options = array();
586 $s_name = htmlspecialchars($this->name);
587 foreach ($this->form->config->get($this->name) as $option) {
589 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
590 $s_option = htmlspecialchars($option[0]);
591 $checked = trim($option[0]) == trim($this->default_value) ? ' checked="checked"' : '';
593 $retval .= '<input type="radio"' .
594 ' name="' . $s_name . '"' .
595 ' id="' . $s_id . '"' .
596 ' value="' . $s_option . '"' .
598 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
604 function get_key($str)
609 function get_value($value)
611 $options = & $this->_options;
614 if (! isset($options[$name])) {
615 $values = array_map('reset', $this->form->config->get($name));
616 $options[$name] = array_flip($values); // array('value0' => 0, 'value1' => 1, ...)
619 return isset($options[$name][$value]) ? $options[$name][$value] : $value;
623 class Tracker_field_select extends Tracker_field_radio
625 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
627 function get_tag($empty = FALSE)
629 $s_name = htmlspecialchars($this->name);
630 $s_size = (isset($this->values[0]) && is_numeric($this->values[0])) ?
631 ' size="' . htmlspecialchars($this->values[0]) . '"' :
633 $s_multiple = (isset($this->values[1]) && strtolower($this->values[1]) == 'multiple') ?
634 ' multiple="multiple"' :
637 $retval = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>' . "\n";
638 if ($empty) $retval .= ' <option value=""></option>' . "\n";
639 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
640 foreach ($this->form->config->get($this->name) as $option) {
641 $s_option = htmlspecialchars($option[0]);
642 $selected = isset($defaults[trim($option[0])]) ? ' selected="selected"' : '';
643 $retval .= ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>' . "\n";
645 $retval .= '</select>';
651 class Tracker_field_checkbox extends Tracker_field_radio
653 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
660 $s_name = htmlspecialchars($this->name);
661 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
662 foreach ($this->form->config->get($this->name) as $option) {
664 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
665 $s_option = htmlspecialchars($option[0]);
666 $checked = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
668 $retval .= '<input type="checkbox"' .
669 ' name="' . $s_name . '[]"' .
670 ' id="' . $s_id . '"' .
671 ' value="' . $s_option . '"' .
673 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
680 class Tracker_field_hidden extends Tracker_field_radio
682 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
686 return '<input type="hidden"' .
687 ' name="' . htmlspecialchars($this->name) . '"' .
688 ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
692 class Tracker_field_submit extends Tracker_field
698 $s_title = htmlspecialchars($this->title);
699 $s_base = htmlspecialchars($form->base);
700 $s_refer = htmlspecialchars($form->refer);
701 $s_config = htmlspecialchars($form->config->config_name);
704 <input type="submit" value="$s_title" />
705 <input type="hidden" name="plugin" value="tracker" />
706 <input type="hidden" name="_refer" value="$s_refer" />
707 <input type="hidden" name="_base" value="$s_base" />
708 <input type="hidden" name="_config" value="$s_config" />
713 class Tracker_field_date extends Tracker_field
715 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
717 function format_cell($timestamp)
719 return format_date($timestamp);
723 class Tracker_field_past extends Tracker_field
725 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
727 function format_cell($timestamp)
729 return get_passage($timestamp, FALSE);
732 function get_value($value)
734 return UTIME - $value;
738 ///////////////////////////////////////////////////////////////////////////
739 // tracker_list plugin
741 function plugin_tracker_list_convert()
745 $base = $refer = isset($vars['page']) ? $vars['page'] : '';
746 $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
747 $list = PLUGIN_TRACKER_DEFAULT_LIST;
748 $limit = PLUGIN_TRACKER_DEFAULT_LIMIT;
749 $order = PLUGIN_TRACKER_DEFAULT_ORDER;
751 $args = func_get_args();
752 $argc = count($args);
754 return PLUGIN_TRACKER_LIST_USAGE . '<br />';
757 case 4: $limit = $args[3]; /*FALLTHROUGH*/
758 case 3: $order = $args[2]; /*FALLTHROUGH*/
760 $arg = get_fullname($args[1], $base);
761 if (is_pagename($arg)) $base = $arg;
765 if ($args[0] != '') {
766 $arg = explode('/', $args[0], 2);
767 if ($arg[0] != '' ) $config_name = $arg[0];
768 if (isset($arg[1])) $list = $arg[1];
771 unset($args, $argc, $arg);
773 return plugin_tracker_list_render($base, $refer, $config_name, $list, $order, $limit);
776 function plugin_tracker_list_action()
780 $base = isset($get['base']) ? $get['base'] : ''; // Base directory to load
782 if (isset($get['refer'])) {
783 $refer = $get['refer']; // Where to #tracker_list
784 if ($base == '') $base = $refer;
789 $config = isset($get['config']) ? $get['config'] : '';
790 $list = isset($get['list']) ? $get['list'] : 'list';
791 $order = isset($get['order']) ? $get['order'] : PLUGIN_TRACKER_DEFAULT_ORDER;
792 $limit = isset($get['limit']) ? $get['limit'] : 0;
794 $s_refer = make_pagelink($refer);
796 'msg' => plugin_tracker_message('msg_list'),
797 'body'=> str_replace('$1', $s_refer, plugin_tracker_message('msg_back')) .
798 plugin_tracker_list_render($base, $refer, $config, $list, $order, $limit)
802 function plugin_tracker_list_render($base, $refer, $config_name, $list, $order_commands = '', $limit = 0)
805 if ($base == '') return '#tracker_list: Base not specified' . '<br />';
807 $refer = trim($refer);
808 if (! is_page($refer)) {
809 return '#tracker_list: Refer page not found: ' . htmlspecialchars($refer) . '<br />';
812 $config_name = trim($config_name);
813 if ($config_name == '') $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
816 if (! is_numeric($limit)) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
817 $limit = intval($limit);
819 $config = new Config('plugin/tracker/' . $config_name);
820 if (! $config->read()) {
821 return '#tracker_list: Config not found: ' . htmlspecialchars($config_name) . '<br />';
823 $config->config_name = $config_name;
824 if (! is_page($config->page . '/' . $list)) {
825 return '#tracker_list: List not found: ' . make_pagelink($config->page . '/' . $list) . '<br />';
828 $list = & new Tracker_list($base, $refer, $config, $list);
829 if ($list->setSortOrder($order_commands) === FALSE) {
830 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
832 $result = $list->toString($limit);
833 if ($result === FALSE) {
834 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
838 return convert_html($result);
844 var $form; // class Tracker_form
852 var $orders = array();
853 var $error = ''; // Error message
856 var $_added = array();
860 var $_the_first_character_of_the_line;
862 function Tracker_list($base, $refer, & $config, $list)
864 $form = & new Tracker_form($base, $refer, $config);
869 // Add multiple pages at a time
872 $base = $this->form->base . '/';
873 $len = strlen($base);
874 $regex = '#^' . preg_quote($base, '#') . '#';
876 // Adding $this->rows
877 foreach (preg_grep($regex, array_values(get_existpages())) as $pagename) {
878 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, substr($pagename, $len))) {
881 if ($this->addRow($pagename) === FALSE) return FALSE;
883 if (empty($this->rows)) {
884 $this->error = 'Pages not found under: ' . $base;
891 // addRow(): Generate regex to load a page
892 function _generate_regex()
894 $template_page = $this->form->config->page . '/page';
895 $fields = $this->form->fields;
898 $pattern_fields = array();
900 $template = plugin_tracker_get_source($template_page, TRUE);
901 if ($template === FALSE || empty($template)) {
902 $this->error = 'Page not found or seems empty: ' . $template_page;
906 // Block-plugins to pseudo fields (#convert => [_block_convert])
907 $template = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $template);
909 // Now, $template = array('*someting*', 'fieldname', '*someting*', 'fieldname', ...)
910 $template = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($template, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
912 // NOTE: if the page has garbages between [field]s, it will fail to be load
913 while (! empty($template)) {
914 // Just ignore these _fixed_ data
915 $pattern[] = preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($template)) . '\\s*)');
916 if (empty($template)) continue;
918 $fieldname = array_shift($template);
919 if (isset($fields[$fieldname])) {
920 $pattern[] = '(.*?)'; // Just capture it
921 $pattern_fields[] = $fieldname; // Capture it as this $filedname
923 $pattern[] = '.*?'; // Just ignore pseudo fields etc
926 $this->pattern = '/' . implode('', $pattern) . '/sS';
927 $this->pattern_fields = $pattern_fields;
933 function addRow($pagename, $rescan = FALSE)
935 if (isset($this->_added[$pagename])) return TRUE;
936 $this->_added[$pagename] = TRUE;
938 $source = plugin_tracker_get_source($pagename, TRUE);
939 if ($source === FALSE) $source = '';
941 // Compat: 'move to [[page]]' (like bugtrack plugin)
943 if (! $rescan && ! empty($source) && preg_match('/move\sto\s(.+)/', $source, $matches)) {
944 $to_page = strip_bracket(trim($matches[1]));
945 if (is_page($to_page)) {
946 unset($source, $matches); // Release
947 return $this->addRow($to_page, TRUE); // Recurse(Rescan) once
952 $filetime = get_filetime($pagename);
954 // column => default data of the cell
955 '_page' => $pagename, // TODO: Redudant column pair [1]
956 '_real' => $pagename, // TODO: Redudant column pair [1]
957 '_update' => $filetime, // TODO: Redudant column pair [2]
958 '_past' => $filetime, // TODO: Redudant column pair [2]
961 // Load / Redefine cell
963 if (preg_match($this->pattern, $source, $matches)) {
964 array_shift($matches); // $matches[0] = all of the captured string
965 foreach ($this->pattern_fields as $key => $fieldname) {
966 $row[$fieldname] = trim($matches[$key]);
967 unset($matches[$key]);
969 $this->rows[] = $row;
970 } else if (PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE) {
971 $this->rows[] = $row; // Error
978 function _order_commands2orders($order_commands = '')
980 $order_commands = trim($order_commands);
981 if ($order_commands == '') return array();
986 foreach (explode(';', $order_commands) as $command) {
987 $command = trim($command);
988 if ($command == '') continue;
990 $arg = explode(':', $command, 2);
991 $fieldname = isset($arg[0]) ? trim($arg[0]) : '';
992 $order = isset($arg[1]) ? trim($arg[1]) : '';
994 $_order = $this->_sortkey_string2define($order);
995 if ($_order === FALSE) {
996 $this->error = 'Invalid sort key: ' . $order;
998 } else if (isset($orders[$fieldname])) {
999 $this->error = 'Sort key already set for: ' . $fieldname;
1003 if (PLUGIN_TRACKER_LIST_SORT_LIMIT <= $i) continue;
1006 $orders[$fieldname] = $_order;
1012 // Set commands for sort()
1013 function setSortOrder($order_commands = '')
1015 $orders = $this->_order_commands2orders($order_commands);
1016 if ($orders === FALSE) {
1017 $this->orders = array();
1020 $this->orders = $orders;
1024 // sortRows(): Internal sort type => PHP sort define
1025 function _sort_type_dropout($order)
1028 case PLUGIN_TRACKER_SORT_TYPE_REGULAR: return SORT_REGULAR;
1029 case PLUGIN_TRACKER_SORT_TYPE_NUMERIC: return SORT_NUMERIC;
1030 case PLUGIN_TRACKER_SORT_TYPE_STRING: return SORT_STRING;
1031 case PLUGIN_TRACKER_SORT_TYPE_NATURAL: return SORT_NATURAL;
1033 $this->error = 'Invalid sort type';
1038 // sortRows(): Internal sort order => PHP sort define
1039 function _sort_order_dropout($order)
1042 case PLUGIN_TRACKER_SORT_ORDER_ASC: return SORT_ASC;
1043 case PLUGIN_TRACKER_SORT_ORDER_DESC: return SORT_DESC;
1045 $this->error = 'Invalid sort order';
1050 // Sort $this->rows by $this->orders
1053 $fields = $this->form->fields;
1054 $orders = $this->orders;
1056 foreach (array_keys($orders) as $fieldname) {
1057 if (! isset($fields[$fieldname])) {
1058 $this->error = 'No such field: ' . $fieldname;
1063 $params = array(); // Arguments for array_multisort()
1065 foreach ($orders as $fieldname => $order) {
1066 $field = $fields[$fieldname];
1068 $type = $this->_sort_type_dropout($field->sort_type);
1069 if ($type === FALSE) return FALSE;
1071 $order = $this->_sort_order_dropout($order);
1072 if ($order === FALSE) return FALSE;
1075 foreach ($this->rows as $row) {
1076 $column[] = isset($row[$fieldname]) ?
1077 $field->get_value($row[$fieldname]) :
1080 if ($type == SORT_NATURAL) {
1081 natcasesort($column);
1084 foreach (array_keys($column) as $key) {
1085 // Consider the same values there for array_multisort()
1086 if ($last !== $column[$key]) ++$i;
1087 $last = strtolower($column[$key]); // natCASEsort()
1090 ksort($column, SORT_NUMERIC);
1091 $type = SORT_NUMERIC;
1094 // One column set (one-dimensional array, sort type, and sort order)
1095 // for array_multisort()
1096 $params[] = $column;
1101 if (! empty($params) && ! empty($this->rows)) {
1102 $params[] = & $this->rows; // The target
1103 call_user_func_array('array_multisort', $params);
1109 // toString(): Sort key: Define to string (internal var => string)
1110 function _sortkey_define2string($sortkey)
1113 case PLUGIN_TRACKER_SORT_ORDER_ASC: return 'asc';
1114 case PLUGIN_TRACKER_SORT_ORDER_DESC: return 'desc';
1116 $this->error = 'No such define: ' . $sortkey;
1121 // toString(): Sort key: String to define (string => internal var)
1122 function _sortkey_string2define($sortkey)
1124 switch (strtoupper(trim($sortkey))) {
1125 case '': return PLUGIN_TRACKER_SORT_ORDER_DEFAULT; break;
1127 case SORT_ASC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1128 case 'SORT_ASC': /*FALLTHROUGH*/
1129 case 'ASC': return PLUGIN_TRACKER_SORT_ORDER_ASC;
1131 case SORT_DESC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1132 case 'SORT_DESC': /*FALLTHROUGH*/
1133 case 'DESC': return PLUGIN_TRACKER_SORT_ORDER_DESC;
1136 $this->error = 'Invalid sort key: ' . $sortkey;
1141 // toString(): Called within preg_replace_callback()
1142 function _replace_title($matches = array())
1144 $form = $this->form;
1145 $base = $form->base;
1146 $refer = $form->refer;
1147 $fields = $form->fields;
1148 $config_name = $form->config->config_name;
1150 $list = $this->list;
1151 $orders = $this->orders;
1153 $fieldname = isset($matches[1]) ? $matches[1] : '';
1154 if (! isset($fields[$fieldname])) {
1155 // Invalid $fieldname or user's own string or something. Nothing to do
1156 return isset($matches[0]) ? $matches[0] : '';
1158 if ($fieldname == '_name' || $fieldname == '_page') $fieldname = '_real';
1160 // This column seems sorted or not
1161 if (isset($orders[$fieldname])) {
1162 $is_asc = ($orders[$fieldname] == PLUGIN_TRACKER_SORT_ORDER_ASC);
1164 $indexes = array_flip(array_keys($orders));
1165 $index = $indexes[$fieldname] + 1;
1168 $arrow = '&br;' . ($is_asc ? '↑' : '↓') . '(' . $index . ')';
1169 // Allow flip, if this is the first column
1170 if (($index == 1) xor $is_asc) {
1171 $order = PLUGIN_TRACKER_SORT_ORDER_ASC;
1173 $order = PLUGIN_TRACKER_SORT_ORDER_DESC;
1177 $order = PLUGIN_TRACKER_SORT_ORDER_DEFAULT;
1180 // This column will be the first position , if you click
1181 $orders = array($fieldname => $order) + $orders;
1184 foreach ($orders as $_fieldname => $_order) {
1185 if ($_order == PLUGIN_TRACKER_SORT_ORDER_DEFAULT) {
1186 $_orders[] = $_fieldname;
1188 $_orders[] = $_fieldname . ':' . $this->_sortkey_define2string($_order);
1192 $script = get_script_uri();
1193 $r_base = ($refer != $base) ?
1194 '&base=' . rawurlencode($base) : '';
1195 $r_config = ($config_name != PLUGIN_TRACKER_DEFAULT_CONFIG) ?
1196 '&config=' . rawurlencode($config_name) : '';
1197 $r_list = ($list != PLUGIN_TRACKER_DEFAULT_LIST) ?
1198 '&list=' . rawurlencode($list) : '';
1199 $r_order = ! empty($_orders) ?
1200 '&order=' . rawurlencode(join(';', $_orders)) : '';
1204 $fields[$fieldname]->title . $arrow .
1206 $script . '?plugin=tracker_list' .
1207 '&refer=' . rawurlencode($refer) . // Try to show 'page title' properly
1208 $r_base . $r_config . $r_list . $r_order .
1212 // toString(): Called within preg_replace_callback()
1213 function _replace_item($matches = array())
1215 $fields = $this->form->fields;
1217 $tfc = $this->_the_first_character_of_the_line ;
1219 $params = isset($matches[1]) ? explode(',', $matches[1]) : array();
1220 $fieldname = isset($params[0]) ? $params[0] : '';
1221 $stylename = isset($params[1]) ? $params[1] : $fieldname;
1225 if ($fieldname != '') {
1226 if (! isset($row[$fieldname])) {
1227 // Maybe load miss of the page
1228 if (isset($fields[$fieldname])) {
1229 $str = '[page_err]'; // Exactlly
1231 $str = isset($matches[0]) ? $matches[0] : ''; // Nothing to do
1234 $str = $row[$fieldname];
1235 if (isset($fields[$fieldname])) {
1236 $str = $fields[$fieldname]->format_cell($str);
1241 if (isset($fields[$stylename]) && isset($row[$stylename])) {
1242 $_style = $fields[$stylename]->get_style($row[$stylename]);
1243 $str = sprintf($_style, $str);
1246 return plugin_tracker_escape($str, $tfc);
1249 // Output a part of Wiki text
1250 function toString($limit = 0)
1252 $form = & $this->form;
1253 $list = $form->config->page . '/' . $this->list;
1255 $regex = '/\[([^\[\]]+)\]/';
1258 $template = plugin_tracker_get_source($list, TRUE);
1259 if ($template === FALSE || empty($template)) {
1260 $this->error = 'Page not found or seems empty: ' . $template;
1264 // Creating $form->fields just you need
1265 if ($form->initFields('_real') === FALSE ||
1266 $form->initFields(plugin_tracker_field_pickup($template)) === FALSE ||
1267 $form->initFields(array_keys($this->orders)) === FALSE) {
1268 $this->error = $form->error;
1272 // Generate regex for $form->fields
1273 if ($this->_generate_regex() === FALSE) return FALSE;
1275 // Load and sort $this->rows
1276 if ($this->loadRows() === FALSE || $this->sortRows() === FALSE) return FALSE;
1277 $rows = $this->rows;
1280 $count = count($this->rows);
1281 $limit = intval($limit);
1282 if ($limit != 0) $limit = max(1, $limit);
1283 if ($limit != 0 && $count > $limit) {
1284 $source[] = str_replace(
1286 array($count, $limit),
1287 plugin_tracker_message('msg_limit')
1289 $rows = array_slice($this->rows, 0, $limit);
1293 // TODO: How do you feel single/multiple table rows with 'c'(decolation)?
1294 $matches = $t_header = $t_body = $t_footer = array();
1295 $template = plugin_tracker_get_source($list);
1296 if ($template === FALSE) {
1297 $this->error = 'Page not found or seems empty: ' . $list;
1300 foreach ($template as $line) {
1301 if (preg_match('/^\|.+\|([hfc])$/i', $line, $matches)) {
1302 if (strtolower($matches[1]) == 'f') {
1303 $t_footer[] = $line; // Table footer
1305 $t_header[] = $line; // Table header, or decoration
1313 // Header and decolation
1314 foreach($t_header as $line) {
1315 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1319 foreach ($rows as $row) {
1322 foreach ($t_body as $line) {
1323 if (ltrim($line) != '') {
1324 $this->_the_first_character_of_the_line = $line[0];
1325 $line = preg_replace_callback($regex, array(& $this, '_replace_item'), $line);
1332 foreach($t_footer as $line) {
1333 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1337 return implode('', $source);
1341 // Roughly checking listed fields from template
1342 // " [field1] [field2,style1] " => array('fielld', 'field2')
1343 function plugin_tracker_field_pickup($string = '')
1345 if (! is_string($string) || empty($string)) return array();
1347 $fieldnames = array();
1350 preg_match_all('/\[([^\[\]]+)\]/', $string, $matches);
1353 foreach ($matches[1] as $match) {
1354 $params = explode(',', $match, 2);
1355 if (isset($params[0])) {
1356 $fieldnames[$params[0]] = TRUE;
1360 return array_keys($fieldnames);
1363 function plugin_tracker_get_source($page, $join = FALSE)
1365 $source = get_source($page, TRUE, $join);
1366 if ($source === FALSE) return FALSE;
1368 return preg_replace(
1371 '/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', // Remove fixed-heading anchors
1381 // Escape special characters not to break Wiki syntax
1382 function plugin_tracker_escape($string, $syntax_hint = '')
1384 // Default: line-oriented
1385 $from = array("\n", "\r" );
1386 $to = array('&br;', '&br;');
1388 if ($syntax_hint == '|' || $syntax_hint == ':') {
1389 // <table> or <dl> Wiki syntax: Excape '|'
1392 } else if ($syntax_hint == ',') {
1397 return str_replace($from, $to, $string);
1400 function plugin_tracker_message($key)
1402 global $_tracker_messages;
1403 return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';