OSDN Git Service

Cleanup/Simplify/Todo
[pukiwiki/pukiwiki.git] / plugin / tracker.inc.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.46 2007/09/08 16:37:16 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][[,base][,field:sort[;field:sort ...][,limit]]])');
11
12 define('PLUGIN_TRACKER_DEFAULT_CONFIG', 'default');
13 define('PLUGIN_TRACKER_DEFAULT_FORM',   'form');
14
15 // #tracker_list: Excluding pattern
16 define('PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN','#^SubMenu$|/#');  // 'SubMenu' and using '/'
17 //define('PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN','#(?!)#');               // Nothing excluded
18
19 // #tracker_list: Show error rows (can't capture columns properly)
20 define('PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE', 1);
21
22
23 // Show a form
24 function plugin_tracker_convert()
25 {
26         global $vars;
27
28         if (PKWK_READONLY) return ''; // Show nothing
29
30         $base = $refer = isset($vars['page']) ? $vars['page'] : '';
31         $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
32         $form        = PLUGIN_TRACKER_DEFAULT_FORM;
33
34         $args = func_get_args();
35         $argc = count($args);
36         if ($argc > 2) {
37                 return PLUGIN_TRACKER_USAGE . '<br />';
38         } else {
39                 if ($argc > 1) {
40                         // Base page name
41                         $arg = get_fullname($args[1], $base);
42                         if (is_pagename($arg)) $base = $arg;
43                 }
44                 if ($argc > 0 && $args[0] != '') {
45                         // Configuration name AND form name
46                         $arg = explode('/', $args[0], 2);
47                         if ($arg[0] != '' ) $config_name = $arg[0];
48                         if (isset($arg[1])) $form        = $arg[1];
49                 }
50         }
51
52         $config = new Config('plugin/tracker/' . $config_name);
53         if (! $config->read()) {
54                 return '#tracker: Config \'' . htmlspecialchars($config_name) . '\' not found<br />';
55         }
56         $config->config_name = $config_name;
57         $form = $config->page . '/' . $form;
58         if (! is_page($form)) {
59                 return '#tracker: Form \'' . make_pagelink($form) . '\' not found<br />';
60         }
61
62         $from = $to = $hidden = array();
63         $fields = plugin_tracker_get_fields($base, $refer, $config);
64         foreach (array_keys($fields) as $field) {
65                 $from[] = '[' . $field . ']';
66                 $_to    = $fields[$field]->get_tag();
67                 if (is_a($fields[$field], 'Tracker_field_hidden')) {
68                         $to[]     = '';
69                         $hidden[] = $_to;
70                 } else {
71                         $to[]     = $_to;
72                 }
73                 $fields[$field]->dispose();
74                 unset($fields[$field]);
75         }
76
77         $script = get_script_uri();
78         $retval = str_replace($from, $to, convert_html(plugin_tracker_get_source($form)));
79         $hidden = implode('<br />' . "\n", $hidden);
80         return <<<EOD
81 <form enctype="multipart/form-data" action="$script" method="post">
82 <div>
83 $retval
84 $hidden
85 </div>
86 </form>
87 EOD;
88 }
89
90 // Add new page
91 function plugin_tracker_action()
92 {
93         global $post, $vars, $now;
94
95         if (PKWK_READONLY) die_message('PKWK_READONLY prohibits editing');
96
97         $base  = isset($post['_base'])  ? $post['_base']  : '';
98         $refer = isset($post['_refer']) ? $post['_refer'] : $base;
99         if (! is_pagename($refer)) {
100                 return array(
101                         'msg'  => 'Cannot write',
102                         'body' => 'Page name (' . htmlspecialchars($refer) . ') invalid'
103                 );
104         }
105
106         // $page name to add will be decided here
107         $num  = 0;
108         $name = isset($post['_name']) ? $post['_name'] : '';
109         if (isset($post['_page'])) {
110                 $real = $page = $post['_page'];
111         } else {
112                 $real = is_pagename($name) ? $name : ++$num;
113                 $page = get_fullname('./' . $real, $base);
114         }
115         if (! is_pagename($page)) $page = $base;
116         while (is_page($page)) {
117                 $real = ++$num;
118                 $page = $base . '/' . $real;
119         }
120
121         // Loading configuration
122         $config_name = isset($post['_config']) ? $post['_config'] : '';
123         $config = new Config('plugin/tracker/' . $config_name);
124         if (! $config->read()) {
125                 return '<p>config file \'' . htmlspecialchars($config_name) . '\' not found.</p>';
126         }
127         $config->config_name = $config_name;
128         $template_page = $config->page . '/page';
129         if (! is_page($template_page)) {
130                 return array(
131                         'msg'  => 'Cannot write',
132                         'body' => 'Page template (' . htmlspecialchars($template_page) . ') not exists'
133                 );
134         }
135
136         // Default
137         $_post = array_merge($post, $_FILES);
138         $_post['_date'] = $now;
139         $_post['_page'] = $page;
140         $_post['_name'] = $name;
141         $_post['_real'] = $real;
142         // $_post['_refer'] = $_post['refer'];
143
144         // Creating an empty page, before attaching files
145         pkwk_touch_file(get_filename($page));
146
147         $from = $to = array();
148         $fields = plugin_tracker_get_fields($page, $refer, $config);
149         foreach (array_keys($fields) as $field) {
150                 $from[] = '[' . $field . ']';
151                 $to[]   = isset($_post[$field]) ? $fields[$field]->format_value($_post[$field]) : '';
152                 $fields[$field]->dispose();
153                 unset($fields[$field]);
154         }
155
156         // Load $template
157         $template = plugin_tracker_get_source($template_page);
158
159         // Repalace every [$field]s to real values in the $template
160         $subject = $subject_e = array();
161         foreach (array_keys($template) as $num) {
162                 if (trim($template[$num]) == '') continue;
163                 $letter = $template[$num]{0};
164                 if ($letter == '|' || $letter == ':') {
165                         // Escape for some TextFormattingRules: <table> and <dr>
166                         $subject_e[$num] = $template[$num];
167                 } else {
168                         $subject[$num]   = $template[$num];
169                 }
170         }
171         foreach (str_replace($from,   $to,   $subject  ) as $num => $line) {
172                 $template[$num] = $line;
173         }
174         // Escape for some TextFormattingRules: <table> and <dr>
175         if ($subject_e) {
176                 $to_e = array();
177                 foreach($to as $value) {
178                         if (strpos($value, '|') !== FALSE) {
179                                 // Escape for some TextFormattingRules: <table> and <dr>
180                                 $to_e[] = str_replace('|', '&#x7c;', $value);
181                         } else{
182                                 $to_e[] = $value;       
183                         }
184                 }
185                 foreach (str_replace($from, $to_e, $subject_e) as $num => $line) {
186                         $template[$num] = $line;
187                 }
188         }
189
190         // Write $template, without touch
191         page_write($page, join('', $template));
192
193         pkwk_headers_sent();
194         header('Location: ' . get_script_uri() . '?' . rawurlencode($page));
195         exit;
196 }
197
198 // Construct $fields (an array of Tracker_field objects)
199 function plugin_tracker_get_fields($base, $refer, & $config)
200 {
201         global $now;
202
203         $fields = array();
204
205         foreach ($config->get('fields') as $field) {
206                 // $field[0]: Field name
207                 // $field[1]: Field name (for display)
208                 // $field[2]: Field type
209                 // $field[3]: Option ("size", "cols", "rows", etc)
210                 // $field[4]: Default value
211                 $class = 'Tracker_field_' . $field[2];
212                 if (! class_exists($class)) {
213                         // Default
214                         $field[2] = 'text';
215                         $class    = 'Tracker_field_' . $field[2];
216                         $field[3] = '20';
217                 }
218                 $fieldname = $field[0];
219                 $fields[$fieldname] = & new $class($field, $base, $refer, $config);
220         }
221
222         foreach (
223                 array(
224                         // Reserved ones
225                         '_date'   => 'text',    // Post date
226                         '_update' => 'date',    // Last modified date
227                         '_past'   => 'past',    // Elapsed time (passage)
228                         '_page'   => 'page',    // Page name
229                         '_name'   => 'text',    // Page name specified by poster
230                         '_real'   => 'real',    // Page name (Real)
231                         '_refer'  => 'page',    // Page name refer from this (Page who has forms)
232                         '_base'   => 'page',
233                         '_submit' => 'submit'
234                 ) as $fieldname => $type)
235         {
236                 if (isset($fields[$fieldname])) continue;
237                 $field = array($fieldname, plugin_tracker_message('btn' . $fieldname), '', '20', '');
238                 $class = 'Tracker_field_' . $type;
239                 $fields[$fieldname] = & new $class($field, $base, $refer, $config);
240         }
241
242         return $fields;
243 }
244
245 // Field classes
246 // TODO: Not to use static variables (except $id)
247 class Tracker_field
248 {
249         var $name;
250         var $title;
251         var $values;
252         var $default_value;
253         var $page;
254         var $refer;
255         var $config;
256         var $data;
257         var $sort_type = SORT_REGULAR;
258         var $id        = 0;
259
260         function Tracker_field($field, $page, $refer, & $config)
261         {
262                 global $post;
263                 static $id = 0; // Unique id per instance
264
265                 $this->id     = ++$id;
266                 $this->name   = $field[0];
267                 $this->title  = $field[1];
268                 $this->values = explode(',', $field[3]);
269                 $this->default_value = $field[4];
270                 $this->page   = $page;
271                 $this->refer  = $refer;
272                 $this->config = & $config;
273                 $this->data   = isset($post[$this->name]) ? $post[$this->name] : '';
274         }
275
276         // XHTML part inside a form
277         function get_tag()
278         {
279                 return '';
280         }
281
282         function get_style()
283         {
284                 return '%s';
285         }
286
287         function format_value($value)
288         {
289                 return $value;
290         }
291
292         function format_cell($str)
293         {
294                 return $str;
295         }
296
297         // Compare key for Tracker_list->sort()
298         function get_value($value)
299         {
300                 return $value;  // Default: $value itself
301         }
302
303         // Release the resources
304         function dispose()
305         {
306         }
307 }
308
309 class Tracker_field_text extends Tracker_field
310 {
311         var $sort_type = SORT_STRING;
312
313         function get_tag()
314         {
315                 return '<input type="text"' .
316                                 ' name="'  . htmlspecialchars($this->name)          . '"' .
317                                 ' size="'  . htmlspecialchars($this->values[0])     . '"' .
318                                 ' value="' . htmlspecialchars($this->default_value) . '" />';
319         }
320 }
321
322 class Tracker_field_page extends Tracker_field_text
323 {
324         var $sort_type = SORT_STRING;
325
326         function format_value($value)
327         {
328                 $value = strip_bracket($value);
329                 if (is_pagename($value)) $value = '[[' . $value . ']]';
330                 return parent::format_value($value);
331         }
332 }
333
334 class Tracker_field_real extends Tracker_field_text
335 {
336         var $sort_type = SORT_REGULAR;
337 }
338
339 class Tracker_field_title extends Tracker_field_text
340 {
341         var $sort_type = SORT_STRING;
342
343         function format_cell($str)
344         {
345                 make_heading($str);
346                 return $str;
347         }
348 }
349
350 class Tracker_field_textarea extends Tracker_field
351 {
352         var $sort_type = SORT_STRING;
353
354         function get_tag()
355         {
356                 return '<textarea' .
357                         ' name="' . htmlspecialchars($this->name)      . '"' .
358                         ' cols="' . htmlspecialchars($this->values[0]) . '"' .
359                         ' rows="' . htmlspecialchars($this->values[1]) . '">' .
360                                                 htmlspecialchars($this->default_value) .
361                         '</textarea>';
362         }
363
364         function format_cell($str)
365         {
366                 $str = preg_replace('/[\r\n]+/', '', $str);
367                 if (! empty($this->values[2]) && strlen($str) > ($this->values[2] + 3)) {
368                         $str = mb_substr($str, 0, $this->values[2]) . '...';
369                 }
370                 return $str;
371         }
372 }
373
374 class Tracker_field_format extends Tracker_field
375 {
376         var $sort_type = SORT_STRING;
377         var $styles    = array();
378         var $formats   = array();
379
380         function Tracker_field_format($field, $page, $refer, & $config)
381         {
382                 parent::Tracker_field($field, $page, $refer, $config);
383
384                 foreach ($this->config->get($this->name) as $option) {
385                         list($key, $style, $format) =
386                                 array_pad(array_map(create_function('$a', 'return trim($a);'), $option), 3, '');
387                         if ($style  != '') $this->styles[$key]  = $style;
388                         if ($format != '') $this->formats[$key] = $format;
389                 }
390         }
391
392         function get_tag()
393         {
394                 return '<input type="text"' .
395                         ' name="' . htmlspecialchars($this->name)      . '"' .
396                         ' size="' . htmlspecialchars($this->values[0]) . '" />';
397         }
398
399         function get_key($str)
400         {
401                 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
402         }
403
404         function format_value($str)
405         {
406                 if (is_array($str)) {
407                         return join(', ', array_map(array($this, 'format_value'), $str));
408                 }
409
410                 $key = $this->get_key($str);
411                 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
412         }
413
414         function get_style($str)
415         {
416                 $key = $this->get_key($str);
417                 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
418         }
419 }
420
421 class Tracker_field_file extends Tracker_field_format
422 {
423         var $sort_type = SORT_STRING;
424
425         function get_tag()
426         {
427                 return '<input type="file"' .
428                         ' name="' . htmlspecialchars($this->name)      . '"' .
429                         ' size="' . htmlspecialchars($this->values[0]) . '" />';
430         }
431
432         function format_value()
433         {
434                 if (isset($_FILES[$this->name])) {
435
436                         require_once(PLUGIN_DIR . 'attach.inc.php');
437
438                         $result = attach_upload($_FILES[$this->name], $this->page);
439                         if ($result['result']) {
440                                 // Upload success
441                                 return parent::format_value($this->page . '/' . $_FILES[$this->name]['name']);
442                         }
443                 }
444
445                 // Filename not specified, or Fail to upload
446                 return parent::format_value('');
447         }
448 }
449
450 class Tracker_field_radio extends Tracker_field_format
451 {
452         var $sort_type = SORT_NUMERIC;
453
454         function get_tag()
455         {
456                 $retval = '';
457
458                 $id = 0;
459                 $s_name = htmlspecialchars($this->name);
460                 foreach ($this->config->get($this->name) as $option) {
461                         ++$id;
462                         $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
463                         $s_option = htmlspecialchars($option[0]);
464                         $checked  = trim($option[0]) == trim($this->default_value) ? ' checked="checked"' : '';
465
466                         $retval .= '<input type="radio"' .
467                                 ' name="'  . $s_name   . '"' .
468                                 ' id="'    . $s_id     . '"' .
469                                 ' value="' . $s_option . '"' .
470                                 $checked . ' />' .
471                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
472                 }
473
474                 return $retval;
475         }
476
477         function get_key($str)
478         {
479                 return $str;
480         }
481
482         function get_value($value)
483         {
484                 static $options = array();
485
486                 if ($value === NULL) {
487                         $options = array();
488                         return NULL;
489                 }
490
491                 if (! isset($options[$this->name])) {
492                         $options[$this->name] = array_flip(
493                                 array_map(
494                                         create_function('$arr', 'return $arr[0];'),
495                                         $this->config->get($this->name)
496                                 )
497                         );
498                 }
499
500                 // Int or $value
501                 return isset($options[$this->name][$value]) ? $options[$this->name][$value] : $value;
502         }
503         
504         function dispose()
505         {
506                 $this->get_value(NULL);
507         }
508 }
509
510 class Tracker_field_select extends Tracker_field_radio
511 {
512         var $sort_type = SORT_NUMERIC;
513
514         function get_tag($empty = FALSE)
515         {
516                 $s_name = htmlspecialchars($this->name);
517                 $s_size = (isset($this->values[0]) && is_numeric($this->values[0])) ?
518                         ' size="' . htmlspecialchars($this->values[0]) . '"' :
519                         '';
520                 $s_multiple = (isset($this->values[1]) && strtolower($this->values[1]) == 'multiple') ?
521                         ' multiple="multiple"' :
522                         '';
523
524                 $retval = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>' . "\n";
525                 if ($empty) $retval .= ' <option value=""></option>' . "\n";
526                 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
527                 foreach ($this->config->get($this->name) as $option) {
528                         $s_option = htmlspecialchars($option[0]);
529                         $selected = isset($defaults[trim($option[0])]) ? ' selected="selected"' : '';
530                         $retval  .= ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>' . "\n";
531                 }
532                 $retval .= '</select>';
533
534                 return $retval;
535         }
536 }
537
538 class Tracker_field_checkbox extends Tracker_field_radio
539 {
540         var $sort_type = SORT_NUMERIC;
541
542         function get_tag()
543         {
544                 $retval = '';
545
546                 $id = 0;
547                 $s_name   = htmlspecialchars($this->name);
548                 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
549                 foreach ($this->config->get($this->name) as $option)
550                 {
551                         ++$id;
552                         $s_id     = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
553                         $s_option = htmlspecialchars($option[0]);
554                         $checked  = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
555
556                         $retval .= '<input type="checkbox"' .
557                                 ' name="' . $s_name . '[]"' .
558                                 ' id="' . $s_id . '"' .
559                                 ' value="' . $s_option . '"' .
560                                 $checked . ' />' .
561                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
562                 }
563
564                 return $retval;
565         }
566 }
567
568 class Tracker_field_hidden extends Tracker_field_radio
569 {
570         var $sort_type = SORT_NUMERIC;
571
572         function get_tag()
573         {
574                 return '<input type="hidden"' .
575                         ' name="'  . htmlspecialchars($this->name)          . '"' .
576                         ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
577         }
578 }
579
580 class Tracker_field_submit extends Tracker_field
581 {
582         function get_tag()
583         {
584                 $s_title  = htmlspecialchars($this->title);
585                 $s_page   = htmlspecialchars($this->page);
586                 $s_refer  = htmlspecialchars($this->refer);
587                 $s_config = htmlspecialchars($this->config->config_name);
588
589                 return <<<EOD
590 <input type="submit" value="$s_title" />
591 <input type="hidden" name="plugin" value="tracker" />
592 <input type="hidden" name="_refer" value="$s_refer" />
593 <input type="hidden" name="_base" value="$s_page" />
594 <input type="hidden" name="_config" value="$s_config" />
595 EOD;
596         }
597 }
598
599 class Tracker_field_date extends Tracker_field
600 {
601         var $sort_type = SORT_NUMERIC;
602
603         function format_cell($timestamp)
604         {
605                 return format_date($timestamp);
606         }
607 }
608
609 class Tracker_field_past extends Tracker_field
610 {
611         var $sort_type = SORT_NUMERIC;
612
613         function format_cell($timestamp)
614         {
615                 return get_passage($timestamp, FALSE);
616         }
617
618         function get_value($value)
619         {
620                 return UTIME - $value;
621         }
622 }
623
624 ///////////////////////////////////////////////////////////////////////////
625 // tracker_list plugin
626
627 function plugin_tracker_list_convert()
628 {
629         global $vars;
630
631         $config = PLUGIN_TRACKER_DEFAULT_CONFIG;
632         $page   = $refer = isset($vars['page']) ? $vars['page'] : '';
633         $order  = '';
634         $list   = 'list';
635         $limit  = NULL;
636
637         // TODO: SHOW USAGE OR ERROR CLEARLY
638         if (func_num_args()) {
639                 $args = func_get_args();
640                 switch (count($args)) {
641                 case 4:
642                         if (! is_numeric($args[3])) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
643                         $limit = intval($args[3]);
644                 case 3:
645                         $order = $args[2];
646                 case 2:
647                         $arg = get_fullname($args[1], $page);
648                         if (is_pagename($arg)) $page = $arg;
649                 case 1:
650                         if ($args[0] != '') $config = $args[0];
651                         list($config, $list) = array_pad(explode('/', $config, 2), 2, $list);
652                 }
653         }
654         return plugin_tracker_list_render($page, $refer, $config, $list, $order, $limit);
655 }
656
657 function plugin_tracker_list_action()
658 {
659         global $vars;
660
661         $page   = $refer = isset($vars['refer']) ? $vars['refer'] : '';
662         $config = isset($vars['config']) ? $vars['config'] : '';
663         $list   = isset($vars['list'])   ? $vars['list']   : 'list';
664         $order  = isset($vars['order'])  ? $vars['order']  : '_real:SORT_DESC';
665
666         $s_page = make_pagelink($page);
667         return array(
668                 'msg' => plugin_tracker_message('msg_list'),
669                 'body'=> str_replace('$1', $s_page, plugin_tracker_message('msg_back')) .
670                         plugin_tracker_list_render($page, $refer, $config, $list, $order)
671         );
672 }
673
674 function plugin_tracker_list_render($page, $refer, $config_name, $list, $order_commands = '', $limit = NULL)
675 {
676         $config = new Config('plugin/tracker/' . $config_name);
677         if (! $config->read()) {
678                 return '#tracker_list: Config \'' . htmlspecialchars($config_name) . '\' not found<br />';
679         }
680         $config->config_name = $config_name;
681         if (! is_page($config->page . '/' . $list)) {
682                 return '#tracker_list: List \'' . make_pagelink($config->page . '/' . $list) . '\' not found<br />';
683         }
684
685         $list = & new Tracker_list($page, $refer, $config, $list);
686         $list->sort($order_commands);
687         $result = $list->toString($limit);
688         if ($result == FALSE) {
689                 $result = '#tracker_list: Pages under \'' . htmlspecialchars($page) . '/\' not found' . '<br />';
690         }
691         $list->dispose();
692
693         return $result;
694 }
695
696 // Listing class
697 // TODO: Not to use static variable
698 class Tracker_list
699 {
700         var $page;
701         var $config;
702         var $list;
703         var $fields;
704         var $pattern;
705         var $pattern_fields;
706         var $rows;
707         var $order;
708
709         function Tracker_list($page, $refer, & $config, $list)
710         {
711                 $this->page    = $page;
712                 $this->config  = & $config;
713                 $this->list    = $list;
714                 $this->fields  = plugin_tracker_get_fields($page, $refer, $config);
715                 $this->pattern = '';
716                 $this->pattern_fields = array();
717                 $this->rows    = array();
718                 $this->order   = array();
719
720                 $pattern = plugin_tracker_get_source($config->page . '/page', TRUE);
721                 // TODO: if (is FALSE) OR file_exists()
722
723                 // Convert block-plugins to fields
724                 // Incleasing and decreasing around #comment etc, will be covererd with [_block_xxx]
725                 $pattern = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $pattern);
726
727                 // Generate regexes
728                 $pattern = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($pattern, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
729                 while (! empty($pattern)) {
730                         $this->pattern .= preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($pattern)) . '\\s*)');
731                         if (! empty($pattern)) {
732                                 $field = array_shift($pattern);
733                                 $this->pattern_fields[] = $field;
734                                 $this->pattern         .= '(.*)';
735                         }
736                 }
737
738                 // Listing
739                 $pattern     = $page . '/';
740                 $pattern_len = strlen($pattern);
741                 foreach (get_existpages() as $_page) {
742                         if (strpos($_page, $pattern) === 0) {
743                                 $name = substr($_page, $pattern_len);
744                                 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, $name)) continue;
745                                 $this->add($_page, $name);
746                         }
747                 }
748         }
749
750         function add($page, $name)
751         {
752                 static $done = array();
753
754                 if (isset($done[$page])) return;
755
756                 if ($page === NULL) {
757                         $done = array();
758                         return;
759                 }
760
761                 $done[$page] = TRUE;
762
763                 $source  = plugin_tracker_get_source($page);
764
765                 // Compat: 'move to [[page]]' (bugtrack plugin)
766                 $matches = array();
767                 if (! empty($source) && preg_match('/move\sto\s(.+)/', $source[0], $matches)) {
768                         $to_page = strip_bracket(trim($matches[1]));
769                         if (is_page($to_page)) {
770                                 unset($source); // Release
771                                 $this->add($to_page, $name);    // Recurse(Rescan)
772                                 return;
773                         } else {
774                                 return; // Invalid
775                         }
776                 }
777
778                 // Default
779                 $filetime = get_filetime($page);
780                 $row = array(
781                         '_page'   => '[[' . $page . ']]',
782                         '_refer'  => $this->page,
783                         '_real'   => $name,
784                         '_update' => $filetime,
785                         '_past'   => $filetime,
786                         '_match'  => FALSE,
787                 );
788
789                 // Redefine
790                 $matches = array();
791                 $row['_match'] = preg_match('/' . $this->pattern . '/s', implode('', $source), $matches);
792                 unset($source);
793                 if ($row['_match']) {
794                         array_shift($matches);
795                         foreach ($this->pattern_fields as $key => $field) {
796                                 $row[$field] = trim($matches[$key]);
797                         }
798                 }
799
800                 $this->rows[$name] = $row;
801         }
802
803         // Sort $this->rows with $order_commands
804         function sort($order_commands = '')
805         {
806                 if ($order_commands == '') {
807                         $this->order = array();
808                         return TRUE;
809                 }
810
811                 $orders = array();
812                 $params = array();      // Arguments for array_multisort()
813                 $names  = array_flip(array_keys($this->fields));
814
815                 foreach (explode(';', $order_commands) as $command) {
816                         // TODO: ???
817                         list($fieldname, $order) = array_pad(explode(':', $command), 1, 'SORT_ASC');
818                         $fieldname = trim($fieldname);
819
820                         if (! isset($names[$fieldname])) {
821                                 // TODO: SHOW INVALID FIELDNAME CLEARLY
822                                 return FALSE;
823                         }
824
825                         // TODO: SHOULD NOT TO USE DEFINES AT THIS string WORLD
826                         switch (strtoupper(trim($order))) {
827                         case SORT_ASC:
828                         case 'SORT_ASC':
829                         case 'ASC':
830                                 $order = SORT_ASC;
831                                 break;
832                         case SORT_DESC:
833                         case 'SORT_DESC':
834                         case 'DESC':
835                                 $order = SORT_DESC;
836                                 break;
837                         default:
838                                 continue;
839                         }
840
841                         $orders[$fieldname] = $order;
842                 }
843                 // TODO: LIMIT (count($orders) < N < count(fields)) TO LIMIT array_multisort()
844
845                 foreach ($orders as $fieldname => $order) {
846                         // One column set (one-dimensional array(), sort type, and order-by)
847                         $array = array();
848                         foreach ($this->rows as $row) {
849                                 $array[] = isset($row[$fieldname]) ?
850                                         $this->fields[$fieldname]->get_value($row[$fieldname]) :
851                                         '';
852                         }
853                         $params[] = $array;
854                         $params[] = $this->fields[$fieldname]->sort_type;
855                         $params[] = $order;
856                 }
857                 $params[] = & $this->rows;
858
859                 call_user_func_array('array_multisort', $params);
860                 $this->order = $orders;
861
862                 return TRUE; 
863         }
864
865         // Used with preg_replace_callback() at toString()
866         function replace_item($arr)
867         {
868                 $params = explode(',', $arr[1]);
869                 $name   = array_shift($params);
870                 if ($name == '') {
871                         $str = '';
872                 } else if (isset($this->items[$name])) {
873                         $str = $this->items[$name];
874                         if (isset($this->fields[$name])) {
875                                 $str = $this->fields[$name]->format_cell($str);
876                         }
877                 } else {
878                         return $this->pipe ? str_replace('|', '&#x7c;', $arr[0]) : $arr[0];
879                 }
880
881                 $style = empty($params) ? $name : $params[0];
882                 if (isset($this->items[$style]) && isset($this->fields[$style])) {
883                         $str = sprintf($this->fields[$style]->get_style($this->items[$style]), $str);
884                 }
885
886                 return $this->pipe ? str_replace('|', '&#x7c;', $str) : $str;
887         }
888
889         // Used with preg_replace_callback() at toString()
890         function replace_title($arr)
891         {
892                 $field = $sort = $arr[1];
893                 if (! isset($this->fields[$field])) return $arr[0];
894
895                 if ($sort == '_name' || $sort == '_page') $sort = '_real';
896
897                 $dir   = SORT_ASC;
898                 $arrow = '';
899                 $order = $this->order;
900                 if (is_array($order) && isset($order[$sort])) {
901                         // BugTrack2/106: Only variables can be passed by reference from PHP 5.0.5
902                         $order_keys = array_keys($order); // with array_shift();
903
904                         $index   = array_flip($order_keys);
905                         $pos     = 1 + $index[$sort];
906                         $b_end   = ($sort == array_shift($order_keys));
907                         $b_order = ($order[$sort] == SORT_ASC);
908                         $dir     = ($b_end xor $b_order) ? SORT_ASC : SORT_DESC;
909                         $arrow   = '&br;' . ($b_order ? '&uarr;' : '&darr;') . '(' . $pos . ')';
910
911                         unset($order[$sort], $order_keys);
912                 }
913                 $title  = $this->fields[$field]->title;
914                 $r_page = rawurlencode($this->page);
915                 $r_config = rawurlencode($this->config->config_name);
916                 $r_list = rawurlencode($this->list);
917                 $_order = array($sort . ':' . $dir);
918                 if (is_array($order)) {
919                         foreach ($order as $key => $value) {
920                                 $_order[] = $key . ':' . $value;
921                         }
922                 }
923                 $r_order = rawurlencode(join(';', $_order));
924
925                 $script = get_script_uri();
926                 return '[[' . $title . $arrow . '>' .
927                                 $script . '?plugin=tracker_list&refer=' . $r_page .
928                                 '&config=' . $r_config .
929                                 '&list=' . $r_list . '&order=' . $r_order . ']]';
930         }
931
932         function toString($limit = 10)
933         {
934                 if (empty($this->rows)) return FALSE;
935
936                 $limit = max(1, intval($limit));
937
938                 $count = $_count = count($this->rows);
939                 if ($limit != 0 && $count > $limit) {
940                         $rows   = array_slice($this->rows, 0, $limit);
941                         $_count = count($rows);
942                 } else {
943                         $rows   = $this->rows;
944                 }
945
946                 $source = array();
947
948                 if ($count > $_count) {
949                         // Message
950                         $source[] = str_replace(
951                                 array('$1',   '$2'  ),
952                                 array($count, $_count),
953                                 plugin_tracker_message('msg_limit')
954                         ) . "\n";
955                 }
956
957                 $body   = array();
958                 foreach (plugin_tracker_get_source($this->config->page . '/' . $this->list) as $line) {
959                         if (preg_match('/^\|(.+)\|[hfc]$/i', $line)) {
960                                 // Table decolations
961                                 $source[] = preg_replace_callback('/\[([^\[\]]+)\]/', array(& $this, 'replace_title'), $line);
962                         } else {
963                                 $body[] = $line;
964                         }
965                 }
966                 foreach ($rows as $row) {
967                         if (! PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE && ! $row['_match']) continue;
968
969                         $this->items = $row;
970                         foreach ($body as $line) {
971                                 if (ltrim($line) == '') {
972                                         $source[] = $line;
973                                 } else {
974                                         $this->pipe = ($line{0} == '|' || $line{0} == ':');
975                                         $source[] = preg_replace_callback('/\[([^\[\]]+)\]/', array(& $this, 'replace_item'), $line);
976                                 }
977                         }
978                 }
979
980                 return convert_html(implode('', $source));
981         }
982
983         function dispose()
984         {
985                 $this->add(NULL, NULL);
986                 return;
987         }
988 }
989
990 function plugin_tracker_get_source($page, $join = FALSE)
991 {
992         $source = get_source($page, TRUE, $join);
993
994         // Remove fixed-heading anchors
995         $source = preg_replace('/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', '$1$2', $source);
996
997         // Remove #freeze-es
998         return preg_replace('/^#freeze\s*$/im', '', $source);
999 }
1000
1001 function plugin_tracker_message($key)
1002 {
1003         global $_tracker_messages;
1004         return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';
1005 }
1006
1007 ?>