2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.103 2007/10/06 06:30:18 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 // Output a part of XHTML form for the field
404 // Format user input before write
405 function format_value($value)
410 // Compare key for Tracker_list->sort()
411 function get_value($value)
416 // Format table cell data before output the wiki text
417 function format_cell($str)
422 // Format-string for sprintf() before output the wiki text
429 class Tracker_field_text extends Tracker_field
431 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
435 return '<input type="text"' .
436 ' name="' . htmlspecialchars($this->name) . '"' .
437 ' size="' . htmlspecialchars($this->values[0]) . '"' .
438 ' value="' . htmlspecialchars($this->default_value) . '" />';
442 // Special type: Page name with link syntax
443 class Tracker_field_page extends Tracker_field_text
445 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
447 function _format($page)
449 $page = strip_bracket($page);
450 if (is_pagename($page)) $page = '[[' . $page . ']]';
454 function format_value($value)
456 return $this->_format($value);
459 function format_cell($value)
461 return $this->_format($value);
465 // Special type: Page name minus 'base'
467 // page name: Tracker/sales/100
468 // base : Tracker/sales
470 class Tracker_field_real extends Tracker_field_text
472 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NATURAL;
474 function format_cell($value)
476 // basename(): Rough but work with this(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN prohibits '/') situation
477 return basename($value);
481 class Tracker_field_title extends Tracker_field_text
483 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
485 function format_cell($str)
492 class Tracker_field_textarea extends Tracker_field
494 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
499 ' name="' . htmlspecialchars($this->name) . '"' .
500 ' cols="' . htmlspecialchars($this->values[0]) . '"' .
501 ' rows="' . htmlspecialchars($this->values[1]) . '">' .
502 htmlspecialchars($this->default_value) .
506 function format_cell($str)
508 $str = preg_replace('/[\r\n]+/', '', $str);
509 if (! empty($this->values[2]) && strlen($str) > ($this->values[2] + 3)) {
510 $str = mb_substr($str, 0, $this->values[2]) . '...';
516 // Writing text with formatting if trim($cell) != ''
517 // See also: http://home.arino.jp/?tracker.inc.php%2F41
518 class Tracker_field_format extends Tracker_field
520 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
522 var $styles = array();
523 var $formats = array();
525 function Tracker_field_format(& $tracker_form, $field)
527 parent::Tracker_field($tracker_form, $field);
528 foreach ($this->form->config->get($this->name) as $option) {
529 list($key, $style, $format) = array_pad(array_map('trim', $option), 3, '');
530 if ($style != '') $this->styles[$key] = $style;
531 if ($format != '') $this->formats[$key] = $format;
535 function _get_key($str)
537 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
542 return '<input type="text"' .
543 ' name="' . htmlspecialchars($this->name) . '"' .
544 ' size="' . htmlspecialchars($this->values[0]) . '" />';
547 function format_value($str)
549 if (is_array($str)) {
550 return join(', ', array_map(array($this, 'format_value'), $str));
553 $key = $this->_get_key($str);
554 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
557 function get_style($str)
559 $key = $this->_get_key($str);
560 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
564 class Tracker_field_file extends Tracker_field_format
566 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
570 return '<input type="file"' .
571 ' name="' . htmlspecialchars($this->name) . '"' .
572 ' size="' . htmlspecialchars($this->values[0]) . '" />';
575 function format_value()
577 if (isset($_FILES[$this->name])) {
579 require_once(PLUGIN_DIR . 'attach.inc.php');
581 $base = $this->form->base;
582 $result = attach_upload($_FILES[$this->name], $base);
583 if (isset($result['result']) && $result['result']) {
585 return parent::format_value($base . '/' . $_FILES[$this->name]['name']);
589 // Filename not specified, or Fail to upload
590 return parent::format_value('');
594 class Tracker_field_radio extends Tracker_field_format
596 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
597 var $_options = array();
604 $s_name = htmlspecialchars($this->name);
605 foreach ($this->form->config->get($this->name) as $option) {
607 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
608 $s_option = htmlspecialchars($option[0]);
609 $checked = trim($option[0]) == trim($this->default_value) ? ' checked="checked"' : '';
611 $retval .= '<input type="radio"' .
612 ' name="' . $s_name . '"' .
613 ' id="' . $s_id . '"' .
614 ' value="' . $s_option . '"' .
616 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
622 function get_value($value)
624 $options = & $this->_options;
627 if (! isset($options[$name])) {
628 $values = array_map('reset', $this->form->config->get($name));
629 $options[$name] = array_flip($values); // array('value0' => 0, 'value1' => 1, ...)
632 return isset($options[$name][$value]) ? $options[$name][$value] : $value;
636 class Tracker_field_select extends Tracker_field_radio
638 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
642 function get_tag($empty = FALSE)
644 if (! isset($this->_defaults)) {
645 $this->_defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
647 $defaults = $this->_defaults;
651 $s_name = htmlspecialchars($this->name);
652 $s_size = (isset($this->values[0]) && is_numeric($this->values[0])) ?
653 ' size="' . htmlspecialchars($this->values[0]) . '"' : '';
654 $s_multiple = (isset($this->values[1]) && strtolower($this->values[1]) == 'multiple') ?
655 ' multiple="multiple"' : '';
656 $retval[] = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>';
658 if ($empty) $retval[] = ' <option value=""></option>';
660 foreach ($this->form->config->get($this->name) as $option) {
661 $option = reset($option);
662 $s_option = htmlspecialchars($option);
663 $selected = isset($defaults[trim($option)]) ? ' selected="selected"' : '';
664 $retval[] = ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>';
667 $retval[] = '</select>';
669 return implode("\n", $retval);
673 class Tracker_field_checkbox extends Tracker_field_radio
675 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
682 $s_name = htmlspecialchars($this->name);
683 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
684 foreach ($this->form->config->get($this->name) as $option) {
686 $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
687 $s_option = htmlspecialchars($option[0]);
688 $checked = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
690 $retval .= '<input type="checkbox"' .
691 ' name="' . $s_name . '[]"' .
692 ' id="' . $s_id . '"' .
693 ' value="' . $s_option . '"' .
695 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
702 class Tracker_field_hidden extends Tracker_field_radio
704 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
708 return '<input type="hidden"' .
709 ' name="' . htmlspecialchars($this->name) . '"' .
710 ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
714 class Tracker_field_submit extends Tracker_field
720 $s_title = htmlspecialchars($this->title);
721 $s_base = htmlspecialchars($form->base);
722 $s_refer = htmlspecialchars($form->refer);
723 $s_config = htmlspecialchars($form->config->config_name);
726 <input type="submit" value="$s_title" />
727 <input type="hidden" name="plugin" value="tracker" />
728 <input type="hidden" name="_refer" value="$s_refer" />
729 <input type="hidden" name="_base" value="$s_base" />
730 <input type="hidden" name="_config" value="$s_config" />
735 class Tracker_field_date extends Tracker_field
737 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
739 function format_cell($timestamp)
741 return format_date($timestamp);
745 class Tracker_field_past extends Tracker_field
747 var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
749 function get_value($value)
751 return UTIME - $value;
754 function format_cell($timestamp)
756 return get_passage($timestamp, FALSE);
760 ///////////////////////////////////////////////////////////////////////////
761 // tracker_list plugin
763 function plugin_tracker_list_convert()
767 $base = $refer = isset($vars['page']) ? $vars['page'] : '';
768 $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
769 $list = PLUGIN_TRACKER_DEFAULT_LIST;
770 $limit = PLUGIN_TRACKER_DEFAULT_LIMIT;
771 $order = PLUGIN_TRACKER_DEFAULT_ORDER;
773 $args = func_get_args();
774 $argc = count($args);
776 return PLUGIN_TRACKER_LIST_USAGE . '<br />';
779 case 4: $limit = $args[3]; /*FALLTHROUGH*/
780 case 3: $order = $args[2]; /*FALLTHROUGH*/
782 $arg = get_fullname($args[1], $base);
783 if (is_pagename($arg)) $base = $arg;
787 if ($args[0] != '') {
788 $arg = explode('/', $args[0], 2);
789 if ($arg[0] != '' ) $config_name = $arg[0];
790 if (isset($arg[1])) $list = $arg[1];
793 unset($args, $argc, $arg);
795 return plugin_tracker_list_render($base, $refer, $config_name, $list, $order, $limit);
798 function plugin_tracker_list_action()
802 $base = isset($get['base']) ? $get['base'] : ''; // Base directory to load
804 if (isset($get['refer'])) {
805 $refer = $get['refer']; // Where to #tracker_list
806 if ($base == '') $base = $refer;
811 $config = isset($get['config']) ? $get['config'] : '';
812 $list = isset($get['list']) ? $get['list'] : 'list';
813 $order = isset($get['order']) ? $get['order'] : PLUGIN_TRACKER_DEFAULT_ORDER;
814 $limit = isset($get['limit']) ? $get['limit'] : 0;
816 $s_refer = make_pagelink($refer);
818 'msg' => plugin_tracker_message('msg_list'),
819 'body'=> str_replace('$1', $s_refer, plugin_tracker_message('msg_back')) .
820 plugin_tracker_list_render($base, $refer, $config, $list, $order, $limit)
824 function plugin_tracker_list_render($base, $refer, $config_name, $list, $order_commands = '', $limit = 0)
827 if ($base == '') return '#tracker_list: Base not specified' . '<br />';
829 $refer = trim($refer);
830 if (! is_page($refer)) {
831 return '#tracker_list: Refer page not found: ' . htmlspecialchars($refer) . '<br />';
834 $config_name = trim($config_name);
835 if ($config_name == '') $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
838 if (! is_numeric($limit)) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
839 $limit = intval($limit);
841 $config = new Config('plugin/tracker/' . $config_name);
842 if (! $config->read()) {
843 return '#tracker_list: Config not found: ' . htmlspecialchars($config_name) . '<br />';
845 $config->config_name = $config_name;
846 if (! is_page($config->page . '/' . $list)) {
847 return '#tracker_list: List not found: ' . make_pagelink($config->page . '/' . $list) . '<br />';
850 $list = & new Tracker_list($base, $refer, $config, $list);
851 if ($list->setSortOrder($order_commands) === FALSE) {
852 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
854 $result = $list->toString($limit);
855 if ($result === FALSE) {
856 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
860 return convert_html($result);
866 var $form; // class Tracker_form
874 var $orders = array();
875 var $error = ''; // Error message
878 var $_added = array();
882 var $_the_first_character_of_the_line;
884 function Tracker_list($base, $refer, & $config, $list)
886 $form = & new Tracker_form($base, $refer, $config);
891 // Add multiple pages at a time
894 $base = $this->form->base . '/';
895 $len = strlen($base);
896 $regex = '#^' . preg_quote($base, '#') . '#';
898 // Adding $this->rows
899 foreach (preg_grep($regex, array_values(get_existpages())) as $pagename) {
900 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, substr($pagename, $len))) {
903 if ($this->addRow($pagename) === FALSE) return FALSE;
905 if (empty($this->rows)) {
906 $this->error = 'Pages not found under: ' . $base;
913 // addRow(): Generate regex to load a page
914 function _generate_regex()
916 $template_page = $this->form->config->page . '/page';
917 $fields = $this->form->fields;
920 $pattern_fields = array();
922 $template = plugin_tracker_get_source($template_page, TRUE);
923 if ($template === FALSE || empty($template)) {
924 $this->error = 'Page not found or seems empty: ' . $template_page;
928 // Block-plugins to pseudo fields (#convert => [_block_convert])
929 $template = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $template);
931 // Now, $template = array('*someting*', 'fieldname', '*someting*', 'fieldname', ...)
932 $template = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($template, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
934 // NOTE: if the page has garbages between [field]s, it will fail to be load
935 while (! empty($template)) {
936 // Just ignore these _fixed_ data
937 $pattern[] = preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($template)) . '\\s*)');
938 if (empty($template)) continue;
940 $fieldname = array_shift($template);
941 if (isset($fields[$fieldname])) {
942 $pattern[] = '(.*?)'; // Just capture it
943 $pattern_fields[] = $fieldname; // Capture it as this $filedname
945 $pattern[] = '.*?'; // Just ignore pseudo fields etc
948 $this->pattern = '/' . implode('', $pattern) . '/sS';
949 $this->pattern_fields = $pattern_fields;
955 function addRow($pagename, $rescan = FALSE)
957 if (isset($this->_added[$pagename])) return TRUE;
958 $this->_added[$pagename] = TRUE;
960 $source = plugin_tracker_get_source($pagename, TRUE);
961 if ($source === FALSE) $source = '';
963 // Compat: 'move to [[page]]' (like bugtrack plugin)
965 if (! $rescan && ! empty($source) && preg_match('/move\sto\s(.+)/', $source, $matches)) {
966 $to_page = strip_bracket(trim($matches[1]));
967 if (is_page($to_page)) {
968 unset($source, $matches); // Release
969 return $this->addRow($to_page, TRUE); // Recurse(Rescan) once
974 $filetime = get_filetime($pagename);
976 // column => default data of the cell
977 '_page' => $pagename, // TODO: Redudant column pair [1]
978 '_real' => $pagename, // TODO: Redudant column pair [1]
979 '_update' => $filetime, // TODO: Redudant column pair [2]
980 '_past' => $filetime, // TODO: Redudant column pair [2]
983 // Load / Redefine cell
985 if (preg_match($this->pattern, $source, $matches)) {
986 array_shift($matches); // $matches[0] = all of the captured string
987 foreach ($this->pattern_fields as $key => $fieldname) {
988 $row[$fieldname] = trim($matches[$key]);
989 unset($matches[$key]);
991 $this->rows[] = $row;
992 } else if (PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE) {
993 $this->rows[] = $row; // Error
1000 function _order_commands2orders($order_commands = '')
1002 $order_commands = trim($order_commands);
1003 if ($order_commands == '') return array();
1008 foreach (explode(';', $order_commands) as $command) {
1009 $command = trim($command);
1010 if ($command == '') continue;
1012 $arg = explode(':', $command, 2);
1013 $fieldname = isset($arg[0]) ? trim($arg[0]) : '';
1014 $order = isset($arg[1]) ? trim($arg[1]) : '';
1016 $_order = $this->_sortkey_string2define($order);
1017 if ($_order === FALSE) {
1018 $this->error = 'Invalid sort key: ' . $order;
1020 } else if (isset($orders[$fieldname])) {
1021 $this->error = 'Sort key already set for: ' . $fieldname;
1025 if (PLUGIN_TRACKER_LIST_SORT_LIMIT <= $i) continue;
1028 $orders[$fieldname] = $_order;
1034 // Set commands for sort()
1035 function setSortOrder($order_commands = '')
1037 $orders = $this->_order_commands2orders($order_commands);
1038 if ($orders === FALSE) {
1039 $this->orders = array();
1042 $this->orders = $orders;
1046 // sortRows(): Internal sort type => PHP sort define
1047 function _sort_type_dropout($order)
1050 case PLUGIN_TRACKER_SORT_TYPE_REGULAR: return SORT_REGULAR;
1051 case PLUGIN_TRACKER_SORT_TYPE_NUMERIC: return SORT_NUMERIC;
1052 case PLUGIN_TRACKER_SORT_TYPE_STRING: return SORT_STRING;
1053 case PLUGIN_TRACKER_SORT_TYPE_NATURAL: return SORT_NATURAL;
1055 $this->error = 'Invalid sort type';
1060 // sortRows(): Internal sort order => PHP sort define
1061 function _sort_order_dropout($order)
1064 case PLUGIN_TRACKER_SORT_ORDER_ASC: return SORT_ASC;
1065 case PLUGIN_TRACKER_SORT_ORDER_DESC: return SORT_DESC;
1067 $this->error = 'Invalid sort order';
1072 // Sort $this->rows by $this->orders
1075 $fields = $this->form->fields;
1076 $orders = $this->orders;
1079 $fieldnames = array_keys($orders); // Field names to sort
1081 foreach ($fieldnames as $fieldname) {
1082 if (! isset($fields[$fieldname])) {
1083 $this->error = 'No such field: ' . $fieldname;
1086 $types[$fieldname] = $this->_sort_type_dropout($fields[$fieldname]->sort_type);
1087 $orders[$fieldname] = $this->_sort_order_dropout($orders[$fieldname]);
1088 if ($types[$fieldname] === FALSE || $orders[$fieldname] === FALSE) return FALSE;
1092 foreach ($this->rows as $row) {
1093 foreach ($fieldnames as $fieldname) {
1094 if (isset($row[$fieldname])) {
1095 $columns[$fieldname][] = $fields[$fieldname]->get_value($row[$fieldname]);
1097 $columns[$fieldname][] = '';
1103 foreach ($fieldnames as $fieldname) {
1105 if ($types[$fieldname] == SORT_NATURAL) {
1106 $column = & $columns[$fieldname];
1107 natcasesort($column);
1110 foreach (array_keys($column) as $key) {
1111 // Consider the same values there, for array_multisort()
1112 if ($last !== $column[$key]) ++$i;
1113 $last = strtolower($column[$key]); // natCASEsort()
1116 ksort($column, SORT_NUMERIC); // Revert the order
1117 $types[$fieldname] = SORT_NUMERIC;
1120 // One column set (one-dimensional array, sort type, and sort order)
1121 // for array_multisort()
1122 $params[] = $columns[$fieldname];
1123 $params[] = $types[$fieldname];
1124 $params[] = $orders[$fieldname];
1126 if (! empty($orders) && ! empty($this->rows)) {
1127 $params[] = & $this->rows; // The target
1128 call_user_func_array('array_multisort', $params);
1134 // toString(): Sort key: Define to string (internal var => string)
1135 function _sortkey_define2string($sortkey)
1138 case PLUGIN_TRACKER_SORT_ORDER_ASC: return 'asc';
1139 case PLUGIN_TRACKER_SORT_ORDER_DESC: return 'desc';
1141 $this->error = 'No such define: ' . $sortkey;
1146 // toString(): Sort key: String to define (string => internal var)
1147 function _sortkey_string2define($sortkey)
1149 switch (strtoupper(trim($sortkey))) {
1150 case '': return PLUGIN_TRACKER_SORT_ORDER_DEFAULT; break;
1152 case SORT_ASC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1153 case 'SORT_ASC': /*FALLTHROUGH*/
1154 case 'ASC': return PLUGIN_TRACKER_SORT_ORDER_ASC;
1156 case SORT_DESC: /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1157 case 'SORT_DESC': /*FALLTHROUGH*/
1158 case 'DESC': return PLUGIN_TRACKER_SORT_ORDER_DESC;
1161 $this->error = 'Invalid sort key: ' . $sortkey;
1166 // toString(): Called within preg_replace_callback()
1167 function _replace_title($matches = array())
1169 $form = $this->form;
1170 $base = $form->base;
1171 $refer = $form->refer;
1172 $fields = $form->fields;
1173 $config_name = $form->config->config_name;
1175 $list = $this->list;
1176 $orders = $this->orders;
1178 $fieldname = isset($matches[1]) ? $matches[1] : '';
1179 if (! isset($fields[$fieldname])) {
1180 // Invalid $fieldname or user's own string or something. Nothing to do
1181 return isset($matches[0]) ? $matches[0] : '';
1184 // This column seems sorted or not
1185 if (isset($orders[$fieldname])) {
1186 $is_asc = ($orders[$fieldname] == PLUGIN_TRACKER_SORT_ORDER_ASC);
1188 $indexes = array_flip(array_keys($orders));
1189 $index = $indexes[$fieldname] + 1;
1192 $arrow = '&br;' . ($is_asc ? '↑' : '↓') . '(' . $index . ')';
1193 // Allow flip, if this is the first column
1194 if (($index == 1) xor $is_asc) {
1195 $order = PLUGIN_TRACKER_SORT_ORDER_ASC;
1197 $order = PLUGIN_TRACKER_SORT_ORDER_DESC;
1201 $order = PLUGIN_TRACKER_SORT_ORDER_DEFAULT;
1204 // This column will be the first position , if you click
1205 $orders = array($fieldname => $order) + $orders;
1208 foreach ($orders as $_fieldname => $_order) {
1209 if ($_order == PLUGIN_TRACKER_SORT_ORDER_DEFAULT) {
1210 $_orders[] = $_fieldname;
1212 $_orders[] = $_fieldname . ':' . $this->_sortkey_define2string($_order);
1216 $script = get_script_uri();
1217 $r_base = ($refer != $base) ?
1218 '&base=' . rawurlencode($base) : '';
1219 $r_config = ($config_name != PLUGIN_TRACKER_DEFAULT_CONFIG) ?
1220 '&config=' . rawurlencode($config_name) : '';
1221 $r_list = ($list != PLUGIN_TRACKER_DEFAULT_LIST) ?
1222 '&list=' . rawurlencode($list) : '';
1223 $r_order = ! empty($_orders) ?
1224 '&order=' . rawurlencode(join(';', $_orders)) : '';
1228 $fields[$fieldname]->title . $arrow .
1230 $script . '?plugin=tracker_list' .
1231 '&refer=' . rawurlencode($refer) . // Try to show 'page title' properly
1232 $r_base . $r_config . $r_list . $r_order .
1236 // toString(): Called within preg_replace_callback()
1237 function _replace_item($matches = array())
1239 $fields = $this->form->fields;
1241 $tfc = $this->_the_first_character_of_the_line ;
1243 $params = isset($matches[1]) ? explode(',', $matches[1]) : array();
1244 $fieldname = isset($params[0]) ? $params[0] : '';
1245 $stylename = isset($params[1]) ? $params[1] : $fieldname;
1249 if ($fieldname != '') {
1250 if (! isset($row[$fieldname])) {
1251 // Maybe load miss of the page
1252 if (isset($fields[$fieldname])) {
1253 $str = '[page_err]'; // Exactlly
1255 $str = isset($matches[0]) ? $matches[0] : ''; // Nothing to do
1258 $str = $row[$fieldname];
1259 if (isset($fields[$fieldname])) {
1260 $str = $fields[$fieldname]->format_cell($str);
1265 if (isset($fields[$stylename]) && isset($row[$stylename])) {
1266 $_style = $fields[$stylename]->get_style($row[$stylename]);
1267 $str = sprintf($_style, $str);
1270 return plugin_tracker_escape($str, $tfc);
1273 // Output a part of Wiki text
1274 function toString($limit = 0)
1276 $form = & $this->form;
1277 $list = $form->config->page . '/' . $this->list;
1279 $regex = '/\[([^\[\]]+)\]/';
1282 $template = plugin_tracker_get_source($list, TRUE);
1283 if ($template === FALSE || empty($template)) {
1284 $this->error = 'Page not found or seems empty: ' . $template;
1288 // Creating $form->fields just you need
1289 if ($form->initFields('_real') === FALSE ||
1290 $form->initFields(plugin_tracker_field_pickup($template)) === FALSE ||
1291 $form->initFields(array_keys($this->orders)) === FALSE) {
1292 $this->error = $form->error;
1296 // Generate regex for $form->fields
1297 if ($this->_generate_regex() === FALSE) return FALSE;
1299 // Load and sort $this->rows
1300 if ($this->loadRows() === FALSE || $this->sortRows() === FALSE) return FALSE;
1301 $rows = $this->rows;
1304 $count = count($this->rows);
1305 $limit = intval($limit);
1306 if ($limit != 0) $limit = max(1, $limit);
1307 if ($limit != 0 && $count > $limit) {
1308 $source[] = str_replace(
1310 array($count, $limit),
1311 plugin_tracker_message('msg_limit')
1313 $rows = array_slice($this->rows, 0, $limit);
1317 // TODO: How do you feel single/multiple table rows with 'c'(decolation)?
1318 $matches = $t_header = $t_body = $t_footer = array();
1319 $template = plugin_tracker_get_source($list);
1320 if ($template === FALSE) {
1321 $this->error = 'Page not found or seems empty: ' . $list;
1324 foreach ($template as $line) {
1325 if (preg_match('/^\|.+\|([hfc])$/i', $line, $matches)) {
1326 if (strtolower($matches[1]) == 'f') {
1327 $t_footer[] = $line; // Table footer
1329 $t_header[] = $line; // Table header, or decoration
1337 // Header and decolation
1338 foreach($t_header as $line) {
1339 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1343 foreach ($rows as $row) {
1346 foreach ($t_body as $line) {
1347 if (ltrim($line) != '') {
1348 $this->_the_first_character_of_the_line = $line[0];
1349 $line = preg_replace_callback($regex, array(& $this, '_replace_item'), $line);
1356 foreach($t_footer as $line) {
1357 $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1361 return implode('', $source);
1365 // Roughly checking listed fields from template
1366 // " [field1] [field2,style1] " => array('fielld', 'field2')
1367 function plugin_tracker_field_pickup($string = '')
1369 if (! is_string($string) || empty($string)) return array();
1371 $fieldnames = array();
1374 preg_match_all('/\[([^\[\]]+)\]/', $string, $matches);
1377 foreach ($matches[1] as $match) {
1378 $params = explode(',', $match, 2);
1379 if (isset($params[0])) {
1380 $fieldnames[$params[0]] = TRUE;
1384 return array_keys($fieldnames);
1387 function plugin_tracker_get_source($page, $join = FALSE)
1389 $source = get_source($page, TRUE, $join);
1390 if ($source === FALSE) return FALSE;
1392 return preg_replace(
1395 '/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', // Remove fixed-heading anchors
1405 // Escape special characters not to break Wiki syntax
1406 function plugin_tracker_escape($string, $syntax_hint = '')
1408 // Default: line-oriented
1409 $from = array("\n", "\r" );
1410 $to = array('&br;', '&br;');
1412 if ($syntax_hint == '|' || $syntax_hint == ':') {
1413 // <table> or <dl> Wiki syntax: Excape '|'
1416 } else if ($syntax_hint == ',') {
1421 return str_replace($from, $to, $string);
1424 function plugin_tracker_message($key)
1426 global $_tracker_messages;
1427 return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';