OSDN Git Service

Added credits for yu-ji by tracking pukiwiki.php(r1.1.1.1)
[pukiwiki/pukiwiki.git] / lib / file.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone.
3 // $Id: file.php,v 1.25 2005/04/30 05:21:00 henoheno Exp $
4 // Copyright (C)
5 //   2002-2005 PukiWiki Developers 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 // Get source(wiki text) data of the page
12 function get_source($page = NULL)
13 {
14         // Removing line-feeds: Because file() doesn't remove them.
15         return is_page($page) ? str_replace("\r", '', file(get_filename($page))) : array();
16 }
17
18 // Get last-modified filetime of the page
19 function get_filetime($page)
20 {
21         return is_page($page) ? filemtime(get_filename($page)) - LOCALZONE : 0;
22 }
23
24 // Get physical file name of the page
25 function get_filename($page)
26 {
27         return DATA_DIR . encode($page) . '.txt';
28 }
29
30 // Put a data(wiki text) into a physical file(diff, backup, text)
31 function page_write($page, $postdata, $notimestamp = FALSE)
32 {
33         global $trackback;
34
35         if (PKWK_READONLY) return; // Do nothing
36
37         $postdata = make_str_rules($postdata);
38
39         // Create and write diff
40         $oldpostdata = is_page($page) ? join('', get_source($page)) : '';
41         $diffdata    = do_diff($oldpostdata, $postdata);
42         file_write(DIFF_DIR, $page, $diffdata);
43
44         // Create backup
45         make_backup($page, $postdata == ''); // Is $postdata null?
46
47         // Create wiki text
48         file_write(DATA_DIR, $page, $postdata, $notimestamp);
49
50         if ($trackback) {
51                 // TrackBack Ping
52                 $_diff = explode("\n", $diffdata);
53                 $plus  = join("\n", preg_replace('/^\+/', '', preg_grep('/^\+/', $_diff)));
54                 $minus = join("\n", preg_replace('/^-/',  '', preg_grep('/^-/',  $_diff)));
55                 tb_send($page, $plus, $minus);
56         }
57
58         links_update($page);
59 }
60
61 // User-defined rules (replace the source)
62 function make_str_rules($str)
63 {
64         global $str_rules, $fixed_heading_anchor;
65
66         $arr = explode("\n", $str);
67
68         $retvars = $matches = array();
69         foreach ($arr as $str) {
70                 if ($str != '' && $str{0} != ' ' && $str{0} != "\t")
71                         foreach ($str_rules as $rule => $replace)
72                                 $str = preg_replace('/' . $rule . '/', $replace, $str);
73                 
74                 // Adding fixed anchor into headings
75                 if ($fixed_heading_anchor &&
76                         preg_match('/^(\*{1,3}(.(?!\[#[A-Za-z][\w-]+\]))+)$/', $str, $matches))
77                 {
78                         // Generate ID:
79                         // A random alphabetic letter + 7 letters of random strings from md()
80                         $anchor = chr(mt_rand(ord('a'), ord('z'))) .
81                                 substr(md5(uniqid(substr($matches[1], 0, 100), 1)), mt_rand(0, 24), 7);
82                         $str = rtrim($matches[1]) . ' [#' . $anchor . ']';
83                 }
84                 $retvars[] = $str;
85         }
86
87         return join("\n", $retvars);
88 }
89
90 // Output to a file
91 function file_write($dir, $page, $str, $notimestamp = FALSE)
92 {
93         global $update_exec, $_msg_invalidiwn;
94         global $notify, $notify_diff_only, $notify_to, $notify_subject, $notify_header;
95         global $smtp_server, $smtp_auth;
96         global $whatsdeleted, $maxshow_deleted;
97
98         if (PKWK_READONLY) return; // Do nothing
99
100         if (! is_pagename($page))
101                 die_message(str_replace('$1', htmlspecialchars($page),
102                             str_replace('$2', 'WikiName', $_msg_invalidiwn)));
103
104         $page      = strip_bracket($page);
105         $timestamp = FALSE;
106         $file      = $dir . encode($page) . '.txt';
107
108         if ($dir == DATA_DIR && $str == '' && file_exists($file)) {
109                 unlink($file);
110                 add_recent($page, $whatsdeleted, '', $maxshow_deleted); // RecentDeleted
111         }
112
113         if ($str != '') {
114                 $str = preg_replace('/' . "\r" . '/', '', $str);
115                 $str = rtrim($str) . "\n";
116
117                 if ($notimestamp && file_exists($file))
118                         $timestamp = filemtime($file) - LOCALZONE;
119
120                 $fp = fopen($file, 'w') or die('fopen() failed: ' .
121                         htmlspecialchars(basename($dir) . '/' . encode($page) . '.txt') .       
122                         '<br />' . "\n" .
123                         'Maybe permission is not writable or filename is too long');
124
125                 set_file_buffer($fp, 0);
126                 flock($fp, LOCK_EX);
127                 rewind($fp);
128                 fputs($fp, $str);
129                 flock($fp, LOCK_UN);
130                 fclose($fp);
131
132                 if ($timestamp) pkwk_touch_file($file, $timestamp + LOCALZONE);
133         }
134
135         // Clear is_page() cache
136         is_page($page, TRUE);
137
138         if (! $timestamp && $dir == DATA_DIR)
139                 put_lastmodified();
140
141         // Execute $update_exec here
142         if ($update_exec && $dir == DATA_DIR)
143                 system($update_exec . ' > /dev/null &');
144
145         if ($notify && $dir == DIFF_DIR) {
146                 if ($notify_diff_only) $str = preg_replace('/^[^-+].*\n/m', '', $str);
147                 $str .= "\n" .
148                         str_repeat('-', 30) . "\n" .
149                         'URI: ' . get_script_uri() . '?' . rawurlencode($page) . "\n" .
150                         'REMOTE_ADDR: ' . $_SERVER['REMOTE_ADDR'] . "\n";
151
152                 $subject = str_replace('$page', $page, $notify_subject);
153                 ini_set('SMTP', $smtp_server);
154                 mb_language(LANG);
155
156                 if ($smtp_auth) pop_before_smtp();
157                 mb_send_mail($notify_to, $subject, $str, $notify_header);
158         }
159 }
160
161 // Update RecentDeleted
162 function add_recent($page, $recentpage, $subject = '', $limit = 0)
163 {
164         if (PKWK_READONLY || $limit == 0 || $page == '' || $recentpage == '') return;
165
166         // Load
167         $lines = $matches = array();
168         foreach (get_source($recentpage) as $line)
169                 if (preg_match('/^-(.+) - (\[\[.+\]\])$/', $line, $matches))
170                         $lines[$matches[2]] = $line;
171
172         $_page = '[[' . $page . ']]';
173
174         // Remove a report about the same page
175         if (isset($lines[$_page])) unset($lines[$_page]);
176
177         // Add
178         array_unshift($lines, '-' . format_date(UTIME) . ' - ' . $_page .
179                 htmlspecialchars($subject) . "\n");
180
181         // Get latest $limit reports
182         $lines = array_splice($lines, 0, $limit);
183
184         // Update
185         $fp = fopen(get_filename($recentpage), 'w') or
186                 die_message('Cannot write page file ' .
187                 htmlspecialchars($recentpage) .
188                 '<br />Maybe permission is not writable or filename is too long');
189         set_file_buffer($fp, 0);
190         flock($fp, LOCK_EX);
191         rewind($fp);
192         fputs($fp, '#freeze'    . "\n");
193         fputs($fp, '#norelated' . "\n"); // :)
194         fputs($fp, join('', $lines));
195         flock($fp, LOCK_UN);
196         fclose($fp);
197 }
198
199 // Update RecentChanges
200 function put_lastmodified()
201 {
202         global $maxshow, $whatsnew, $non_list, $autolink;
203
204         if (PKWK_READONLY) return; // Do nothing
205
206         $pages = get_existpages();
207         $recent_pages = array();
208         $non_list_pattern = '/' . $non_list . '/';
209         foreach($pages as $page)
210                 if ($page != $whatsnew && ! preg_match($non_list_pattern, $page))
211                         $recent_pages[$page] = get_filetime($page);
212
213         // Sort decending order of last-modification date
214         arsort($recent_pages, SORT_NUMERIC);
215
216         // Create recent.dat (for recent.inc.php)
217         $fp = fopen(CACHE_DIR . 'recent.dat', 'w') or
218                 die_message('Cannot write cache file ' .
219                 CACHE_DIR . 'recent.dat' .
220                 '<br />Maybe permission is not writable or filename is too long');
221
222         set_file_buffer($fp, 0);
223         flock($fp, LOCK_EX);
224         rewind($fp);
225         foreach ($recent_pages as $page=>$time)
226                 fputs($fp, $time . "\t" . $page . "\n");
227         flock($fp, LOCK_UN);
228         fclose($fp);
229
230         // Create RecentChanges
231         $fp = fopen(get_filename($whatsnew), 'w') or
232                 die_message('Cannot write page file ' .
233                 htmlspecialchars($whatsnew) .
234                 '<br />Maybe permission is not writable or filename is too long');
235
236         set_file_buffer($fp, 0);
237         flock($fp, LOCK_EX);
238         rewind($fp);
239         foreach (array_splice(array_keys($recent_pages), 0, $maxshow) as $page) {
240                 $time      = $recent_pages[$page];
241                 $s_lastmod = htmlspecialchars(format_date($time));
242                 $s_page    = htmlspecialchars($page);
243                 fputs($fp, '-' . $s_lastmod . ' - [[' . $s_page . ']]' . "\n");
244         }
245         fputs($fp, '#norelated' . "\n"); // :)
246         flock($fp, LOCK_UN);
247         fclose($fp);
248
249         // For AutoLink
250         if ($autolink) {
251                 list($pattern, $pattern_a, $forceignorelist) =
252                         get_autolink_pattern($pages);
253
254                 $fp = fopen(CACHE_DIR . 'autolink.dat', 'w') or
255                         die_message('Cannot write autolink file ' .
256                         CACHE_DIR . '/autolink.dat' .
257                         '<br />Maybe permission is not writable');
258                 set_file_buffer($fp, 0);
259                 flock($fp, LOCK_EX);
260                 rewind($fp);
261                 fputs($fp, $pattern   . "\n");
262                 fputs($fp, $pattern_a . "\n");
263                 fputs($fp, join("\t", $forceignorelist) . "\n");
264                 flock($fp, LOCK_UN);
265                 fclose($fp);
266         }
267 }
268
269 // Get elapsed date of the pate
270 function get_pg_passage($page, $sw = TRUE)
271 {
272         global $show_passage;
273         if (! $show_passage) return '';
274
275         $time = get_filetime($page);
276         $pg_passage = ($time != 0) ? get_passage($time) : '';
277
278         return $sw ? '<small>' . $pg_passage . '</small>' : ' ' . $pg_passage;
279 }
280
281 // Last-Modified header
282 function header_lastmod($page = NULL)
283 {
284         global $lastmod;
285
286         if ($lastmod && is_page($page)) {
287                 pkwk_headers_sent();
288                 header('Last-Modified: ' .
289                         date('D, d M Y H:i:s', get_filetime($page)) . ' GMT');
290         }
291 }
292
293 // Get a page list of this wiki
294 function get_existpages($dir = DATA_DIR, $ext = '.txt')
295 {
296         $aryret = array();
297
298         $pattern = '((?:[0-9A-F]{2})+)';
299         if ($ext != '') $ext = preg_quote($ext, '/');
300         $pattern = '/^' . $pattern . $ext . '$/';
301
302         $dp = @opendir($dir) or
303                 die_message($dir . ' is not found or not readable.');
304         $matches = array();
305         while ($file = readdir($dp))
306                 if (preg_match($pattern, $file, $matches))
307                         $aryret[$file] = decode($matches[1]);
308         closedir($dp);
309
310         return $aryret;
311 }
312
313 // Get PageReading(pronounce-annotated) data in an array()
314 function get_readings()
315 {
316         global $pagereading_enable, $pagereading_kanji2kana_converter;
317         global $pagereading_kanji2kana_encoding, $pagereading_chasen_path;
318         global $pagereading_kakasi_path, $pagereading_config_page;
319         global $pagereading_config_dict;
320
321         $pages = get_existpages();
322
323         $readings = array();
324         foreach ($pages as $page) 
325                 $readings[$page] = '';
326
327         $deletedPage = FALSE;
328         $matches = array();
329         foreach (get_source($pagereading_config_page) as $line) {
330                 $line = chop($line);
331                 if(preg_match('/^-\[\[([^]]+)\]\]\s+(.+)$/', $line, $matches)) {
332                         if(isset($readings[$matches[1]])) {
333                                 // This page is not clear how to be pronounced
334                                 $readings[$matches[1]] = $matches[2];
335                         } else {
336                                 // This page seems deleted
337                                 $deletedPage = TRUE;
338                         }
339                 }
340         }
341
342         // If enabled ChaSen/KAKASI execution
343         if($pagereading_enable) {
344
345                 // Check there's non-clear-pronouncing page
346                 $unknownPage = FALSE;
347                 foreach ($readings as $page => $reading) {
348                         if($reading == '') {
349                                 $unknownPage = TRUE;
350                                 break;
351                         }
352                 }
353
354                 // Execute ChaSen/KAKASI, and get annotation
355                 if($unknownPage) {
356                         switch(strtolower($pagereading_kanji2kana_converter)) {
357                         case 'chasen':
358                                 if(! file_exists($pagereading_chasen_path))
359                                         die_message('ChaSen not found: ' . $pagereading_chasen_path);
360
361                                 $tmpfname = tempnam(CACHE_DIR, 'PageReading');
362                                 $fp = fopen($tmpfname, 'w') or
363                                         die_message('Cannot write temporary file "' . $tmpfname . '".' . "\n");
364                                 foreach ($readings as $page => $reading) {
365                                         if($reading != '') continue;
366                                         fputs($fp, mb_convert_encoding($page . "\n",
367                                                 $pagereading_kanji2kana_encoding, SOURCE_ENCODING));
368                                 }
369                                 fclose($fp);
370
371                                 $chasen = "$pagereading_chasen_path -F %y $tmpfname";
372                                 $fp     = popen($chasen, 'r');
373                                 if($fp === FALSE) {
374                                         unlink($tmpfname);
375                                         die_message('ChaSen execution failed: ' . $chasen);
376                                 }
377                                 foreach ($readings as $page => $reading) {
378                                         if($reading != '') continue;
379
380                                         $line = fgets($fp);
381                                         $line = mb_convert_encoding($line, SOURCE_ENCODING,
382                                                 $pagereading_kanji2kana_encoding);
383                                         $line = chop($line);
384                                         $readings[$page] = $line;
385                                 }
386                                 pclose($fp);
387
388                                 unlink($tmpfname) or
389                                         die_message('Temporary file can not be removed: ' . $tmpfname);
390                                 break;
391
392                         case 'kakasi':  /*FALLTHROUGH*/
393                         case 'kakashi':
394                                 if(! file_exists($pagereading_kakasi_path))
395                                         die_message('KAKASI not found: ' . $pagereading_kakasi_path);
396
397                                 $tmpfname = tempnam(CACHE_DIR, 'PageReading');
398                                 $fp       = fopen($tmpfname, 'w') or
399                                         die_message('Cannot write temporary file "' . $tmpfname . '".' . "\n");
400                                 foreach ($readings as $page => $reading) {
401                                         if($reading != '') continue;
402                                         fputs($fp, mb_convert_encoding($page . "\n",
403                                                 $pagereading_kanji2kana_encoding, SOURCE_ENCODING));
404                                 }
405                                 fclose($fp);
406
407                                 $kakasi = "$pagereading_kakasi_path -kK -HK -JK < $tmpfname";
408                                 $fp     = popen($kakasi, 'r');
409                                 if($fp === FALSE) {
410                                         unlink($tmpfname);
411                                         die_message('KAKASI execution failed: ' . $kakasi);
412                                 }
413
414                                 foreach ($readings as $page => $reading) {
415                                         if($reading != '') continue;
416
417                                         $line = fgets($fp);
418                                         $line = mb_convert_encoding($line, SOURCE_ENCODING,
419                                                 $pagereading_kanji2kana_encoding);
420                                         $line = chop($line);
421                                         $readings[$page] = $line;
422                                 }
423                                 pclose($fp);
424
425                                 unlink($tmpfname) or
426                                         die_message('Temporary file can not be removed: ' . $tmpfname);
427                                 break;
428
429                         case 'none':
430                                 $patterns = $replacements = $matches = array();
431                                 foreach (get_source($pagereading_config_dict) as $line) {
432                                         $line = chop($line);
433                                         if(preg_match('|^ /([^/]+)/,\s*(.+)$|', $line, $matches)) {
434                                                 $patterns[]     = $matches[1];
435                                                 $replacements[] = $matches[2];
436                                         }
437                                 }
438                                 foreach ($readings as $page => $reading) {
439                                         if($reading != '') continue;
440
441                                         $readings[$page] = $page;
442                                         foreach ($patterns as $no => $pattern)
443                                                 $readings[$page] = mb_convert_kana(mb_ereg_replace($pattern,
444                                                         $replacements[$no], $readings[$page]), 'aKCV');
445                                 }
446                                 break;
447
448                         default:
449                                 die_message('Unknown kanji-kana converter: ' . $pagereading_kanji2kana_converter . '.');
450                                 break;
451                         }
452                 }
453
454                 if($unknownPage || $deletedPage) {
455
456                         asort($readings); // Sort by pronouncing(alphabetical/reading) order
457                         $body = '';
458                         foreach ($readings as $page => $reading)
459                                 $body .= '-[[' . $page . ']] ' . $reading . "\n";
460
461                         page_write($pagereading_config_page, $body);
462                 }
463         }
464
465         // Pages that are not prounouncing-clear, return pagenames of themselves
466         foreach ($pages as $page) {
467                 if($readings[$page] == '')
468                         $readings[$page] = $page;
469         }
470
471         return $readings;
472 }
473
474 // Get a list of encoded files (must specify a directory and a suffix)
475 function get_existfiles($dir, $ext)
476 {
477         $pattern = '/^(?:[0-9A-F]{2})+' . preg_quote($ext, '/') . '$/';
478         $aryret = array();
479         $dp = @opendir($dir) or die_message($dir . ' is not found or not readable.');
480         while ($file = readdir($dp))
481                 if (preg_match($pattern, $file))
482                         $aryret[] = $dir . $file;
483         closedir($dp);
484         return $aryret;
485 }
486
487 // Get a list of related pages of the page
488 function links_get_related($page)
489 {
490         global $vars, $related;
491         static $links = array();
492
493         if (isset($links[$page])) return $links[$page];
494
495         // If possible, merge related pages generated by make_link()
496         $links[$page] = ($page == $vars['page']) ? $related : array();
497
498         // Get repated pages from DB
499         $links[$page] += links_get_related_db($vars['page']);
500
501         return $links[$page];
502 }
503
504 // _If needed_, re-create the file to change/correct ownership into PHP's
505 // NOTE: Not works for Windows
506 function pkwk_chown($filename, $preserve_time = TRUE)
507 {
508         static $php_uid; // PHP's UID
509
510         if (! isset($php_uid)) {
511                 if (extension_loaded('posix')) {
512                         $php_uid = posix_getuid(); // Unix
513                 } else {
514                         $php_uid = 0; // Windows
515                 }
516         }
517
518         // Lock for pkwk_chown()
519         $lockfile = CACHE_DIR . 'pkwk_chown.lock';
520         $flock = fopen($lockfile, 'a') or
521                 die('pkwk_chown(): fopen() failed for: CACHEDIR/' .
522                         basename(htmlspecialchars($lockfile)));
523         flock($flock, LOCK_EX) or die('pkwk_chown(): flock() failed for lock');
524
525         // Check owner
526         $stat = stat($filename) or
527                 die('pkwk_chown(): stat() failed for: '  . basename(htmlspecialchars($filename)));
528         if ($stat[4] === $php_uid) {
529                 // NOTE: Windows always here
530                 $result = TRUE; // Seems the same UID. Nothing to do
531         } else {
532                 $tmp = $filename . '.' . getmypid() . '.tmp';
533
534                 // Lock source $filename to avoid file corruption
535                 // NOTE: Not 'r+'. Don't check write permission here
536                 $ffile = fopen($filename, 'r') or
537                         die('pkwk_chown(): fopen() failed for: ' .
538                                 basename(htmlspecialchars($filename)));
539
540                 // Try to chown by re-creating files
541                 // NOTE:
542                 //   * touch() before copy() is for 'rw-r--r--' instead of 'rwxr-xr-x' (with umask 022).
543                 //   * (PHP 4 < PHP 4.2.0) touch() with the third argument is not implemented and retuns NULL and Warn.
544                 //   * @unlink() before rename() is for Windows but here's for Unix only
545                 flock($ffile, LOCK_EX) or die('pkwk_chown(): flock() failed');
546                 $result = touch($tmp) && copy($filename, $tmp) &&
547                         ($preserve_time ? (touch($tmp, $stat[9], $stat[8]) || touch($tmp, $stat[9])) : TRUE) &&
548                         rename($tmp, $filename);
549                 flock($ffile, LOCK_UN) or die('pkwk_chown(): flock() failed');
550
551                 fclose($ffile) or die('pkwk_chown(): fclose() failed');
552
553                 if ($result === FALSE) @unlink($tmp);
554         }
555
556         // Unlock for pkwk_chown()
557         flock($flock, LOCK_UN) or die('pkwk_chown(): flock() failed for lock');
558         fclose($flock) or die('pkwk_chown(): fclose() failed for lock');
559
560         return $result;
561 }
562
563 // touch() with trying pkwk_chown()
564 function pkwk_touch_file($filename, $time = FALSE, $atime = FALSE)
565 {
566         // Is the owner incorrected and unable to correct?
567         if (! file_exists($filename) || pkwk_chown($filename)) {
568                 if ($time === FALSE) {
569                         $result = touch($filename);
570                 } else if ($atime === FALSE) {
571                         $result = touch($filename, $time);
572                 } else {
573                         $result = touch($filename, $time, $atime);
574                 }
575                 return $result;
576         } else {
577                 die('pkwk_touch_file(): Invalid UID and (not writable for the directory or not a flie): ' .
578                         htmlspecialchars(basename($filename)));
579         }
580 }
581 ?>