OSDN Git Service

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