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.45 2007/09/08 16:30:25 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 class Tracker_field
247 {
248         var $name;
249         var $title;
250         var $values;
251         var $default_value;
252         var $page;
253         var $refer;
254         var $config;
255         var $data;
256         var $sort_type = SORT_REGULAR;
257         var $id        = 0;
258
259         function Tracker_field($field, $page, $refer, & $config)
260         {
261                 global $post;
262                 static $id = 0; // Unique id per instance
263
264                 $this->id     = ++$id;
265                 $this->name   = $field[0];
266                 $this->title  = $field[1];
267                 $this->values = explode(',', $field[3]);
268                 $this->default_value = $field[4];
269                 $this->page   = $page;
270                 $this->refer  = $refer;
271                 $this->config = & $config;
272                 $this->data   = isset($post[$this->name]) ? $post[$this->name] : '';
273         }
274
275         // XHTML part inside a form
276         function get_tag()
277         {
278                 return '';
279         }
280
281         function get_style()
282         {
283                 return '%s';
284         }
285
286         function format_value($value)
287         {
288                 return $value;
289         }
290
291         function format_cell($str)
292         {
293                 return $str;
294         }
295
296         // Compare key for Tracker_list->sort()
297         function get_value($value)
298         {
299                 return $value;  // Default: $value itself
300         }
301
302         // Release the resources
303         function dispose()
304         {
305         }
306 }
307
308 class Tracker_field_text extends Tracker_field
309 {
310         var $sort_type = SORT_STRING;
311
312         function get_tag()
313         {
314                 return '<input type="text"' .
315                                 ' name="'  . htmlspecialchars($this->name)          . '"' .
316                                 ' size="'  . htmlspecialchars($this->values[0])     . '"' .
317                                 ' value="' . htmlspecialchars($this->default_value) . '" />';
318         }
319 }
320
321 class Tracker_field_page extends Tracker_field_text
322 {
323         var $sort_type = SORT_STRING;
324
325         function format_value($value)
326         {
327                 $value = strip_bracket($value);
328                 if (is_pagename($value)) $value = '[[' . $value . ']]';
329                 return parent::format_value($value);
330         }
331 }
332
333 class Tracker_field_real extends Tracker_field_text
334 {
335         var $sort_type = SORT_REGULAR;
336 }
337
338 class Tracker_field_title extends Tracker_field_text
339 {
340         var $sort_type = SORT_STRING;
341
342         function format_cell($str)
343         {
344                 make_heading($str);
345                 return $str;
346         }
347 }
348
349 class Tracker_field_textarea extends Tracker_field
350 {
351         var $sort_type = SORT_STRING;
352
353         function get_tag()
354         {
355                 return '<textarea' .
356                         ' name="' . htmlspecialchars($this->name)      . '"' .
357                         ' cols="' . htmlspecialchars($this->values[0]) . '"' .
358                         ' rows="' . htmlspecialchars($this->values[1]) . '">' .
359                                                 htmlspecialchars($this->default_value) .
360                         '</textarea>';
361         }
362
363         function format_cell($str)
364         {
365                 $str = preg_replace('/[\r\n]+/', '', $str);
366                 if (! empty($this->values[2]) && strlen($str) > ($this->values[2] + 3)) {
367                         $str = mb_substr($str, 0, $this->values[2]) . '...';
368                 }
369                 return $str;
370         }
371 }
372
373 class Tracker_field_format extends Tracker_field
374 {
375         var $sort_type = SORT_STRING;
376         var $styles    = array();
377         var $formats   = array();
378
379         function Tracker_field_format($field, $page, $refer, & $config)
380         {
381                 parent::Tracker_field($field, $page, $refer, $config);
382
383                 foreach ($this->config->get($this->name) as $option) {
384                         list($key, $style, $format) =
385                                 array_pad(array_map(create_function('$a', 'return trim($a);'), $option), 3, '');
386                         if ($style  != '') $this->styles[$key]  = $style;
387                         if ($format != '') $this->formats[$key] = $format;
388                 }
389         }
390
391         function get_tag()
392         {
393                 return '<input type="text"' .
394                         ' name="' . htmlspecialchars($this->name)      . '"' .
395                         ' size="' . htmlspecialchars($this->values[0]) . '" />';
396         }
397
398         function get_key($str)
399         {
400                 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
401         }
402
403         function format_value($str)
404         {
405                 if (is_array($str)) {
406                         return join(', ', array_map(array($this, 'format_value'), $str));
407                 }
408
409                 $key = $this->get_key($str);
410                 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
411         }
412
413         function get_style($str)
414         {
415                 $key = $this->get_key($str);
416                 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
417         }
418 }
419
420 class Tracker_field_file extends Tracker_field_format
421 {
422         var $sort_type = SORT_STRING;
423
424         function get_tag()
425         {
426                 return '<input type="file"' .
427                         ' name="' . htmlspecialchars($this->name)      . '"' .
428                         ' size="' . htmlspecialchars($this->values[0]) . '" />';
429         }
430
431         function format_value()
432         {
433                 if (isset($_FILES[$this->name])) {
434
435                         require_once(PLUGIN_DIR . 'attach.inc.php');
436
437                         $result = attach_upload($_FILES[$this->name], $this->page);
438                         if ($result['result']) {
439                                 // Upload success
440                                 return parent::format_value($this->page . '/' . $_FILES[$this->name]['name']);
441                         }
442                 }
443
444                 // Filename not specified, or Fail to upload
445                 return parent::format_value('');
446         }
447 }
448
449 class Tracker_field_radio extends Tracker_field_format
450 {
451         var $sort_type = SORT_NUMERIC;
452
453         function get_tag()
454         {
455                 $retval = '';
456
457                 $id = 0;
458                 $s_name = htmlspecialchars($this->name);
459                 foreach ($this->config->get($this->name) as $option) {
460                         ++$id;
461                         $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
462                         $s_option = htmlspecialchars($option[0]);
463                         $checked  = trim($option[0]) == trim($this->default_value) ? ' checked="checked"' : '';
464
465                         $retval .= '<input type="radio"' .
466                                 ' name="'  . $s_name   . '"' .
467                                 ' id="'    . $s_id     . '"' .
468                                 ' value="' . $s_option . '"' .
469                                 $checked . ' />' .
470                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
471                 }
472
473                 return $retval;
474         }
475
476         function get_key($str)
477         {
478                 return $str;
479         }
480
481         function get_value($value)
482         {
483                 static $options = array();
484
485                 if ($value === NULL) {
486                         $options = array();
487                         return NULL;
488                 }
489
490                 if (! isset($options[$this->name])) {
491                         $options[$this->name] = array_flip(
492                                 array_map(
493                                         create_function('$arr', 'return $arr[0];'),
494                                         $this->config->get($this->name)
495                                 )
496                         );
497                 }
498
499                 // Int or $value
500                 return isset($options[$this->name][$value]) ? $options[$this->name][$value] : $value;
501         }
502         
503         function dispose()
504         {
505                 $this->get_value(NULL);
506         }
507 }
508
509 class Tracker_field_select extends Tracker_field_radio
510 {
511         var $sort_type = SORT_NUMERIC;
512
513         function get_tag($empty = FALSE)
514         {
515                 $s_name = htmlspecialchars($this->name);
516                 $s_size = (isset($this->values[0]) && is_numeric($this->values[0])) ?
517                         ' size="' . htmlspecialchars($this->values[0]) . '"' :
518                         '';
519                 $s_multiple = (isset($this->values[1]) && strtolower($this->values[1]) == 'multiple') ?
520                         ' multiple="multiple"' :
521                         '';
522
523                 $retval = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>' . "\n";
524                 if ($empty) $retval .= ' <option value=""></option>' . "\n";
525                 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
526                 foreach ($this->config->get($this->name) as $option) {
527                         $s_option = htmlspecialchars($option[0]);
528                         $selected = isset($defaults[trim($option[0])]) ? ' selected="selected"' : '';
529                         $retval  .= ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>' . "\n";
530                 }
531                 $retval .= '</select>';
532
533                 return $retval;
534         }
535 }
536
537 class Tracker_field_checkbox extends Tracker_field_radio
538 {
539         var $sort_type = SORT_NUMERIC;
540
541         function get_tag()
542         {
543                 $retval = '';
544
545                 $id = 0;
546                 $s_name   = htmlspecialchars($this->name);
547                 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
548                 foreach ($this->config->get($this->name) as $option)
549                 {
550                         ++$id;
551                         $s_id     = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
552                         $s_option = htmlspecialchars($option[0]);
553                         $checked  = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
554
555                         $retval .= '<input type="checkbox"' .
556                                 ' name="' . $s_name . '[]"' .
557                                 ' id="' . $s_id . '"' .
558                                 ' value="' . $s_option . '"' .
559                                 $checked . ' />' .
560                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
561                 }
562
563                 return $retval;
564         }
565 }
566
567 class Tracker_field_hidden extends Tracker_field_radio
568 {
569         var $sort_type = SORT_NUMERIC;
570
571         function get_tag()
572         {
573                 return '<input type="hidden"' .
574                         ' name="'  . htmlspecialchars($this->name)          . '"' .
575                         ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
576         }
577 }
578
579 class Tracker_field_submit extends Tracker_field
580 {
581         function get_tag()
582         {
583                 $s_title  = htmlspecialchars($this->title);
584                 $s_page   = htmlspecialchars($this->page);
585                 $s_refer  = htmlspecialchars($this->refer);
586                 $s_config = htmlspecialchars($this->config->config_name);
587
588                 return <<<EOD
589 <input type="submit" value="$s_title" />
590 <input type="hidden" name="plugin" value="tracker" />
591 <input type="hidden" name="_refer" value="$s_refer" />
592 <input type="hidden" name="_base" value="$s_page" />
593 <input type="hidden" name="_config" value="$s_config" />
594 EOD;
595         }
596 }
597
598 class Tracker_field_date extends Tracker_field
599 {
600         var $sort_type = SORT_NUMERIC;
601
602         function format_cell($timestamp)
603         {
604                 return format_date($timestamp);
605         }
606 }
607
608 class Tracker_field_past extends Tracker_field
609 {
610         var $sort_type = SORT_NUMERIC;
611
612         function format_cell($timestamp)
613         {
614                 return get_passage($timestamp, FALSE);
615         }
616
617         function get_value($value)
618         {
619                 return UTIME - $value;
620         }
621 }
622
623 ///////////////////////////////////////////////////////////////////////////
624 // tracker_list plugin
625
626 function plugin_tracker_list_convert()
627 {
628         global $vars;
629
630         $config = PLUGIN_TRACKER_DEFAULT_CONFIG;
631         $page   = $refer = isset($vars['page']) ? $vars['page'] : '';
632         $order  = '';
633         $list   = 'list';
634         $limit  = NULL;
635
636         // TODO: SHOW USAGE OR ERROR CLEARLY
637         if (func_num_args()) {
638                 $args = func_get_args();
639                 switch (count($args)) {
640                 case 4:
641                         if (! is_numeric($args[3])) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
642                         $limit = intval($args[3]);
643                 case 3:
644                         $order = $args[2];
645                 case 2:
646                         $arg = get_fullname($args[1], $page);
647                         if (is_pagename($arg)) $page = $arg;
648                 case 1:
649                         if ($args[0] != '') $config = $args[0];
650                         list($config, $list) = array_pad(explode('/', $config, 2), 2, $list);
651                 }
652         }
653         return plugin_tracker_list_render($page, $refer, $config, $list, $order, $limit);
654 }
655
656 function plugin_tracker_list_action()
657 {
658         global $vars;
659
660         $page   = $refer = isset($vars['refer']) ? $vars['refer'] : '';
661         $config = isset($vars['config']) ? $vars['config'] : '';
662         $list   = isset($vars['list'])   ? $vars['list']   : 'list';
663         $order  = isset($vars['order'])  ? $vars['order']  : '_real:SORT_DESC';
664
665         $s_page = make_pagelink($page);
666         return array(
667                 'msg' => plugin_tracker_message('msg_list'),
668                 'body'=> str_replace('$1', $s_page, plugin_tracker_message('msg_back')) .
669                         plugin_tracker_list_render($page, $refer, $config, $list, $order)
670         );
671 }
672
673 function plugin_tracker_list_render($page, $refer, $config_name, $list, $order_commands = '', $limit = NULL)
674 {
675         $config = new Config('plugin/tracker/' . $config_name);
676         if (! $config->read()) {
677                 return '#tracker_list: Config \'' . htmlspecialchars($config_name) . '\' not found<br />';
678         }
679         $config->config_name = $config_name;
680         if (! is_page($config->page . '/' . $list)) {
681                 return '#tracker_list: List \'' . make_pagelink($config->page . '/' . $list) . '\' not found<br />';
682         }
683
684         $list = & new Tracker_list($page, $refer, $config, $list);
685         $list->sort($order_commands);
686         $result = $list->toString($limit);
687         if ($result == FALSE) {
688                 $result = '#tracker_list: Pages under \'' . htmlspecialchars($page) . '/\' not found' . '<br />';
689         }
690         $list->dispose();
691
692         return $result;
693 }
694
695 // Listing class
696 class Tracker_list
697 {
698         var $page;
699         var $config;
700         var $list;
701         var $fields;
702         var $pattern;
703         var $pattern_fields;
704         var $rows;
705         var $order;
706
707         function Tracker_list($page, $refer, & $config, $list)
708         {
709                 $this->page    = $page;
710                 $this->config  = & $config;
711                 $this->list    = $list;
712                 $this->fields  = plugin_tracker_get_fields($page, $refer, $config);
713                 $this->pattern = '';
714                 $this->pattern_fields = array();
715                 $this->rows    = array();
716                 $this->order   = array();
717
718                 $pattern = plugin_tracker_get_source($config->page . '/page', TRUE);
719                 // TODO: if (is FALSE) OR file_exists()
720
721                 // Convert block-plugins to fields
722                 // Incleasing and decreasing around #comment etc, will be covererd with [_block_xxx]
723                 $pattern = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $pattern);
724
725                 // Generate regexes
726                 $pattern = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($pattern, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
727                 while (! empty($pattern)) {
728                         $this->pattern .= preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($pattern)) . '\\s*)');
729                         if (! empty($pattern)) {
730                                 $field = array_shift($pattern);
731                                 $this->pattern_fields[] = $field;
732                                 $this->pattern         .= '(.*)';
733                         }
734                 }
735
736                 // Listing
737                 $pattern     = $page . '/';
738                 $pattern_len = strlen($pattern);
739                 foreach (get_existpages() as $_page) {
740                         if (strpos($_page, $pattern) === 0) {
741                                 $name = substr($_page, $pattern_len);
742                                 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, $name)) continue;
743                                 $this->add($_page, $name);
744                         }
745                 }
746         }
747
748         function add($page, $name)
749         {
750                 static $done = array();
751
752                 if (isset($done[$page])) return;
753
754                 if ($page === NULL) {
755                         $done = array();
756                         return;
757                 }
758
759                 $done[$page] = TRUE;
760
761                 $source  = plugin_tracker_get_source($page);
762
763                 // Compat: 'move to [[page]]' (bugtrack plugin)
764                 $matches = array();
765                 if (! empty($source) && preg_match('/move\sto\s(.+)/', $source[0], $matches)) {
766                         $to_page = strip_bracket(trim($matches[1]));
767                         if (is_page($to_page)) {
768                                 unset($source); // Release
769                                 $this->add($to_page, $name);    // Recurse(Rescan)
770                                 return;
771                         } else {
772                                 return; // Invalid
773                         }
774                 }
775
776                 // Default
777                 $filetime = get_filetime($page);
778                 $row = array(
779                         '_page'   => '[[' . $page . ']]',
780                         '_refer'  => $this->page,
781                         '_real'   => $name,
782                         '_update' => $filetime,
783                         '_past'   => $filetime,
784                         '_match'  => FALSE,
785                 );
786
787                 // Redefine
788                 $matches = array();
789                 $row['_match'] = preg_match('/' . $this->pattern . '/s', implode('', $source), $matches);
790                 unset($source);
791                 if ($row['_match']) {
792                         array_shift($matches);
793                         foreach ($this->pattern_fields as $key => $field) {
794                                 $row[$field] = trim($matches[$key]);
795                         }
796                 }
797
798                 $this->rows[$name] = $row;
799         }
800
801         // Sort $this->rows with $order_commands
802         function sort($order_commands = '')
803         {
804                 if ($order_commands == '') {
805                         $this->order = array();
806                         return TRUE;
807                 }
808
809                 $orders = array();
810                 $params = array();      // Arguments for array_multisort()
811                 $names  = array_flip(array_keys($this->fields));
812
813                 foreach (explode(';', $order_commands) as $command) {
814                         // TODO: ???
815                         list($fieldname, $order) = array_pad(explode(':', $command), 1, 'SORT_ASC');
816                         $fieldname = trim($fieldname);
817
818                         if (! isset($names[$fieldname])) {
819                                 // TODO: SHOW INVALID FIELDNAME CLEARLY
820                                 return FALSE;
821                         }
822
823                         // TODO: SHOULD NOT TO USE DEFINES AT THIS string WORLD
824                         switch (strtoupper(trim($order))) {
825                         case SORT_ASC:
826                         case 'SORT_ASC':
827                         case 'ASC':
828                                 $order = SORT_ASC;
829                                 break;
830                         case SORT_DESC:
831                         case 'SORT_DESC':
832                         case 'DESC':
833                                 $order = SORT_DESC;
834                                 break;
835                         default:
836                                 continue;
837                         }
838
839                         $orders[$fieldname] = $order;
840                 }
841                 // TODO: LIMIT (count($orders) < N < count(fields)) TO LIMIT array_multisort()
842
843                 foreach ($orders as $fieldname => $order) {
844                         // One column set (one-dimensional array(), sort type, and order-by)
845                         $array = array();
846                         foreach ($this->rows as $row) {
847                                 $array[] = isset($row[$fieldname]) ?
848                                         $this->fields[$fieldname]->get_value($row[$fieldname]) :
849                                         '';
850                         }
851                         $params[] = $array;
852                         $params[] = $this->fields[$fieldname]->sort_type;
853                         $params[] = $order;
854                 }
855                 $params[] = & $this->rows;
856
857                 call_user_func_array('array_multisort', $params);
858                 $this->order = $orders;
859
860                 return TRUE; 
861         }
862
863         // Used with preg_replace_callback() at toString()
864         function replace_item($arr)
865         {
866                 $params = explode(',', $arr[1]);
867                 $name   = array_shift($params);
868                 if ($name == '') {
869                         $str = '';
870                 } else if (isset($this->items[$name])) {
871                         $str = $this->items[$name];
872                         if (isset($this->fields[$name])) {
873                                 $str = $this->fields[$name]->format_cell($str);
874                         }
875                 } else {
876                         return $this->pipe ? str_replace('|', '&#x7c;', $arr[0]) : $arr[0];
877                 }
878
879                 $style = empty($params) ? $name : $params[0];
880                 if (isset($this->items[$style]) && isset($this->fields[$style])) {
881                         $str = sprintf($this->fields[$style]->get_style($this->items[$style]), $str);
882                 }
883
884                 return $this->pipe ? str_replace('|', '&#x7c;', $str) : $str;
885         }
886
887         // Used with preg_replace_callback() at toString()
888         function replace_title($arr)
889         {
890                 $field = $sort = $arr[1];
891                 if (! isset($this->fields[$field])) return $arr[0];
892
893                 if ($sort == '_name' || $sort == '_page') $sort = '_real';
894
895                 $dir   = SORT_ASC;
896                 $arrow = '';
897                 $order = $this->order;
898                 if (is_array($order) && isset($order[$sort])) {
899                         // BugTrack2/106: Only variables can be passed by reference from PHP 5.0.5
900                         $order_keys = array_keys($order); // with array_shift();
901
902                         $index   = array_flip($order_keys);
903                         $pos     = 1 + $index[$sort];
904                         $b_end   = ($sort == array_shift($order_keys));
905                         $b_order = ($order[$sort] == SORT_ASC);
906                         $dir     = ($b_end xor $b_order) ? SORT_ASC : SORT_DESC;
907                         $arrow   = '&br;' . ($b_order ? '&uarr;' : '&darr;') . '(' . $pos . ')';
908
909                         unset($order[$sort], $order_keys);
910                 }
911                 $title  = $this->fields[$field]->title;
912                 $r_page = rawurlencode($this->page);
913                 $r_config = rawurlencode($this->config->config_name);
914                 $r_list = rawurlencode($this->list);
915                 $_order = array($sort . ':' . $dir);
916                 if (is_array($order)) {
917                         foreach ($order as $key => $value) {
918                                 $_order[] = $key . ':' . $value;
919                         }
920                 }
921                 $r_order = rawurlencode(join(';', $_order));
922
923                 $script = get_script_uri();
924                 return '[[' . $title . $arrow . '>' .
925                                 $script . '?plugin=tracker_list&refer=' . $r_page .
926                                 '&config=' . $r_config .
927                                 '&list=' . $r_list . '&order=' . $r_order . ']]';
928         }
929
930         function toString($limit = 10)
931         {
932                 if (empty($this->rows)) return FALSE;
933
934                 $limit = max(1, intval($limit));
935
936                 $count = $_count = count($this->rows);
937                 if ($limit != 0 && $count > $limit) {
938                         $rows   = array_slice($this->rows, 0, $limit);
939                         $_count = count($rows);
940                 } else {
941                         $rows   = $this->rows;
942                 }
943
944                 $source = array();
945
946                 if ($count > $_count) {
947                         // Message
948                         $source[] = str_replace(
949                                 array('$1',   '$2'  ),
950                                 array($count, $_count),
951                                 plugin_tracker_message('msg_limit')
952                         ) . "\n";
953                 }
954
955                 $body   = array();
956                 foreach (plugin_tracker_get_source($this->config->page . '/' . $this->list) as $line) {
957                         if (preg_match('/^\|(.+)\|[hfc]$/i', $line)) {
958                                 // Table decolations
959                                 $source[] = preg_replace_callback('/\[([^\[\]]+)\]/', array(& $this, 'replace_title'), $line);
960                         } else {
961                                 $body[] = $line;
962                         }
963                 }
964                 foreach ($rows as $row) {
965                         if (! PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE && ! $row['_match']) continue;
966
967                         $this->items = $row;
968                         foreach ($body as $line) {
969                                 if (ltrim($line) == '') {
970                                         $source[] = $line;
971                                 } else {
972                                         $this->pipe = ($line{0} == '|' || $line{0} == ':');
973                                         $source[] = preg_replace_callback('/\[([^\[\]]+)\]/', array(& $this, 'replace_item'), $line);
974                                 }
975                         }
976                 }
977
978                 return convert_html(implode('', $source));
979         }
980
981         function dispose()
982         {
983                 $this->add(NULL, NULL);
984                 return;
985         }
986 }
987
988 function plugin_tracker_get_source($page, $join = FALSE)
989 {
990         $source = get_source($page, TRUE, $join);
991
992         // Remove fixed-heading anchors
993         $source = preg_replace('/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', '$1$2', $source);
994
995         // Remove #freeze-es
996         return preg_replace('/^#freeze\s*$/im', '', $source);
997 }
998
999 function plugin_tracker_message($key)
1000 {
1001         global $_tracker_messages;
1002         return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';
1003 }
1004
1005 ?>