OSDN Git Service

Simplify:
[pukiwiki/pukiwiki.git] / plugin / tracker.inc.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone
3 // $Id: tracker.inc.php,v 1.108 2007/10/12 16:00:27 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 // Allow N columns sorted 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 type
31 define('PLUGIN_TRACKER_SORT_TYPE_REGULAR',       0);
32 define('PLUGIN_TRACKER_SORT_TYPE_NUMERIC',       1);
33 define('PLUGIN_TRACKER_SORT_TYPE_STRING',        2);
34 //define('PLUGIN_TRACKER_SORT_TYPE_LOCALE_STRING', 5);
35 define('PLUGIN_TRACKER_SORT_TYPE_NATURAL',       6);
36 if (! defined('SORT_NATURAL')) define('SORT_NATURAL', PLUGIN_TRACKER_SORT_TYPE_NATURAL);
37
38 // Sort order
39 define('PLUGIN_TRACKER_SORT_ORDER_DESC',    3);
40 define('PLUGIN_TRACKER_SORT_ORDER_ASC',     4);
41 define('PLUGIN_TRACKER_SORT_ORDER_DEFAULT', PLUGIN_TRACKER_SORT_ORDER_ASC);
42
43
44 // Show a form
45 function plugin_tracker_convert()
46 {
47         global $vars;
48
49         if (PKWK_READONLY) return ''; // Show nothing
50
51         $base = $refer = isset($vars['page']) ? $vars['page'] : '';
52         $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
53         $form        = PLUGIN_TRACKER_DEFAULT_FORM;
54
55         $args = func_get_args();
56         $argc = count($args);
57         if ($argc > 2) {
58                 return PLUGIN_TRACKER_USAGE . '<br />';
59         }
60         switch ($argc) {
61         case 2:
62                 $arg = get_fullname($args[1], $base);
63                 if (is_pagename($arg)) $base = $arg;
64                 /*FALLTHROUGH*/
65         case 1:
66                 // Config/form
67                 if ($args[0] != '') {
68                         $arg = explode('/', trim($args[0]), 2);
69                         if ($arg[0] != '' ) $config_name = trim($arg[0]);
70                         if (isset($arg[1])) $form        = trim($arg[1]);
71                 }
72         }
73         unset($args, $argc, $arg);
74
75         $config = new Config('plugin/tracker/' . $config_name);
76         if (! $config->read()) {
77                 return '#tracker: Config \'' . htmlspecialchars($config_name) . '\' not found<br />';
78         }
79         $config->config_name = $config_name;
80
81         $form     = $config->page . '/' . $form;
82         $template = plugin_tracker_get_source($form, TRUE);
83         if ($template === FALSE || empty($template)) {
84                 return '#tracker: Form \'' . make_pagelink($form) . '\' not found or seems empty<br />';
85         }
86
87         $_form = & new Tracker_form($base, $refer, $config);
88         $_form->initFields(plugin_tracker_field_pickup($template));
89         $_form->initHiddenFields();
90         $fields = $_form->fields;
91
92         $from = $to = $hidden = array();
93         foreach (array_keys($fields) as $fieldname) {
94                 $from[] = '[' . $fieldname . ']';
95                 $_to    = $fields[$fieldname]->get_tag();
96                 if (is_a($fields[$fieldname], 'Tracker_field_hidden')) {
97                         $to[]     = '';
98                         $hidden[] = $_to;
99                 } else {
100                         $to[]     = $_to;
101                 }
102                 unset($fields[$fieldname]);
103         }
104
105         $script   = get_script_uri();
106         $template = str_replace($from, $to, convert_html($template));
107         $hidden   = implode('<br />' . "\n", $hidden);
108         return <<<EOD
109 <form enctype="multipart/form-data" action="$script" method="post">
110 <div>
111 $template
112 $hidden
113 </div>
114 </form>
115 EOD;
116 }
117
118 // Add new page
119 function plugin_tracker_action()
120 {
121         global $post, $vars, $now;
122
123         if (PKWK_READONLY) die_message('PKWK_READONLY prohibits editing');
124
125         $base  = isset($post['_base'])  ? $post['_base']  : '';
126         $refer = isset($post['_refer']) ? $post['_refer'] : $base;
127         if (! is_pagename($refer)) {
128                 return array(
129                         'msg'  => 'Cannot write',
130                         'body' => 'Page name (' . htmlspecialchars($refer) . ') invalid'
131                 );
132         }
133
134         // $page name to add will be decided here
135         $num  = 0;
136         $name = isset($post['_name']) ? $post['_name'] : '';
137         if (isset($post['_page'])) {
138                 $real = $page = $post['_page'];
139         } else {
140                 $real = is_pagename($name) ? $name : ++$num;
141                 $page = get_fullname('./' . $real, $base);
142         }
143         if (! is_pagename($page)) $page = $base;
144         while (is_page($page)) {
145                 $real = ++$num;
146                 $page = $base . '/' . $real;
147         }
148
149         // Loading configuration
150         $config_name = isset($post['_config']) ? $post['_config'] : '';
151         $config = new Config('plugin/tracker/' . $config_name);
152         if (! $config->read()) {
153                 return '<p>config file \'' . htmlspecialchars($config_name) . '\' not found.</p>';
154         }
155         $config->config_name = $config_name;
156
157         // Default
158         $_post = array_merge($post, $_FILES);
159         $_post['_date'] = $now;
160         $_post['_page'] = $page;
161         $_post['_name'] = $name;
162         $_post['_real'] = $real;
163         // $_post['_refer'] = $_post['refer'];
164
165         // Creating an empty page, before attaching files
166         pkwk_touch_file(get_filename($page));
167
168         $from = $to = array();
169
170         // Load $template
171         $template_page = $config->page . '/page';
172         $template = plugin_tracker_get_source($template_page);
173         if ($template === FALSE || empty($template)) {
174                 return array(
175                         'msg'  => 'Cannot write',
176                         'body' => 'Page template (' . htmlspecialchars($template_page) . ') not exists or seems empty'
177                 );
178         }
179
180         $form = & new Tracker_form($base, $refer, $config);
181         $form->initFields(plugin_tracker_field_pickup(implode('', $template)));
182         $fields = & $form->fields;      // unset()
183         foreach (array_keys($fields) as $field) {
184                 $from[] = '[' . $field . ']';
185                 $to[]   = isset($_post[$field]) ? $fields[$field]->format_value($_post[$field]) : '';
186                 unset($fields[$field]);
187         }
188
189         // Repalace every [$field]s (found inside $template) to real values
190         $subject = $escape = array();
191         foreach (array_keys($template) as $linenum) {
192                 if (trim($template[$linenum]) == '') continue;
193
194                 // Escape some TextFormattingRules
195                 $letter = $template[$linenum][0];
196                 if ($letter == '|' || $letter == ':') {
197                         $escape['|'][$linenum] = $template[$linenum];
198                 } else if ($letter == ',') {
199                         $escape[','][$linenum] = $template[$linenum];
200                 } else {
201                         // TODO: Escape "\n" except multiline-allowed fields
202                         $subject[$linenum]     = $template[$linenum];
203                 }
204         }
205         foreach (str_replace($from, $to, $subject) as $linenum => $line) {
206                 $template[$linenum] = $line;
207         }
208         if ($escape) {
209                 // Escape for some TextFormattingRules
210                 foreach(array_keys($escape) as $hint) {
211                         $to_e = plugin_tracker_escape($to, $hint);
212                         foreach (str_replace($from, $to_e, $escape[$hint]) as $linenum => $line) {
213                                 $template[$linenum] = $line;
214                         }
215                 }
216                 unset($to_e);
217         }
218         unset($from, $to);
219
220         // Write $template, without touch
221         page_write($page, join('', $template));
222
223         pkwk_headers_sent();
224         header('Location: ' . get_script_uri() . '?' . rawurlencode($page));
225         exit;
226 }
227
228 // Data set of XHTML form or something
229 class Tracker_form
230 {
231         var $id;        // Unique id per instance
232
233         var $base;
234         var $refer;
235         var $config;
236
237         var $raw_fields;
238         var $fields = array();
239
240         var $error  = '';       // Error message
241
242         function Tracker_form($base, $refer, & $config)
243         {
244                 static $id = 0;
245                 $this->id = ++$id;
246
247                 $this->base   = $base;
248                 $this->refer  = $refer;
249                 $this->config = & $config;
250         }
251
252         // Init $this->raw_fields and $this->fields
253         // TODO: Using func_get_args() to shrink the code?
254         function initFields($requests = NULL)
255         {
256                 if (! isset($this->raw_fields)) {
257                         $raw_fields = array();
258                         // From config
259                         foreach ($this->config->get('fields') as $field) {
260                                 $fieldname = isset($field[0]) ? $field[0] : '';
261                                 $raw_fields[$fieldname] = array(
262                                         'display' => isset($field[1]) ? $field[1] : '',
263                                         'type'    => isset($field[2]) ? $field[2] : '',
264                                         'options' => isset($field[3]) ? $field[3] : '',
265                                         'default' => isset($field[4]) ? $field[4] : '',
266                                 );
267                         }
268                         // From reserved
269                         $default = array('options' => '20', 'default' => '');
270                         foreach (array(
271                                 '_date'   => 'text',    // Post date
272                                 '_update' => 'date',    // Last modified date
273                                 '_past'   => 'past',    // Elapsed time (passage)
274                                 '_page'   => 'page',    // Page name
275                                 '_name'   => 'text',    // Page name specified by poster
276                                 '_real'   => 'real',    // Page name (Real)
277                                 '_refer'  => 'page',    // Page name refer from this (Page who has forms)
278                                 '_base'   => 'page',
279                                 '_submit' => 'submit'
280                         ) as $fieldname => $type) {
281                                 if (isset($raw_fields[$fieldname])) continue;
282                                 $raw_fields[$fieldname] = array(
283                                         'display' => plugin_tracker_message('btn' . $fieldname),
284                                         'type'    => $type,
285                                 ) + $default;
286                         }
287                         $this->raw_fields = & $raw_fields;
288                 } else {
289                         $raw_fields = & $this->raw_fields;
290                 }
291
292                 if ($requests === NULL) {
293                         // (The rest of) All, defined order
294                         foreach ($raw_fields as $fieldname => $field) {
295                                 $err = $this->addField(
296                                         $fieldname,
297                                         $field['display'],
298                                         $field['type'],
299                                         $field['options'],
300                                         $field['default']
301                                 );
302                                 if ($err === FALSE) return FALSE;
303                         }
304                         $raw_fields = array();
305                 } else {
306                         // Part of, specific order
307                         if (! is_array($requests)) $requests = array($requests);
308                         foreach ($requests as $fieldname) {
309                                 if (! isset($raw_fields[$fieldname])) continue;
310                                 $field = $raw_fields[$fieldname];
311                                 $err = $this->addField(
312                                         $fieldname,
313                                         $field['display'],
314                                         $field['type'],
315                                         $field['options'],
316                                         $field['default']
317                                 );
318                                 unset($raw_fields[$fieldname]);
319                                 if ($err === FALSE) return FALSE;
320                         }
321                 }
322
323                 return TRUE;
324         }
325
326         function initHiddenFields()
327         {
328                 // Make sure to init $this->raw_fields
329                 $this->initFields(array());
330
331                 $fields = array();
332                 foreach ($this->raw_fields as $fieldname => $field) {
333                         if ($field['type'] == 'hidden') {
334                                 $fields[] = $fieldname;
335                         }
336                 }
337
338                 $this->initFields($fields);
339         }
340
341         // Called from InitFields()
342         function addField($fieldname, $displayname, $type = 'text', $options = '20', $default = '')
343         {
344                 // Not Init
345                 if (isset($this->fields[$fieldname])) {
346                         $this->error = "No such field: " . $fieldname;
347                         return FALSE;
348                 }
349
350                 $class = 'Tracker_field_' . $type;
351                 if (! class_exists($class)) {
352                         $this->error = "No such type: " . $type;
353                         return FALSE;
354                 }
355
356                 $this->fields[$fieldname] = & new $class(
357                         $this,                  // Reference
358                         array(
359                                 $fieldname,
360                                 $displayname,
361                                 NULL,           // $type
362                                 $options,
363                                 $default
364                         )
365                 );
366
367                 return TRUE;
368         }
369 }
370
371 // TODO: Why a filter sometimes created so many?
372 // Field classes within a form
373 class Tracker_field
374 {
375         var $id;        // Unique id per instance, and per class(extended-class)
376
377         var $form;      // Parent (class Tracker_form)
378
379         var $name;
380         var $title;
381         var $options;
382         var $default_value;
383
384         var $data;
385
386         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_REGULAR;
387
388         function Tracker_field(& $tracker_form, $field)
389         {
390                 global $post;
391                 static $id = 0;
392
393                 $this->id = ++$id;
394
395                 $this->form          = & $tracker_form;
396                 $this->name          = isset($field[0]) ? $field[0] : '';
397                 $this->title         = isset($field[1]) ? $field[1] : '';
398                 $this->options       = isset($field[3]) ? explode(',', $field[3]) : array();
399                 $this->default_value = isset($field[4]) ? $field[4] : '';
400
401                 $this->data = isset($post[$this->name]) ? $post[$this->name] : '';
402         }
403
404         // Output a part of XHTML form for the field
405         function get_tag()
406         {
407                 return '';
408         }
409
410         // Format user input before write
411         function format_value($value)
412         {
413                 return $value;
414         }
415
416         // Compare key for Tracker_list->sort()
417         function get_value($value)
418         {
419                 return $value;
420         }
421
422         // Format table cell data before output the wiki text
423         function format_cell($str)
424         {
425                 return $str;
426         }
427
428         // Format-string for sprintf() before output the wiki text
429         function get_style()
430         {
431                 return '%s';
432         }
433 }
434
435 class Tracker_field_text extends Tracker_field
436 {
437         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
438
439         function get_tag()
440         {
441                 $s_name  = htmlspecialchars($this->name);
442                 $s_size  = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
443                 $s_value = htmlspecialchars($this->default_value);
444
445                 return '<input type="text"' .
446                                 ' name="'  . $s_name  . '"' .
447                                 ' size="'  . $s_size  . '"' .
448                                 ' value="' . $s_value . '" />';
449         }
450 }
451
452 // Special type: Page name with link syntax
453 class Tracker_field_page extends Tracker_field_text
454 {
455         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
456
457         function _format($page)
458         {
459                 $page = strip_bracket($page);
460                 if (is_pagename($page)) $page = '[[' . $page . ']]';
461                 return $page;
462         }
463
464         function format_value($value)
465         {
466                 return $this->_format($value);
467         }
468
469         function format_cell($value)
470         {
471                 return $this->_format($value);
472         }
473 }
474
475 // Special type: Page name minus 'base'
476 // e.g.
477 //  page name: Tracker/sales/100
478 //  base     : Tracker/sales
479 //  _real    : 100
480 //
481 // NOTE:
482 //   Don't consider using within ":config/plugin/tracker/*/page".
483 //   This value comes from _the_page_name_ itself.
484 class Tracker_field_real extends Tracker_field_text
485 {
486         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NATURAL;
487
488         function format_cell($value)
489         {
490                 // basename(): Rough but work with this(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN prohibits '/') situation
491                 return basename($value);
492         }
493 }
494
495 // Special type: For headings cleaning
496 class Tracker_field_title extends Tracker_field_text
497 {
498         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
499
500         function format_cell($str)
501         {
502                 make_heading($str);
503                 return $str;
504         }
505 }
506
507 class Tracker_field_textarea extends Tracker_field
508 {
509         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
510
511         function get_tag()
512         {
513                 $s_name = htmlspecialchars($this->name);
514                 $s_cols = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
515                 $s_rows = isset($this->options[1]) ? htmlspecialchars($this->options[1]) : '';
516                 $s_default = htmlspecialchars($this->default_value);
517
518                 return '<textarea' .
519                                 ' name="' . $s_name . '"' .
520                                 ' cols="' . $s_cols . '"' .
521                                 ' rows="' . $s_rows . '">' .
522                                 $s_default .
523                         '</textarea>';
524         }
525
526         function format_cell($str)
527         {
528                 // Cut too long ones
529                 // TODO: Why store all of them to the memory?
530                 if (isset($this->options[2])) {
531                         $limit = max(0, $this->options[2]);
532                         $len = mb_strlen($str);
533                         if ($len > ($limit + 3)) {      // 3 = mb_strlen('...')
534                                 $str = mb_substr($str, 0, $limit) . '...';
535                         }
536                 }
537                 return $str;
538         }
539 }
540
541 // Writing text with formatting if trim($cell) != ''
542 // See also: http://home.arino.jp/?tracker.inc.php%2F41
543 class Tracker_field_format extends Tracker_field
544 {
545         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
546
547         var $styles    = array();
548         var $formats   = array();
549
550         function Tracker_field_format(& $tracker_form, $field)
551         {
552                 parent::Tracker_field($tracker_form, $field);
553                 foreach ($this->form->config->get($this->name) as $option) {
554                         list($key, $style, $format) = array_pad(array_map('trim', $option), 3, '');
555                         if ($style  != '') $this->styles[$key]  = $style;
556                         if ($format != '') $this->formats[$key] = $format;
557                 }
558         }
559
560         function _get_key($str)
561         {
562                 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
563         }
564
565         function get_tag()
566         {
567                 $s_name = htmlspecialchars($this->name);
568                 $s_size = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
569
570                 return '<input type="text" name="' . $s_name . '" size="' . $s_size . '" />';
571         }
572
573         function format_value($str)
574         {
575                 if (is_array($str)) {
576                         return join(', ', array_map(array($this, 'format_value'), $str));
577                 }
578
579                 $key = $this->_get_key($str);
580                 return isset($this->formats[$key]) ? str_replace('%s', $str, $this->formats[$key]) : $str;
581         }
582
583         function get_style($str)
584         {
585                 $key = $this->_get_key($str);
586                 return isset($this->styles[$key]) ? $this->styles[$key] : '%s';
587         }
588 }
589
590 class Tracker_field_file extends Tracker_field_format
591 {
592         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_STRING;
593
594         function get_tag()
595         {
596                 $s_name = htmlspecialchars($this->name);
597                 $s_size = isset($this->options[0]) ? htmlspecialchars($this->options[0]) : '';
598
599                 return '<input type="file" name="' . $s_name . '" size="' . $s_size . '" />';
600         }
601
602         function format_value()
603         {
604                 if (isset($_FILES[$this->name])) {
605
606                         require_once(PLUGIN_DIR . 'attach.inc.php');
607
608                         $base = $this->form->base;
609                         $result = attach_upload($_FILES[$this->name], $base);
610                         if (isset($result['result']) && $result['result']) {
611                                 // Upload success
612                                 return parent::format_value($base . '/' . $_FILES[$this->name]['name']);
613                         }
614                 }
615
616                 // Filename not specified, or Fail to upload
617                 return parent::format_value('');
618         }
619 }
620
621 class Tracker_field_radio extends Tracker_field_format
622 {
623         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
624         var $_options  = array();
625
626         function get_tag()
627         {
628                 $retval = '';
629
630                 $id = 0;
631                 $s_name = htmlspecialchars($this->name);
632                 foreach ($this->form->config->get($this->name) as $option) {
633                         ++$id;
634                         $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
635                         $s_option = htmlspecialchars($option[0]);
636                         $checked  = trim($option[0]) === trim($this->default_value) ? ' checked="checked"' : '';
637
638                         $retval .= '<input type="radio"' .
639                                 ' name="'  . $s_name   . '"' .
640                                 ' id="'    . $s_id     . '"' .
641                                 ' value="' . $s_option . '"' .
642                                 $checked . ' />' .
643                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
644                 }
645
646                 return $retval;
647         }
648
649         function get_value($value)
650         {
651                 $options = & $this->_options;
652                 $name    = $this->name;
653
654                 if (! isset($options[$name])) {
655                         $values = array_map('reset', $this->form->config->get($name));
656                         $options[$name] = array_flip($values);  // array('value0' => 0, 'value1' => 1, ...)
657                 }
658
659                 return isset($options[$name][$value]) ? $options[$name][$value] : $value;
660         }
661 }
662
663 class Tracker_field_select extends Tracker_field_radio
664 {
665         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
666
667         var $_defaults;
668
669         function get_tag($empty = FALSE)
670         {
671                 if (! isset($this->_defaults)) {
672                         $this->_defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
673                 }
674                 $defaults = $this->_defaults;
675
676                 $retval = array();
677
678                 $s_name = htmlspecialchars($this->name);
679                 $s_size = (isset($this->options[0]) && is_numeric($this->options[0])) ?
680                         ' size="' . htmlspecialchars($this->options[0]) . '"' : '';
681                 $s_multiple = (isset($this->options[1]) && strtolower($this->options[1]) == 'multiple') ?
682                         ' multiple="multiple"' : '';
683                 $retval[] = '<select name="' . $s_name . '[]"' . $s_size . $s_multiple . '>';
684
685                 if ($empty) $retval[] = ' <option value=""></option>';
686
687                 foreach ($this->form->config->get($this->name) as $option) {
688                         $option   = reset($option);
689                         $s_option = htmlspecialchars($option);
690                         $selected = isset($defaults[trim($option)]) ? ' selected="selected"' : '';
691                         $retval[] = ' <option value="' . $s_option . '"' . $selected . '>' . $s_option . '</option>';
692                 }
693
694                 $retval[] = '</select>';
695
696                 return implode("\n", $retval);
697         }
698 }
699
700 class Tracker_field_checkbox extends Tracker_field_radio
701 {
702         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
703
704         function get_tag()
705         {
706                 $retval = '';
707
708                 $id = 0;
709                 $s_name   = htmlspecialchars($this->name);
710                 $defaults = array_flip(preg_split('/\s*,\s*/', $this->default_value, -1, PREG_SPLIT_NO_EMPTY));
711                 foreach ($this->form->config->get($this->name) as $option) {
712                         ++$id;
713                         $s_id     = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
714                         $s_option = htmlspecialchars($option[0]);
715                         $checked  = isset($defaults[trim($option[0])]) ? ' checked="checked"' : '';
716
717                         $retval .= '<input type="checkbox"' .
718                                 ' name="' . $s_name . '[]"' .
719                                 ' id="' . $s_id . '"' .
720                                 ' value="' . $s_option . '"' .
721                                 $checked . ' />' .
722                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
723                 }
724
725                 return $retval;
726         }
727 }
728
729 class Tracker_field_hidden extends Tracker_field_radio
730 {
731         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
732
733         function get_tag()
734         {
735                 return '<input type="hidden"' .
736                         ' name="'  . htmlspecialchars($this->name)          . '"' .
737                         ' value="' . htmlspecialchars($this->default_value) . '" />' . "\n";
738         }
739 }
740
741 class Tracker_field_submit extends Tracker_field
742 {
743         function get_tag()
744         {
745                 $form = $this->form;
746
747                 $s_title  = htmlspecialchars($this->title);
748                 $s_base   = htmlspecialchars($form->base);
749                 $s_refer  = htmlspecialchars($form->refer);
750                 $s_config = htmlspecialchars($form->config->config_name);
751
752                 return <<<EOD
753 <input type="submit" value="$s_title" />
754 <input type="hidden" name="plugin"  value="tracker" />
755 <input type="hidden" name="_refer"  value="$s_refer" />
756 <input type="hidden" name="_base"   value="$s_base" />
757 <input type="hidden" name="_config" value="$s_config" />
758 EOD;
759         }
760 }
761
762 class Tracker_field_date extends Tracker_field
763 {
764         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
765
766         function format_cell($timestamp)
767         {
768                 return format_date($timestamp);
769         }
770 }
771
772 class Tracker_field_past extends Tracker_field
773 {
774         var $sort_type = PLUGIN_TRACKER_SORT_TYPE_NUMERIC;
775
776         function get_value($value)
777         {
778                 return UTIME - $value;
779         }
780
781         function format_cell($timestamp)
782         {
783                 return get_passage($timestamp, FALSE);
784         }
785 }
786
787 ///////////////////////////////////////////////////////////////////////////
788 // tracker_list plugin
789
790 function plugin_tracker_list_convert()
791 {
792         global $vars;
793
794         $base = $refer = isset($vars['page']) ? $vars['page'] : '';
795         $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
796         $list        = PLUGIN_TRACKER_DEFAULT_LIST;
797         $limit       = PLUGIN_TRACKER_DEFAULT_LIMIT;
798         $order       = PLUGIN_TRACKER_DEFAULT_ORDER;
799
800         $args = func_get_args();
801         $argc = count($args);
802         if ($argc > 4) {
803                 return PLUGIN_TRACKER_LIST_USAGE . '<br />';
804         }
805         switch ($argc) {
806         case 4: $limit = $args[3];      /*FALLTHROUGH*/
807         case 3: $order = $args[2];      /*FALLTHROUGH*/
808         case 2:
809                 $arg = get_fullname($args[1], $base);
810                 if (is_pagename($arg)) $base = $arg;
811                 /*FALLTHROUGH*/
812         case 1:
813                 // Config/list
814                 if ($args[0] != '') {
815                         $arg = explode('/', $args[0], 2);
816                         if ($arg[0] != '' ) $config_name = $arg[0];
817                         if (isset($arg[1])) $list        = $arg[1];
818                 }
819         }
820         unset($args, $argc, $arg);
821
822         return plugin_tracker_list_render($base, $refer, $config_name, $list, $order, $limit);
823 }
824
825 function plugin_tracker_list_action()
826 {
827         global $get, $vars;
828
829         $base = isset($get['base']) ? $get['base'] : '';        // Base directory to load
830
831         if (isset($get['refer'])) {
832                 $refer = $get['refer']; // Where to #tracker_list
833                 if ($base == '') $base = $refer;
834         } else {
835                 $refer = $base;
836         }
837
838         $config = isset($get['config']) ? $get['config'] : '';
839         $list   = isset($get['list'])   ? $get['list']   : 'list';
840         $order  = isset($get['order'])  ? $get['order']  : PLUGIN_TRACKER_DEFAULT_ORDER;
841         $limit  = isset($get['limit'])  ? $get['limit']  : 0;
842
843         $s_refer = make_pagelink($refer);
844         return array(
845                 'msg' => plugin_tracker_message('msg_list'),
846                 'body'=> str_replace('$1', $s_refer, plugin_tracker_message('msg_back')) .
847                         plugin_tracker_list_render($base, $refer, $config, $list, $order, $limit)
848         );
849 }
850
851 function plugin_tracker_list_render($base, $refer, $config_name, $list, $order_commands = '', $limit = 0)
852 {
853         $base  = trim($base);
854         if ($base == '') return '#tracker_list: Base not specified' . '<br />';
855
856         $refer = trim($refer);
857         if (! is_page($refer)) {
858                 return '#tracker_list: Refer page not found: ' . htmlspecialchars($refer) . '<br />';
859         }
860
861         $config_name = trim($config_name);
862         if ($config_name == '') $config_name = PLUGIN_TRACKER_DEFAULT_CONFIG;
863
864         $list  = trim($list);
865         if (! is_numeric($limit)) return PLUGIN_TRACKER_LIST_USAGE . '<br />';
866         $limit = intval($limit);
867
868         $config = new Config('plugin/tracker/' . $config_name);
869         if (! $config->read()) {
870                 return '#tracker_list: Config not found: ' . htmlspecialchars($config_name) . '<br />';
871         }
872         $config->config_name = $config_name;
873         if (! is_page($config->page . '/' . $list)) {
874                 return '#tracker_list: List not found: ' . make_pagelink($config->page . '/' . $list) . '<br />';
875         }
876
877         $tracker_list = & new Tracker_list($base, $refer, $config);
878         if ($tracker_list->setSortOrder($order_commands) === FALSE) {
879                 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
880         }
881         $result = $tracker_list->toString($list, $limit);
882         if ($result === FALSE) {
883                 return '#tracker_list: ' . htmlspecialchars($list->error) . '<br />';
884         }
885         unset($tracker_list);
886
887         return convert_html($result);
888 }
889
890 // Listing class
891 class Tracker_list
892 {
893         var $form;      // class Tracker_form
894
895         var $rows   = array();
896         var $orders = array();
897         var $error  = '';       // Error message
898
899         // _generate_regex()
900         var $pattern;
901         var $pattern_fields;
902
903         // add()
904         var $_added = array();
905
906         // toString()
907         var $_list;
908         var $_row;
909         var $_the_first_character_of_the_line;
910
911         function Tracker_list($base, $refer, & $config)
912         {
913                 $this->form = & new Tracker_form($base, $refer, $config);
914         }
915
916         // Adding $this->rows
917         // Add multiple pages at a time
918         function loadRows()
919         {
920                 $base  = $this->form->base . '/';
921                 $len   = strlen($base);
922                 $regex = '#^' . preg_quote($base, '#') . '#';
923
924                 foreach (preg_grep($regex, array_values(get_existpages())) as $pagename) {
925                         if (preg_match(PLUGIN_TRACKER_LIST_EXCLUDE_PATTERN, substr($pagename, $len))) {
926                                 continue;
927                         }
928                         if ($this->addRow($pagename) === FALSE) return FALSE;
929                 }
930                 if (empty($this->rows)) {
931                         $this->error = 'Pages not found under: ' . $base;
932                         return FALSE;
933                 }
934
935                 return TRUE;
936         }
937
938         // addRow(): Generate regex to load a page
939         function _generate_regex()
940         {
941                 $template_page = $this->form->config->page . '/page';
942                 $fields        = $this->form->fields;
943                 
944                 $pattern        = array();
945                 $pattern_fields = array();
946
947                 $template = plugin_tracker_get_source($template_page, TRUE);
948                 if ($template === FALSE || empty($template)) {
949                         $this->error = 'Page not found or seems empty: ' . $template_page;
950                         return FALSE;
951                 }
952
953                 // Block-plugins to pseudo fields (#convert => [_block_convert])
954                 $template = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m', '[_block_$1]', $template);
955
956                 // Now, $template = array('*someting*', 'fieldname', '*someting*', 'fieldname', ...)
957                 $template = preg_split('/\\\\\[(\w+)\\\\\]/', preg_quote($template, '/'), -1, PREG_SPLIT_DELIM_CAPTURE);
958
959                 // NOTE: if the page has garbages between [field]s, it will fail to be load
960                 while (! empty($template)) {
961                         // Just ignore these _fixed_ data
962                         $pattern[] = preg_replace('/\s+/', '\\s*', '(?>\\s*' . trim(array_shift($template)) . '\\s*)');
963                         if (empty($template)) continue;
964
965                         $fieldname = array_shift($template);
966                         if (isset($fields[$fieldname])) {
967                                 $pattern[] = '(.*?)';   // Just capture it
968                                 $pattern_fields[] = $fieldname; // Capture it as this $filedname
969                         } else {
970                                 $pattern[] = '.*?';     // Just ignore pseudo fields etc
971                         }
972                 }
973                 $this->pattern        = '/' . implode('', $pattern) . '/sS';
974                 $this->pattern_fields = $pattern_fields;
975
976                 return TRUE;
977         }
978
979         // Add one pages
980         function addRow($pagename, $rescan = FALSE)
981         {
982                 if (isset($this->_added[$pagename])) return TRUE;
983                 $this->_added[$pagename] = TRUE;
984
985                 $source = plugin_tracker_get_source($pagename, TRUE);
986                 if ($source === FALSE) $source = '';
987
988                 // Compat: 'move to [[page]]' (like bugtrack plugin)
989                 $matches = array();
990                 if (! $rescan && ! empty($source) && preg_match('/move\sto\s(.+)/', $source, $matches)) {
991                         $to_page = strip_bracket(trim($matches[1]));
992                         if (is_page($to_page)) {
993                                 unset($source, $matches);       // Release
994                                 return $this->addRow($to_page, TRUE);   // Recurse(Rescan) once
995                         }
996                 }
997
998                 // Default column
999                 $filetime = get_filetime($pagename);
1000                 $row = array(
1001                         // column => default data of the cell
1002                         '_page'   => $pagename, // TODO: Redudant column pair [1]
1003                         '_real'   => $pagename, // TODO: Redudant column pair [1]
1004                         '_update' => $filetime, // TODO: Redudant column pair [2]
1005                         '_past'   => $filetime, // TODO: Redudant column pair [2]
1006                 );
1007
1008                 // Load / Redefine cell
1009                 $matches = array();
1010                 if (preg_match($this->pattern, $source, $matches)) {
1011                         array_shift($matches);  // $matches[0] = all of the captured string
1012                         foreach ($this->pattern_fields as $key => $fieldname) {
1013                                 $row[$fieldname] = trim($matches[$key]);
1014                                 unset($matches[$key]);
1015                         }
1016                         $this->rows[] = $row;
1017                 } else if (PLUGIN_TRACKER_LIST_SHOW_ERROR_PAGE) {
1018                         $this->rows[] = $row;   // Error
1019                 }
1020
1021                 return TRUE;
1022         }
1023
1024         // setSortOrder()
1025         function _order_commands2orders($order_commands = '')
1026         {
1027                 $order_commands = trim($order_commands);
1028                 if ($order_commands == '') return array();
1029
1030                 $orders = array();
1031
1032                 $i = 0;
1033                 foreach (explode(';', $order_commands) as $command) {
1034                         $command = trim($command);
1035                         if ($command == '') continue;
1036
1037                         $arg = explode(':', $command, 2);
1038                         $fieldname = isset($arg[0]) ? trim($arg[0]) : '';
1039                         $order     = isset($arg[1]) ? trim($arg[1]) : '';
1040
1041                         $_order = $this->_sortkey_string2define($order);
1042                         if ($_order === FALSE) {
1043                                 $this->error =  'Invalid sort key: ' . $order;
1044                                 return FALSE;
1045                         } else if (isset($orders[$fieldname])) {
1046                                 $this->error =  'Sort key already set for: ' . $fieldname;
1047                                 return FALSE;
1048                         }
1049
1050                         if (PLUGIN_TRACKER_LIST_SORT_LIMIT <= $i) continue;
1051                         ++$i;
1052
1053                         $orders[$fieldname] = $_order;
1054                 }
1055
1056                 return $orders;
1057         }
1058
1059         // Set commands for sort()
1060         function setSortOrder($order_commands = '')
1061         {
1062                 $orders = $this->_order_commands2orders($order_commands);
1063                 if ($orders === FALSE) {
1064                         $this->orders = array();
1065                         return FALSE;
1066                 }
1067                 $this->orders = $orders;
1068                 return TRUE;
1069         }
1070
1071         // sortRows(): Internal sort type => PHP sort define
1072         function _sort_type_dropout($order)
1073         {
1074                 switch ($order) {
1075                 case PLUGIN_TRACKER_SORT_TYPE_REGULAR: return SORT_REGULAR;
1076                 case PLUGIN_TRACKER_SORT_TYPE_NUMERIC: return SORT_NUMERIC;
1077                 case PLUGIN_TRACKER_SORT_TYPE_STRING:  return SORT_STRING;
1078                 case PLUGIN_TRACKER_SORT_TYPE_NATURAL: return SORT_NATURAL;
1079                 default:
1080                         $this->error = 'Invalid sort type';
1081                         return FALSE;
1082                 }
1083         }
1084
1085         // sortRows(): Internal sort order => PHP sort define
1086         function _sort_order_dropout($order)
1087         {
1088                 switch ($order) {
1089                 case PLUGIN_TRACKER_SORT_ORDER_ASC:  return SORT_ASC;
1090                 case PLUGIN_TRACKER_SORT_ORDER_DESC: return SORT_DESC;
1091                 default:
1092                         $this->error = 'Invalid sort order';
1093                         return FALSE;
1094                 }
1095         }
1096
1097         // Sort $this->rows by $this->orders
1098         function sortRows()
1099         {
1100                 $fields = $this->form->fields;
1101                 $orders = $this->orders;
1102                 $types  = array();
1103
1104                 $fieldnames = array_keys($orders);      // Field names to sort
1105
1106                 foreach ($fieldnames as $fieldname) {
1107                         if (! isset($fields[$fieldname])) {
1108                                 $this->error =  'No such field: ' . $fieldname;
1109                                 return FALSE;
1110                         }
1111                         $types[$fieldname]  = $this->_sort_type_dropout($fields[$fieldname]->sort_type);
1112                         $orders[$fieldname] = $this->_sort_order_dropout($orders[$fieldname]);
1113                         if ($types[$fieldname] === FALSE || $orders[$fieldname] === FALSE) return FALSE;
1114                 }
1115
1116                 $columns = array();
1117                 foreach ($this->rows as $row) {
1118                         foreach ($fieldnames as $fieldname) {
1119                                 if (isset($row[$fieldname])) {
1120                                         $columns[$fieldname][] = $fields[$fieldname]->get_value($row[$fieldname]);
1121                                 } else {
1122                                         $columns[$fieldname][] = '';
1123                                 }
1124                         }
1125                 }
1126
1127                 $params = array();
1128                 foreach ($fieldnames as $fieldname) {
1129
1130                         if ($types[$fieldname] == SORT_NATURAL) {
1131                                 $column = & $columns[$fieldname];
1132                                 natcasesort($column);
1133                                 $i = 0;
1134                                 $last = NULL;
1135                                 foreach (array_keys($column) as $key) {
1136                                         // Consider the same values there, for array_multisort()
1137                                         if ($last !== $column[$key]) ++$i;
1138                                         $last = strtolower($column[$key]);      // natCASEsort()
1139                                         $column[$key] = $i;
1140                                 }
1141                                 ksort($column, SORT_NUMERIC);   // Revert the order
1142                                 $types[$fieldname] = SORT_NUMERIC;
1143                         }
1144
1145                         // One column set (one-dimensional array, sort type, and sort order)
1146                         // for array_multisort()
1147                         $params[] = $columns[$fieldname];
1148                         $params[] = $types[$fieldname];
1149                         $params[] = $orders[$fieldname];
1150                 }
1151                 if (! empty($orders) && ! empty($this->rows)) {
1152                         $params[] = & $this->rows;      // The target
1153                         call_user_func_array('array_multisort', $params);
1154                 }
1155
1156                 return TRUE; 
1157         }
1158
1159         // toString(): Sort key: Define to string (internal var => string)
1160         function _sortkey_define2string($sortkey)
1161         {
1162                 switch ($sortkey) {
1163                 case PLUGIN_TRACKER_SORT_ORDER_ASC:     return 'asc';
1164                 case PLUGIN_TRACKER_SORT_ORDER_DESC:    return 'desc';
1165                 default:
1166                         $this->error =  'No such define: ' . $sortkey;
1167                         return FALSE;
1168                 }
1169         }
1170
1171         // toString(): Sort key: String to define (string => internal var)
1172         function _sortkey_string2define($sortkey)
1173         {
1174                 switch (strtoupper(trim($sortkey))) {
1175                 case '':          return PLUGIN_TRACKER_SORT_ORDER_DEFAULT; break;
1176
1177                 case SORT_ASC:    /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1178                 case 'SORT_ASC':  /*FALLTHROUGH*/
1179                 case 'ASC':       return PLUGIN_TRACKER_SORT_ORDER_ASC;
1180
1181                 case SORT_DESC:   /*FALLTHROUGH*/ // Compat, will be removed at 1.4.9 or later
1182                 case 'SORT_DESC': /*FALLTHROUGH*/
1183                 case 'DESC':      return PLUGIN_TRACKER_SORT_ORDER_DESC;
1184
1185                 default:
1186                         $this->error =  'Invalid sort key: ' . $sortkey;
1187                         return FALSE;
1188                 }
1189         }
1190
1191         // toString(): Called within preg_replace_callback()
1192         function _replace_title($matches = array())
1193         {
1194                 $form        = $this->form;
1195                 $base        = $form->base;
1196                 $refer       = $form->refer;
1197                 $fields      = $form->fields;
1198                 $config_name = $form->config->config_name;
1199
1200                 $orders      = $this->orders;
1201                 $list        = $this->_list;
1202
1203                 $fieldname = isset($matches[1]) ? $matches[1] : '';
1204                 if (! isset($fields[$fieldname])) {
1205                         // Invalid $fieldname or user's own string or something. Nothing to do
1206                         return isset($matches[0]) ? $matches[0] : '';
1207                 }
1208
1209                 // This column seems sorted or not
1210                 if (isset($orders[$fieldname])) {
1211                         $is_asc = ($orders[$fieldname] == PLUGIN_TRACKER_SORT_ORDER_ASC);
1212
1213                         $indexes = array_flip(array_keys($orders));
1214                         $index   = $indexes[$fieldname] + 1;
1215                         unset($indexes);
1216
1217                         $arrow = '&br;' . ($is_asc ? '&uarr;' : '&darr;') . '(' . $index . ')';
1218                         // Allow flip, if this is the first column
1219                         if (($index == 1) xor $is_asc) {
1220                                 $order = PLUGIN_TRACKER_SORT_ORDER_ASC;
1221                         } else {
1222                                 $order = PLUGIN_TRACKER_SORT_ORDER_DESC;
1223                         }
1224                 } else {
1225                         $arrow = '';
1226                         $order = PLUGIN_TRACKER_SORT_ORDER_DEFAULT;
1227                 }
1228
1229                 // This column will be the first position , if you click
1230                 $orders = array($fieldname => $order) + $orders;
1231
1232                 $_orders = array();
1233                 foreach ($orders as $_fieldname => $_order) {
1234                         if ($_order == PLUGIN_TRACKER_SORT_ORDER_DEFAULT) {
1235                                 $_orders[] = $_fieldname;
1236                         } else {
1237                                 $_orders[] = $_fieldname . ':' . $this->_sortkey_define2string($_order);
1238                         }
1239                 }
1240
1241                 $script = get_script_uri();
1242                 $r_base   = ($refer != $base) ?
1243                         '&base='  . rawurlencode($base) : '';
1244                 $r_config = ($config_name != PLUGIN_TRACKER_DEFAULT_CONFIG) ?
1245                         '&config=' . rawurlencode($config_name) : '';
1246                 $r_list   = ($list != PLUGIN_TRACKER_DEFAULT_LIST) ?
1247                         '&list=' . rawurlencode($list) : '';
1248                 $r_order  = ! empty($_orders) ?
1249                         '&order=' . rawurlencode(join(';', $_orders)) : '';
1250
1251                 return
1252                          '[[' .
1253                                 $fields[$fieldname]->title . $arrow .
1254                         '>' .
1255                                 $script . '?plugin=tracker_list' .
1256                                 '&refer=' . rawurlencode($refer) .      // Try to show 'page title' properly
1257                                 $r_base . $r_config . $r_list . $r_order  .
1258                         ']]';
1259         }
1260
1261         // toString(): Called within preg_replace_callback()
1262         function _replace_item($matches = array())
1263         {
1264                 $fields = $this->form->fields;
1265                 $row    = $this->_row;
1266                 $tfc    = $this->_the_first_character_of_the_line ;
1267
1268                 $params    = isset($matches[1]) ? explode(',', $matches[1]) : array();
1269                 $fieldname = isset($params[0])  ? $params[0] : '';
1270                 $stylename = isset($params[1])  ? $params[1] : $fieldname;
1271
1272                 $str = '';
1273
1274                 if ($fieldname != '') {
1275                         if (! isset($row[$fieldname])) {
1276                                 // Maybe load miss of the page
1277                                 if (isset($fields[$fieldname])) {
1278                                         $str = '[page_err]';    // Exactlly
1279                                 } else {
1280                                         $str = isset($matches[0]) ? $matches[0] : '';   // Nothing to do
1281                                 }
1282                         } else {
1283                                 $str = $row[$fieldname];
1284                                 if (isset($fields[$fieldname])) {
1285                                         $str = $fields[$fieldname]->format_cell($str);
1286                                 }
1287                         }
1288                 }
1289
1290                 if (isset($fields[$stylename]) && isset($row[$stylename])) {
1291                         $_style = $fields[$stylename]->get_style($row[$stylename]);
1292                         $str    = sprintf($_style, $str);
1293                 }
1294
1295                 return plugin_tracker_escape($str, $tfc);
1296         }
1297
1298         // Output a part of Wiki text
1299         function toString($list, $limit = 0)
1300         {
1301                 $form   = & $this->form;
1302                 $list   = $form->config->page . '/' . $list;
1303
1304                 $source = array();
1305                 $regex  = '/\[([^\[\]]+)\]/';
1306
1307                 // Loading template
1308                 $template = plugin_tracker_get_source($list, TRUE);
1309                 if ($template === FALSE || empty($template)) {
1310                         $this->error = 'Page not found or seems empty: ' . $template;
1311                         return FALSE;
1312                 }
1313
1314                 // Try to create $form->fields just you need
1315                 if ($form->initFields('_real') === FALSE ||
1316                     $form->initFields(plugin_tracker_field_pickup($template)) === FALSE ||
1317                     $form->initFields(array_keys($this->orders)) === FALSE) {
1318                         $this->error = $form->error;
1319                         return FALSE;
1320                 }
1321
1322                 // TODO: Check isset($this->rows) or something
1323                 // Generate regex for $form->fields
1324                 if ($this->_generate_regex() === FALSE) return FALSE;
1325
1326                 // TODO: Check isset($this->rows) or something
1327                 // Load and sort $this->rows
1328                 if ($this->loadRows() === FALSE || $this->sortRows() === FALSE) return FALSE;
1329                 $rows = $this->rows;
1330
1331
1332                 // toString()
1333                 $this->_list = $list;   // For _replace_title() only
1334                 $count = count($this->rows);
1335                 $limit = intval($limit);
1336                 if ($limit != 0) $limit = max(1, $limit);
1337                 if ($limit != 0 && $count > $limit) {
1338                         $source[] = str_replace(
1339                                 array('$1',   '$2'  ),
1340                                 array($count, $limit),
1341                                 plugin_tracker_message('msg_limit')
1342                         ) . "\n";
1343                         $rows  = array_slice($this->rows, 0, $limit);
1344                 }
1345
1346                 // Loading template
1347                 // TODO: How do you feel single/multiple table rows with 'c'(decolation)?
1348                 $matches = $t_header = $t_body = $t_footer = array();
1349                 $template = plugin_tracker_get_source($list);
1350                 if ($template === FALSE) {
1351                         $this->error = 'Page not found or seems empty: ' . $list;
1352                         return FALSE;
1353                 }
1354                 foreach ($template as $line) {
1355                         if (preg_match('/^\|.+\|([hfc])$/i', $line, $matches)) {
1356                                 if (strtolower($matches[1]) == 'f') {
1357                                         $t_footer[] = $line;    // Table footer
1358                                 } else {
1359                                         $t_header[] = $line;    // Table header, or decoration
1360                                 }
1361                         } else {
1362                                 $t_body[]   = $line;
1363                         }
1364                 }
1365                 unset($template);
1366
1367                 // Header and decolation
1368                 foreach($t_header as $line) {
1369                         $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1370                 }
1371                 unset($t_header);
1372                 // Repeat
1373                 foreach ($rows as $row) {
1374                         $this->_row = $row;
1375                         // Body
1376                         foreach ($t_body as $line) {
1377                                 if (ltrim($line) != '') {
1378                                         $this->_the_first_character_of_the_line = $line[0];
1379                                         $line = preg_replace_callback($regex, array(& $this, '_replace_item'), $line);
1380                                 }
1381                                 $source[] = $line;
1382                         }
1383                 }
1384                 unset($t_body);
1385                 // Footer
1386                 foreach($t_footer as $line) {
1387                         $source[] = preg_replace_callback($regex, array(& $this, '_replace_title'), $line);
1388                 }
1389                 unset($t_footer);
1390
1391                 return implode('', $source);
1392         }
1393 }
1394
1395 // Roughly checking listed fields from template
1396 // " [field1] [field2,style1] " => array('fielld', 'field2')
1397 function plugin_tracker_field_pickup($string = '')
1398 {
1399         if (! is_string($string) || empty($string)) return array();
1400
1401         $fieldnames = array();
1402
1403         $matches = array();
1404         preg_match_all('/\[([^\[\]]+)\]/', $string, $matches);
1405         unset($matches[0]);
1406
1407         foreach ($matches[1] as $match) {
1408                 $params = explode(',', $match, 2);
1409                 if (isset($params[0])) {
1410                         $fieldnames[$params[0]] = TRUE;
1411                 }
1412         }
1413
1414         return array_keys($fieldnames);
1415 }
1416
1417 function plugin_tracker_get_source($page, $join = FALSE)
1418 {
1419         $source = get_source($page, TRUE, $join);
1420         if ($source === FALSE) return FALSE;
1421
1422         return preg_replace(
1423                  array(
1424                         '/^#freeze\s*$/im',
1425                         '/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m',      // Remove fixed-heading anchors
1426                 ),
1427                 array(
1428                         '',
1429                         '$1$2',
1430                 ),
1431                 $source
1432         );
1433 }
1434
1435 // Escape special characters not to break Wiki syntax
1436 function plugin_tracker_escape($string, $syntax_hint = '')
1437 {
1438         // Default: line-oriented
1439         $from = array("\n",   "\r"  );
1440         $to   = array('&br;', '&br;');
1441
1442         if ($syntax_hint == '|' || $syntax_hint == ':') {
1443                 // <table> or <dl> Wiki syntax: Excape '|'
1444                 $from[] = '|';
1445                 $to[]   = '&#x7c;';
1446         } else if ($syntax_hint == ',') {
1447                 // <table> by comma
1448                 $from[] = ',';
1449                 $to[]   = '&#x2c;';
1450         }
1451         return str_replace($from, $to, $string);
1452 }
1453
1454 function plugin_tracker_message($key)
1455 {
1456         global $_tracker_messages;
1457         return isset($_tracker_messages[$key]) ? $_tracker_messages[$key] : 'NOMESSAGE';
1458 }
1459
1460 ?>