OSDN Git Service

TODO: if (is FALSE) OR file_exists()
[pukiwiki/pukiwiki.git] / plugin / tracker.inc.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.70 2007/09/23 04:47:06 henoheno Exp $
4 // Copyright (C) 2003-2005, 2007 PukiWiki Developers Team
5 // License: GPL v2 or (at your option) any later version
6 //
7 // Issue tracker plugin (See Also bugtrack plugin)
8
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]]])');
11
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'
17
18 // Sort N columns at a time
19 define('PLUGIN_TRACKER_LIST_SORT_LIMIT', 3);
20
21 // Excluding pattern
22 define('PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN','#^SubMenu$|/#');  // 'SubMenu' and using '/'
23 //define('PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN','#(?!)#');               // Nothing excluded
24
25 // Show error rows (can't capture columns properly)
26 define('PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE', 1);
27
28 // ----
29
30 // Sort options
31 define('PLUGIN_TRACKER_LIST_SORT_DESC',    3);
32 define('PLUGIN_TRACKER_LIST_SORT_ASC',     4);
33 define('PLUGIN_TRACKER_LIST_SORT_DEFAULT', PLUGIN_TRACKER_LIST_SORT_ASC);
34
35 // Show a form
36 function plugin_tracker_convert()
37 {
38         global $vars;
39
40         if (PKWK_READONLY) return ''; // Show nothing
41
42         $base = $refer = isset($vars['page']) ? $vars['page'] : '';
43         $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
44         $form        = PLUGIN_TRACKER_DEFAULT_FORM;
45
46         $args = func_get_args();
47         $argc = count($args);
48         if ($argc > 2) {
49                 return PLUGIN_TRACKER_USAGE . '<br />';
50         }
51         switch ($argc) {
52         case 2:
53                 $arg = get_fullname($args[1], $base);
54                 if (is_pagename($arg)) $base = $arg;
55                 /*FALLTHROUGH*/
56         case 1:
57                 // Config/form
58                 if ($args[0] != '') {
59                         $arg = explode('/', $args[0], 2);
60                         if ($arg[0] != '' ) $config_name = $arg[0];
61                         if (isset($arg[1])) $form        = $arg[1];
62                 }
63         }
64         unset($args, $argc, $arg);
65
66         $config = new Config('plugin/tracker/' . $config_name);
67         if (! $config->read()) {
68                 return '#tracker: Config \'' . htmlspecialchars($config_name) . '\' not found<br />';
69         }
70         $config->config_name = $config_name;
71
72         $from = $to = $hidden = array();
73         $fields = plugin_tracker_get_fields($base, $refer, $config);
74         foreach (array_keys($fields) as $field) {
75                 $from[] = '[' . $field . ']';
76                 $_to    = $fields[$field]->get_tag();
77                 if (is_a($fields[$field], 'Tracker_field_hidden')) {
78                         $to[]     = '';
79                         $hidden[] = $_to;
80                 } else {
81                         $to[]     = $_to;
82                 }
83                 unset($fields[$field]);
84         }
85
86         $form = $config->page . '/' . $form;
87         $retval = plugin_tracker_get_source($form);
88         if ($retval === FALSE || empty($retval)) {
89                 return '#tracker: Form \'' . make_pagelink($form) . '\' not found or seems empty<br />';
90         }
91
92         $script = get_script_uri();
93         $retval = str_replace($from, $to, convert_html($retval));
94         $hidden = implode('<br />' . "\n", $hidden);
95         return <<<EOD
96 <form enctype="multipart/form-data" action="$script" method="post">
97 <div>
98 $retval
99 $hidden
100 </div>
101 </form>
102 EOD;
103 }
104
105 // Add new page
106 function plugin_tracker_action()
107 {
108         global $post, $vars, $now;
109
110         if (PKWK_READONLY) die_message('PKWK_READONLY prohibits editing');
111
112         $base  = isset($post['_base'])  ? $post['_base']  : '';
113         $refer = isset($post['_refer']) ? $post['_refer'] : $base;
114         if (! is_pagename($refer)) {
115                 return array(
116                         'msg'  => 'Cannot write',
117                         'body' => 'Page name (' . htmlspecialchars($refer) . ') invalid'
118                 );
119         }
120
121         // $page name to add will be decided here
122         $num  = 0;
123         $name = isset($post['_name']) ? $post['_name'] : '';
124         if (isset($post['_page'])) {
125                 $real = $page = $post['_page'];
126         } else {
127                 $real = is_pagename($name) ? $name : ++$num;
128                 $page = get_fullname('./' . $real, $base);
129         }
130         if (! is_pagename($page)) $page = $base;
131         while (is_page($page)) {
132                 $real = ++$num;
133                 $page = $base . '/' . $real;
134         }
135
136         // Loading configuration
137         $config_name = isset($post['_config']) ? $post['_config'] : '';
138         $config = new Config('plugin/tracker/' . $config_name);
139         if (! $config->read()) {
140                 return '<p>config file \'' . htmlspecialchars($config_name) . '\' not found.</p>';
141         }
142         $config->config_name = $config_name;
143
144         // Default
145         $_post = array_merge($post, $_FILES);
146         $_post['_date'] = $now;
147         $_post['_page'] = $page;
148         $_post['_name'] = $name;
149         $_post['_real'] = $real;
150         // $_post['_refer'] = $_post['refer'];
151
152         // Creating an empty page, before attaching files
153         pkwk_touch_file(get_filename($page));
154
155         $from = $to = array();
156         $fields = plugin_tracker_get_fields($page, $refer, $config);
157         foreach (array_keys($fields) as $field) {
158                 $from[] = '[' . $field . ']';
159                 $to[]   = isset($_post[$field]) ? $fields[$field]->format_value($_post[$field]) : '';
160                 unset($fields[$field]);
161         }
162
163         // Load $template
164         $template_page = $config->page . '/page';
165         $template = plugin_tracker_get_source($template_page);
166         if ($template === FALSE || empty($template)) {
167                 return array(
168                         'msg'  => 'Cannot write',
169                         'body' => 'Page template (' . htmlspecialchars($template_page) . ') not exists or seems empty'
170                 );
171         }
172
173         // Repalace every [$field]s to real values in the $template
174         $subject = $subject_e = array();
175         foreach (array_keys($template) as $num) {
176                 if (trim($template[$num]) == '') continue;
177                 $letter = $template[$num]{0};
178                 if ($letter == '|' || $letter == ':') {
179                         // Escape for some TextFormattingRules: <table> and <dr>
180                         $subject_e[$num] = $template[$num];
181                 } else {
182                         $subject[$num]   = $template[$num];
183                 }
184         }
185         foreach (str_replace($from,   $to,   $subject  ) as $num => $line) {
186                 $template[$num] = $line;
187         }
188         // Escape for some TextFormattingRules: <table> and <dr>
189         if ($subject_e) {
190                 $to_e = array();
191                 foreach($to as $value) {
192                         if (strpos($value, '|') !== FALSE) {
193                                 // Escape for some TextFormattingRules: <table> and <dr>
194                                 $to_e[] = str_replace('|', '&#x7c;', $value);
195                         } else{
196                                 $to_e[] = $value;       
197                         }
198                 }
199                 foreach (str_replace($from, $to_e, $subject_e) as $num => $line) {
200                         $template[$num] = $line;
201                 }
202         }
203
204         // Write $template, without touch
205         page_write($page, join('', $template));
206
207         pkwk_headers_sent();
208         header('Location: ' . get_script_uri() . '?' . rawurlencode($page));
209         exit;
210 }
211
212 // Construct $fields (an array of Tracker_field objects)
213 function plugin_tracker_get_fields($base, $refer, & $config)
214 {
215         global $now;
216
217         $fields = array();
218
219         foreach ($config->get('fields') as $field) {
220                 // $field[0]: Field name
221                 // $field[1]: Field name (for display)
222                 // $field[2]: Field type
223                 // $field[3]: Option ("size", "cols", "rows", etc)
224                 // $field[4]: Default value
225                 $class = 'Tracker_field_' . $field[2];
226                 if (! class_exists($class)) {
227                         // Default
228                         $field[2] = 'text';
229                         $class    = 'Tracker_field_' . $field[2];
230                         $field[3] = '20';
231                 }
232                 $fieldname = $field[0];
233                 $fields[$fieldname] = & new $class($field, $base, $refer, $config);
234         }
235
236         foreach (
237                 array(
238                         // Reserved ones
239                         '_date'   => 'text',    // Post date
240                         '_update' => 'date',    // Last modified date
241                         '_past'   => 'past',    // Elapsed time (passage)
242                         '_page'   => 'page',    // Page name
243                         '_name'   => 'text',    // Page name specified by poster
244                         '_real'   => 'real',    // Page name (Real)
245                         '_refer'  => 'page',    // Page name refer from this (Page who has forms)
246                         '_base'   => 'page',
247                         '_submit' => 'submit'
248                 ) as $fieldname => $type)
249         {
250                 if (isset($fields[$fieldname])) continue;
251                 $field = array($fieldname, plugin_tracker_message('btn' . $fieldname), '', '20', '');
252                 $class = 'Tracker_field_' . $type;
253                 $fields[$fieldname] = & new $class($field, $base, $refer, $config);
254         }
255
256         return $fields;
257 }
258
259 // Field classes
260 class Tracker_field
261 {
262         var $name;
263         var $title;
264         var $values;
265         var $default_value;
266         var $base;
267         var $refer;
268         var $config;
269         var $data;
270         var $sort_type = SORT_REGULAR;
271         var $id        = 0;
272
273         function Tracker_field($field, $base, $refer, & $config)
274         {
275                 global $post;
276                 static $id = 0; // Unique id per instance
277
278                 $this->id     = ++$id;
279                 $this->name   = $field[0];
280                 $this->title  = $field[1];
281                 $this->values = explode(',', $field[3]);
282                 $this->default_value = $field[4];
283                 $this->base   = $base;
284                 $this->refer  = $refer;
285                 $this->config = & $config;
286                 $this->data   = isset($post[$this->name]) ? $post[$this->name] : '';
287         }
288
289         // XHTML part inside a form
290         function get_tag()
291         {
292                 return '';
293         }
294
295         function get_style()
296         {
297                 return '%s';
298         }
299
300         function format_value($value)
301         {
302                 return $value;
303         }
304
305         function format_cell($str)
306         {
307                 return $str;
308         }
309
310         // Compare key for Tracker_list->sort()
311         function get_value($value)
312         {
313                 return $value;  // Default: $value itself
314         }
315 }
316
317 class Tracker_field_text extends Tracker_field
318 {
319         var $sort_type = SORT_STRING;
320
321         function get_tag()
322         {
323                 return '<input type="text"' .
324                                 ' name="'  . htmlspecialchars($this->name)          . '"' .
325                                 ' size="'  . htmlspecialchars($this->values[0])     . '"' .
326                                 ' value="' . htmlspecialchars($this->default_value) . '" />';
327         }
328 }
329
330 class Tracker_field_page extends Tracker_field_text
331 {
332         var $sort_type = SORT_STRING;
333
334         function format_value($value)
335         {
336                 $value = strip_bracket($value);
337                 if (is_pagename($value)) $value = '[[' . $value . ']]';
338                 return parent::format_value($value);
339         }
340 }
341
342 class Tracker_field_real extends Tracker_field_text
343 {
344         var $sort_type = SORT_REGULAR;
345 }
346
347 class Tracker_field_title extends Tracker_field_text
348 {
349         var $sort_type = SORT_STRING;
350
351         function format_cell($str)
352         {
353                 make_heading($str);
354                 return $str;
355         }
356 }
357
358 class Tracker_field_textarea extends Tracker_field
359 {
360         var $sort_type = SORT_STRING;
361
362         function get_tag()
363         {
364                 return '<textarea' .
365                         ' name="' . htmlspecialchars($this->name)      . '"' .
366                         ' cols="' . htmlspecialchars($this->values[0]) . '"' .
367                         ' rows="' . htmlspecialchars($this->values[1]) . '">' .
368                                                 htmlspecialchars($this->default_value) .
369                         '</textarea>';
370         }
371
372         function format_cell($str)
373         {
374                 $str = preg_replace('/[\r\n]+/', '', $str);
375                 if (! empty($this->values[2]) && strlen($str) > ($this->values[2] + 3)) {
376                         $str = mb_substr($str, 0, $this->values[2]) . '...';
377                 }
378                 return $str;
379         }
380 }
381
382 class Tracker_field_format extends Tracker_field
383 {
384         var $sort_type = SORT_STRING;
385         var $styles    = array();
386         var $formats   = array();
387
388         function Tracker_field_format($field, $base, $refer, & $config)
389         {
390                 parent::Tracker_field($field, $base, $refer, $config);
391
392                 foreach ($this->config->get($this->name) as $option) {
393                         list($key, $style, $format) =
394                                 array_pad(array_map(create_function('$a', 'return trim($a);'), $option), 3, '');
395                         if ($style  != '') $this->styles[$key]  = $style;
396                         if ($format != '') $this->formats[$key] = $format;
397                 }
398         }
399
400         function get_tag()
401         {
402                 return '<input type="text"' .
403                         ' name="' . htmlspecialchars($this->name)      . '"' .
404                         ' size="' . htmlspecialchars($this->values[0]) . '" />';
405         }
406
407         function get_key($str)
408         {
409                 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
410         }
411
412         function format_value($str)
413         {
414                 if (is_array($str)) {
415                         return join(', ', array_map(array($this, 'format_value'), $str));
416                 }
417
418                 $key = $this->get_key($str);
419                 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
420         }
421
422         function get_style($str)
423         {
424                 $key = $this->get_key($str);
425                 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
426         }
427 }
428
429 class Tracker_field_file extends Tracker_field_format
430 {
431         var $sort_type = SORT_STRING;
432
433         function get_tag()
434         {
435                 return '<input type="file"' .
436                         ' name="' . htmlspecialchars($this->name)      . '"' .
437                         ' size="' . htmlspecialchars($this->values[0]) . '" />';
438         }
439
440         function format_value()
441         {
442                 if (isset($_FILES[$this->name])) {
443
444                         require_once(PLUGIN_DIR . 'attach.inc.php');
445
446                         $result = attach_upload($_FILES[$this->name], $this->base);
447                         if (isset($result['result']) && $result['result']) {
448                                 // Upload success
449                                 return parent::format_value($this->base . '/' . $_FILES[$this->name]['name']);
450                         }
451                 }
452
453                 // Filename not specified, or Fail to upload
454                 return parent::format_value('');
455         }
456 }
457
458 class Tracker_field_radio extends Tracker_field_format
459 {
460         var $sort_type = SORT_NUMERIC;
461         var $_options  = array();
462
463         function get_tag()
464         {
465                 $retval = '';
466
467                 $id = 0;
468                 $s_name = htmlspecialchars($this->name);
469                 foreach ($this->config->get($this->name) as $option) {
470                         ++$id;
471                         $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
472                         $s_option = htmlspecialchars($option[0]);
473                         $checked  = trim($option[0]) == trim($this->default_value) ? ' checked="checked"' : '';
474
475                         $retval .= '<input type="radio"' .
476                                 ' name="'  . $s_name   . '"' .
477                                 ' id="'    . $s_id     . '"' .
478                                 ' value="' . $s_option . '"' .
479                                 $checked . ' />' .
480                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
481                 }
482
483                 return $retval;
484         }
485
486         function get_key($str)
487         {
488                 return $str;
489         }
490
491         function get_value($value)
492         {
493                 $options = & $this->_options;
494                 $name    = $this->name;
495
496                 if (! isset($options[$name])) {
497                         $values = array_map(
498                                 create_function('$array', 'return $array[0];'),
499                                 $this->config->get($name)
500                         );
501                         $options[$name] = array_flip($values);  // array('value0' => 0, 'value1' => 1, ...)
502                 }
503
504                 return isset($options[$name][$value]) ? $options[$name][$value] : $value;
505         }
506 }
507
508 class Tracker_field_select extends Tracker_field_radio
509 {
510         var $sort_type = SORT_NUMERIC;
511
512         function get_tag($empty = FALSE)
513         {
514                 $s_name = htmlspecialchars($this->name);
515                 $s_size = (isset($this->values[0]) && is_numeric($this->values[0])) ?
516                         ' size="' . htmlspecialchars($this->values[0]) . '"' :
517                         '';
518                 $s_multiple = (isset($this->values[1]) && strtolower($this->values[1]) == 'multiple') ?
519                         ' multiple="multiple"' :
520                         '';
521
522                 $retval = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>' . "\n";
523                 if ($empty) $retval .= ' <option value=""></option>' . "\n";
524                 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
525                 foreach ($this->config->get($this->name) as $option) {
526                         $s_option = htmlspecialchars($option[0]);
527                         $selected = isset($defaults[trim($option[0])]) ? ' selected="selected"' : '';
528                         $retval  .= ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>' . "\n";
529                 }
530                 $retval .= '</select>';
531
532                 return $retval;
533         }
534 }
535
536 class Tracker_field_checkbox extends Tracker_field_radio
537 {
538         var $sort_type = SORT_NUMERIC;
539
540         function get_tag()
541         {
542                 $retval = '';
543
544                 $id = 0;
545                 $s_name   = htmlspecialchars($this->name);
546                 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
547                 foreach ($this->config->get($this->name) as $option)
548                 {
549                         ++$id;
550                         $s_id     = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
551                         $s_option = htmlspecialchars($option[0]);
552                         $checked  = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
553
554                         $retval .= '<input type="checkbox"' .
555                                 ' name="' . $s_name . '[]"' .
556                                 ' id="' . $s_id . '"' .
557                                 ' value="' . $s_option . '"' .
558                                 $checked . ' />' .
559                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
560                 }
561
562                 return $retval;
563         }
564 }
565
566 class Tracker_field_hidden extends Tracker_field_radio
567 {
568         var $sort_type = SORT_NUMERIC;
569
570         function get_tag()
571         {
572                 return '<input type="hidden"' .
573                         ' name="'  . htmlspecialchars($this->name)          . '"' .
574                         ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
575         }
576 }
577
578 class Tracker_field_submit extends Tracker_field
579 {
580         function get_tag()
581         {
582                 $s_title  = htmlspecialchars($this->title);
583                 $s_base   = htmlspecialchars($this->base);
584                 $s_refer  = htmlspecialchars($this->refer);
585                 $s_config = htmlspecialchars($this->config->config_name);
586
587                 return <<<EOD
588 <input type="submit" value="$s_title" />
589 <input type="hidden" name="plugin"  value="tracker" />
590 <input type="hidden" name="_refer"  value="$s_refer" />
591 <input type="hidden" name="_base"   value="$s_base" />
592 <input type="hidden" name="_config" value="$s_config" />
593 EOD;
594         }
595 }
596
597 class Tracker_field_date extends Tracker_field
598 {
599         var $sort_type = SORT_NUMERIC;
600
601         function format_cell($timestamp)
602         {
603                 return format_date($timestamp);
604         }
605 }
606
607 class Tracker_field_past extends Tracker_field
608 {
609         var $sort_type = SORT_NUMERIC;
610
611         function format_cell($timestamp)
612         {
613                 return get_passage($timestamp, FALSE);
614         }
615
616         function get_value($value)
617         {
618                 return UTIME - $value;
619         }
620 }
621
622 ///////////////////////////////////////////////////////////////////////////
623 // tracker_list plugin
624
625 function plugin_tracker_list_convert()
626 {
627         global $vars;
628
629         $base = $refer = isset($vars['page']) ? $vars['page'] : '';
630         $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
631         $list        = PLUGIN_TRACKER_DEFAULT_LIST;
632         $limit       = PLUGIN_TRACKER_DEFAULT_LIMIT;
633         $order       = PLUGIN_TRACKER_DEFAULT_ORDER;
634
635         $args = func_get_args();
636         $argc = count($args);
637         if ($argc > 4) {
638                 return PLUGIN_TRACKER_LIST_USAGE . '<br />';
639         }
640         switch ($argc) {
641         case 4: $limit = $args[3];      /*FALLTHROUGH*/
642         case 3: $order = $args[2];      /*FALLTHROUGH*/
643         case 2:
644                 $arg = get_fullname($args[1], $base);
645                 if (is_pagename($arg)) $base = $arg;
646                 /*FALLTHROUGH*/
647         case 1:
648                 // Config/list
649                 if ($args[0] != '') {
650                         $arg = explode('/', $args[0], 2);
651                         if ($arg[0] != '' ) $config_name = $arg[0];
652                         if (isset($arg[1])) $list        = $arg[1];
653                 }
654         }
655         unset($args, $argc, $arg);
656
657         return plugin_tracker_list_render($base, $refer, $config_name, $list, $order, $limit);
658 }
659
660 function plugin_tracker_list_action()
661 {
662         global $get, $vars;
663
664         $base   = isset($get['base'])   ? $get['base']   : '';          // Base directory to load
665         $refer  = isset($get['refer'])  ? $get['refer']  : $base;       // Where to #tracker_list
666         if ($base == '') $base = $refer;        // Compat before 1.4.8
667
668         $config = isset($get['config']) ? $get['config'] : '';
669         $list   = isset($get['list'])   ? $get['list']   : 'list';
670         $order  = isset($vars['order']) ? $vars['order'] : PLUGIN_TRACKER_DEFAULT_ORDER;
671         $limit  = isset($vars['limit']) ? $vars['limit'] : 0;
672
673         $s_refer = make_pagelink(trim($refer));
674         return array(
675                 'msg' => plugin_tracker_message('msg_list'),
676                 'body'=> str_replace('$1', $s_refer, plugin_tracker_message('msg_back')) .
677                         plugin_tracker_list_render($base, $refer, $config, $list, $order, $limit)
678         );
679 }
680
681 function plugin_tracker_list_render($base, $refer, $config_name, $list, $order_commands = '', $limit = 0)
682 {
683         $base  = trim($base);
684         if ($base == '') return '#tracker_list: Base not specified' . '<br />';
685
686         // TODO: is_page
687         $refer = trim($refer);
688         if (! is_page($refer)) {
689                 return '#tracker_list: Refer page not found: ' . htmlspecialchars($refer) . '<br />';
690         }
691
692         $config_name = trim($config_name);
693         if ($config_name == '') $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
694
695         $list  = trim($list);
696         if (! is_numeric($limit)) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
697         $limit = intval($limit);
698
699         $config = new Config('plugin/tracker/' . $config_name);
700         if (! $config->read()) {
701                 return '#tracker_list: Config not found: ' . htmlspecialchars($config_name) . '<br />';
702         }
703         $config->config_name = $config_name;
704         if (! is_page($config->page . '/' . $list)) {
705                 return '#tracker_list: List not found: ' . make_pagelink($config->page . '/' . $list) . '<br />';
706         }
707
708         $list = & new Tracker_list($base, $refer, $config, $list);
709         if ($list->setOrder($order_commands) === FALSE) {
710                 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
711         }
712         $result = $list->toString($limit);
713         if ($result === FALSE) {
714                 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
715         }
716         unset($list);
717
718         return convert_html($result);
719 }
720
721 // Listing class
722 class Tracker_list
723 {
724         var $base;
725         var $refer;
726         var $config;
727         var $list;
728         var $fields;
729         var $pattern;
730         var $pattern_fields;
731
732         var $rows   = array();
733         var $orders = array();
734         var $error  = '';       // Error message
735
736         // add()
737         var $_added = array();
738
739         // toString()
740         var $_itmes;
741         var $_the_first_character_of_the_line;
742
743         function Tracker_list($base, $refer, & $config, $list)
744         {
745                 $this->base     = $base;
746                 $this->refer    = $refer;
747                 $this->config   = & $config;
748                 $this->list     = $list;
749                 $this->fields   = plugin_tracker_get_fields($base, $refer, $config);
750         }
751         
752         // Load pages
753         function _load()
754         {
755                 $pattern     = $this->base . '/';
756                 $pattern_len = strlen($pattern);
757
758                 foreach (get_existpages() as $_page) {
759                         if (strpos($_page, $pattern) === 0) {
760                                 $name = substr($_page, $pattern_len);
761                                 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, $name)) continue;
762
763                                 // Adding $this->rows
764                                 if ($this->add($_page, $name) === FALSE) return FALSE;
765                         }
766                 }
767                 if (empty($this->rows)) {
768                         $this->error = 'Pages not found under: ' . $pattern;
769                         return FALSE;
770                 }
771
772                 return TRUE;
773         }
774
775         // add(): Generate regexes
776         function _generate_regex()
777         {
778                 $config_page = $this->config->page . '/page';
779                 $fields      = $this->fields;
780
781                 $pattern        = array();
782                 $pattern_fields = array();
783
784                 $source = plugin_tracker_get_source($config_page, TRUE);
785                 if ($source === FALSE || empty($source)) {
786                         $this->error = 'Page not found or seems empty: ' . $config_page;
787                         return FALSE;
788                 }
789
790                 // Block-plugins to pseudo fields (#convert => [_block_convert])
791                 $source = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $source);
792
793                 // Now, $source = array('*someting*', 'fieldname', '*someting*', 'fieldname', ...)
794                 $source = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($source, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
795
796                 // NOTE: if the page has garbages between fields, it will fail to be load
797                 while (! empty($source)) {
798                         // Just ignore these _fixed_ data
799                         $pattern[] = preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($source)) . '\\s*)');
800                         if (empty($source)) continue;
801
802                         $fieldname = array_shift($source);
803                         if (isset($fields[$fieldname])) {
804                                 $pattern[] = '(.*?)';   // Just capture it
805                                 $pattern_fields[] = $fieldname; // Capture it as this $filedname
806                         } else {
807                                 $pattern[] = '.*?';     // Just ignore pseudo fields etc
808                         }
809                 }
810                 $this->pattern        = '/' . implode('', $pattern) . '/sS';
811                 $this->pattern_fields = $pattern_fields;
812
813                 return TRUE;
814         }
815
816         function add($page, $name, $rescan = FALSE)
817         {
818                 if (isset($this->_added[$page])) return TRUE;
819                 $this->_added[$page] = TRUE;
820
821                 $source = plugin_tracker_get_source($page, TRUE);
822                 if ($source === FALSE) $source = '';
823
824                 // Compat: 'move to [[page]]' (bugtrack plugin)
825                 $matches = array();
826                 if (! $rescan && ! empty($source) && preg_match('/move\sto\s(.+)/', $source, $matches)) {
827                         $to_page = strip_bracket(trim($matches[1]));
828                         if (is_page($to_page)) {
829                                 unset($source, $matches);       // Release
830                                 return $this->add($to_page, $name, TRUE);       // Recurse(Rescan) once
831                         }
832                 }
833
834                 // Default column
835                 $filetime = get_filetime($page);
836                 $row = array(
837                         // column => default data of the cell
838                         '_page'   => '[[' . $page . ']]',
839                         '_real'   => $name,
840                         '_update' => $filetime,
841                         '_past'   => $filetime,
842                         '_match'  => FALSE,
843                 );
844
845                 // Load / Redefine cell
846                 $matches = array();
847                 $row['_match'] = preg_match($this->pattern, $source, $matches);
848                 unset($source);
849                 if ($row['_match']) {
850                         array_shift($matches);  // $matches[0] = all of the captured string
851                         foreach ($this->pattern_fields as $key => $fieldname) {
852                                 $row[$fieldname] = trim($matches[$key]);
853                                 unset($matches[$key]);
854                         }
855                 }
856
857                 $this->rows[$name] = $row;
858                 return TRUE;
859         }
860
861         // setOrder()
862         function _order_commands2orders($order_commands = '')
863         {
864                 $order_commands = trim($order_commands);
865                 if ($order_commands == '') return array();
866
867                 $orders = array();
868                 $fields = $this->fields;
869
870                 $i = 0;
871                 foreach (explode(';', $order_commands) as $command) {
872                         $command = trim($command);
873                         if ($command == '') continue;
874                         $arg = explode(':', $command, 2);
875                         $fieldname = isset($arg[0]) ? trim($arg[0]) : '';
876                         $order     = isset($arg[1]) ? trim($arg[1]) : '';
877
878                         if (! isset($fields[$fieldname])) {
879                                 $this->error =  'No such field: ' . $fieldname;
880                                 return FALSE;
881                         }
882
883                         $_order = $this->_sortkey_string2define($order);
884                         if ($_order === FALSE) {
885                                 $this->error =  'Invalid sortkey: ' . $order;
886                                 return FALSE;
887                         } else if (isset($orders[$fieldname])) {
888                                 $this->error =  'Sortkey already set: ' . $fieldname;
889                                 return FALSE;
890                         }
891
892                         if (PLUGIN_TRACKER_LIST_SORT_LIMIT <= $i) continue;     // Ignore
893                         ++$i;
894
895                         $orders[$fieldname] = $_order;
896                 }
897
898                 return $orders;
899         }
900
901         // Set commands for sort()
902         function setOrder($order_commands = '')
903         {
904                 $orders = $this->_order_commands2orders($order_commands);
905                 if ($orders === FALSE) {
906                         $this->orders = array();
907                         return FALSE;
908                 }
909                 $this->orders = $orders;
910                 return TRUE;
911         }
912
913         // Sort $this->rows by $this->orders
914         function _sort()
915         {
916                 $orders = $this->orders;
917                 $fields = $this->fields;
918
919                 $params = array();      // Arguments for array_multisort()
920                 foreach ($orders as $fieldname => $order) {
921                         // One column set (one-dimensional array(), sort type, and order-by)
922
923                         if ($order == PLUGIN_TRACKER_LIST_SORT_ASC) {
924                                 $order = SORT_ASC;
925                         } else if ($order == PLUGIN_TRACKER_LIST_SORT_DESC) {
926                                 $order = SORT_DESC;
927                         } else {
928                                 $this->error = 'Invalid sort order for array_multisort()';
929                                 return FALSE;
930                         }
931
932                         $array = array();
933                         foreach ($this->rows as $row) {
934                                 $array[] = isset($row[$fieldname]) ?
935                                         $fields[$fieldname]->get_value($row[$fieldname]) :
936                                         '';
937                         }
938                         $params[] = $array;
939                         $params[] = $fields[$fieldname]->sort_type;
940                         $params[] = $order;
941                 }
942                 $params[] = & $this->rows;
943
944                 call_user_func_array('array_multisort', $params);
945
946                 return TRUE; 
947         }
948
949         // toString(): Sort key: Define to string (internal var => string)
950         function _sortkey_define2string($sortkey)
951         {
952                 switch ($sortkey) {
953                 case PLUGIN_TRACKER_LIST_SORT_ASC:  $sortkey = 'SORT_ASC';  break;
954                 case PLUGIN_TRACKER_LIST_SORT_DESC: $sortkey = 'SORT_DESC'; break;
955                 default:
956                         $this->error =  'No such define: ' . $sortkey;
957                         $sortkey = FALSE;
958                 }
959                 return $sortkey;
960         }
961
962         // toString(): Sort key: String to define (string => internal var)
963         function _sortkey_string2define($sortkey)
964         {
965                 switch (strtoupper(trim($sortkey))) {
966                 case '':          $sortkey = PLUGIN_TRACKER_LIST_SORT_DEFAULT; break;
967
968                 case SORT_ASC:    /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
969                 case 'SORT_ASC':  /*FALLTHROUGH*/
970                 case 'ASC':       $sortkey = PLUGIN_TRACKER_LIST_SORT_ASC; break;
971
972                 case SORT_DESC:   /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
973                 case 'SORT_DESC': /*FALLTHROUGH*/
974                 case 'DESC':      $sortkey = PLUGIN_TRACKER_LIST_SORT_DESC; break;
975
976                 default:
977                         $this->error =  'Invalid sort key: ' . $sortkey;
978                         $sortkey = FALSE;
979                 }
980                 return $sortkey;
981         }
982
983         // toString(): Escape special characters not to break Wiki syntax
984         function _escape($syntax_hint = '|', $string)
985         {
986                 $from = array("\n",   "\r"  );
987                 $to   = array('&br;', '&br;');
988                 if ($syntax_hint == '|' || $syntax_hint == ':') {
989                         // <table> or <dl> Wiki syntax: Excape '|'
990                         $from[] = '|';
991                         $to[]   = '&#x7c;';
992                 } else if ($syntax_hint == ',') {
993                         // <table> by comma
994                         $from[] = ',';
995                         $to[]   = '&#x2c;';
996                 }
997                 return str_replace($from, $to, $string);
998         }
999
1000         // toString(): Called within preg_replace_callback()
1001         function _replace_title($matches = array())
1002         {
1003                 static $script;
1004
1005                 $fields = $this->fields;
1006                 $orders = $this->orders;
1007                 $base   = $this->base;
1008                 $refer  = $this->refer;
1009                 $config_name = $this->config->config_name;
1010                 $list   = $this->list;
1011
1012                 $fieldname = isset($matches[1]) ? $matches[1] : '';
1013                 if (! isset($fields[$fieldname])) {
1014                         // Invalid $fieldname or user's own string or something. Nothing to do
1015                         return isset($matches[0]) ? $matches[0] : '';
1016                 }
1017                 if ($fieldname == '_name' || $fieldname == '_page') $fieldname = '_real';
1018
1019                 $arrow  = '';
1020                 if (isset($orders[$fieldname])) {
1021                         // Sorted
1022                         $order_keys = array_keys($orders);
1023
1024                         // Toggle
1025                         $b_end   = ($fieldname == (isset($order_keys[0]) ? $order_keys[0] : ''));
1026                         $b_order = ($orders[$fieldname] === PLUGIN_TRACKER_LIST_SORT_ASC);
1027                         $order   = ($b_end xor $b_order)
1028                                 ? PLUGIN_TRACKER_LIST_SORT_ASC
1029                                 : PLUGIN_TRACKER_LIST_SORT_DESC;
1030
1031                         // Arrow decoration
1032                         $index   = array_flip($order_keys);
1033                         $pos     = 1 + $index[$fieldname];
1034                         $arrow   = '&br;' . ($b_order ? '&uarr;' : '&darr;') . '(' . $pos . ')';
1035
1036                         unset($order_keys, $index);
1037                         unset($orders[$fieldname]);     // $fieldname will be added to the first
1038                 } else {
1039                         // Not sorted yet, but
1040                         $order = PLUGIN_TRACKER_LIST_SORT_ASC;  // Default
1041                 }
1042
1043                 // $fieldname become the first, if you click this link
1044                 $_order = array($fieldname . ':' . $this->_sortkey_define2string($order));
1045                 foreach ($orders as $key => $value) {
1046                         $_order[] = $key . ':' . $this->_sortkey_define2string($value);
1047                 }
1048
1049                 if (! isset($script)) $script = get_script_uri();
1050                 $r_refer  = ($refer != $base) ?
1051                         '&refer=' . rawurlencode($refer) : '';
1052                 $r_config = ($config_name != PLUGIN_TRACKER_DEFAULT_CONFIG) ?
1053                         '&config=' . rawurlencode($config_name) : '';
1054                 $r_list   = ($list != PLUGIN_TRACKER_DEFAULT_LIST) ?
1055                         '&list=' . rawurlencode($list) : '';
1056                 $r_order  = ! empty($_order) ?
1057                         '&order=' . rawurlencode(join(';', $_order)) : '';
1058
1059                 return
1060                          '[[' .
1061                                 $fields[$fieldname]->title . $arrow .
1062                         '>' .
1063                                 $script . '?plugin=tracker_list' .
1064                                 '&base=' . rawurlencode($base) .
1065                                 $r_refer . $r_config . $r_list . $r_order  .
1066                         ']]';
1067         }
1068
1069         // toString(): Called within preg_replace_callback()
1070         function _replace_item($matches = array())
1071         {       
1072                 $fields = $this->fields;
1073                 $items  = $this->_items;
1074                 $tfc    = $this->_the_first_character_of_the_line ;
1075
1076                 $params    = isset($matches[1]) ? explode(',', $matches[1]) : array();
1077                 $fieldname = isset($params[0])  ? $params[0] : '';
1078                 $stylename = isset($params[1])  ? $params[1] : $fieldname;
1079
1080                 if ($fieldname == '') return '';        // Invalid
1081
1082                 if (! isset($items[$fieldname])) {
1083                         // Maybe load miss of the page
1084                         if (isset($fields[$fieldname])) {
1085                                 $str = '[page_err]';    // Exactlly
1086                         } else {
1087                                 $str = isset($matches[0]) ? $matches[0] : '';   // Nothing to do
1088                         }
1089                 } else {
1090                         $str = $items[$fieldname];
1091                         if (isset($fields[$fieldname])) {
1092                                 $str    = $fields[$fieldname]->format_cell($str);
1093                         }
1094                         if (isset($fields[$stylename]) && isset($items[$stylename])) {
1095                                 $_style = $fields[$stylename]->get_style($items[$stylename]);
1096                                 $str    = sprintf($_style, $str);
1097                         }
1098                 }
1099
1100                 return $this->_escape($tfc, $str);
1101         }
1102
1103         // Output a part of Wiki text
1104         function toString($limit = 0)
1105         {
1106                 $source = array();
1107                 $list   = $this->config->page . '/' . $this->list;
1108                 $regex  = '/\[([^\[\]]+)\]/';
1109
1110                 // Loading template: Roughly checking listed fields
1111                 $matches        = array();
1112                 $used_fieldname = array('_real' => TRUE);
1113                 $template       = plugin_tracker_get_source($list, TRUE);
1114                 if ($template === FALSE || empty($template)) {
1115                         $this->error = 'Page not found or seems empty: ' . $template;
1116                         return FALSE;
1117                 }
1118                 preg_match_all($regex, $template, $matches);
1119                 unset($matches[0]);
1120                 foreach ($matches[1] as $match) {
1121                         $params = explode(',', $match);
1122                         if (isset($params[0]) && ! isset($used_fieldname[$params[0]])) {
1123                                 $used_fieldname[$params[0]] = TRUE;
1124                         }
1125                 }
1126                 unset($matches[1]);
1127                 foreach (array_keys($this->orders) as $fieldname) {
1128                         if (! isset($used_fieldname[$fieldname])) {
1129                                 $used_fieldname[$fieldname] = TRUE;
1130                         }
1131                 }
1132
1133                 // Remove unused $this->fields
1134                 $fields = $this->fields;
1135                 $new_filds = array();
1136                 foreach (array_keys($fields) as $fieldname) {
1137                         if (isset($used_fieldname[$fieldname])) {
1138                                 $new_filds[$fieldname] = & $fields[$fieldname];
1139                         }
1140                 }
1141                 $this->fields = $new_filds;
1142
1143                 // Generate regex for $this->fields
1144                 if ($this->_generate_regex() === FALSE) return FALSE;
1145
1146                 // Load $this->rows
1147                 if ($this->_load() === FALSE) return FALSE;
1148
1149                 // Sort $this->rows
1150                 if ($this->_sort() === FALSE) return FALSE;
1151                 $rows   = $this->rows;
1152
1153                 // toString()
1154                 $count = count($this->rows);
1155                 $limit = intval($limit);
1156                 if ($limit != 0) $limit = max(1, $limit);
1157                 if ($limit != 0 && $count > $limit) {
1158                         $source[] = str_replace(
1159                                 array('$1',   '$2'  ),
1160                                 array($count, $limit),
1161                                 plugin_tracker_message('msg_limit')
1162                         ) . "\n";
1163                         $rows  = array_slice($this->rows, 0, $limit);
1164                 }
1165
1166                 // Loading template
1167                 $header = $body = array();
1168                 foreach (plugin_tracker_get_source($list) as $line) {
1169                         if (preg_match('/^\|(.+)\|[hfc]$/i', $line)) {
1170                                 // TODO: Why c and f  here
1171                                 $header[] = $line;      // Table header, footer, and decoration
1172                         } else {
1173                                 $body[]   = $line;      // The others
1174                         }
1175                 }
1176
1177                 foreach($header as $line) {
1178                         $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1179                 }
1180                 foreach ($rows as $row) {
1181                         if (! PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE && ! $row['_match']) continue;
1182                         $this->_items = $row;
1183                         foreach ($body as $line) {
1184                                 if (ltrim($line) != '') {
1185                                         $this->_the_first_character_of_the_line = $line[0];
1186                                         $line = preg_replace_callback($regex, array(& $this, '_replace_item'), $line);
1187                                 }
1188                                 $source[] = $line;
1189                         }
1190                 }
1191
1192                 return implode('', $source);
1193         }
1194 }
1195
1196 function plugin_tracker_get_source($page, $join = FALSE)
1197 {
1198         $source = get_source($page, TRUE, $join);
1199         if ($source === FALSE) return FALSE;
1200
1201         // Remove fixed-heading anchors
1202         $source = preg_replace('/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', '$1$2', $source);
1203
1204         // Remove #freeze-es
1205         return preg_replace('/^#freeze\s*$/im', '', $source);
1206 }
1207
1208 function plugin_tracker_message($key)
1209 {
1210         global $_tracker_messages;
1211         return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';
1212 }
1213
1214 ?>