OSDN Git Service

preg_replace('/[\r\n]+/', '', $str) before output table cell
[pukiwiki/pukiwiki.git] / plugin / tracker.inc.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.56 2007/09/18 14:29:30 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 preg_replace('/[\r\n]+/', '', $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 parent::format_cell($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 = parent::format_cell($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->sort($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 $config;
722         var $list;
723         var $fields;
724         var $pattern;
725         var $pattern_fields;
726
727         var $rows   = array();
728         var $order  = array();
729         var $_added = array();
730
731         var $error  = '';       // Error message
732
733         // Used by toString() only
734         var $_itmes;
735         var $_escape;
736
737         // TODO: Why list here
738         function Tracker_list($base, $refer, & $config, $list)
739         {
740                 $this->base     = $base;
741                 $this->config   = & $config;
742                 $this->list     = $list;
743
744                 $fields         = plugin_tracker_get_fields($base, $refer, $config);
745                 $pattern        = array();
746                 $pattern_fields = array();
747
748                 // Generate regexes:
749
750                 // TODO: if (is FALSE) OR file_exists()
751                 $source = plugin_tracker_get_source($config->page . '/page', TRUE);
752                 // Block-plugins to pseudo fields (#convert => [_block_convert])
753                 $source = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $source);
754
755                 // Now, $source = array('*someting*', 'fieldname', '*someting*', 'fieldname', ...)
756                 $source = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($source, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
757
758                 while (! empty($source)) {
759                         // Just ignore these _fixed_ data
760                         // NOTE: if a page has garbages between fields, it will fail to be load
761                         $pattern[] = preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($source)) . '\\s*)');
762                         if (! empty($source)) {
763                                 $fieldname = array_shift($source);
764                                 if (isset($fields[$fieldname])) {
765                                         $pattern[]        = '(.*)';             // Just capture it
766                                         $pattern_fields[] = $fieldname; // Capture it as this $filedname
767                                 } else {
768                                         $pattern[]        = '.*';               // Just ignore pseudo fields
769                                 }
770                         }
771                 }
772                 $this->fields         = $fields;
773                 $this->pattern        = implode('', $pattern);
774                 $this->pattern_fields = $pattern_fields;
775
776                 // Listing
777                 $pattern     = $base . '/';
778                 $pattern_len = strlen($pattern);
779                 foreach (get_existpages() as $_page) {
780                         if (strpos($_page, $pattern) === 0) {
781                                 $name = substr($_page, $pattern_len);
782                                 if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, $name)) continue;
783                                 $this->add($_page, $name);
784                         }
785                 }
786         }
787
788         function add($page, $name)
789         {
790                 if (isset($this->_added[$page])) return;
791                 $this->_added[$page] = TRUE;
792
793                 $source = plugin_tracker_get_source($page);
794
795                 // Compat: 'move to [[page]]' (bugtrack plugin)
796                 $matches = array();
797                 if (! empty($source) && preg_match('/move\sto\s(.+)/', $source[0], $matches)) {
798                         $to_page = strip_bracket(trim($matches[1]));
799                         if (is_page($to_page)) {
800                                 unset($source); // Release
801                                 $this->add($to_page, $name);    // Recurse(Rescan)
802                                 return;
803                         } else {
804                                 return; // Invalid
805                         }
806                 }
807
808                 // Default column
809                 $filetime = get_filetime($page);
810                 $row = array(
811                         // column => default data of the cell
812                         '_page'   => '[[' . $page . ']]',
813                         '_real'   => $name,
814                         '_update' => $filetime,
815                         '_past'   => $filetime,
816                         '_match'  => FALSE,
817                 );
818
819                 // Load / Redefine cell
820                 $matches = array();
821                 $row['_match'] = preg_match('/' . $this->pattern . '/s', implode('', $source), $matches);
822                 unset($source);
823                 if ($row['_match']) {
824                         array_shift($matches);  // $matches[0] = all of the captured string
825                         foreach ($this->pattern_fields as $key => $field) {
826                                 $row[$field] = trim($matches[$key]);
827                         }
828                 }
829
830                 $this->rows[$name] = $row;
831         }
832
833         // Sort $this->rows by $order_commands
834         function sort($order_commands = '')
835         {
836                 $order_commands = trim($order_commands);
837                 if ($order_commands == '') {
838                         $this->order = array();
839                         return TRUE;
840                 }
841
842                 $fields = $this->fields;
843
844                 $i = 0;
845                 $orders = array();
846                 foreach (explode(';', $order_commands) as $command) {
847                         $command = trim($command);
848                         if ($command == '') continue;
849                         $arg = explode(':', $command, 2);
850                         $fieldname = isset($arg[0]) ? trim($arg[0]) : '';
851                         $order     = isset($arg[1]) ? trim($arg[1]) : '';
852
853                         if (! isset($fields[$fieldname])) {
854                                 $this->error =  'No such field: ' . $fieldname;
855                                 return FALSE;
856                         }
857                         $_order = $this->_sortkey_string2define($order);
858                         if ($_order === FALSE) {
859                                 $this->error =  'Invalid sortkey: ' . $order;
860                                 return FALSE;
861                         }
862
863                         if (! isset($orders[$fieldname]) && PLUGIN_TRACKER_LIST_SORT_LIMIT < ++$i) continue;
864                         $orders[$fieldname] = $_order;  // Set or override
865                 }
866
867                 $params = array();      // Arguments for array_multisort()
868                 foreach ($orders as $fieldname => $order) {
869                         // One column set (one-dimensional array(), sort type, and order-by)
870                         $array = array();
871                         foreach ($this->rows as $row) {
872                                 $array[] = isset($row[$fieldname]) ?
873                                         $fields[$fieldname]->get_value($row[$fieldname]) :
874                                         '';
875                         }
876                         $params[] = $array;
877                         $params[] = $fields[$fieldname]->sort_type;
878                         $params[] = $order;
879                 }
880                 $params[] = & $this->rows;
881
882                 call_user_func_array('array_multisort', $params);
883                 $this->order = $orders;
884
885                 return TRUE; 
886         }
887
888         // toString(): Sort key: Define to string (internal var => string)
889         function _sortkey_define2string($sortkey)
890         {
891                 switch ($sortkey) {
892                 case PLUGIN_TRACKER_LIST_SORT_ASC:  $sortkey = 'SORT_ASC';  break;
893                 case PLUGIN_TRACKER_LIST_SORT_DESC: $sortkey = 'SORT_DESC'; break;
894                 default:
895                         $this->error =  'No such define: ' . $sortkey;
896                         $sortkey = FALSE;
897                 }
898                 return $sortkey;
899         }
900
901         // toString(): Sort key: String to define (string => internal var)
902         function _sortkey_string2define($sortkey)
903         {
904                 switch (strtoupper(trim($sortkey))) {
905                 case '':          $sortkey = PLUGIN_TRACKER_LIST_SORT_DEFAULT; break;
906
907                 case SORT_ASC:    /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
908                 case 'SORT_ASC':  /*FALLTHROUGH*/
909                 case 'ASC':       $sortkey = PLUGIN_TRACKER_LIST_SORT_ASC; break;
910
911                 case SORT_DESC:   /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
912                 case 'SORT_DESC': /*FALLTHROUGH*/
913                 case 'DESC':      $sortkey = PLUGIN_TRACKER_LIST_SORT_DESC; break;
914
915                 default:
916                         $this->error =  'Invalid sort key: ' . $sortkey;
917                         $sortkey = FALSE;
918                 }
919                 return $sortkey;
920         }
921
922         // toString(): Called within preg_replace_callback()
923         function _replace_item($matches = array())
924         {
925                 $fields = $this->fields;
926                 $items  = $this->_items;
927                 $escape = isset($this->_escape) ? (bool)$this->_escape : FALSE;
928
929                 $params    = isset($matches[1]) ? explode(',', $matches[1]) : array();
930                 $fieldname = isset($params[0]) ? $params[0] : '';
931                 $stylename = isset($params[1]) ? $params[1] : $fieldname;
932
933                 if ($fieldname == '') return '';        // Invalid
934
935                 if (! isset($items[$fieldname])) {
936                         // Maybe load miss of the page
937                         if (isset($fields[$fieldname])) {
938                                 $str = '[page_err]';    // Exactlly
939                         } else {
940                                 $str = isset($matches[0]) ? $matches[0] : '';   // Nothing to do
941                         }
942                 } else {
943                         $str = $items[$fieldname];
944                         if (isset($fields[$fieldname])) {
945                                 $str    = $fields[$fieldname]->format_cell($str);
946                         }
947                         if (isset($fields[$stylename]) && isset($items[$stylename])) {
948                                 $_style = $fields[$stylename]->get_style($items[$stylename]);
949                                 $str    = sprintf($_style, $str);
950                         }
951                 }
952
953                 return $escape ? str_replace('|', '&#x7c;', $str) : $str;
954         }
955
956         // toString(): Called within preg_replace_callback()
957         function _replace_title($matches = array())
958         {
959                 $fields = $this->fields;
960                 $orders = $this->order;
961
962                 $fieldname = isset($matches[1]) ? $matches[1] : '';
963                 if (! isset($fields[$fieldname])) {
964                         // Invalid $fieldname or user's own string or something. Nothing to do
965                         return isset($matches[0]) ? $matches[0] : '';
966                 }
967                 if ($fieldname == '_name' || $fieldname == '_page') $fieldname = '_real';
968
969                 $arrow  = '';
970                 if (isset($orders[$fieldname])) {
971                         // Sorted
972                         $order_keys = array_keys($orders);
973                         $index   = array_flip($order_keys);
974                         $pos     = 1 + $index[$fieldname];
975                         $b_end   = ($fieldname == (isset($order_keys[0]) ? $order_keys[0] : ''));
976                         $b_order = ($orders[$fieldname] === PLUGIN_TRACKER_LIST_SORT_ASC);
977                         $order   = ($b_end xor $b_order)
978                                 ? PLUGIN_TRACKER_LIST_SORT_ASC
979                                 : PLUGIN_TRACKER_LIST_SORT_DESC;
980                         $arrow   = '&br;' . ($b_order ? '&uarr;' : '&darr;') . '(' . $pos . ')';
981                         unset($order_keys, $index);
982                         unset($orders[$fieldname]);
983                 } else {
984                         // Not sorted yet, but
985                         $order = PLUGIN_TRACKER_LIST_SORT_ASC;  // Default
986                 }
987
988                 // $fieldname become the first, if you click this link
989                 $_order = array($fieldname . ':' . $this->_sortkey_define2string($order));
990                 foreach ($orders as $key => $value) {
991                         $_order[] = $key . ':' . $this->_sortkey_define2string($value);
992                 }
993
994                 $r_config = ($this->config->config_name != PLUGIN_TRACKER_DEFAULT_CONFIG) ?
995                         '&config=' . rawurlencode($this->config->config_name) : '';
996                 $r_list   = ($this->list != PLUGIN_TRACKER_DEFAULT_LIST) ?
997                         '&list=' . rawurlencode($this->list) : '';
998                 return '[[' .
999                                 $fields[$fieldname]->title . $arrow .
1000                                 '>' . get_script_uri() .
1001                                 '?plugin=tracker_list' .
1002                                 '&base=' . rawurlencode($this->base) .
1003                                 $r_config .
1004                                 $r_list .
1005                                 '&order=' . rawurlencode(join(';', $_order)) .
1006                                 ']]';
1007         }
1008
1009         // Output a part of Wiki text
1010         function toString($limit = 0)
1011         {
1012                 if (empty($this->rows)) {
1013                         $this->error = 'Pages not found under: ' . $this->base . '/';
1014                         return FALSE;
1015                 }
1016
1017                 $rows   = $this->rows;
1018                 $source = array();
1019
1020                 $count = count($this->rows);
1021                 $limit = intval($limit);
1022                 if ($limit != 0) $limit = max(1, $limit);
1023                 if ($limit != 0 && $count > $limit) {
1024                         $source[] = str_replace(
1025                                 array('$1',   '$2'  ),
1026                                 array($count, $limit),
1027                                 plugin_tracker_message('msg_limit')
1028                         ) . "\n";
1029                         $rows  = array_slice($this->rows, 0, $limit);
1030                 }
1031
1032                 // Loading template
1033                 $header = $body = array();
1034                 foreach (plugin_tracker_get_source($this->config->page . '/' . $this->list) as $line) {
1035                         if (preg_match('/^\|(.+)\|[hfc]$/i', $line)) {
1036                                 // TODO: Why c and f  here
1037                                 $header[] = $line;      // Table header, footer, and decoration
1038                         } else {
1039                                 $body[]   = $line;      // The others
1040                         }
1041                 }
1042
1043                 foreach($header as $line) {
1044                         $source[] = preg_replace_callback('/\[([^\[\]]+)\]/', array(& $this, '_replace_title'), $line);
1045                 }
1046                 foreach ($rows as $row) {
1047                         if (! PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE && ! $row['_match']) continue;
1048                         $this->_items = $row;
1049                         foreach ($body as $line) {
1050                                 if (ltrim($line) != '') {
1051                                         $this->_escape = ($line[0] == '|' || $line[0] == ':');  // The first letter
1052                                         $line = preg_replace_callback('/\[([^\[\]]+)\]/', array(& $this, '_replace_item'), $line);
1053                                 }
1054                                 $source[] = $line;
1055                         }
1056                 }
1057
1058                 return implode('', $source);
1059         }
1060 }
1061
1062 function plugin_tracker_get_source($page, $join = FALSE)
1063 {
1064         $source = get_source($page, TRUE, $join);
1065
1066         // Remove fixed-heading anchors
1067         $source = preg_replace('/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m', '$1$2', $source);
1068
1069         // Remove #freeze-es
1070         return preg_replace('/^#freeze\s*$/im', '', $source);
1071 }
1072
1073 function plugin_tracker_message($key)
1074 {
1075         global $_tracker_messages;
1076         return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';
1077 }
1078
1079 ?>