OSDN Git Service

BugTrack/560 Get timestamps of tracker_list pages from cache
[pukiwiki/pukiwiki.git] / plugin / tracker.inc.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone
3 // tracker.inc.php
4 // Copyright 2003-2018 PukiWiki Development Team
5 // License: GPL v2 or (at your option) any later version
6 //
7 // Issue tracker plugin (See Also bugtrack plugin)
8
9 // tracker_listで表示しないページ名(正規表現で)
10 // 'SubMenu'ページ および '/'を含むページを除外する
11 define('TRACKER_LIST_EXCLUDE_PATTERN','#^SubMenu$|/#');
12 // 制限しない場合はこちら
13 //define('TRACKER_LIST_EXCLUDE_PATTERN','#(?!)#');
14
15 // 項目の取り出しに失敗したページを一覧に表示する
16 define('TRACKER_LIST_SHOW_ERROR_PAGE',TRUE);
17
18 // Use cache
19 define('TRACKER_LIST_USE_CACHE', TRUE);
20
21 function plugin_tracker_convert()
22 {
23         global $vars;
24
25         $script = get_base_uri();
26         if (PKWK_READONLY) return ''; // Show nothing
27
28         $base = $refer = $vars['page'];
29
30         $config_name = 'default';
31         $form = 'form';
32         $options = array();
33         if (func_num_args())
34         {
35                 $args = func_get_args();
36                 switch (count($args))
37                 {
38                         case 3:
39                                 $options = array_splice($args,2);
40                         case 2:
41                                 $args[1] = get_fullname($args[1],$base);
42                                 $base = is_pagename($args[1]) ? $args[1] : $base;
43                         case 1:
44                                 $config_name = ($args[0] != '') ? $args[0] : $config_name;
45                                 list($config_name,$form) = array_pad(explode('/',$config_name,2),2,$form);
46                 }
47         }
48
49         $config = new Config('plugin/tracker/'.$config_name);
50
51         if (!$config->read())
52         {
53                 return "<p>config file '".htmlsc($config_name)."' not found.</p>";
54         }
55
56         $config->config_name = $config_name;
57
58         $fields = plugin_tracker_get_fields($base,$refer,$config);
59
60         $form = $config->page.'/'.$form;
61         if (!is_page($form))
62         {
63                 return "<p>config file '".make_pagelink($form)."' not found.</p>";
64         }
65         $retval = convert_html(plugin_tracker_get_source($form));
66         $hiddens = '';
67
68         foreach (array_keys($fields) as $name)
69         {
70                 $replace = $fields[$name]->get_tag();
71                 if (is_a($fields[$name],'Tracker_field_hidden'))
72                 {
73                         $hiddens .= $replace;
74                         $replace = '';
75                 }
76                 $retval = str_replace("[$name]",$replace,$retval);
77         }
78         return <<<EOD
79 <form enctype="multipart/form-data" action="$script" method="post">
80 <div>
81 $retval
82 $hiddens
83 </div>
84 </form>
85 EOD;
86 }
87 function plugin_tracker_action()
88 {
89         global $post, $vars, $now;
90
91         if (PKWK_READONLY) die_message('PKWK_READONLY prohibits editing');
92
93         $config_name = array_key_exists('_config',$post) ? $post['_config'] : '';
94
95         $config = new Config('plugin/tracker/'.$config_name);
96         if (!$config->read())
97         {
98                 return "<p>config file '".htmlsc($config_name)."' not found.</p>";
99         }
100         $config->config_name = $config_name;
101         $source = $config->page.'/page';
102
103         $refer = array_key_exists('_refer',$post) ? $post['_refer'] : $post['_base'];
104
105         if (!is_pagename($refer))
106         {
107                 return array(
108                         'msg'=>'cannot write',
109                         'body'=>'page name ('.htmlsc($refer).') is not valid.'
110                 );
111         }
112         if (!is_page($source))
113         {
114                 return array(
115                         'msg'=>'cannot write',
116                         'body'=>'page template ('.htmlsc($source).') is not exist.'
117                 );
118         }
119         // ページ名を決定
120         $base = $post['_base'];
121         if (!is_pagename($base))
122         {
123                 return array(
124                         'msg'=>'cannot write',
125                         'body'=>'page name ('.htmlsc($base).') is not valid.'
126                 );
127         }
128         $name = (array_key_exists('_name',$post)) ? $post['_name'] : '';
129         $_page = (array_key_exists('_page',$post)) ? $post['_page'] : '';
130         if (is_pagename($_page)) {
131                 // Create _page page if _page is in parameters
132                 $page = $real = $_page;
133         } else if (is_pagename($name)) {
134                 // Create "$base/$name" page if _name is in parameters
135                 $real = $name;
136                 $page = get_fullname('./' . $name, $base);
137         } else {
138                 $page = '';
139         }
140         if (!is_pagename($page) || is_page($page)) {
141                 // Need new page name => Get last article number + 1
142                 $page_list = plugin_tracker_get_page_list($base, false);
143                 usort($page_list, '_plugin_tracker_list_paganame_compare');
144                 if (count($page_list) === 0) {
145                         $num = 1;
146                 } else {
147                         $latest_page = $page_list[count($page_list) - 1]['name'];
148                         $num = intval(substr($latest_page, strlen($base) + 1)) + 1;
149                 }
150                 $real = '' . $num;
151                 $page = $base . '/' . $num;
152         }
153         // ページデータを生成
154         $postdata = plugin_tracker_get_source($source);
155
156         // 規定のデータ
157         $_post = array_merge($post,$_FILES);
158         $_post['_date'] = $now;
159         $_post['_page'] = $page;
160         $_post['_name'] = $name;
161         $_post['_real'] = $real;
162         // $_post['_refer'] = $_post['refer'];
163
164         $fields = plugin_tracker_get_fields($page,$refer,$config);
165
166         check_editable($page, true, true);
167         // Creating an empty page, before attaching files
168         touch(get_filename($page));
169
170         foreach (array_keys($fields) as $key)
171         {
172                 $value = array_key_exists($key,$_post) ?
173                         $fields[$key]->format_value($_post[$key]) : '';
174
175                 foreach (array_keys($postdata) as $num)
176                 {
177                         if (trim($postdata[$num]) == '')
178                         {
179                                 continue;
180                         }
181                         $postdata[$num] = str_replace(
182                                 "[$key]",
183                                 ($postdata[$num]{0} == '|' or $postdata[$num]{0} == ':') ?
184                                         str_replace('|','&#x7c;',$value) : $value,
185                                 $postdata[$num]
186                         );
187                 }
188         }
189
190         // Writing page data, without touch
191         page_write($page, join('', $postdata));
192         pkwk_headers_sent();
193         header('Location: ' . get_page_uri($page, PKWK_URI_ROOT));
194         exit;
195 }
196
197 /**
198  * Page_list comparator
199  */
200 function _plugin_tracker_list_paganame_compare($a, $b)
201 {
202         return strnatcmp($a['name'], $b['name']);
203 }
204
205 /**
206  * Get page list for "$page/"
207  */
208 function plugin_tracker_get_page_list($page, $needs_filetime) {
209         $page_list = array();
210         $pattern = $page . '/';
211         $pattern_len = strlen($pattern);
212         foreach (get_existpages() as $p) {
213                 if (strncmp($p, $pattern, $pattern_len) === 0 && pkwk_ctype_digit(substr($p, $pattern_len))) {
214                         if ($needs_filetime) {
215                                 $page_list[] = array('name'=>$p,'filetime'=>get_filetime($p));
216                         } else {
217                                 $page_list[] = array('name'=>$p);
218                         }
219                 }
220         }
221         return $page_list;
222 }
223
224
225 /*
226 function plugin_tracker_inline()
227 {
228         global $vars;
229
230         if (PKWK_READONLY) return ''; // Show nothing
231
232         $args = func_get_args();
233         if (count($args) < 3)
234         {
235                 return FALSE;
236         }
237         $body = array_pop($args);
238         list($config_name,$field) = $args;
239
240         $config = new Config('plugin/tracker/'.$config_name);
241
242         if (!$config->read())
243         {
244                 return "config file '".htmlsc($config_name)."' not found.";
245         }
246
247         $config->config_name = $config_name;
248
249         $fields = plugin_tracker_get_fields($vars['page'],$vars['page'],$config);
250         $fields[$field]->default_value = $body;
251         return $fields[$field]->get_tag();
252 }
253 */
254 // フィールドオブジェクトを構築する
255 function plugin_tracker_get_fields($base,$refer,&$config)
256 {
257         global $now,$_tracker_messages;
258
259         $fields = array();
260         // 予約語
261         foreach (array(
262                 '_date'=>'text',    // 投稿日時
263                 '_update'=>'date',  // 最終更新
264                 '_past'=>'past',    // 経過(passage)
265                 '_page'=>'page',    // ページ名
266                 '_name'=>'text',    // 指定されたページ名
267                 '_real'=>'real',    // 実際のページ名
268                 '_refer'=>'page',   // 参照元(フォームのあるページ)
269                 '_base'=>'page',    // 基準ページ
270                 '_submit'=>'submit' // 追加ボタン
271                 ) as $field=>$class)
272         {
273                 $class = 'Tracker_field_'.$class;
274                 $fields[$field] = new $class(array($field,$_tracker_messages["btn$field"],'','20',''),$base,$refer,$config);
275         }
276
277         foreach ($config->get('fields') as $field)
278         {
279                 // 0=>項目名 1=>見出し 2=>形式 3=>オプション 4=>デフォルト値
280                 $class = 'Tracker_field_'.$field[2];
281                 if (!class_exists($class))
282                 { // デフォルト
283                         $class = 'Tracker_field_text';
284                         $field[2] = 'text';
285                         $field[3] = '20';
286                 }
287                 $fields[$field[0]] = new $class($field,$base,$refer,$config);
288         }
289         return $fields;
290 }
291 // フィールドクラス
292 class Tracker_field
293 {
294         var $name;
295         var $title;
296         var $values;
297         var $default_value;
298         var $page;
299         var $refer;
300         var $config;
301         var $data;
302         var $sort_type = SORT_REGULAR;
303         var $id = 0;
304
305         function Tracker_field($field,$page,$refer,&$config)
306         {
307                 $this->__construct($field, $page, $refer, $config);
308         }
309         function __construct($field,$page,$refer,&$config)
310         {
311                 global $post;
312                 static $id = 0;
313
314                 $this->id = ++$id;
315                 $this->name = $field[0];
316                 $this->title = $field[1];
317                 $this->values = explode(',',$field[3]);
318                 $this->default_value = $field[4];
319                 $this->page = $page;
320                 $this->refer = $refer;
321                 $this->config = &$config;
322                 $this->data = array_key_exists($this->name,$post) ? $post[$this->name] : '';
323         }
324         function get_tag()
325         {
326         }
327         function get_style($str)
328         {
329                 return '%s';
330         }
331         function format_value($value)
332         {
333                 return $value;
334         }
335         function format_cell($str)
336         {
337                 return $str;
338         }
339         function get_value($value)
340         {
341                 return $value;
342         }
343 }
344 class Tracker_field_text extends Tracker_field
345 {
346         var $sort_type = SORT_STRING;
347
348         function get_tag()
349         {
350                 $s_name = htmlsc($this->name);
351                 $s_size = htmlsc($this->values[0]);
352                 $s_value = htmlsc($this->default_value);
353                 return "<input type=\"text\" name=\"$s_name\" size=\"$s_size\" value=\"$s_value\" />";
354         }
355 }
356 class Tracker_field_page extends Tracker_field_text
357 {
358         var $sort_type = SORT_STRING;
359
360         function format_value($value)
361         {
362                 global $WikiName;
363
364                 $value = strip_bracket($value);
365                 if (is_pagename($value))
366                 {
367                         $value = "[[$value]]";
368                 }
369                 return parent::format_value($value);
370         }
371 }
372 class Tracker_field_real extends Tracker_field_text
373 {
374         var $sort_type = SORT_REGULAR;
375 }
376 class Tracker_field_title extends Tracker_field_text
377 {
378         var $sort_type = SORT_STRING;
379
380         function format_cell($str)
381         {
382                 make_heading($str);
383                 return $str;
384         }
385 }
386 class Tracker_field_textarea extends Tracker_field
387 {
388         var $sort_type = SORT_STRING;
389
390         function get_tag()
391         {
392                 $s_name = htmlsc($this->name);
393                 $s_cols = htmlsc($this->values[0]);
394                 $s_rows = htmlsc($this->values[1]);
395                 $s_value = htmlsc($this->default_value);
396                 return "<textarea name=\"$s_name\" cols=\"$s_cols\" rows=\"$s_rows\">$s_value</textarea>";
397         }
398         function format_cell($str)
399         {
400                 $str = preg_replace('/[\r\n]+/','',$str);
401                 if (!empty($this->values[2]) and strlen($str) > ($this->values[2] + 3))
402                 {
403                         $str = mb_substr($str,0,$this->values[2]).'...';
404                 }
405                 return $str;
406         }
407 }
408
409 class Tracker_field_format extends Tracker_field
410 {
411         var $sort_type = SORT_STRING;
412
413         var $styles = array();
414         var $formats = array();
415
416         function Tracker_field_format($field,$page,$refer,&$config)
417         {
418                 $this->__construct($field, $page, $refer, $config);
419         }
420         function __construct($field,$page,$refer,&$config)
421         {
422                 parent::__construct($field,$page,$refer,$config);
423
424                 foreach ($this->config->get($this->name) as $option)
425                 {
426                         list($key,$style,$format) = array_pad(array_map('trim',$option),3,'');
427                         if ($style != '')
428                         {
429                                 $this->styles[$key] = $style;
430                         }
431                         if ($format != '')
432                         {
433                                 $this->formats[$key] = $format;
434                         }
435                 }
436         }
437         function get_tag()
438         {
439                 $s_name = htmlsc($this->name);
440                 $s_size = htmlsc($this->values[0]);
441                 return "<input type=\"text\" name=\"$s_name\" size=\"$s_size\" />";
442         }
443         function get_key($str)
444         {
445                 return ($str == '') ? 'IS NULL' : 'IS NOT NULL';
446         }
447         function format_value($str)
448         {
449                 if (is_array($str))
450                 {
451                         return join(', ',array_map(array($this,'format_value'),$str));
452                 }
453                 $key = $this->get_key($str);
454                 return array_key_exists($key,$this->formats) ? str_replace('%s',$str,$this->formats[$key]) : $str;
455         }
456         function get_style($str)
457         {
458                 $key = $this->get_key($str);
459                 return array_key_exists($key,$this->styles) ? $this->styles[$key] : '%s';
460         }
461 }
462 class Tracker_field_file extends Tracker_field_format
463 {
464         var $sort_type = SORT_STRING;
465
466         function get_tag()
467         {
468                 $s_name = htmlsc($this->name);
469                 $s_size = htmlsc($this->values[0]);
470                 return "<input type=\"file\" name=\"$s_name\" size=\"$s_size\" />";
471         }
472         function format_value($str)
473         {
474                 if (array_key_exists($this->name,$_FILES))
475                 {
476                         require_once(PLUGIN_DIR.'attach.inc.php');
477                         $result = attach_upload($_FILES[$this->name],$this->page);
478                         if ($result['result']) // アップロード成功
479                         {
480                                 return parent::format_value($this->page.'/'.$_FILES[$this->name]['name']);
481                         }
482                 }
483                 // ファイルが指定されていないか、アップロードに失敗
484                 return parent::format_value('');
485         }
486 }
487 class Tracker_field_radio extends Tracker_field_format
488 {
489         var $sort_type = SORT_NUMERIC;
490
491         function get_tag()
492         {
493                 $s_name = htmlsc($this->name);
494                 $retval = '';
495                 $id = 0;
496                 foreach ($this->config->get($this->name) as $option)
497                 {
498                         $s_option = htmlsc($option[0]);
499                         $checked = trim($option[0]) == trim($this->default_value) ? ' checked="checked"' : '';
500                         ++$id;
501                         $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
502                         $retval .= '<input type="radio" name="' .  $s_name . '" id="' . $s_id .
503                                 '" value="' . $s_option . '"' . $checked . ' />' .
504                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
505                 }
506
507                 return $retval;
508         }
509         function get_key($str)
510         {
511                 return $str;
512         }
513         function get_value($value)
514         {
515                 static $options = array();
516                 if (!array_key_exists($this->name,$options))
517                 {
518                         // 'reset' means function($arr) { return $arr[0]; }
519                         $options[$this->name] = array_flip(array_map('reset',$this->config->get($this->name)));
520                 }
521                 return array_key_exists($value,$options[$this->name]) ? $options[$this->name][$value] : $value;
522         }
523 }
524 class Tracker_field_select extends Tracker_field_radio
525 {
526         var $sort_type = SORT_NUMERIC;
527
528         function get_tag($empty=FALSE)
529         {
530                 $s_name = htmlsc($this->name);
531                 $s_size = (array_key_exists(0,$this->values) and is_numeric($this->values[0])) ?
532                         ' size="'.htmlsc($this->values[0]).'"' : '';
533                 $s_multiple = (array_key_exists(1,$this->values) and strtolower($this->values[1]) == 'multiple') ?
534                         ' multiple="multiple"' : '';
535                 $retval = "<select name=\"{$s_name}[]\"$s_size$s_multiple>\n";
536                 if ($empty)
537                 {
538                         $retval .= " <option value=\"\"></option>\n";
539                 }
540                 $defaults = array_flip(preg_split('/\s*,\s*/',$this->default_value,-1,PREG_SPLIT_NO_EMPTY));
541                 foreach ($this->config->get($this->name) as $option)
542                 {
543                         $s_option = htmlsc($option[0]);
544                         $selected = array_key_exists(trim($option[0]),$defaults) ? ' selected="selected"' : '';
545                         $retval .= " <option value=\"$s_option\"$selected>$s_option</option>\n";
546                 }
547                 $retval .= "</select>";
548
549                 return $retval;
550         }
551 }
552 class Tracker_field_checkbox extends Tracker_field_radio
553 {
554         var $sort_type = SORT_NUMERIC;
555
556         function get_tag($empty=FALSE)
557         {
558                 $s_name = htmlsc($this->name);
559                 $defaults = array_flip(preg_split('/\s*,\s*/',$this->default_value,-1,PREG_SPLIT_NO_EMPTY));
560                 $retval = '';
561                 $id = 0;
562                 foreach ($this->config->get($this->name) as $option)
563                 {
564                         $s_option = htmlsc($option[0]);
565                         $checked = array_key_exists(trim($option[0]),$defaults) ?
566                                 ' checked="checked"' : '';
567                         ++$id;
568                         $s_id = '_p_tracker_' . $s_name . '_' . $this->id . '_' . $id;
569                         $retval .= '<input type="checkbox" name="' . $s_name .
570                                 '[]" id="' . $s_id . '" value="' . $s_option . '"' . $checked . ' />' .
571                                 '<label for="' . $s_id . '">' . $s_option . '</label>' . "\n";
572                 }
573
574                 return $retval;
575         }
576 }
577 class Tracker_field_hidden extends Tracker_field_radio
578 {
579         var $sort_type = SORT_NUMERIC;
580
581         function get_tag($empty=FALSE)
582         {
583                 $s_name = htmlsc($this->name);
584                 $s_default = htmlsc($this->default_value);
585                 $retval = "<input type=\"hidden\" name=\"$s_name\" value=\"$s_default\" />\n";
586
587                 return $retval;
588         }
589 }
590 class Tracker_field_submit extends Tracker_field
591 {
592         function get_tag()
593         {
594                 $s_title = htmlsc($this->title);
595                 $s_page = htmlsc($this->page);
596                 $s_refer = htmlsc($this->refer);
597                 $s_config = htmlsc($this->config->config_name);
598
599                 return <<<EOD
600 <input type="submit" value="$s_title" />
601 <input type="hidden" name="plugin" value="tracker" />
602 <input type="hidden" name="_refer" value="$s_refer" />
603 <input type="hidden" name="_base" value="$s_page" />
604 <input type="hidden" name="_config" value="$s_config" />
605 EOD;
606         }
607 }
608 class Tracker_field_date extends Tracker_field
609 {
610         var $sort_type = SORT_NUMERIC;
611
612         function format_cell($timestamp)
613         {
614                 return format_date($timestamp);
615         }
616 }
617 class Tracker_field_past extends Tracker_field
618 {
619         var $sort_type = SORT_NUMERIC;
620
621         function format_cell($timestamp)
622         {
623                 return '&passage("' . get_date_atom($timestamp + LOCALZONE) . '");';
624         }
625         function get_value($value)
626         {
627                 return UTIME - $value;
628         }
629 }
630 ///////////////////////////////////////////////////////////////////////////
631 // 一覧表示
632 function plugin_tracker_list_convert()
633 {
634         global $vars, $_title_cannotread;
635
636         $config = 'default';
637         $page = $refer = $vars['page'];
638         $field = '_page';
639         $order = '';
640         $list = 'list';
641         $limit = NULL;
642         if (func_num_args())
643         {
644                 $args = func_get_args();
645                 switch (count($args))
646                 {
647                         case 4:
648                                 $limit = is_numeric($args[3]) ? $args[3] : $limit;
649                         case 3:
650                                 $order = $args[2];
651                         case 2:
652                                 $args[1] = get_fullname($args[1],$page);
653                                 $page = is_pagename($args[1]) ? $args[1] : $page;
654                         case 1:
655                                 $config = ($args[0] != '') ? $args[0] : $config;
656                                 list($config,$list) = array_pad(explode('/',$config,2),2,$list);
657                 }
658         }
659         if (!is_page_readable($page)) {
660                 $body = str_replace('$1', htmlsc($page), $_title_cannotread);
661                 return $body;
662         }
663         return plugin_tracker_getlist($page,$refer,$config,$list,$order,$limit);
664 }
665 function plugin_tracker_list_action()
666 {
667         global $vars, $_tracker_messages, $_title_cannotread;
668
669         $page = $refer = $vars['refer'];
670         $s_page = make_pagelink($page);
671         $config = $vars['config'];
672         $list = array_key_exists('list',$vars) ? $vars['list'] : 'list';
673         $order = array_key_exists('order',$vars) ? $vars['order'] : '_real:SORT_DESC';
674
675         if (!is_page_readable($page)) {
676                 $body = str_replace('$1', htmlsc($page), $_title_cannotread);
677                 return array(
678                         'msg' => $body,
679                         'body' => $body
680                 );
681         }
682         return array(
683                 'msg' => $_tracker_messages['msg_list'],
684                 'body'=> str_replace('$1',$s_page,$_tracker_messages['msg_back']).
685                         plugin_tracker_getlist($page,$refer,$config,$list,$order)
686         );
687 }
688 function plugin_tracker_getlist($page,$refer,$config_name,$list,$order='',$limit=NULL)
689 {
690         global $whatsdeleted;
691
692         $config = new Config('plugin/tracker/'.$config_name);
693         if (!$config->read())
694         {
695                 return "<p>config file '".htmlsc($config_name)."' is not exist.</p>";
696         }
697         $config->config_name = $config_name;
698
699         if (!is_page($config->page.'/'.$list))
700         {
701                 return "<p>config file '".make_pagelink($config->page.'/'.$list)."' not found.</p>";
702         }
703
704         $cache_enabled = defined('TRACKER_LIST_USE_CACHE') && TRACKER_LIST_USE_CACHE &&
705                 defined('JSON_UNESCAPED_UNICODE') && defined('PKWK_UTF8_ENABLE');
706         $cache_filepath = CACHE_DIR . encode($page) . '.tracker';
707         $cachedata = null;
708         $cache_format_version = 1;
709         if ($cache_enabled) {
710                 $config_filetime = get_filetime($config->page);
711                 $config_list_filetime = get_filetime($config->page.'/'. $list);
712                 if (file_exists($cache_filepath)) {
713                         $json_cached = pkwk_file_get_contents($cache_filepath);
714                         if ($json_cached) {
715                                 $wrapdata = json_decode($json_cached, true);
716                                 if (is_array($wrapdata) && isset($wrapdata['version'],
717                                         $wrapdata['html'], $wrapdata['refreshed_at'])) {
718                                         $cache_time_prev = $wrapdata['refreshed_at'];
719                                         if ($cache_format_version === $wrapdata['version']) {
720                                                 if ($config_filetime === $wrapdata['config_updated_at'] &&
721                                                         $config_list_filetime === $wrapdata['config_list_updated_at']) {
722                                                         $cachedata = $wrapdata;
723                                                 } else {
724                                                         // (Ignore) delete file
725                                                         unlink($cache_filepath);
726                                                 }
727                                         }
728                                 }
729                         }
730                 }
731         }
732         // Check recent.dat timestamp
733         $recent_dat_filemtime = filemtime(CACHE_DIR . PKWK_MAXSHOW_CACHE);
734         // Check RecentDeleted timestamp
735         $recent_deleted_filetime = get_filetime($whatsdeleted);
736         if (is_null($cachedata)) {
737                 $cachedata = array();
738         } else {
739                 if ($recent_dat_filemtile !== false) {
740                         if ($recent_dat_filemtime === $cachedata['recent_dat_filemtime'] &&
741                                 $recent_deleted_filetime === $cachedata['recent_deleted_filetime'] &&
742                                 $order === $cachedata['order']) {
743                                 // recent.dat is unchanged
744                                 // RecentDeleted is unchanged
745                                 // order is unchanged
746                                 return $cachedata['html'];
747                         }
748                 }
749         }
750         $cache_holder = $cachedata;
751         $tracker_list = new Tracker_list($page,$refer,$config,$list,$cache_holder);
752         if ($order === $cache_holder['order'] &&
753                 empty($tracker_list->newly_deleted_pages) &&
754                 empty($tracker_list->newly_updated_pages) &&
755                 !$tracker_list->link_update_required) {
756                 $result = $cache_holder['html'];
757         } else {
758                 $tracker_list->sort($order);
759                 $result = $tracker_list->toString($limit);
760         }
761         if ($cache_enabled) {
762                 $refreshed_at = time();
763                 $json = array(
764                         'refreshed_at' => $refreshed_at,
765                         'rows' => $tracker_list->rows,
766                         'html' => $result,
767                         'order' => $order,
768                         'config_updated_at' => $config_filetime,
769                         'config_list_updated_at' => $config_list_filetime,
770                         'recent_dat_filemtime' => $recent_dat_filemtime,
771                         'recent_deleted_filetime' => $recent_deleted_filetime,
772                         'link_pages' => $tracker_list->link_pages,
773                         'version' => $cache_format_version);
774                 $cache_body = json_encode($json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
775                 file_put_contents($cache_filepath, $cache_body, LOCK_EX);
776         }
777         return $result;
778 }
779
780 // 一覧クラス
781 class Tracker_list
782 {
783         var $page;
784         var $config;
785         var $list;
786         var $fields;
787         var $pattern;
788         var $pattern_fields;
789         var $rows;
790         var $order;
791         var $sort_keys;
792         var $newly_deleted_pages = array();
793         var $newly_updated_pages = array();
794
795         function Tracker_list($page,$refer,&$config,$list,&$cache_holder)
796         {
797                 $this->__construct($page, $refer, $config, $list, $cache_holder);
798         }
799         function __construct($page,$refer,&$config,$list,&$cache_holder)
800         {
801                 global $whatsdeleted, $_cached_page_filetime;
802                 $this->page = $page;
803                 $this->config = &$config;
804                 $this->list = $list;
805                 $this->fields = plugin_tracker_get_fields($page,$refer,$config);
806
807                 $pattern = join('',plugin_tracker_get_source($config->page.'/page'));
808                 // ブロックプラグインをフィールドに置換
809                 // #commentなどで前後に文字列の増減があった場合に、[_block_xxx]に吸い込ませるようにする
810                 $pattern = preg_replace('/^\#([^\(\s]+)(?:\((.*)\))?\s*$/m','[_block_$1]',$pattern);
811
812                 // パターンを生成
813                 $this->pattern = '';
814                 $this->pattern_fields = array();
815                 $pattern = preg_split('/\\\\\[(\w+)\\\\\]/',preg_quote($pattern,'/'),-1,PREG_SPLIT_DELIM_CAPTURE);
816                 while (count($pattern))
817                 {
818                         $this->pattern .= preg_replace('/\s+/','\\s*','(?>\\s*'.trim(array_shift($pattern)).'\\s*)');
819                         if (count($pattern))
820                         {
821                                 $field = array_shift($pattern);
822                                 $this->pattern_fields[] = $field;
823                                 $this->pattern .= '(.*?)';
824                         }
825                 }
826                 if (empty($cache_holder)) {
827                         // List pages and get contents (non-cache behavior)
828                         $this->rows = array();
829                         $pattern = "$page/";
830                         $pattern_len = strlen($pattern);
831                         foreach (get_existpages() as $_page)
832                         {
833                                 if (substr($_page, 0, $pattern_len) === $pattern)
834                                 {
835                                         $name = substr($_page,$pattern_len);
836                                         if (preg_match(TRACKER_LIST_EXCLUDE_PATTERN,$name))
837                                         {
838                                                 continue;
839                                         }
840                                         $this->add($_page,$name);
841                                 }
842                         }
843                         $this->link_pages = $this->get_filetimes($this->get_all_links());
844                 } else {
845                         // Cache-available behavior
846                         // Check RecentDeleted timestamp
847                         $cached_rows = $this->decode_cached_rows($cache_holder['rows']);
848                         $updated_linked_pages = array();
849                         $newly_deleted_pages = array();
850                         $pattern = "$page/";
851                         $pattern_len = strlen($pattern);
852                         $recent_deleted_filetime = get_filetime($whatsdeleted);
853                         $deleted_page_list = array();
854                         if ($recent_deleted_filetime !== $cache_holder['recent_deleted_filetime']) {
855                                 foreach (plugin_tracker_get_source($whatsdeleted) as $line) {
856                                         $m = null;
857                                         if (preg_match('#\[\[([^\]]+)\]\]#', $line, $m)) {
858                                                 $_page = $m[1];
859                                                 if (is_pagename($_page)) {
860                                                         $deleted_page_list[] = $m[1];
861                                                 }
862                                         }
863                                 }
864                                 foreach ($deleted_page_list as $_page) {
865                                         if (substr($_page, 0, $pattern_len) === $pattern) {
866                                                 $name = substr($_page, $pattern_len);
867                                                 if (!is_page($_page) && isset($cached_rows[$name]) &&
868                                                         !preg_match(TRACKER_LIST_EXCLUDE_PATTERN, $name)) {
869                                                         // This page was just deleted
870                                                         array_push($newly_deleted_pages, $_page);
871                                                         unset($cached_rows[$name]);
872                                                 }
873                                         }
874                                 }
875                         }
876                         $this->newly_deleted_pages = $newly_deleted_pages;
877                         $updated_pages = array();
878                         $this->rows = $cached_rows;
879                         // Check recent.dat timestamp
880                         $recent_dat_filemtime = filemtime(CACHE_DIR . PKWK_MAXSHOW_CACHE);
881                         $updated_page_list = array();
882                         if ($recent_dat_filemtime !== $cache_holder['recent_dat_filemtime']) {
883                                 // recent.dat was updated. Search which page was updated.
884                                 $target_pages = array();
885                                 // Active page file time (1 hour before timestamp of recent.dat)
886                                 $target_filetime = $cache_holder['recent_dat_filemtime'] - LOCALZONE - 60 * 60;
887                                 foreach (get_recent_files() as $_page=>$time) {
888                                         if ($time <= $target_filetime) {
889                                                 // Older updated pages
890                                                 break;
891                                         }
892                                         $updated_page_list[$_page] = $time;
893                                         $name = substr($_page, $pattern_len);
894                                         if (substr($_page, 0, $pattern_len) === $pattern) {
895                                                 $name = substr($_page, $pattern_len);
896                                                 if (preg_match(TRACKER_LIST_EXCLUDE_PATTERN, $name)) {
897                                                         continue;
898                                                 }
899                                                 // Tracker target page
900                                                 if (isset($this->rows[$name])) {
901                                                         // Existing page
902                                                         $row = $this->rows[$name];
903                                                         if ($row['_update'] === get_filetime($_page)) {
904                                                                 // Same as cache
905                                                                 continue;
906                                                         } else {
907                                                                 // Found updated page
908                                                                 $updated_pages[] = $_page;
909                                                                 unset($this->rows[$name]);
910                                                                 $this->add($_page, $name);
911                                                         }
912                                                 } else {
913                                                         // Add new page
914                                                         $updated_pages[] = $_page;
915                                                         $this->add($_page, $name);
916                                                 }
917                                         }
918                                 }
919                         }
920                         $this->newly_updated_pages = $updated_pages;
921                         $new_link_names = $this->get_all_links();
922                         $old_link_map = array();
923                         foreach ($cache_holder['link_pages'] as $link_page) {
924                                 $old_link_map[$link_page['page']] = $link_page['filetime'];
925                         }
926                         $new_link_map = $old_link_map;
927                         $link_update_required = false;
928                         foreach ($deleted_page_list as $_page) {
929                                 if (in_array($_page, $new_link_names)) {
930                                         if (isset($old_link_map[$_page])) {
931                                                 // This link keeps existing
932                                                 if (!is_page($_page)) {
933                                                         // OK. Confirmed the page doesn't exist
934                                                         if ($old_link_map[$_page] === 0) {
935                                                                 // Do nothing (From no-page to no-page)
936                                                         } else {
937                                                                 // This page was just deleted
938                                                                 $new_link_map[$_page] = get_filetime($_page);
939                                                                 $link_update_required = true;
940                                                         }
941                                                 }
942                                         } else {
943                                                 // This link was just added
944                                                 $new_link_map[$_page] = get_filetime($_page);
945                                                 $link_update_required = true;
946                                         }
947                                 }
948                         }
949                         foreach ($updated_page_list as $_page=>$time) {
950                                 if (in_array($_page, $new_link_names)) {
951                                         if (isset($old_link_map[$_page])) {
952                                                 // This link keeps existing
953                                                 if (is_page($_page)) {
954                                                         // OK. Confirmed the page now exists
955                                                         if ($old_link_map[$_page] === 0) {
956                                                                 // This page was just added
957                                                                 $new_link_map[$_page] = get_filetime($_page);
958                                                                 $link_update_required = true;
959                                                         } else {
960                                                                 // Do nothing (existing-page to existing-page)
961                                                         }
962                                                 }
963                                         } else {
964                                                 // This link was just added
965                                                 $new_link_map[$_page] = get_filetime($_page);
966                                                 $link_update_required = true;
967                                         }
968                                 }
969                         }
970                         $new_link_pages = array();
971                         foreach ($new_link_map as $_page => $time) {
972                                 $new_link_pages[] = array(
973                                         'page' => $_page,
974                                         'filetime' => $time,
975                                 );
976                         }
977                         $this->link_pages = $new_link_pages;
978                         $this->link_update_required = $link_update_required;
979                         $time_map_for_cache = $new_link_map;
980                         foreach ($this->rows as $row) {
981                                 $time_map_for_cache[$this->page . '/' . $row['_real']] = $row['_update'];
982                         }
983                         $_cached_page_filetime = $time_map_for_cache;
984                 }
985         }
986         function decode_cached_rows($decoded_rows)
987         {
988                 $ar = array();
989                 foreach ($decoded_rows as $row) {
990                         $ar[$row['_real']] = $row;
991                 }
992                 return $ar;
993         }
994         function get_all_links() {
995                 $ar = array();
996                 foreach ($this->rows as $row) {
997                         foreach ($row['_links'] as $link) {
998                                 $ar[$link] = 0;
999                         }
1000                 }
1001                 return array_keys($ar);
1002         }
1003         function get_filetimes($pages) {
1004                 $filetimes = array();
1005                 foreach ($pages as $page) {
1006                         $filetimes[] = array(
1007                                 'page' => $page,
1008                                 'filetime' => get_filetime($page),
1009                         );
1010                 }
1011                 return $filetimes;
1012         }
1013         function add($page,$name)
1014         {
1015                 static $moved = array();
1016
1017                 // 無限ループ防止
1018                 if (array_key_exists($name,$this->rows))
1019                 {
1020                         return;
1021                 }
1022
1023                 $source = plugin_tracker_get_source($page);
1024                 if (preg_match('/move\sto\s(.+)/',$source[0],$matches))
1025                 {
1026                         $page = strip_bracket(trim($matches[1]));
1027                         if (array_key_exists($page,$moved) or !is_page($page))
1028                         {
1029                                 return;
1030                         }
1031                         $moved[$page] = TRUE;
1032                         return $this->add($page,$name);
1033                 }
1034                 $source = join('',preg_replace('/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/','$1$2',$source));
1035
1036                 // Default value
1037                 $page_filetime = get_filetime($page);
1038                 $row = array(
1039                         '_page'  => "[[$page]]",
1040                         '_refer' => $this->page,
1041                         '_real'  => $name,
1042                         '_update'=> $page_filetime,
1043                         '_past'  => $page_filetime,
1044                 );
1045                 $links = array();
1046                 if ($row['_match'] = preg_match("/{$this->pattern}/s",$source,$matches))
1047                 {
1048                         array_shift($matches);
1049                         foreach ($this->pattern_fields as $key=>$field)
1050                         {
1051                                 $row[$field] = trim($matches[$key]);
1052                                 if ($field === '_refer') {
1053                                         continue;
1054                                 }
1055                                 $lmatch = null;
1056                                 if (preg_match('/\[\[([^\]\]]+)\]/', $row[$field], $lmatch)) {
1057                                         $link = $lmatch[1];
1058                                         if (is_pagename($link) && $link !== $this->page && $link !== $page) {
1059                                                 if (!in_array($link, $links)) {
1060                                                         $links[] = $link;
1061                                                 }
1062                                         }
1063                                 }
1064                         }
1065                 }
1066                 $row['_links'] = $links;
1067                 $this->rows[$name] = $row;
1068         }
1069         function compare($a, $b)
1070         {
1071                 foreach ($this->sort_keys as $sort_key)
1072                 {
1073                         $field = $sort_key['field'];
1074                         $dir = $sort_key['dir'];
1075                         $f = $this->fields[$field];
1076                         $sort_type = $f->sort_type;
1077                         $aVal = isset($a[$field]) ? $f->get_value($a[$field]) : '';
1078                         $bVal = isset($b[$field]) ? $f->get_value($b[$field]) : '';
1079                         $c = strnatcmp($aVal, $bVal) * ($dir === SORT_ASC ? 1 : -1);
1080                         if ($c === 0) continue;
1081                         return $c;
1082                 }
1083                 return 0;
1084         }
1085         function sort($order)
1086         {
1087                 if ($order == '')
1088                 {
1089                         return;
1090                 }
1091                 $names = array_flip(array_keys($this->fields));
1092                 $this->order = array();
1093                 foreach (explode(';',$order) as $item)
1094                 {
1095                         list($key,$dir) = array_pad(explode(':',$item),1,'ASC');
1096                         if (!array_key_exists($key,$names))
1097                         {
1098                                 continue;
1099                         }
1100                         switch (strtoupper($dir))
1101                         {
1102                                 case 'SORT_ASC':
1103                                 case 'ASC':
1104                                 case SORT_ASC:
1105                                         $dir = SORT_ASC;
1106                                         break;
1107                                 case 'SORT_DESC':
1108                                 case 'DESC':
1109                                 case SORT_DESC:
1110                                         $dir = SORT_DESC;
1111                                         break;
1112                                 default:
1113                                         continue;
1114                         }
1115                         $this->order[$key] = $dir;
1116                 }
1117                 $sort_keys = array();
1118                 foreach ($this->order as $field=>$order)
1119                 {
1120                         if (!array_key_exists($field,$names))
1121                         {
1122                                 continue;
1123                         }
1124                         $sort_keys[] = array('field' => $field, 'dir' => $order);
1125                 }
1126                 $this->sort_keys = $sort_keys;
1127                 usort($this->rows, array($this, 'compare'));
1128         }
1129         function replace_item($arr)
1130         {
1131                 $params = explode(',',$arr[1]);
1132                 $name = array_shift($params);
1133                 if ($name == '')
1134                 {
1135                         $str = '';
1136                 }
1137                 else if (array_key_exists($name,$this->items))
1138                 {
1139                         $str = $this->items[$name];
1140                         if (array_key_exists($name,$this->fields))
1141                         {
1142                                 $str = $this->fields[$name]->format_cell($str);
1143                         }
1144                 }
1145                 else
1146                 {
1147                         return $this->pipe ? str_replace('|','&#x7c;',$arr[0]) : $arr[0];
1148                 }
1149                 $style = count($params) ? $params[0] : $name;
1150                 if (array_key_exists($style,$this->items)
1151                         and array_key_exists($style,$this->fields))
1152                 {
1153                         $str = sprintf($this->fields[$style]->get_style($this->items[$style]),$str);
1154                 }
1155                 return $this->pipe ? str_replace('|','&#x7c;',$str) : $str;
1156         }
1157         function replace_title($arr)
1158         {
1159                 $field = $sort = $arr[1];
1160                 if ($sort == '_name' or $sort == '_page')
1161                 {
1162                         $sort = '_real';
1163                 }
1164                 if (!array_key_exists($field,$this->fields))
1165                 {
1166                         return $arr[0];
1167                 }
1168                 $dir = SORT_ASC;
1169                 $arrow = '';
1170                 $order = $this->order;
1171
1172                 if (is_array($order) && isset($order[$sort]))
1173                 {
1174                         // BugTrack2/106: Only variables can be passed by reference from PHP 5.0.5
1175                         $order_keys = array_keys($order); // with array_shift();
1176
1177                         $index = array_flip($order_keys);
1178                         $pos = 1 + $index[$sort];
1179                         $b_end = ($sort == array_shift($order_keys));
1180                         $b_order = ($order[$sort] == SORT_ASC);
1181                         $dir = ($b_end xor $b_order) ? SORT_ASC : SORT_DESC;
1182                         $arrow = '&br;'.($b_order ? '&uarr;' : '&darr;')."($pos)";
1183
1184                         unset($order[$sort], $order_keys);
1185                 }
1186                 $title = $this->fields[$field]->title;
1187                 $r_page = rawurlencode($this->page);
1188                 $r_config = rawurlencode($this->config->config_name);
1189                 $r_list = rawurlencode($this->list);
1190                 $_order = array("$sort:$dir");
1191                 if (is_array($order))
1192                         foreach ($order as $key=>$value)
1193                                 $_order[] = "$key:$value";
1194                 $r_order = rawurlencode(join(';',$_order));
1195
1196                 $script = get_base_uri(PKWK_URI_ABSOLUTE);
1197                 return "[[$title$arrow>$script?plugin=tracker_list&refer=$r_page&config=$r_config&list=$r_list&order=$r_order]]";
1198         }
1199         function toString($limit=NULL)
1200         {
1201                 global $_tracker_messages;
1202
1203                 $source = '';
1204                 $body = array();
1205
1206                 if ($limit !== NULL and count($this->rows) > $limit)
1207                 {
1208                         $source = str_replace(
1209                                 array('$1','$2'),
1210                                 array(count($this->rows),$limit),
1211                                 $_tracker_messages['msg_limit'])."\n";
1212                         $this->rows = array_splice($this->rows,0,$limit);
1213                 }
1214                 if (count($this->rows) == 0)
1215                 {
1216                         return '';
1217                 }
1218                 foreach (plugin_tracker_get_source($this->config->page.'/'.$this->list) as $line)
1219                 {
1220                         if (preg_match('/^\|(.+)\|[hHfFcC]$/',$line))
1221                         {
1222                                 $source .= preg_replace_callback('/\[([^\[\]]+)\]/',array(&$this,'replace_title'),$line);
1223                         }
1224                         else
1225                         {
1226                                 $body[] = $line;
1227                         }
1228                 }
1229                 foreach ($this->rows as $key=>$row)
1230                 {
1231                         if (!TRACKER_LIST_SHOW_ERROR_PAGE and !$row['_match'])
1232                         {
1233                                 continue;
1234                         }
1235                         $this->items = $row;
1236                         foreach ($body as $line)
1237                         {
1238                                 if (trim($line) == '')
1239                                 {
1240                                         // Ignore empty line
1241                                         continue;
1242                                 }
1243                                 $this->pipe = ($line{0} == '|' or $line{0} == ':');
1244                                 $source .= preg_replace_callback('/\[([^\[\]]+)\]/',array(&$this,'replace_item'),$line);
1245                         }
1246                 }
1247                 return convert_html($source);
1248         }
1249 }
1250 function plugin_tracker_get_source($page)
1251 {
1252         $source = get_source($page);
1253         // Delete anchor part of Headings (Example: "*Heading1 [#id] AAA" to "*Heading1 AAA")
1254         $s2 = preg_replace('/^(\*{1,3}.*)\[#[A-Za-z][\w-]+\](.*)$/m','$1$2',$source);
1255         // Delete #freeze
1256         $s3 = preg_replace('/^#freeze\s*$/im', '', $s2);
1257         // Delete #author line
1258         $s4 = preg_replace('/^#author\b[^\r\n]*$/im', '', $s3);
1259         return $s4;
1260 }