OSDN Git Service

BugTrack/2508 Fix search result (FullWidth char / Ignore case)
[pukiwiki/pukiwiki.git] / lib / file.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone.
3 // file.php
4 // Copyright
5 //   2002-2020 PukiWiki Development Team
6 //   2001-2002 Originally written by yu-ji
7 // License: GPL v2 or (at your option) any later version
8 //
9 // File related functions
10
11 // RecentChanges
12 define('PKWK_MAXSHOW_ALLOWANCE', 10);
13 define('PKWK_MAXSHOW_CACHE', 'recent.dat');
14
15 // AutoLink
16 define('PKWK_AUTOLINK_REGEX_CACHE', 'autolink.dat');
17
18 // AutoAlias
19 define('PKWK_AUTOALIAS_REGEX_CACHE', 'autoalias.dat');
20
21 /**
22  * Get source(wiki text) data of the page
23  *
24  * @param $page page name
25  * @param $lock lock
26  * @param $join true: return string, false: return array of string
27  * @param $raw true: return file content as-is
28  * @return FALSE if error occurerd
29  */
30 function get_source($page = NULL, $lock = TRUE, $join = FALSE, $raw = FALSE)
31 {
32         //$result = NULL;       // File is not found
33         $result = $join ? '' : array();
34                 // Compat for "implode('', get_source($file))",
35                 //      -- this is slower than "get_source($file, TRUE, TRUE)"
36                 // Compat for foreach(get_source($file) as $line) {} not to warns
37
38         $path = get_filename($page);
39         if (file_exists($path)) {
40
41                 if ($lock) {
42                         $fp = @fopen($path, 'r');
43                         if ($fp === FALSE) return FALSE;
44                         flock($fp, LOCK_SH);
45                 }
46
47                 if ($join) {
48                         // Returns a value
49                         $size = filesize($path);
50                         if ($size === FALSE) {
51                                 $result = FALSE;
52                         } else if ($size == 0) {
53                                 $result = '';
54                         } else {
55                                 $result = fread($fp, $size);
56                                 if ($result !== FALSE) {
57                                         if ($raw) {
58                                                 return $result;
59                                         }
60                                         // Removing Carriage-Return
61                                         $result = str_replace("\r", '', $result);
62                                 }
63                         }
64                 } else {
65                         // Returns an array
66                         $result = file($path);
67                         if ($result !== FALSE) {
68                                 // Removing Carriage-Return
69                                 $result = str_replace("\r", '', $result);
70                         }
71                 }
72
73                 if ($lock) {
74                         flock($fp, LOCK_UN);
75                         @fclose($fp);
76                 }
77         }
78
79         return $result;
80 }
81
82 // Get last-modified filetime of the page
83 function get_filetime($page)
84 {
85         return is_page($page) ? filemtime(get_filename($page)) - LOCALZONE : 0;
86 }
87
88 /**
89  * Get last-modified filemtime (plain value) of the page.
90  *
91  * @param $page
92  */
93 function get_page_date_atom($page)
94 {
95         if (is_page($page)) {
96                 return get_date_atom(filemtime(get_filename($page)));
97         }
98         return null;
99 }
100
101 // Get physical file name of the page
102 function get_filename($page)
103 {
104         return DATA_DIR . encode($page) . '.txt';
105 }
106
107 // Put a data(wiki text) into a physical file(diff, backup, text)
108 function page_write($page, $postdata, $notimestamp = FALSE)
109 {
110         global $autoalias, $aliaspage;
111
112         if (PKWK_READONLY) return; // Do nothing
113
114         $postdata = make_str_rules($postdata);
115         $timestamp_to_keep = null;
116         if ($notimestamp) {
117                 $timestamp_to_keep = get_filetime($page);
118         }
119         $text_without_author = remove_author_info($postdata);
120         $postdata = add_author_info($text_without_author, $timestamp_to_keep);
121         $is_delete = empty($text_without_author);
122
123         // Do nothing when it has no changes
124         $oldpostdata = is_page($page) ? join('', get_source($page)) : '';
125         $oldtext_without_author = remove_author_info($oldpostdata);
126         if (!$is_delete && $text_without_author === $oldtext_without_author) {
127                 // Do nothing on updating with unchanged content
128                 return;
129         }
130         // Create and write diff
131         $diffdata    = do_diff($oldpostdata, $postdata);
132         file_write(DIFF_DIR, $page, $diffdata);
133
134         // Create backup
135         make_backup($page, $is_delete, $postdata); // Is $postdata null?
136
137         // Create wiki text
138         file_write(DATA_DIR, $page, $postdata, $notimestamp, $is_delete);
139
140         links_update($page);
141
142         // Update autoalias.dat (AutoAliasName)
143         if ($autoalias && $page === $aliaspage) {
144                 update_autoalias_cache_file();
145         }
146 }
147
148 // Modify original text with user-defined / system-defined rules
149 function make_str_rules($source)
150 {
151         global $str_rules, $fixed_heading_anchor;
152
153         $lines = explode("\n", $source);
154         $count = count($lines);
155
156         $modify    = TRUE;
157         $multiline = 0;
158         $matches   = array();
159         for ($i = 0; $i < $count; $i++) {
160                 $line = & $lines[$i]; // Modify directly
161
162                 // Ignore null string and preformatted texts
163                 if ($line == '' || $line{0} == ' ' || $line{0} == "\t") continue;
164
165                 // Modify this line?
166                 if ($modify) {
167                         if (! PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK &&
168                             $multiline == 0 &&
169                             preg_match('/#[^{]*(\{\{+)\s*$/', $line, $matches)) {
170                                 // Multiline convert plugin start
171                                 $modify    = FALSE;
172                                 $multiline = strlen($matches[1]); // Set specific number
173                         }
174                 } else {
175                         if (! PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK &&
176                             $multiline != 0 &&
177                             preg_match('/^\}{' . $multiline . '}\s*$/', $line)) {
178                                 // Multiline convert plugin end
179                                 $modify    = TRUE;
180                                 $multiline = 0;
181                         }
182                 }
183                 if ($modify === FALSE) continue;
184
185                 // Replace with $str_rules
186                 foreach ($str_rules as $pattern => $replacement)
187                         $line = preg_replace('/' . $pattern . '/', $replacement, $line);
188                 
189                 // Adding fixed anchor into headings
190                 if ($fixed_heading_anchor &&
191                     preg_match('/^(\*{1,3}.*?)(?:\[#([A-Za-z][\w-]*)\]\s*)?$/', $line, $matches) &&
192                     (! isset($matches[2]) || $matches[2] == '')) {
193                         // Generate unique id
194                         $anchor = generate_fixed_heading_anchor_id($matches[1]);
195                         $line = rtrim($matches[1]) . ' [#' . $anchor . ']';
196                 }
197         }
198
199         // Multiline part has no stopper
200         if (! PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK &&
201             $modify === FALSE && $multiline != 0)
202                 $lines[] = str_repeat('}', $multiline);
203
204         return implode("\n", $lines);
205 }
206
207 /**
208  * Add author plugin text for wiki text body
209  *
210  * @param string $wikitext
211  * @param integer $timestamp_to_keep Set null when not to keep timestamp
212  */
213 function add_author_info($wikitext, $timestamp_to_keep)
214 {
215         global $auth_user, $auth_user_fullname;
216         $author = preg_replace('/"/', '', $auth_user);
217         $fullname = $auth_user_fullname;
218         if (!$fullname && $author) {
219                 // Fullname is empty, use $author as its fullname
220                 $fullname = preg_replace('/^[^:]*:/', '', $author);
221         }
222         $datetime_to_keep = '';
223         if (!is_null($timestamp_to_keep)) {
224                 $datetime_to_keep .= ';' . get_date_atom($timestamp_to_keep + LOCALZONE);
225         }
226         $displayname = preg_replace('/"/', '', $fullname);
227         $user_prefix = get_auth_user_prefix();
228         $author_text = sprintf('#author("%s","%s","%s")',
229                 get_date_atom(UTIME + LOCALZONE) . $datetime_to_keep,
230                 ($author ? $user_prefix . $author : ''),
231                 $displayname) . "\n";
232         return $author_text . $wikitext;
233 }
234
235 function remove_author_info($wikitext)
236 {
237         return preg_replace('/^\s*#author\([^\n]*(\n|$)/m', '', $wikitext);
238 }
239
240 /**
241  * Remove author line from wikitext
242  */
243 function remove_author_header($wikitext)
244 {
245         $start = 0;
246         while (($pos = strpos($wikitext, "\n", $start)) != false) {
247                 $line = substr($wikitext, $start, $pos);
248                 $m = null;
249                 if (preg_match('/^#author\(/', $line, $m)) {
250                         // fond #author line, Remove this line only
251                         if ($start === 0) {
252                                 return substr($wikitext, $pos + 1);
253                         } else {
254                                 return substr($wikitext, 0, $start - 1) .
255                                         substr($wikitext, $pos + 1);
256                         }
257                 } else if (preg_match('/^#freeze(\W|$)/', $line, $m)) {
258                         // Found #freeze still in header
259                 } else {
260                         // other line, #author not found
261                         return $wikitext;
262                 }
263                 $start = $pos + 1;
264         }
265         return $wikitext;
266 }
267
268 /**
269  * Get author info from wikitext
270  */
271 function get_author_info($wikitext)
272 {
273         $start = 0;
274         while (($pos = strpos($wikitext, "\n", $start)) != false) {
275                 $line = substr($wikitext, $start, $pos);
276                 $m = null;
277                 if (preg_match('/^#author\(/', $line, $m)) {
278                         return $line;
279                 } else if (preg_match('/^#freeze(\W|$)/', $line, $m)) {
280                         // Found #freeze still in header
281                 } else {
282                         // other line, #author not found
283                         return null;
284                 }
285                 $start = $pos + 1;
286         }
287         return null;
288 }
289
290 /**
291  * Get updated datetime from author
292  */
293 function get_update_datetime_from_author($author_line) {
294         $m = null;
295         if (preg_match('/^#author\(\"([^\";]+)(?:;([^\";]+))?/', $author_line, $m)) {
296                 if ($m[2]) {
297                         return $m[2];
298                 } else if ($m[1]) {
299                         return $m[1];
300                 }
301         }
302         return null;
303 }
304
305 // Generate ID
306 function generate_fixed_heading_anchor_id($seed)
307 {
308         // A random alphabetic letter + 7 letters of random strings from md5()
309         return chr(mt_rand(ord('a'), ord('z'))) .
310                 substr(md5(uniqid(substr($seed, 0, 100), TRUE)),
311                 mt_rand(0, 24), 7);
312 }
313
314 // Read top N lines as an array
315 // (Use PHP file() function if you want to get ALL lines)
316 function file_head($file, $count = 1, $lock = TRUE, $buffer = 8192)
317 {
318         $array = array();
319
320         $fp = @fopen($file, 'r');
321         if ($fp === FALSE) return FALSE;
322         set_file_buffer($fp, 0);
323         if ($lock) flock($fp, LOCK_SH);
324         rewind($fp);
325         $index = 0;
326         while (! feof($fp)) {
327                 $line = fgets($fp, $buffer);
328                 if ($line != FALSE) $array[] = $line;
329                 if (++$index >= $count) break;
330         }
331         if ($lock) flock($fp, LOCK_UN);
332         if (! fclose($fp)) return FALSE;
333
334         return $array;
335 }
336
337 // Output to a file
338 function file_write($dir, $page, $str, $notimestamp = FALSE, $is_delete = FALSE)
339 {
340         global $_msg_invalidiwn, $notify, $notify_diff_only, $notify_subject;
341         global $whatsdeleted, $maxshow_deleted;
342
343         if (PKWK_READONLY) return; // Do nothing
344         if ($dir != DATA_DIR && $dir != DIFF_DIR) die('file_write(): Invalid directory');
345
346         $page = strip_bracket($page);
347         $file = $dir . encode($page) . '.txt';
348         $file_exists = file_exists($file);
349
350         // ----
351         // Delete?
352
353         if ($dir == DATA_DIR && $is_delete) {
354                 // Page deletion
355                 if (! $file_exists) return; // Ignore null posting for DATA_DIR
356
357                 // Update RecentDeleted (Add the $page)
358                 add_recent($page, $whatsdeleted, '', $maxshow_deleted);
359
360                 // Remove the page
361                 unlink($file);
362
363                 // Update RecentDeleted, and remove the page from RecentChanges
364                 lastmodified_add($whatsdeleted, $page);
365
366                 // Clear is_page() cache
367                 is_page($page, TRUE);
368
369                 return;
370
371         } else if ($dir == DIFF_DIR && $str === " \n") {
372                 return; // Ignore null posting for DIFF_DIR
373         }
374
375         // ----
376         // File replacement (Edit)
377
378         if (! is_pagename($page))
379                 die_message(str_replace('$1', htmlsc($page),
380                             str_replace('$2', 'WikiName', $_msg_invalidiwn)));
381
382         $str = rtrim(preg_replace('/' . "\r" . '/', '', $str)) . "\n";
383         $timestamp = ($file_exists && $notimestamp) ? filemtime($file) : FALSE;
384
385         $fp = fopen($file, 'a') or die('fopen() failed: ' .
386                 htmlsc(basename($dir) . '/' . encode($page) . '.txt') . 
387                 '<br />' . "\n" .
388                 'Maybe permission is not writable or filename is too long');
389         set_file_buffer($fp, 0);
390         flock($fp, LOCK_EX);
391         ftruncate($fp, 0);
392         rewind($fp);
393         fputs($fp, $str);
394         flock($fp, LOCK_UN);
395         fclose($fp);
396
397         if ($timestamp) pkwk_touch_file($file, $timestamp);
398
399         // Optional actions
400         if ($dir == DATA_DIR) {
401                 // Update RecentChanges (Add or renew the $page)
402                 if ($timestamp === FALSE) lastmodified_add($page);
403
404                 // Command execution per update
405                 if (defined('PKWK_UPDATE_EXEC') && PKWK_UPDATE_EXEC)
406                         system(PKWK_UPDATE_EXEC . ' > /dev/null &');
407
408         } else if ($dir == DIFF_DIR && $notify) {
409                 if ($notify_diff_only) $str = preg_replace('/^[^-+].*\n/m', '', $str);
410                 $footer['ACTION'] = 'Page update';
411                 $footer['PAGE']   = $page;
412                 $footer['URI']    = get_page_uri($page, PKWK_URI_ABSOLUTE);
413                 $footer['USER_AGENT']  = TRUE;
414                 $footer['REMOTE_ADDR'] = TRUE;
415                 pkwk_mail_notify($notify_subject, $str, $footer) or
416                         die('pkwk_mail_notify(): Failed');
417         }
418         if ($dir === DIFF_DIR) {
419                 pkwk_log_updates($page, $str);
420         }
421         is_page($page, TRUE); // Clear is_page() cache
422 }
423
424 // Update RecentDeleted
425 function add_recent($page, $recentpage, $subject = '', $limit = 0)
426 {
427         if (PKWK_READONLY || $limit == 0 || $page == '' || $recentpage == '' ||
428             check_non_list($page)) return;
429
430         // Load
431         $lines = $matches = array();
432         foreach (get_source($recentpage) as $line)
433                 if (preg_match('/^-(.+) - (\[\[.+\]\])$/', $line, $matches))
434                         $lines[$matches[2]] = $line;
435
436         $_page = '[[' . $page . ']]';
437
438         // Remove a report about the same page
439         if (isset($lines[$_page])) unset($lines[$_page]);
440
441         // Add
442         array_unshift($lines, '-' . format_date(UTIME) . ' - ' . $_page .
443                 htmlsc($subject) . "\n");
444
445         // Get latest $limit reports
446         $lines = array_splice($lines, 0, $limit);
447
448         // Update
449         $fp = fopen(get_filename($recentpage), 'w') or
450                 die_message('Cannot write page file ' .
451                 htmlsc($recentpage) .
452                 '<br />Maybe permission is not writable or filename is too long');
453         set_file_buffer($fp, 0);
454         flock($fp, LOCK_EX);
455         rewind($fp);
456         fputs($fp, '#freeze'    . "\n");
457         fputs($fp, '#norelated' . "\n"); // :)
458         fputs($fp, join('', $lines));
459         flock($fp, LOCK_UN);
460         fclose($fp);
461 }
462
463 // Update PKWK_MAXSHOW_CACHE itself (Add or renew about the $page) (Light)
464 // Use without $autolink
465 function lastmodified_add($update = '', $remove = '')
466 {
467         global $maxshow, $whatsnew, $autolink;
468
469         // AutoLink implimentation needs everything, for now
470         if ($autolink) {
471                 put_lastmodified(); // Try to (re)create ALL
472                 return;
473         }
474
475         if (($update == '' || check_non_list($update)) && $remove == '')
476                 return; // No need
477
478         $file = CACHE_DIR . PKWK_MAXSHOW_CACHE;
479         if (! file_exists($file)) {
480                 put_lastmodified(); // Try to (re)create ALL
481                 return;
482         }
483
484         // Open
485         pkwk_touch_file($file);
486         $fp = fopen($file, 'r+') or
487                 die_message('Cannot open ' . 'CACHE_DIR/' . PKWK_MAXSHOW_CACHE);
488         set_file_buffer($fp, 0);
489         flock($fp, LOCK_EX);
490
491         // Read (keep the order of the lines)
492         $recent_pages = $matches = array();
493         foreach(file_head($file, $maxshow + PKWK_MAXSHOW_ALLOWANCE, FALSE) as $line)
494                 if (preg_match('/^([0-9]+)\t(.+)/', $line, $matches))
495                         $recent_pages[$matches[2]] = $matches[1];
496
497         // Remove if it exists inside
498         if (isset($recent_pages[$update])) unset($recent_pages[$update]);
499         if (isset($recent_pages[$remove])) unset($recent_pages[$remove]);
500
501         // Add to the top: like array_unshift()
502         if ($update != '')
503                 $recent_pages = array($update => get_filetime($update)) + $recent_pages;
504
505         // Check
506         $abort = count($recent_pages) < $maxshow;
507
508         if (! $abort) {
509                 // Write
510                 ftruncate($fp, 0);
511                 rewind($fp);
512                 foreach ($recent_pages as $_page=>$time)
513                         fputs($fp, $time . "\t" . $_page . "\n");
514         }
515
516         flock($fp, LOCK_UN);
517         fclose($fp);
518
519         if ($abort) {
520                 put_lastmodified(); // Try to (re)create ALL
521                 return;
522         }
523
524
525
526         // ----
527         // Update the page 'RecentChanges'
528
529         $recent_pages = array_splice($recent_pages, 0, $maxshow);
530         $file = get_filename($whatsnew);
531
532         // Open
533         pkwk_touch_file($file);
534         $fp = fopen($file, 'r+') or
535                 die_message('Cannot open ' . htmlsc($whatsnew));
536         set_file_buffer($fp, 0);
537         flock($fp, LOCK_EX);
538
539         // Recreate
540         ftruncate($fp, 0);
541         rewind($fp);
542         foreach ($recent_pages as $_page=>$time)
543                 fputs($fp, '-' . htmlsc(format_date($time)) .
544                         ' - ' . '[[' . htmlsc($_page) . ']]' . "\n");
545         fputs($fp, '#norelated' . "\n"); // :)
546
547         flock($fp, LOCK_UN);
548         fclose($fp);
549 }
550
551 // Re-create PKWK_MAXSHOW_CACHE (Heavy)
552 function put_lastmodified()
553 {
554         global $maxshow, $whatsnew, $autolink;
555
556         if (PKWK_READONLY) return; // Do nothing
557
558         // Get WHOLE page list
559         $pages = get_existpages();
560
561         // Check ALL filetime
562         $recent_pages = array();
563         foreach($pages as $page)
564                 if ($page !== $whatsnew && ! check_non_list($page))
565                         $recent_pages[$page] = get_filetime($page);
566
567         // Sort decending order of last-modification date
568         arsort($recent_pages, SORT_NUMERIC);
569
570         // Cut unused lines
571         // BugTrack2/179: array_splice() will break integer keys in hashtable
572         $count   = $maxshow + PKWK_MAXSHOW_ALLOWANCE;
573         $_recent = array();
574         foreach($recent_pages as $key=>$value) {
575                 unset($recent_pages[$key]);
576                 $_recent[$key] = $value;
577                 if (--$count < 1) break;
578         }
579         $recent_pages = & $_recent;
580
581         // Re-create PKWK_MAXSHOW_CACHE
582         $file = CACHE_DIR . PKWK_MAXSHOW_CACHE;
583         pkwk_touch_file($file);
584         $fp = fopen($file, 'r+') or
585                 die_message('Cannot open' . 'CACHE_DIR/' . PKWK_MAXSHOW_CACHE);
586         set_file_buffer($fp, 0);
587         flock($fp, LOCK_EX);
588         ftruncate($fp, 0);
589         rewind($fp);
590         foreach ($recent_pages as $page=>$time)
591                 fputs($fp, $time . "\t" . $page . "\n");
592         flock($fp, LOCK_UN);
593         fclose($fp);
594
595         // Create RecentChanges
596         $file = get_filename($whatsnew);
597         pkwk_touch_file($file);
598         $fp = fopen($file, 'r+') or
599                 die_message('Cannot open ' . htmlsc($whatsnew));
600         set_file_buffer($fp, 0);
601         flock($fp, LOCK_EX);
602         ftruncate($fp, 0);
603         rewind($fp);
604         foreach (array_keys($recent_pages) as $page) {
605                 $time      = $recent_pages[$page];
606                 $s_lastmod = htmlsc(format_date($time));
607                 $s_page    = htmlsc($page);
608                 fputs($fp, '-' . $s_lastmod . ' - [[' . $s_page . ']]' . "\n");
609         }
610         fputs($fp, '#norelated' . "\n"); // :)
611         flock($fp, LOCK_UN);
612         fclose($fp);
613
614         // For AutoLink
615         if ($autolink) {
616                 autolink_pattern_write(CACHE_DIR . PKWK_AUTOLINK_REGEX_CACHE,
617                         get_autolink_pattern($pages, $autolink));
618         }
619 }
620
621 /**
622  * Get recent files
623  *
624  * @return Array of (file => time)
625  */
626 function get_recent_files()
627 {
628         $recentfile = CACHE_DIR . PKWK_MAXSHOW_CACHE;
629         $lines = file($recentfile);
630         if (!$lines) return array();
631         $files = array();
632         foreach ($lines as $line) {
633                 list ($time, $file) = explode("\t", rtrim($line));
634                 $files[$file] = $time;
635         }
636         return $files;
637 }
638
639 /**
640  * Update RecentChanges page / Invalidate recent.dat
641  */
642 function delete_recent_changes_cache() {
643         $file = CACHE_DIR . PKWK_MAXSHOW_CACHE;
644         unlink($file);
645 }
646
647 // update autolink data
648 function autolink_pattern_write($filename, $autolink_pattern)
649 {
650         list($pattern, $pattern_a, $forceignorelist) = $autolink_pattern;
651
652         $fp = fopen($filename, 'w') or
653                 die_message('Cannot open ' . $filename);
654         set_file_buffer($fp, 0);
655         flock($fp, LOCK_EX);
656         rewind($fp);
657         fputs($fp, $pattern   . "\n");
658         fputs($fp, $pattern_a . "\n");
659         fputs($fp, join("\t", $forceignorelist) . "\n");
660         flock($fp, LOCK_UN);
661         fclose($fp);
662 }
663
664 // Update AutoAlias regex cache
665 function update_autoalias_cache_file()
666 {
667         global $autoalias; // Disable (0), Enable (min-length)
668         $aliases = get_autoaliases();
669         if (empty($aliases)) {
670                 // Remove
671                 @unlink(CACHE_DIR . PKWK_AUTOALIAS_REGEX_CACHE);
672         } else {
673                 // Create or Update
674                 autolink_pattern_write(CACHE_DIR . PKWK_AUTOALIAS_REGEX_CACHE,
675                         get_autolink_pattern(array_keys($aliases), $autoalias));
676         }
677 }
678
679 // Get elapsed date of the page
680 function get_pg_passage($page, $sw = TRUE)
681 {
682         global $show_passage;
683         if (! $show_passage) return '';
684
685         $time = get_filetime($page);
686         $pg_passage = ($time != 0) ? get_passage($time) : '';
687
688         return $sw ? '<small>' . $pg_passage . '</small>' : ' ' . $pg_passage;
689 }
690
691 // Last-Modified header
692 function header_lastmod($page = NULL)
693 {
694         global $lastmod;
695
696         if ($lastmod && is_page($page)) {
697                 pkwk_headers_sent();
698                 header('Last-Modified: ' .
699                         date('D, d M Y H:i:s', get_filetime($page)) . ' GMT');
700         }
701 }
702
703 // Get a list of encoded files (must specify a directory and a suffix)
704 function get_existfiles($dir = DATA_DIR, $ext = '.txt')
705 {
706         $aryret = array();
707         $pattern = '/^(?:[0-9A-F]{2})+' . preg_quote($ext, '/') . '$/';
708
709         $dp = @opendir($dir) or die_message($dir . ' is not found or not readable.');
710         while (($file = readdir($dp)) !== FALSE) {
711                 if (preg_match($pattern, $file)) {
712                         $aryret[] = $dir . $file;
713                 }
714         }
715         closedir($dp);
716
717         return $aryret;
718 }
719
720 /**
721  * Get/Set pagelist cache enabled for get_existpages()
722  *
723  * @param $newvalue Set true when the system can cache the page list
724  * @return true if can use page list cache
725  */
726 function is_pagelist_cache_enabled($newvalue = null)
727 {
728         static $cache_enabled = null;
729
730         if (!is_null($newvalue)) {
731                 $cache_enabled = $newvalue;
732                 return; // Return nothing on setting newvalue call
733         }
734         if (is_null($cache_enabled)) {
735                 return false;
736         }
737         return $cache_enabled;
738 }
739
740 // Get a page list of this wiki
741 function get_existpages($dir = DATA_DIR, $ext = '.txt')
742 {
743         static $cached_list = null; // Cached wikitext page list
744         $use_cache = false;
745
746         if ($dir === DATA_DIR && $ext === '.txt' && is_pagelist_cache_enabled()) {
747                 // Use pagelist cache for "wiki/*.txt" files
748                 if (!is_null($cached_list)) {
749                         return $cached_list;
750                 }
751                 $use_cache = true;
752         }
753         $aryret = array();
754         $pattern = '/^((?:[0-9A-F]{2})+)' . preg_quote($ext, '/') . '$/';
755         $dp = @opendir($dir) or die_message($dir . ' is not found or not readable.');
756         $matches = array();
757         while (($file = readdir($dp)) !== FALSE) {
758                 if (preg_match($pattern, $file, $matches)) {
759                         $aryret[$file] = decode($matches[1]);
760                 }
761         }
762         closedir($dp);
763         if ($use_cache) {
764                 $cached_list = $aryret;
765         }
766         return $aryret;
767 }
768
769 // Get PageReading(pronounce-annotated) data in an array()
770 function get_readings()
771 {
772         global $pagereading_enable, $pagereading_kanji2kana_converter;
773         global $pagereading_kanji2kana_encoding, $pagereading_chasen_path;
774         global $pagereading_kakasi_path, $pagereading_config_page;
775         global $pagereading_config_dict;
776
777         $pages = get_existpages();
778
779         $readings = array();
780         foreach ($pages as $page) 
781                 $readings[$page] = '';
782
783         $deletedPage = FALSE;
784         $matches = array();
785         foreach (get_source($pagereading_config_page) as $line) {
786                 $line = chop($line);
787                 if(preg_match('/^-\[\[([^]]+)\]\]\s+(.+)$/', $line, $matches)) {
788                         if(isset($readings[$matches[1]])) {
789                                 // This page is not clear how to be pronounced
790                                 $readings[$matches[1]] = $matches[2];
791                         } else {
792                                 // This page seems deleted
793                                 $deletedPage = TRUE;
794                         }
795                 }
796         }
797
798         // If enabled ChaSen/KAKASI execution
799         if($pagereading_enable) {
800
801                 // Check there's non-clear-pronouncing page
802                 $unknownPage = FALSE;
803                 foreach ($readings as $page => $reading) {
804                         if($reading == '') {
805                                 $unknownPage = TRUE;
806                                 break;
807                         }
808                 }
809
810                 // Execute ChaSen/KAKASI, and get annotation
811                 if($unknownPage) {
812                         switch(strtolower($pagereading_kanji2kana_converter)) {
813                         case 'chasen':
814                                 if(! file_exists($pagereading_chasen_path))
815                                         die_message('ChaSen not found: ' . $pagereading_chasen_path);
816
817                                 $tmpfname = tempnam(realpath(CACHE_DIR), 'PageReading');
818                                 $fp = fopen($tmpfname, 'w') or
819                                         die_message('Cannot write temporary file "' . $tmpfname . '".' . "\n");
820                                 foreach ($readings as $page => $reading) {
821                                         if($reading != '') continue;
822                                         fputs($fp, mb_convert_encoding($page . "\n",
823                                                 $pagereading_kanji2kana_encoding, SOURCE_ENCODING));
824                                 }
825                                 fclose($fp);
826
827                                 $chasen = "$pagereading_chasen_path -F %y $tmpfname";
828                                 $fp     = popen($chasen, 'r');
829                                 if($fp === FALSE) {
830                                         unlink($tmpfname);
831                                         die_message('ChaSen execution failed: ' . $chasen);
832                                 }
833                                 foreach ($readings as $page => $reading) {
834                                         if($reading != '') continue;
835
836                                         $line = fgets($fp);
837                                         $line = mb_convert_encoding($line, SOURCE_ENCODING,
838                                                 $pagereading_kanji2kana_encoding);
839                                         $line = chop($line);
840                                         $readings[$page] = $line;
841                                 }
842                                 pclose($fp);
843
844                                 unlink($tmpfname) or
845                                         die_message('Temporary file can not be removed: ' . $tmpfname);
846                                 break;
847
848                         case 'kakasi':  /*FALLTHROUGH*/
849                         case 'kakashi':
850                                 if(! file_exists($pagereading_kakasi_path))
851                                         die_message('KAKASI not found: ' . $pagereading_kakasi_path);
852
853                                 $tmpfname = tempnam(realpath(CACHE_DIR), 'PageReading');
854                                 $fp       = fopen($tmpfname, 'w') or
855                                         die_message('Cannot write temporary file "' . $tmpfname . '".' . "\n");
856                                 foreach ($readings as $page => $reading) {
857                                         if($reading != '') continue;
858                                         fputs($fp, mb_convert_encoding($page . "\n",
859                                                 $pagereading_kanji2kana_encoding, SOURCE_ENCODING));
860                                 }
861                                 fclose($fp);
862
863                                 $kakasi = "$pagereading_kakasi_path -kK -HK -JK < $tmpfname";
864                                 $fp     = popen($kakasi, 'r');
865                                 if($fp === FALSE) {
866                                         unlink($tmpfname);
867                                         die_message('KAKASI execution failed: ' . $kakasi);
868                                 }
869
870                                 foreach ($readings as $page => $reading) {
871                                         if($reading != '') continue;
872
873                                         $line = fgets($fp);
874                                         $line = mb_convert_encoding($line, SOURCE_ENCODING,
875                                                 $pagereading_kanji2kana_encoding);
876                                         $line = chop($line);
877                                         $readings[$page] = $line;
878                                 }
879                                 pclose($fp);
880
881                                 unlink($tmpfname) or
882                                         die_message('Temporary file can not be removed: ' . $tmpfname);
883                                 break;
884
885                         case 'none':
886                                 $patterns = $replacements = $matches = array();
887                                 foreach (get_source($pagereading_config_dict) as $line) {
888                                         $line = chop($line);
889                                         if(preg_match('|^ /([^/]+)/,\s*(.+)$|', $line, $matches)) {
890                                                 $patterns[]     = $matches[1];
891                                                 $replacements[] = $matches[2];
892                                         }
893                                 }
894                                 foreach ($readings as $page => $reading) {
895                                         if($reading != '') continue;
896
897                                         $readings[$page] = $page;
898                                         foreach ($patterns as $no => $pattern)
899                                                 $readings[$page] = mb_convert_kana(mb_ereg_replace($pattern,
900                                                         $replacements[$no], $readings[$page]), 'aKCV');
901                                 }
902                                 break;
903
904                         default:
905                                 die_message('Unknown kanji-kana converter: ' . $pagereading_kanji2kana_converter . '.');
906                                 break;
907                         }
908                 }
909
910                 if($unknownPage || $deletedPage) {
911
912                         asort($readings, SORT_STRING); // Sort by pronouncing(alphabetical/reading) order
913                         $body = '';
914                         foreach ($readings as $page => $reading)
915                                 $body .= '-[[' . $page . ']] ' . $reading . "\n";
916
917                         page_write($pagereading_config_page, $body);
918                 }
919         }
920
921         // Pages that are not prounouncing-clear, return pagenames of themselves
922         foreach ($pages as $page) {
923                 if($readings[$page] == '')
924                         $readings[$page] = $page;
925         }
926
927         return $readings;
928 }
929
930 // Get a list of related pages of the page
931 function links_get_related($page)
932 {
933         global $vars, $related;
934         static $links = array();
935
936         if (isset($links[$page])) return $links[$page];
937
938         // If possible, merge related pages generated by make_link()
939         $links[$page] = ($page === $vars['page']) ? $related : array();
940
941         // Get repated pages from DB
942         $links[$page] += links_get_related_db($vars['page']);
943
944         return $links[$page];
945 }
946
947 // _If needed_, re-create the file to change/correct ownership into PHP's
948 // NOTE: Not works for Windows
949 function pkwk_chown($filename, $preserve_time = TRUE)
950 {
951         static $php_uid; // PHP's UID
952
953         if (! isset($php_uid)) {
954                 if (extension_loaded('posix')) {
955                         $php_uid = posix_getuid(); // Unix
956                 } else {
957                         $php_uid = 0; // Windows
958                 }
959         }
960
961         // Lock for pkwk_chown()
962         $lockfile = CACHE_DIR . 'pkwk_chown.lock';
963         $flock = fopen($lockfile, 'a') or
964                 die('pkwk_chown(): fopen() failed for: CACHEDIR/' .
965                         basename(htmlsc($lockfile)));
966         flock($flock, LOCK_EX) or die('pkwk_chown(): flock() failed for lock');
967
968         // Check owner
969         $stat = stat($filename) or
970                 die('pkwk_chown(): stat() failed for: '  . basename(htmlsc($filename)));
971         if ($stat[4] === $php_uid) {
972                 // NOTE: Windows always here
973                 $result = TRUE; // Seems the same UID. Nothing to do
974         } else {
975                 $tmp = $filename . '.' . getmypid() . '.tmp';
976
977                 // Lock source $filename to avoid file corruption
978                 // NOTE: Not 'r+'. Don't check write permission here
979                 $ffile = fopen($filename, 'r') or
980                         die('pkwk_chown(): fopen() failed for: ' .
981                                 basename(htmlsc($filename)));
982
983                 // Try to chown by re-creating files
984                 // NOTE:
985                 //   * touch() before copy() is for 'rw-r--r--' instead of 'rwxr-xr-x' (with umask 022).
986                 //   * (PHP 4 < PHP 4.2.0) touch() with the third argument is not implemented and retuns NULL and Warn.
987                 //   * @unlink() before rename() is for Windows but here's for Unix only
988                 flock($ffile, LOCK_EX) or die('pkwk_chown(): flock() failed');
989                 $result = touch($tmp) && copy($filename, $tmp) &&
990                         ($preserve_time ? (touch($tmp, $stat[9], $stat[8]) || touch($tmp, $stat[9])) : TRUE) &&
991                         rename($tmp, $filename);
992                 flock($ffile, LOCK_UN) or die('pkwk_chown(): flock() failed');
993
994                 fclose($ffile) or die('pkwk_chown(): fclose() failed');
995
996                 if ($result === FALSE) @unlink($tmp);
997         }
998
999         // Unlock for pkwk_chown()
1000         flock($flock, LOCK_UN) or die('pkwk_chown(): flock() failed for lock');
1001         fclose($flock) or die('pkwk_chown(): fclose() failed for lock');
1002
1003         return $result;
1004 }
1005
1006 // touch() with trying pkwk_chown()
1007 function pkwk_touch_file($filename, $time = FALSE, $atime = FALSE)
1008 {
1009         // Is the owner incorrected and unable to correct?
1010         if (! file_exists($filename) || pkwk_chown($filename)) {
1011                 if ($time === FALSE) {
1012                         $result = touch($filename);
1013                 } else if ($atime === FALSE) {
1014                         $result = touch($filename, $time);
1015                 } else {
1016                         $result = touch($filename, $time, $atime);
1017                 }
1018                 return $result;
1019         } else {
1020                 die('pkwk_touch_file(): Invalid UID and (not writable for the directory or not a flie): ' .
1021                         htmlsc(basename($filename)));
1022         }
1023 }
1024
1025 /**
1026  * Lock-enabled file_get_contents
1027  *
1028  * Require: PHP5+
1029  */
1030 function pkwk_file_get_contents($filename) {
1031         if (! file_exists($filename)) {
1032                 return false;
1033         }
1034         $fp   = fopen($filename, 'rb');
1035         flock($fp, LOCK_SH);
1036         $file = file_get_contents($filename);
1037         flock($fp, LOCK_UN);
1038         return $file;
1039 }
1040
1041 /**
1042  * Prepare some cache files for convert_html()
1043  *
1044  * * Make cache/autolink.dat if needed
1045  */
1046 function prepare_display_materials() {
1047         global $autolink;
1048         if ($autolink) {
1049                 // Make sure 'cache/autolink.dat'
1050                 $file = CACHE_DIR . PKWK_AUTOLINK_REGEX_CACHE;
1051                 if (!file_exists($file)) {
1052                         // Re-create autolink.dat
1053                         put_lastmodified();
1054                 }
1055         }
1056 }
1057
1058 /**
1059  * Prepare page related links and references for links_get_related()
1060  */
1061 function prepare_links_related($page) {
1062         global $defaultpage;
1063
1064         $enc_defaultpage = encode($defaultpage);
1065         if (file_exists(CACHE_DIR . $enc_defaultpage . '.rel')) return;
1066         if (file_exists(CACHE_DIR . $enc_defaultpage . '.ref')) return;
1067         $enc_name = encode($page);
1068         if (file_exists(CACHE_DIR . $enc_name . '.rel')) return;
1069         if (file_exists(CACHE_DIR . $enc_name . '.ref')) return;
1070
1071         $pattern = '/^((?:[0-9A-F]{2})+)' . '(\.ref|\.rel)' . '$/';
1072         $dir = CACHE_DIR;
1073         $dp = @opendir($dir) or die_message('CACHE_DIR/'. ' is not found or not readable.');
1074         $rel_ref_ready = false;
1075         $count = 0;
1076         while (($file = readdir($dp)) !== FALSE) {
1077                 if (preg_match($pattern, $file, $matches)) {
1078                         if ($count++ > 5) {
1079                                 $rel_ref_ready = true;
1080                                 break;
1081                         }
1082                 }
1083         }
1084         closedir($dp);
1085         if (!$rel_ref_ready) {
1086                 if (count(get_existpages()) < 50) {
1087                         // Make link files automatically only if page count < 50.
1088                         // Because large number of update links will cause PHP timeout.
1089                         links_init();
1090                 }
1091         }
1092 }