OSDN Git Service

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