OSDN Git Service

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