2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.108 2007/10/12 16:00:27 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 // Init $this->raw_fields and $this->fields
253 // TODO: Using func_get_args() to shrink the code?
254 function initFields($requests = NULL)
256 if (! isset($this->raw_fields)) {
257 $raw_fields = array();
259 foreach ($this->config->get('fields') as $field) {
260 $fieldname = isset($field[0]) ? $field[0] : '';
261 $raw_fields[$fieldname] = array(
262 'display' => isset($field[1]) ? $field[1] : '',
263 'type' => isset($field[2]) ? $field[2] : '',
264 'options' => isset($field[3]) ? $field[3] : '',
265 'default' => isset($field[4]) ? $field[4] : '',
269 $default = array('options' => '20', 'default' => '');
271 '_date' => 'text', // Post date
272 '_update' => 'date', // Last modified date
273 '_past' => 'past', // Elapsed time (passage)
274 '_page' => 'page', // Page name
275 '_name' => 'text', // Page name specified by poster
276 '_real' => 'real', // Page name (Real)
277 '_refer' => 'page', // Page name refer from this (Page who has forms)
279 '_submit' => 'submit'
280 ) as $fieldname => $type) {
281 if (isset($raw_fields[$fieldname])) continue;
282 $raw_fields[$fieldname] = array(
283 'display' => plugin_tracker_message('btn' . $fieldname),
287 $this->raw_fields = & $raw_fields;
289 $raw_fields = & $this->raw_fields;
292 if ($requests === NULL) {
293 // (The rest of) All, defined order
294 foreach ($raw_fields as $fieldname => $field) {
295 $err = $this->addField(
302 if ($err === FALSE) return FALSE;
304 $raw_fields = array();
306 // Part of, specific order
307 if (! is_array($requests)) $requests = array($requests);
308 foreach ($requests as $fieldname) {
309 if (! isset($raw_fields[$fieldname])) continue;
310 $field = $raw_fields[$fieldname];
311 $err = $this->addField(
318 unset($raw_fields[$fieldname]);
319 if ($err === FALSE) return FALSE;
326 function initHiddenFields()
328 // Make sure to init $this->raw_fields
329 $this->initFields(array());
332 foreach ($this->raw_fields as $fieldname => $field) {
333 if ($field['type'] == 'hidden') {
334 $fields[] = $fieldname;
338 $this->initFields($fields);
341 // Called from InitFields()
342 function addField($fieldname, $displayname, $type = 'text', $options = '20', $default = '')
345 if (isset($this->fields[$fieldname])) {
346 $this->error = "No such field: " . $fieldname;
350 $class = 'Tracker_field_' . $type;
351 if (! class_exists($class)) {
352 $this->error = "No such type: " . $type;
356 $this->fields[$fieldname] = & new $class(
371 // TODO: Why a filter sometimes created so many?
372 // Field classes within a form
375 var $id; // Unique id per instance, and per class(extended-class)
377 var $form; // Parent (class Tracker_form)
386 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_REGULAR;
388 function Tracker_field(& $tracker_form, $field)
395 $this->form = & $tracker_form;
396 $this->name = isset($field[0]) ? $field[0] : '';
397 $this->title = isset($field[1]) ? $field[1] : '';
398 $this->options = isset($field[3]) ? explode(',', $field[3]) : array();
399 $this->default_value = isset($field[4]) ? $field[4] : '';
401 $this->data = isset($post[$this->name]) ? $post[$this->name] : '';
404 // Output a part of XHTML form for the field
410 // Format user input before write
411 function format_value($value)
416 // Compare key for Tracker_list->sort()
417 function get_value($value)
422 // Format table cell data before output the wiki text
423 function format_cell($str)
428 // Format-string for sprintf() before output the wiki text
435 class Tracker_field_text extends Tracker_field
437 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
441 $s_name = htmlspecialchars($this->name);
442 $s_size = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
443 $s_value = htmlspecialchars($this->default_value);
445 return '<input type="text"' .
446 ' name="' . $s_name . '"' .
447 ' size="' . $s_size . '"' .
448 ' value="' . $s_value . '" />';
452 // Special type: Page name with link syntax
453 class Tracker_field_page extends Tracker_field_text
455 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
457 function _format($page)
459 $page = strip_bracket($page);
460 if (is_pagename($page)) $page = '[[' . $page . ']]';
464 function format_value($value)
466 return $this->_format($value);
469 function format_cell($value)
471 return $this->_format($value);
475 // Special type: Page name minus 'base'
477 // page name: Tracker/sales/100
478 // base : Tracker/sales
482 // Don't consider using within ":config/plugin/tracker/*/page".
483 // This value comes from _the_page_name_ itself.
484 class Tracker_field_real extends Tracker_field_text
486 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NATURAL;
488 function format_cell($value)
490 // basename(): Rough but work with this(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN prohibits '/') situation
491 return basename($value);
495 // Special type: For headings cleaning
496 class Tracker_field_title extends Tracker_field_text
498 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
500 function format_cell($str)
507 class Tracker_field_textarea extends Tracker_field
509 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
513 $s_name = htmlspecialchars($this->name);
514 $s_cols = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
515 $s_rows = isset($this->options[1]) ? htmlspecialchars($this->options[1]) : '';
516 $s_default = htmlspecialchars($this->default_value);
519 ' name="' . $s_name . '"' .
520 ' cols="' . $s_cols . '"' .
521 ' rows="' . $s_rows . '">' .
526 function format_cell($str)
529 // TODO: Why store all of them to the memory?
530 if (isset($this->options[2])) {
531 $limit = max(0, $this->options[2]);
532 $len = mb_strlen($str);
533 if ($len > ($limit + 3)) { // 3 = mb_strlen('...')
534 $str = mb_substr($str, 0, $limit) . '...';
541 // Writing text with formatting if trim($cell) != ''
542 // See also: http://home.arino.jp/?tracker.inc.php%2F41
543 class Tracker_field_format extends Tracker_field
545 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
547 var $styles = array();
548 var $formats = array();
550 function Tracker_field_format(& $tracker_form, $field)
552 parent::Tracker_field($tracker_form, $field);
553 foreach ($this->form->config->get($this->name) as $option) {
554 list($key, $style, $format) = array_pad(array_map('trim', $option), 3, '');
555 if ($style != '') $this->styles[$key] = $style;
556 if ($format != '') $this->formats[$key] = $format;
560 function _get_key($str)
562 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
567 $s_name = htmlspecialchars($this->name);
568 $s_size = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
570 return '<input type="text" name="' . $s_name . '" size="' . $s_size . '" />';
573 function format_value($str)
575 if (is_array($str)) {
576 return join(', ', array_map(array($this, 'format_value'), $str));
579 $key = $this->_get_key($str);
580 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
583 function get_style($str)
585 $key = $this->_get_key($str);
586 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
590 class Tracker_field_file extends Tracker_field_format
592 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
596 $s_name = htmlspecialchars($this->name);
597 $s_size = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
599 return '<input type="file" name="' . $s_name . '" size="' . $s_size . '" />';
602 function format_value()
604 if (isset($_FILES[$this->name])) {
606 require_once(PLUGIN_DIR . 'attach.inc.php');
608 $base = $this->form->base;
609 $result = attach_upload($_FILES[$this->name], $base);
610 if (isset($result['result']) && $result['result']) {
612 return parent::format_value($base . '/' . $_FILES[$this->name]['name']);
616 // Filename not specified, or Fail to upload
617 return parent::format_value('');
621 class Tracker_field_radio extends Tracker_field_format
623 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
624 var $_options = array();
631 $s_name = htmlspecialchars($this->name);
632 foreach ($this->form->config->get($this->name) as $option) {
634 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
635 $s_option = htmlspecialchars($option[0]);
636 $checked = trim($option[0]) === trim($this->default_value) ? ' checked="checked"' : '';
638 $retval .= '<input type="radio"' .
639 ' name="' . $s_name . '"' .
640 ' id="' . $s_id . '"' .
641 ' value="' . $s_option . '"' .
643 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
649 function get_value($value)
651 $options = & $this->_options;
654 if (! isset($options[$name])) {
655 $values = array_map('reset', $this->form->config->get($name));
656 $options[$name] = array_flip($values); // array('value0' => 0, 'value1' => 1, ...)
659 return isset($options[$name][$value]) ? $options[$name][$value] : $value;
663 class Tracker_field_select extends Tracker_field_radio
665 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
669 function get_tag($empty = FALSE)
671 if (! isset($this->_defaults)) {
672 $this->_defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
674 $defaults = $this->_defaults;
678 $s_name = htmlspecialchars($this->name);
679 $s_size = (isset($this->options[0]) && is_numeric($this->options[0])) ?
680 ' size="' . htmlspecialchars($this->options[0]) . '"' : '';
681 $s_multiple = (isset($this->options[1]) && strtolower($this->options[1]) == 'multiple') ?
682 ' multiple="multiple"' : '';
683 $retval[] = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>';
685 if ($empty) $retval[] = ' <option value=""></option>';
687 foreach ($this->form->config->get($this->name) as $option) {
688 $option = reset($option);
689 $s_option = htmlspecialchars($option);
690 $selected = isset($defaults[trim($option)]) ? ' selected="selected"' : '';
691 $retval[] = ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>';
694 $retval[] = '</select>';
696 return implode("\n", $retval);
700 class Tracker_field_checkbox extends Tracker_field_radio
702 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
709 $s_name = htmlspecialchars($this->name);
710 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
711 foreach ($this->form->config->get($this->name) as $option) {
713 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
714 $s_option = htmlspecialchars($option[0]);
715 $checked = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
717 $retval .= '<input type="checkbox"' .
718 ' name="' . $s_name . '[]"' .
719 ' id="' . $s_id . '"' .
720 ' value="' . $s_option . '"' .
722 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
729 class Tracker_field_hidden extends Tracker_field_radio
731 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
735 return '<input type="hidden"' .
736 ' name="' . htmlspecialchars($this->name) . '"' .
737 ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
741 class Tracker_field_submit extends Tracker_field
747 $s_title = htmlspecialchars($this->title);
748 $s_base = htmlspecialchars($form->base);
749 $s_refer = htmlspecialchars($form->refer);
750 $s_config = htmlspecialchars($form->config->config_name);
753 <input type="submit" value="$s_title" />
754 <input type="hidden" name="plugin" value="tracker" />
755 <input type="hidden" name="_refer" value="$s_refer" />
756 <input type="hidden" name="_base" value="$s_base" />
757 <input type="hidden" name="_config" value="$s_config" />
762 class Tracker_field_date extends Tracker_field
764 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
766 function format_cell($timestamp)
768 return format_date($timestamp);
772 class Tracker_field_past extends Tracker_field
774 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
776 function get_value($value)
778 return UTIME - $value;
781 function format_cell($timestamp)
783 return get_passage($timestamp, FALSE);
787 ///////////////////////////////////////////////////////////////////////////
788 // tracker_list plugin
790 function plugin_tracker_list_convert()
794 $base = $refer = isset($vars['page']) ? $vars['page'] : '';
795 $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
796 $list = PLUGIN_TRACKER_DEFAULT_LIST;
797 $limit = PLUGIN_TRACKER_DEFAULT_LIMIT;
798 $order = PLUGIN_TRACKER_DEFAULT_ORDER;
800 $args = func_get_args();
801 $argc = count($args);
803 return PLUGIN_TRACKER_LIST_USAGE . '<br />';
806 case 4: $limit = $args[3]; /*FALLTHROUGH*/
807 case 3: $order = $args[2]; /*FALLTHROUGH*/
809 $arg = get_fullname($args[1], $base);
810 if (is_pagename($arg)) $base = $arg;
814 if ($args[0] != '') {
815 $arg = explode('/', $args[0], 2);
816 if ($arg[0] != '' ) $config_name = $arg[0];
817 if (isset($arg[1])) $list = $arg[1];
820 unset($args, $argc, $arg);
822 return plugin_tracker_list_render($base, $refer, $config_name, $list, $order, $limit);
825 function plugin_tracker_list_action()
829 $base = isset($get['base']) ? $get['base'] : ''; // Base directory to load
831 if (isset($get['refer'])) {
832 $refer = $get['refer']; // Where to #tracker_list
833 if ($base == '') $base = $refer;
838 $config = isset($get['config']) ? $get['config'] : '';
839 $list = isset($get['list']) ? $get['list'] : 'list';
840 $order = isset($get['order']) ? $get['order'] : PLUGIN_TRACKER_DEFAULT_ORDER;
841 $limit = isset($get['limit']) ? $get['limit'] : 0;
843 $s_refer = make_pagelink($refer);
845 'msg' => plugin_tracker_message('msg_list'),
846 'body'=> str_replace('$1', $s_refer, plugin_tracker_message('msg_back')) .
847 plugin_tracker_list_render($base, $refer, $config, $list, $order, $limit)
851 function plugin_tracker_list_render($base, $refer, $config_name, $list, $order_commands = '', $limit = 0)
854 if ($base == '') return '#tracker_list: Base not specified' . '<br />';
856 $refer = trim($refer);
857 if (! is_page($refer)) {
858 return '#tracker_list: Refer page not found: ' . htmlspecialchars($refer) . '<br />';
861 $config_name = trim($config_name);
862 if ($config_name == '') $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
865 if (! is_numeric($limit)) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
866 $limit = intval($limit);
868 $config = new Config('plugin/tracker/' . $config_name);
869 if (! $config->read()) {
870 return '#tracker_list: Config not found: ' . htmlspecialchars($config_name) . '<br />';
872 $config->config_name = $config_name;
873 if (! is_page($config->page . '/' . $list)) {
874 return '#tracker_list: List not found: ' . make_pagelink($config->page . '/' . $list) . '<br />';
877 $tracker_list = & new Tracker_list($base, $refer, $config);
878 if ($tracker_list->setSortOrder($order_commands) === FALSE) {
879 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
881 $result = $tracker_list->toString($list, $limit);
882 if ($result === FALSE) {
883 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
885 unset($tracker_list);
887 return convert_html($result);
893 var $form; // class Tracker_form
896 var $orders = array();
897 var $error = ''; // Error message
904 var $_added = array();
909 var $_the_first_character_of_the_line;
911 function Tracker_list($base, $refer, & $config)
913 $this->form = & new Tracker_form($base, $refer, $config);
916 // Adding $this->rows
917 // Add multiple pages at a time
920 $base = $this->form->base . '/';
921 $len = strlen($base);
922 $regex = '#^' . preg_quote($base, '#') . '#';
924 foreach (preg_grep($regex, array_values(get_existpages())) as $pagename) {
925 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, substr($pagename, $len))) {
928 if ($this->addRow($pagename) === FALSE) return FALSE;
930 if (empty($this->rows)) {
931 $this->error = 'Pages not found under: ' . $base;
938 // addRow(): Generate regex to load a page
939 function _generate_regex()
941 $template_page = $this->form->config->page . '/page';
942 $fields = $this->form->fields;
945 $pattern_fields = array();
947 $template = plugin_tracker_get_source($template_page, TRUE);
948 if ($template === FALSE || empty($template)) {
949 $this->error = 'Page not found or seems empty: ' . $template_page;
953 // Block-plugins to pseudo fields (#convert => [_block_convert])
954 $template = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $template);
956 // Now, $template = array('*someting*', 'fieldname', '*someting*', 'fieldname', ...)
957 $template = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($template, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
959 // NOTE: if the page has garbages between [field]s, it will fail to be load
960 while (! empty($template)) {
961 // Just ignore these _fixed_ data
962 $pattern[] = preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($template)) . '\\s*)');
963 if (empty($template)) continue;
965 $fieldname = array_shift($template);
966 if (isset($fields[$fieldname])) {
967 $pattern[] = '(.*?)'; // Just capture it
968 $pattern_fields[] = $fieldname; // Capture it as this $filedname
970 $pattern[] = '.*?'; // Just ignore pseudo fields etc
973 $this->pattern = '/' . implode('', $pattern) . '/sS';
974 $this->pattern_fields = $pattern_fields;
980 function addRow($pagename, $rescan = FALSE)
982 if (isset($this->_added[$pagename])) return TRUE;
983 $this->_added[$pagename] = TRUE;
985 $source = plugin_tracker_get_source($pagename, TRUE);
986 if ($source === FALSE) $source = '';
988 // Compat: 'move to [[page]]' (like bugtrack plugin)
990 if (! $rescan && ! empty($source) && preg_match('/move\sto\s(.+)/', $source, $matches)) {
991 $to_page = strip_bracket(trim($matches[1]));
992 if (is_page($to_page)) {
993 unset($source, $matches); // Release
994 return $this->addRow($to_page, TRUE); // Recurse(Rescan) once
999 $filetime = get_filetime($pagename);
1001 // column => default data of the cell
1002 '_page' => $pagename, // TODO: Redudant column pair [1]
1003 '_real' => $pagename, // TODO: Redudant column pair [1]
1004 '_update' => $filetime, // TODO: Redudant column pair [2]
1005 '_past' => $filetime, // TODO: Redudant column pair [2]
1008 // Load / Redefine cell
1010 if (preg_match($this->pattern, $source, $matches)) {
1011 array_shift($matches); // $matches[0] = all of the captured string
1012 foreach ($this->pattern_fields as $key => $fieldname) {
1013 $row[$fieldname] = trim($matches[$key]);
1014 unset($matches[$key]);
1016 $this->rows[] = $row;
1017 } else if (PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE) {
1018 $this->rows[] = $row; // Error
1025 function _order_commands2orders($order_commands = '')
1027 $order_commands = trim($order_commands);
1028 if ($order_commands == '') return array();
1033 foreach (explode(';', $order_commands) as $command) {
1034 $command = trim($command);
1035 if ($command == '') continue;
1037 $arg = explode(':', $command, 2);
1038 $fieldname = isset($arg[0]) ? trim($arg[0]) : '';
1039 $order = isset($arg[1]) ? trim($arg[1]) : '';
1041 $_order = $this->_sortkey_string2define($order);
1042 if ($_order === FALSE) {
1043 $this->error = 'Invalid sort key: ' . $order;
1045 } else if (isset($orders[$fieldname])) {
1046 $this->error = 'Sort key already set for: ' . $fieldname;
1050 if (PLUGIN_TRACKER_LIST_SORT_LIMIT <= $i) continue;
1053 $orders[$fieldname] = $_order;
1059 // Set commands for sort()
1060 function setSortOrder($order_commands = '')
1062 $orders = $this->_order_commands2orders($order_commands);
1063 if ($orders === FALSE) {
1064 $this->orders = array();
1067 $this->orders = $orders;
1071 // sortRows(): Internal sort type => PHP sort define
1072 function _sort_type_dropout($order)
1075 case PLUGIN_TRACKER_SORT_TYPE_REGULAR: return SORT_REGULAR;
1076 case PLUGIN_TRACKER_SORT_TYPE_NUMERIC: return SORT_NUMERIC;
1077 case PLUGIN_TRACKER_SORT_TYPE_STRING: return SORT_STRING;
1078 case PLUGIN_TRACKER_SORT_TYPE_NATURAL: return SORT_NATURAL;
1080 $this->error = 'Invalid sort type';
1085 // sortRows(): Internal sort order => PHP sort define
1086 function _sort_order_dropout($order)
1089 case PLUGIN_TRACKER_SORT_ORDER_ASC: return SORT_ASC;
1090 case PLUGIN_TRACKER_SORT_ORDER_DESC: return SORT_DESC;
1092 $this->error = 'Invalid sort order';
1097 // Sort $this->rows by $this->orders
1100 $fields = $this->form->fields;
1101 $orders = $this->orders;
1104 $fieldnames = array_keys($orders); // Field names to sort
1106 foreach ($fieldnames as $fieldname) {
1107 if (! isset($fields[$fieldname])) {
1108 $this->error = 'No such field: ' . $fieldname;
1111 $types[$fieldname] = $this->_sort_type_dropout($fields[$fieldname]->sort_type);
1112 $orders[$fieldname] = $this->_sort_order_dropout($orders[$fieldname]);
1113 if ($types[$fieldname] === FALSE || $orders[$fieldname] === FALSE) return FALSE;
1117 foreach ($this->rows as $row) {
1118 foreach ($fieldnames as $fieldname) {
1119 if (isset($row[$fieldname])) {
1120 $columns[$fieldname][] = $fields[$fieldname]->get_value($row[$fieldname]);
1122 $columns[$fieldname][] = '';
1128 foreach ($fieldnames as $fieldname) {
1130 if ($types[$fieldname] == SORT_NATURAL) {
1131 $column = & $columns[$fieldname];
1132 natcasesort($column);
1135 foreach (array_keys($column) as $key) {
1136 // Consider the same values there, for array_multisort()
1137 if ($last !== $column[$key]) ++$i;
1138 $last = strtolower($column[$key]); // natCASEsort()
1141 ksort($column, SORT_NUMERIC); // Revert the order
1142 $types[$fieldname] = SORT_NUMERIC;
1145 // One column set (one-dimensional array, sort type, and sort order)
1146 // for array_multisort()
1147 $params[] = $columns[$fieldname];
1148 $params[] = $types[$fieldname];
1149 $params[] = $orders[$fieldname];
1151 if (! empty($orders) && ! empty($this->rows)) {
1152 $params[] = & $this->rows; // The target
1153 call_user_func_array('array_multisort', $params);
1159 // toString(): Sort key: Define to string (internal var => string)
1160 function _sortkey_define2string($sortkey)
1163 case PLUGIN_TRACKER_SORT_ORDER_ASC: return 'asc';
1164 case PLUGIN_TRACKER_SORT_ORDER_DESC: return 'desc';
1166 $this->error = 'No such define: ' . $sortkey;
1171 // toString(): Sort key: String to define (string => internal var)
1172 function _sortkey_string2define($sortkey)
1174 switch (strtoupper(trim($sortkey))) {
1175 case '': return PLUGIN_TRACKER_SORT_ORDER_DEFAULT; break;
1177 case SORT_ASC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1178 case 'SORT_ASC': /*FALLTHROUGH*/
1179 case 'ASC': return PLUGIN_TRACKER_SORT_ORDER_ASC;
1181 case SORT_DESC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1182 case 'SORT_DESC': /*FALLTHROUGH*/
1183 case 'DESC': return PLUGIN_TRACKER_SORT_ORDER_DESC;
1186 $this->error = 'Invalid sort key: ' . $sortkey;
1191 // toString(): Called within preg_replace_callback()
1192 function _replace_title($matches = array())
1194 $form = $this->form;
1195 $base = $form->base;
1196 $refer = $form->refer;
1197 $fields = $form->fields;
1198 $config_name = $form->config->config_name;
1200 $orders = $this->orders;
1201 $list = $this->_list;
1203 $fieldname = isset($matches[1]) ? $matches[1] : '';
1204 if (! isset($fields[$fieldname])) {
1205 // Invalid $fieldname or user's own string or something. Nothing to do
1206 return isset($matches[0]) ? $matches[0] : '';
1209 // This column seems sorted or not
1210 if (isset($orders[$fieldname])) {
1211 $is_asc = ($orders[$fieldname] == PLUGIN_TRACKER_SORT_ORDER_ASC);
1213 $indexes = array_flip(array_keys($orders));
1214 $index = $indexes[$fieldname] + 1;
1217 $arrow = '&br;' . ($is_asc ? '↑' : '↓') . '(' . $index . ')';
1218 // Allow flip, if this is the first column
1219 if (($index == 1) xor $is_asc) {
1220 $order = PLUGIN_TRACKER_SORT_ORDER_ASC;
1222 $order = PLUGIN_TRACKER_SORT_ORDER_DESC;
1226 $order = PLUGIN_TRACKER_SORT_ORDER_DEFAULT;
1229 // This column will be the first position , if you click
1230 $orders = array($fieldname => $order) + $orders;
1233 foreach ($orders as $_fieldname => $_order) {
1234 if ($_order == PLUGIN_TRACKER_SORT_ORDER_DEFAULT) {
1235 $_orders[] = $_fieldname;
1237 $_orders[] = $_fieldname . ':' . $this->_sortkey_define2string($_order);
1241 $script = get_script_uri();
1242 $r_base = ($refer != $base) ?
1243 '&base=' . rawurlencode($base) : '';
1244 $r_config = ($config_name != PLUGIN_TRACKER_DEFAULT_CONFIG) ?
1245 '&config=' . rawurlencode($config_name) : '';
1246 $r_list = ($list != PLUGIN_TRACKER_DEFAULT_LIST) ?
1247 '&list=' . rawurlencode($list) : '';
1248 $r_order = ! empty($_orders) ?
1249 '&order=' . rawurlencode(join(';', $_orders)) : '';
1253 $fields[$fieldname]->title . $arrow .
1255 $script . '?plugin=tracker_list' .
1256 '&refer=' . rawurlencode($refer) . // Try to show 'page title' properly
1257 $r_base . $r_config . $r_list . $r_order .
1261 // toString(): Called within preg_replace_callback()
1262 function _replace_item($matches = array())
1264 $fields = $this->form->fields;
1266 $tfc = $this->_the_first_character_of_the_line ;
1268 $params = isset($matches[1]) ? explode(',', $matches[1]) : array();
1269 $fieldname = isset($params[0]) ? $params[0] : '';
1270 $stylename = isset($params[1]) ? $params[1] : $fieldname;
1274 if ($fieldname != '') {
1275 if (! isset($row[$fieldname])) {
1276 // Maybe load miss of the page
1277 if (isset($fields[$fieldname])) {
1278 $str = '[page_err]'; // Exactlly
1280 $str = isset($matches[0]) ? $matches[0] : ''; // Nothing to do
1283 $str = $row[$fieldname];
1284 if (isset($fields[$fieldname])) {
1285 $str = $fields[$fieldname]->format_cell($str);
1290 if (isset($fields[$stylename]) && isset($row[$stylename])) {
1291 $_style = $fields[$stylename]->get_style($row[$stylename]);
1292 $str = sprintf($_style, $str);
1295 return plugin_tracker_escape($str, $tfc);
1298 // Output a part of Wiki text
1299 function toString($list, $limit = 0)
1301 $form = & $this->form;
1302 $list = $form->config->page . '/' . $list;
1305 $regex = '/\[([^\[\]]+)\]/';
1308 $template = plugin_tracker_get_source($list, TRUE);
1309 if ($template === FALSE || empty($template)) {
1310 $this->error = 'Page not found or seems empty: ' . $template;
1314 // Try to create $form->fields just you need
1315 if ($form->initFields('_real') === FALSE ||
1316 $form->initFields(plugin_tracker_field_pickup($template)) === FALSE ||
1317 $form->initFields(array_keys($this->orders)) === FALSE) {
1318 $this->error = $form->error;
1322 // TODO: Check isset($this->rows) or something
1323 // Generate regex for $form->fields
1324 if ($this->_generate_regex() === FALSE) return FALSE;
1326 // TODO: Check isset($this->rows) or something
1327 // Load and sort $this->rows
1328 if ($this->loadRows() === FALSE || $this->sortRows() === FALSE) return FALSE;
1329 $rows = $this->rows;
1333 $this->_list = $list; // For _replace_title() only
1334 $count = count($this->rows);
1335 $limit = intval($limit);
1336 if ($limit != 0) $limit = max(1, $limit);
1337 if ($limit != 0 && $count > $limit) {
1338 $source[] = str_replace(
1340 array($count, $limit),
1341 plugin_tracker_message('msg_limit')
1343 $rows = array_slice($this->rows, 0, $limit);
1347 // TODO: How do you feel single/multiple table rows with 'c'(decolation)?
1348 $matches = $t_header = $t_body = $t_footer = array();
1349 $template = plugin_tracker_get_source($list);
1350 if ($template === FALSE) {
1351 $this->error = 'Page not found or seems empty: ' . $list;
1354 foreach ($template as $line) {
1355 if (preg_match('/^\|.+\|([hfc])$/i', $line, $matches)) {
1356 if (strtolower($matches[1]) == 'f') {
1357 $t_footer[] = $line; // Table footer
1359 $t_header[] = $line; // Table header, or decoration
1367 // Header and decolation
1368 foreach($t_header as $line) {
1369 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1373 foreach ($rows as $row) {
1376 foreach ($t_body as $line) {
1377 if (ltrim($line) != '') {
1378 $this->_the_first_character_of_the_line = $line[0];
1379 $line = preg_replace_callback($regex, array(& $this, '_replace_item'), $line);
1386 foreach($t_footer as $line) {
1387 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1391 return implode('', $source);
1395 // Roughly checking listed fields from template
1396 // " [field1] [field2,style1] " => array('fielld', 'field2')
1397 function plugin_tracker_field_pickup($string = '')
1399 if (! is_string($string) || empty($string)) return array();
1401 $fieldnames = array();
1404 preg_match_all('/\[([^\[\]]+)\]/', $string, $matches);
1407 foreach ($matches[1] as $match) {
1408 $params = explode(',', $match, 2);
1409 if (isset($params[0])) {
1410 $fieldnames[$params[0]] = TRUE;
1414 return array_keys($fieldnames);
1417 function plugin_tracker_get_source($page, $join = FALSE)
1419 $source = get_source($page, TRUE, $join);
1420 if ($source === FALSE) return FALSE;
1422 return preg_replace(
1425 '/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', // Remove fixed-heading anchors
1435 // Escape special characters not to break Wiki syntax
1436 function plugin_tracker_escape($string, $syntax_hint = '')
1438 // Default: line-oriented
1439 $from = array("\n", "\r" );
1440 $to = array('&br;', '&br;');
1442 if ($syntax_hint == '|' || $syntax_hint == ':') {
1443 // <table> or <dl> Wiki syntax: Excape '|'
1446 } else if ($syntax_hint == ',') {
1451 return str_replace($from, $to, $string);
1454 function plugin_tracker_message($key)
1456 global $_tracker_messages;
1457 return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';