OSDN Git Service

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