OSDN Git Service

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