OSDN Git Service

BugTrack/2565 Fix read_auth and edit_auth apply condition
[pukiwiki/pukiwiki.git] / lib / file.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone.
3 // file.php
4 // Copyright
5 //   2002-2022 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         // Update the page 'RecentChanges'
526         $recent_pages = array_splice($recent_pages, 0, $maxshow);
527         $file = get_filename($whatsnew);
528
529         // Open
530         pkwk_touch_file($file);
531         $fp = fopen($file, 'r+') or
532                 die_message('Cannot open ' . htmlsc($whatsnew));
533         set_file_buffer($fp, 0);
534         flock($fp, LOCK_EX);
535
536         // Recreate
537         ftruncate($fp, 0);
538         rewind($fp);
539         $do_diff = exist_plugin('diff');
540         foreach ($recent_pages as $_page=>$time) {
541                 $line = get_recentchanges_line($_page, $time, $do_diff);
542                 fputs($fp, $line);
543         }
544         fputs($fp, '#norelated' . "\n"); // :)
545
546         flock($fp, LOCK_UN);
547         fclose($fp);
548 }
549
550 // Re-create PKWK_MAXSHOW_CACHE (Heavy)
551 function put_lastmodified()
552 {
553         global $maxshow, $whatsnew, $autolink;
554
555         if (PKWK_READONLY) return; // Do nothing
556
557         // Get WHOLE page list
558         $pages = get_existpages();
559
560         // Check ALL filetime
561         $recent_pages = array();
562         foreach($pages as $page)
563                 if ($page !== $whatsnew && ! check_non_list($page))
564                         $recent_pages[$page] = get_filetime($page);
565
566         // Sort decending order of last-modification date
567         arsort($recent_pages, SORT_NUMERIC);
568
569         // Cut unused lines
570         // BugTrack2/179: array_splice() will break integer keys in hashtable
571         $count   = $maxshow + PKWK_MAXSHOW_ALLOWANCE;
572         $_recent = array();
573         foreach($recent_pages as $key=>$value) {
574                 unset($recent_pages[$key]);
575                 $_recent[$key] = $value;
576                 if (--$count < 1) break;
577         }
578         $recent_pages = & $_recent;
579
580         // Re-create PKWK_MAXSHOW_CACHE
581         $file = CACHE_DIR . PKWK_MAXSHOW_CACHE;
582         pkwk_touch_file($file);
583         $fp = fopen($file, 'r+') or
584                 die_message('Cannot open' . 'CACHE_DIR/' . PKWK_MAXSHOW_CACHE);
585         set_file_buffer($fp, 0);
586         flock($fp, LOCK_EX);
587         ftruncate($fp, 0);
588         rewind($fp);
589         foreach ($recent_pages as $page=>$time)
590                 fputs($fp, $time . "\t" . $page . "\n");
591         flock($fp, LOCK_UN);
592         fclose($fp);
593
594         // Create RecentChanges
595         $file = get_filename($whatsnew);
596         pkwk_touch_file($file);
597         $fp = fopen($file, 'r+') or
598                 die_message('Cannot open ' . htmlsc($whatsnew));
599         set_file_buffer($fp, 0);
600         flock($fp, LOCK_EX);
601         ftruncate($fp, 0);
602         rewind($fp);
603         $do_diff = exist_plugin('diff');
604         foreach (array_keys($recent_pages) as $page) {
605                 $time = $recent_pages[$page];
606                 $line = get_recentchanges_line($page, $time, $do_diff);
607                 fputs($fp, $line);
608         }
609         fputs($fp, '#norelated' . "\n"); // :)
610         flock($fp, LOCK_UN);
611         fclose($fp);
612
613         // For AutoLink
614         if ($autolink) {
615                 autolink_pattern_write(CACHE_DIR . PKWK_AUTOLINK_REGEX_CACHE,
616                         get_autolink_pattern($pages, $autolink));
617         }
618 }
619
620 /**
621  * Get RecentChanges line.
622  */
623 function get_recentchanges_line($page, $time, $is_diff)
624 {
625         global $do_backup;
626         $lastmod = format_date($time);
627         if ($is_diff) {
628                 $diff = '[ &pageaction("' . $page . '",diff);';
629                 if ($do_backup) {
630                         $diff_backup = $diff . ' | &pageaction("' . $page . '",backup); ]';
631                 } else {
632                         $diff_backup = $diff . ' ]';
633                 }
634         } else {
635                 $diff_backup = '';
636         }
637         $line = '-' . $lastmod . ' - ' . $diff_backup . ' [[' . $page . ']]' . "\n";
638         return $line;
639 }
640
641 /**
642  * Get recent files
643  *
644  * @return Array of (file => time)
645  */
646 function get_recent_files()
647 {
648         $recentfile = CACHE_DIR . PKWK_MAXSHOW_CACHE;
649         $lines = file($recentfile);
650         if (!$lines) return array();
651         $files = array();
652         foreach ($lines as $line) {
653                 list ($time, $file) = explode("\t", rtrim($line));
654                 $files[$file] = $time;
655         }
656         return $files;
657 }
658
659 /**
660  * Update RecentChanges page / Invalidate recent.dat
661  */
662 function delete_recent_changes_cache() {
663         $file = CACHE_DIR . PKWK_MAXSHOW_CACHE;
664         unlink($file);
665 }
666
667 // update autolink data
668 function autolink_pattern_write($filename, $autolink_pattern)
669 {
670         list($pattern, $pattern_a, $forceignorelist) = $autolink_pattern;
671
672         $fp = fopen($filename, 'w') or
673                 die_message('Cannot open ' . $filename);
674         set_file_buffer($fp, 0);
675         flock($fp, LOCK_EX);
676         rewind($fp);
677         fputs($fp, $pattern   . "\n");
678         fputs($fp, $pattern_a . "\n");
679         fputs($fp, join("\t", $forceignorelist) . "\n");
680         flock($fp, LOCK_UN);
681         fclose($fp);
682 }
683
684 // Update AutoAlias regex cache
685 function update_autoalias_cache_file()
686 {
687         global $autoalias; // Disable (0), Enable (min-length)
688         $aliases = get_autoaliases();
689         if (empty($aliases)) {
690                 // Remove
691                 @unlink(CACHE_DIR . PKWK_AUTOALIAS_REGEX_CACHE);
692         } else {
693                 // Create or Update
694                 autolink_pattern_write(CACHE_DIR . PKWK_AUTOALIAS_REGEX_CACHE,
695                         get_autolink_pattern(array_keys($aliases), $autoalias));
696         }
697 }
698
699 // Get elapsed date of the page
700 function get_pg_passage($page, $sw = TRUE)
701 {
702         global $show_passage;
703         if (! $show_passage) return '';
704
705         $time = get_filetime($page);
706         $pg_passage = ($time != 0) ? get_passage($time) : '';
707
708         return $sw ? '<small>' . $pg_passage . '</small>' : ' ' . $pg_passage;
709 }
710
711 // Last-Modified header
712 function header_lastmod($page = NULL)
713 {
714         global $lastmod;
715
716         if ($lastmod && is_page($page)) {
717                 pkwk_headers_sent();
718                 header('Last-Modified: ' .
719                         date('D, d M Y H:i:s', get_filetime($page)) . ' GMT');
720         }
721 }
722
723 // Get a list of encoded files (must specify a directory and a suffix)
724 function get_existfiles($dir = DATA_DIR, $ext = '.txt')
725 {
726         $aryret = array();
727         $pattern = '/^(?:[0-9A-F]{2})+' . preg_quote($ext, '/') . '$/';
728
729         $dp = @opendir($dir) or die_message($dir . ' is not found or not readable.');
730         while (($file = readdir($dp)) !== FALSE) {
731                 if (preg_match($pattern, $file)) {
732                         $aryret[] = $dir . $file;
733                 }
734         }
735         closedir($dp);
736
737         return $aryret;
738 }
739
740 /**
741  * Get/Set pagelist cache enabled for get_existpages()
742  *
743  * @param $newvalue Set true when the system can cache the page list
744  * @return true if can use page list cache
745  */
746 function is_pagelist_cache_enabled($newvalue = null)
747 {
748         static $cache_enabled = null;
749
750         if (!is_null($newvalue)) {
751                 $cache_enabled = $newvalue;
752                 return; // Return nothing on setting newvalue call
753         }
754         if (is_null($cache_enabled)) {
755                 return false;
756         }
757         return $cache_enabled;
758 }
759
760 // Get a page list of this wiki
761 function get_existpages($dir = DATA_DIR, $ext = '.txt')
762 {
763         static $cached_list = null; // Cached wikitext page list
764         $use_cache = false;
765
766         if ($dir === DATA_DIR && $ext === '.txt' && is_pagelist_cache_enabled()) {
767                 // Use pagelist cache for "wiki/*.txt" files
768                 if (!is_null($cached_list)) {
769                         return $cached_list;
770                 }
771                 $use_cache = true;
772         }
773         $aryret = array();
774         $pattern = '/^((?:[0-9A-F]{2})+)' . preg_quote($ext, '/') . '$/';
775         $dp = @opendir($dir) or die_message($dir . ' is not found or not readable.');
776         $matches = array();
777         while (($file = readdir($dp)) !== FALSE) {
778                 if (preg_match($pattern, $file, $matches)) {
779                         $aryret[$file] = decode($matches[1]);
780                 }
781         }
782         closedir($dp);
783         if ($use_cache) {
784                 $cached_list = $aryret;
785         }
786         return $aryret;
787 }
788
789 // Get PageReading(pronounce-annotated) data in an array()
790 function get_readings()
791 {
792         global $pagereading_enable, $pagereading_kanji2kana_converter;
793         global $pagereading_kanji2kana_encoding, $pagereading_chasen_path;
794         global $pagereading_kakasi_path, $pagereading_config_page;
795         global $pagereading_config_dict;
796
797         $pages = get_existpages();
798
799         $readings = array();
800         foreach ($pages as $page) 
801                 $readings[$page] = '';
802
803         $deletedPage = FALSE;
804         $matches = array();
805         foreach (get_source($pagereading_config_page) as $line) {
806                 $line = chop($line);
807                 if(preg_match('/^-\[\[([^]]+)\]\]\s+(.+)$/', $line, $matches)) {
808                         if(isset($readings[$matches[1]])) {
809                                 // This page is not clear how to be pronounced
810                                 $readings[$matches[1]] = $matches[2];
811                         } else {
812                                 // This page seems deleted
813                                 $deletedPage = TRUE;
814                         }
815                 }
816         }
817
818         // If enabled ChaSen/KAKASI execution
819         if($pagereading_enable) {
820
821                 // Check there's non-clear-pronouncing page
822                 $unknownPage = FALSE;
823                 foreach ($readings as $page => $reading) {
824                         if($reading == '') {
825                                 $unknownPage = TRUE;
826                                 break;
827                         }
828                 }
829
830                 // Execute ChaSen/KAKASI, and get annotation
831                 if($unknownPage) {
832                         switch(strtolower($pagereading_kanji2kana_converter)) {
833                         case 'chasen':
834                                 if(! file_exists($pagereading_chasen_path))
835                                         die_message('ChaSen not found: ' . $pagereading_chasen_path);
836
837                                 $tmpfname = tempnam(realpath(CACHE_DIR), 'PageReading');
838                                 $fp = fopen($tmpfname, 'w') or
839                                         die_message('Cannot write temporary file "' . $tmpfname . '".' . "\n");
840                                 foreach ($readings as $page => $reading) {
841                                         if($reading != '') continue;
842                                         fputs($fp, mb_convert_encoding($page . "\n",
843                                                 $pagereading_kanji2kana_encoding, SOURCE_ENCODING));
844                                 }
845                                 fclose($fp);
846
847                                 $chasen = "$pagereading_chasen_path -F %y $tmpfname";
848                                 $fp     = popen($chasen, 'r');
849                                 if($fp === FALSE) {
850                                         unlink($tmpfname);
851                                         die_message('ChaSen execution failed: ' . $chasen);
852                                 }
853                                 foreach ($readings as $page => $reading) {
854                                         if($reading != '') continue;
855
856                                         $line = fgets($fp);
857                                         $line = mb_convert_encoding($line, SOURCE_ENCODING,
858                                                 $pagereading_kanji2kana_encoding);
859                                         $line = chop($line);
860                                         $readings[$page] = $line;
861                                 }
862                                 pclose($fp);
863
864                                 unlink($tmpfname) or
865                                         die_message('Temporary file can not be removed: ' . $tmpfname);
866                                 break;
867
868                         case 'kakasi':  /*FALLTHROUGH*/
869                         case 'kakashi':
870                                 if(! file_exists($pagereading_kakasi_path))
871                                         die_message('KAKASI not found: ' . $pagereading_kakasi_path);
872
873                                 $tmpfname = tempnam(realpath(CACHE_DIR), 'PageReading');
874                                 $fp       = fopen($tmpfname, 'w') or
875                                         die_message('Cannot write temporary file "' . $tmpfname . '".' . "\n");
876                                 foreach ($readings as $page => $reading) {
877                                         if($reading != '') continue;
878                                         fputs($fp, mb_convert_encoding($page . "\n",
879                                                 $pagereading_kanji2kana_encoding, SOURCE_ENCODING));
880                                 }
881                                 fclose($fp);
882
883                                 $kakasi = "$pagereading_kakasi_path -kK -HK -JK < $tmpfname";
884                                 $fp     = popen($kakasi, 'r');
885                                 if($fp === FALSE) {
886                                         unlink($tmpfname);
887                                         die_message('KAKASI execution failed: ' . $kakasi);
888                                 }
889
890                                 foreach ($readings as $page => $reading) {
891                                         if($reading != '') continue;
892
893                                         $line = fgets($fp);
894                                         $line = mb_convert_encoding($line, SOURCE_ENCODING,
895                                                 $pagereading_kanji2kana_encoding);
896                                         $line = chop($line);
897                                         $readings[$page] = $line;
898                                 }
899                                 pclose($fp);
900
901                                 unlink($tmpfname) or
902                                         die_message('Temporary file can not be removed: ' . $tmpfname);
903                                 break;
904
905                         case 'none':
906                                 $patterns = $replacements = $matches = array();
907                                 foreach (get_source($pagereading_config_dict) as $line) {
908                                         $line = chop($line);
909                                         if(preg_match('|^ /([^/]+)/,\s*(.+)$|', $line, $matches)) {
910                                                 $patterns[]     = $matches[1];
911                                                 $replacements[] = $matches[2];
912                                         }
913                                 }
914                                 foreach ($readings as $page => $reading) {
915                                         if($reading != '') continue;
916
917                                         $readings[$page] = $page;
918                                         foreach ($patterns as $no => $pattern)
919                                                 $readings[$page] = mb_convert_kana(mb_ereg_replace($pattern,
920                                                         $replacements[$no], $readings[$page]), 'aKCV');
921                                 }
922                                 break;
923
924                         default:
925                                 die_message('Unknown kanji-kana converter: ' . $pagereading_kanji2kana_converter . '.');
926                                 break;
927                         }
928                 }
929
930                 if($unknownPage || $deletedPage) {
931
932                         asort($readings, SORT_STRING); // Sort by pronouncing(alphabetical/reading) order
933                         $body = '';
934                         foreach ($readings as $page => $reading)
935                                 $body .= '-[[' . $page . ']] ' . $reading . "\n";
936
937                         page_write($pagereading_config_page, $body);
938                 }
939         }
940
941         // Pages that are not prounouncing-clear, return pagenames of themselves
942         foreach ($pages as $page) {
943                 if($readings[$page] == '')
944                         $readings[$page] = $page;
945         }
946
947         return $readings;
948 }
949
950 // Get a list of related pages of the page
951 function links_get_related($page)
952 {
953         global $vars, $related;
954         static $links = array();
955
956         if (isset($links[$page])) return $links[$page];
957
958         // If possible, merge related pages generated by make_link()
959         $links[$page] = ($page === $vars['page']) ? $related : array();
960
961         // Get repated pages from DB
962         $links[$page] += links_get_related_db($vars['page']);
963
964         return $links[$page];
965 }
966
967 // _If needed_, re-create the file to change/correct ownership into PHP's
968 // NOTE: Not works for Windows
969 function pkwk_chown($filename, $preserve_time = TRUE)
970 {
971         static $php_uid; // PHP's UID
972
973         if (! isset($php_uid)) {
974                 if (extension_loaded('posix')) {
975                         $php_uid = posix_getuid(); // Unix
976                 } else {
977                         $php_uid = 0; // Windows
978                 }
979         }
980
981         // Lock for pkwk_chown()
982         $lockfile = CACHE_DIR . 'pkwk_chown.lock';
983         $flock = fopen($lockfile, 'a') or
984                 die('pkwk_chown(): fopen() failed for: CACHEDIR/' .
985                         basename(htmlsc($lockfile)));
986         flock($flock, LOCK_EX) or die('pkwk_chown(): flock() failed for lock');
987
988         // Check owner
989         $stat = stat($filename) or
990                 die('pkwk_chown(): stat() failed for: '  . basename(htmlsc($filename)));
991         if ($stat[4] === $php_uid) {
992                 // NOTE: Windows always here
993                 $result = TRUE; // Seems the same UID. Nothing to do
994         } else {
995                 $tmp = $filename . '.' . getmypid() . '.tmp';
996
997                 // Lock source $filename to avoid file corruption
998                 // NOTE: Not 'r+'. Don't check write permission here
999                 $ffile = fopen($filename, 'r') or
1000                         die('pkwk_chown(): fopen() failed for: ' .
1001                                 basename(htmlsc($filename)));
1002
1003                 // Try to chown by re-creating files
1004                 // NOTE:
1005                 //   * touch() before copy() is for 'rw-r--r--' instead of 'rwxr-xr-x' (with umask 022).
1006                 //   * (PHP 4 < PHP 4.2.0) touch() with the third argument is not implemented and retuns NULL and Warn.
1007                 //   * @unlink() before rename() is for Windows but here's for Unix only
1008                 flock($ffile, LOCK_EX) or
1009                         die('pkwk_chown(): flock() failed - ' . get_htmlsafe_filename($filename));
1010                 $result = touch($tmp) && copy($filename, $tmp) &&
1011                         ($preserve_time ? (touch($tmp, $stat[9], $stat[8]) || touch($tmp, $stat[9])) : TRUE) &&
1012                         rename($tmp, $filename);
1013                 flock($ffile, LOCK_UN) or
1014                         die('pkwk_chown(): flock() failed - ' . get_htmlsafe_filename($filename));
1015
1016                 fclose($ffile) or die('pkwk_chown(): fclose() failed');
1017
1018                 if ($result === FALSE) @unlink($tmp);
1019         }
1020
1021         // Unlock for pkwk_chown()
1022         flock($flock, LOCK_UN) or die('pkwk_chown(): flock() failed for lock');
1023         fclose($flock) or die('pkwk_chown(): fclose() failed for lock');
1024
1025         return $result;
1026 }
1027
1028 // touch() with trying pkwk_chown()
1029 function pkwk_touch_file($filename, $time = FALSE, $atime = FALSE)
1030 {
1031         // Is the owner incorrected and unable to correct?
1032         if (! file_exists($filename) || pkwk_chown($filename)) {
1033                 if ($time === FALSE) {
1034                         $result = touch($filename);
1035                 } else if ($atime === FALSE) {
1036                         $result = touch($filename, $time);
1037                 } else {
1038                         $result = touch($filename, $time, $atime);
1039                 }
1040                 return $result;
1041         } else {
1042                 die('pkwk_touch_file(): Invalid UID and (not writable for the directory or not a flie): ' .
1043                         htmlsc(basename($filename)));
1044         }
1045 }
1046
1047 /**
1048  * Lock-enabled file_get_contents
1049  *
1050  * Require: PHP5+
1051  */
1052 function pkwk_file_get_contents($filename) {
1053         if (! file_exists($filename)) {
1054                 return false;
1055         }
1056         $fp   = fopen($filename, 'rb');
1057         flock($fp, LOCK_SH);
1058         $file = file_get_contents($filename);
1059         flock($fp, LOCK_UN);
1060         return $file;
1061 }
1062
1063 /**
1064  * Prepare some cache files for convert_html()
1065  *
1066  * * Make cache/autolink.dat if needed
1067  */
1068 function prepare_display_materials() {
1069         global $autolink;
1070         if ($autolink) {
1071                 // Make sure 'cache/autolink.dat'
1072                 $file = CACHE_DIR . PKWK_AUTOLINK_REGEX_CACHE;
1073                 if (!file_exists($file)) {
1074                         // Re-create autolink.dat
1075                         put_lastmodified();
1076                 }
1077         }
1078 }
1079
1080 /**
1081  * Prepare page related links and references for links_get_related()
1082  */
1083 function prepare_links_related($page) {
1084         global $defaultpage;
1085
1086         $enc_defaultpage = encode($defaultpage);
1087         if (file_exists(CACHE_DIR . $enc_defaultpage . '.rel')) return;
1088         if (file_exists(CACHE_DIR . $enc_defaultpage . '.ref')) return;
1089         $enc_name = encode($page);
1090         if (file_exists(CACHE_DIR . $enc_name . '.rel')) return;
1091         if (file_exists(CACHE_DIR . $enc_name . '.ref')) return;
1092
1093         $pattern = '/^((?:[0-9A-F]{2})+)' . '(\.ref|\.rel)' . '$/';
1094         $dir = CACHE_DIR;
1095         $dp = @opendir($dir) or die_message('CACHE_DIR/'. ' is not found or not readable.');
1096         $rel_ref_ready = false;
1097         $count = 0;
1098         while (($file = readdir($dp)) !== FALSE) {
1099                 if (preg_match($pattern, $file, $matches)) {
1100                         if ($count++ > 5) {
1101                                 $rel_ref_ready = true;
1102                                 break;
1103                         }
1104                 }
1105         }
1106         closedir($dp);
1107         if (!$rel_ref_ready) {
1108                 if (count(get_existpages()) < 50) {
1109                         // Make link files automatically only if page count < 50.
1110                         // Because large number of update links will cause PHP timeout.
1111                         links_init();
1112                 }
1113         }
1114 }
1115
1116 /**
1117  * Get HTML-safe string filename for die.
1118  */
1119 function get_htmlsafe_filename($filename) {
1120         return preg_replace('#[^\w\/\.\-\$\%]#', '', $filename);
1121 }