xref: /dokuwiki/inc/common.php (revision a15ce62dba2e46a5754ac7f4c603661dd98a429a)
1<?php
2/**
3 * Common DokuWiki functions
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <andi@splitbrain.org>
7 */
8
9  if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../').'/');
10  require_once(DOKU_CONF.'dokuwiki.php');
11  require_once(DOKU_INC.'inc/io.php');
12  require_once(DOKU_INC.'inc/utf8.php');
13  require_once(DOKU_INC.'inc/mail.php');
14  require_once(DOKU_INC.'inc/parserutils.php');
15
16/**
17 * Return info about the current document as associative
18 * array.
19 *
20 * @author Andreas Gohr <andi@splitbrain.org>
21 */
22function pageinfo(){
23  global $ID;
24  global $REV;
25  global $USERINFO;
26  global $conf;
27
28  if($_SERVER['REMOTE_USER']){
29    $info['user']       = $_SERVER['REMOTE_USER'];
30    $info['userinfo']   = $USERINFO;
31    $info['perm']       = auth_quickaclcheck($ID);
32    $info['subscribed'] = is_subscribed($ID,$_SERVER['REMOTE_USER']);
33  }else{
34    $info['user']       = '';
35    $info['perm']       = auth_aclcheck($ID,'',null);
36    $info['subscribed'] = false;
37  }
38
39  $info['namespace'] = getNS($ID);
40  $info['locked']    = checklock($ID);
41  $info['filepath']  = realpath(wikiFN($ID,$REV));
42  $info['exists']    = @file_exists($info['filepath']);
43  if($REV && !$info['exists']){
44    //check if current revision was meant
45    $cur = wikiFN($ID);
46    if(@file_exists($cur) && (@filemtime($cur) == $REV)){
47      $info['filepath'] = realpath($cur);
48      $info['exists']   = true;
49      $REV = '';
50    }
51  }
52  $info['rev'] = $REV;
53  if($info['exists']){
54    $info['writable'] = (is_writable($info['filepath']) &&
55                         ($info['perm'] >= AUTH_EDIT));
56  }else{
57    $info['writable'] = ($info['perm'] >= AUTH_CREATE);
58  }
59  $info['editable']  = ($info['writable'] && empty($info['lock']));
60  $info['lastmod']   = @filemtime($info['filepath']);
61
62  //who's the editor
63  if($REV){
64    $revinfo = getRevisionInfo($ID,$REV);
65  }else{
66    $revinfo = getRevisionInfo($ID,$info['lastmod']);
67  }
68  $info['ip']     = $revinfo['ip'];
69  $info['user']   = $revinfo['user'];
70  $info['sum']    = $revinfo['sum'];
71  $info['editor'] = $revinfo['ip'];
72  if($revinfo['user']){
73    $info['editor'] = $revinfo['user'];
74  }else{
75    $info['editor'] = $revinfo['ip'];
76  }
77
78  return $info;
79}
80
81/**
82 * Build an string of URL parameters
83 *
84 * @author Andreas Gohr
85 */
86function buildURLparams($params){
87  $url = '';
88  $amp = false;
89  foreach($params as $key => $val){
90    if($amp) $url .= '&amp;';
91
92    $url .= $key.'=';
93    $url .= urlencode($val);
94    $amp = true;
95  }
96  return $url;
97}
98
99/**
100 * Build an string of html tag attributes
101 *
102 * @author Andreas Gohr
103 */
104function buildAttributes($params){
105  $url = '';
106  foreach($params as $key => $val){
107    $url .= $key.'="';
108    $url .= htmlspecialchars ($val);
109    $url .= '" ';
110  }
111  return $url;
112}
113
114
115/**
116 * print a message
117 *
118 * If HTTP headers were not sent yet the message is added
119 * to the global message array else it's printed directly
120 * using html_msgarea()
121 *
122 *
123 * Levels can be:
124 *
125 * -1 error
126 *  0 info
127 *  1 success
128 *
129 * @author Andreas Gohr <andi@splitbrain.org>
130 * @see    html_msgarea
131 */
132function msg($message,$lvl=0){
133  global $MSG;
134  $errors[-1] = 'error';
135  $errors[0]  = 'info';
136  $errors[1]  = 'success';
137
138  if(!headers_sent()){
139    if(!isset($MSG)) $MSG = array();
140    $MSG[]=array('lvl' => $errors[$lvl], 'msg' => $message);
141  }else{
142    $MSG = array();
143    $MSG[]=array('lvl' => $errors[$lvl], 'msg' => $message);
144    if(function_exists('html_msgarea')){
145      html_msgarea();
146    }else{
147      print "ERROR($lvl) $message";
148    }
149  }
150}
151
152/**
153 * This builds the breadcrumb trail and returns it as array
154 *
155 * @author Andreas Gohr <andi@splitbrain.org>
156 */
157function breadcrumbs(){
158  // we prepare the breadcrumbs early for quick session closing
159  static $crumbs = null;
160  if($crumbs != null) return $crumbs;
161
162  global $ID;
163  global $ACT;
164  global $conf;
165  $crumbs = $_SESSION[$conf['title']]['bc'];
166
167  //first visit?
168  if (!is_array($crumbs)){
169    $crumbs = array();
170  }
171  //we only save on show and existing wiki documents
172  $file = wikiFN($ID);
173  if($ACT != 'show' || !@file_exists($file)){
174    $_SESSION[$conf['title']]['bc'] = $crumbs;
175    return $crumbs;
176  }
177
178  // page names
179  $name = noNS($ID);
180  if ($conf['useheading']) {
181    // get page title
182    $title = p_get_first_heading($ID);
183    if ($title) {
184      $name = $title;
185    }
186  }
187
188  //remove ID from array
189  if (isset($crumbs[$ID])) {
190    unset($crumbs[$ID]);
191  }
192
193  //add to array
194  $crumbs[$ID] = $name;
195  //reduce size
196  while(count($crumbs) > $conf['breadcrumbs']){
197    array_shift($crumbs);
198  }
199  //save to session
200  $_SESSION[$conf['title']]['bc'] = $crumbs;
201  return $crumbs;
202}
203
204/**
205 * Filter for page IDs
206 *
207 * This is run on a ID before it is outputted somewhere
208 * currently used to replace the colon with something else
209 * on Windows systems and to have proper URL encoding
210 *
211 * Urlencoding is ommitted when the second parameter is false
212 *
213 * @author Andreas Gohr <andi@splitbrain.org>
214 */
215function idfilter($id,$ue=true){
216  global $conf;
217  if ($conf['useslash'] && $conf['userewrite']){
218    $id = strtr($id,':','/');
219  }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
220      $conf['userewrite']) {
221    $id = strtr($id,':',';');
222  }
223  if($ue){
224    $id = urlencode($id);
225    $id = str_replace('%3A',':',$id); //keep as colon
226    $id = str_replace('%2F','/',$id); //keep as slash
227  }
228  return $id;
229}
230
231/**
232 * This builds a link to a wikipage
233 *
234 * It handles URL rewriting and adds additional parameter if
235 * given in $more
236 *
237 * @author Andreas Gohr <andi@splitbrain.org>
238 */
239function wl($id='',$more='',$abs=false){
240  global $conf;
241  if(is_array($more)){
242    $more = buildURLparams($more);
243  }else{
244    $more = str_replace(',','&amp;',$more);
245  }
246
247  $id    = idfilter($id);
248  if($abs){
249    $xlink = DOKU_URL;
250  }else{
251    $xlink = DOKU_BASE;
252  }
253
254  if($conf['userewrite'] == 2){
255    $xlink .= DOKU_SCRIPT.'/'.$id;
256    if($more) $xlink .= '?'.$more;
257  }elseif($conf['userewrite']){
258    $xlink .= $id;
259    if($more) $xlink .= '?'.$more;
260  }else{
261    $xlink .= DOKU_SCRIPT.'?id='.$id;
262    if($more) $xlink .= '&amp;'.$more;
263  }
264
265  return $xlink;
266}
267
268/**
269 * Build a link to a media file
270 *
271 * Will return a link to the detail page if $direct is false
272 */
273function ml($id='',$more='',$direct=true){
274  global $conf;
275  if(is_array($more)){
276    $more = buildURLparams($more);
277  }else{
278    $more = str_replace(',','&amp;',$more);
279  }
280
281  $xlink = DOKU_BASE;
282
283  // external URLs are always direct without rewriting
284  if(preg_match('#^(https?|ftp)://#i',$id)){
285    $xlink .= 'lib/exe/fetch.php';
286    if($more){
287      $xlink .= '?'.$more;
288      $xlink .= '&amp;media='.urlencode($id);
289    }else{
290      $xlink .= '?media='.urlencode($id);
291    }
292    return $xlink;
293  }
294
295  $id = idfilter($id);
296
297  // decide on scriptname
298  if($direct){
299    if($conf['userewrite'] == 1){
300      $script = '_media';
301    }else{
302      $script = 'lib/exe/fetch.php';
303    }
304  }else{
305    if($conf['userewrite'] == 1){
306      $script = '_detail';
307    }else{
308      $script = 'lib/exe/detail.php';
309    }
310  }
311
312  // build URL based on rewrite mode
313   if($conf['userewrite']){
314     $xlink .= $script.'/'.$id;
315     if($more) $xlink .= '?'.$more;
316   }else{
317     if($more){
318       $xlink .= $script.'?'.$more;
319       $xlink .= '&amp;media='.$id;
320     }else{
321       $xlink .= $script.'?media='.$id;
322     }
323   }
324
325  return $xlink;
326}
327
328
329
330/**
331 * Just builds a link to a script
332 *
333 * @todo   maybe obsolete
334 * @author Andreas Gohr <andi@splitbrain.org>
335 */
336function script($script='doku.php'){
337#  $link = getBaseURL();
338#  $link .= $script;
339#  return $link;
340  return DOKU_BASE.DOKU_SCRIPT;
341}
342
343/**
344 * Spamcheck against wordlist
345 *
346 * Checks the wikitext against a list of blocked expressions
347 * returns true if the text contains any bad words
348 *
349 * @author Andreas Gohr <andi@splitbrain.org>
350 */
351function checkwordblock(){
352  global $TEXT;
353  global $conf;
354
355  if(!$conf['usewordblock']) return false;
356
357  $blockfile = file(DOKU_CONF.'wordblock.conf');
358  //how many lines to read at once (to work around some PCRE limits)
359  if(version_compare(phpversion(),'4.3.0','<')){
360    //old versions of PCRE define a maximum of parenthesises even if no
361    //backreferences are used - the maximum is 99
362    //this is very bad performancewise and may even be too high still
363    $chunksize = 40;
364  }else{
365    //read file in chunks of 600 - this should work around the
366    //MAX_PATTERN_SIZE in modern PCRE
367    $chunksize = 600;
368  }
369  while($blocks = array_splice($blockfile,0,$chunksize)){
370    $re = array();
371    #build regexp from blocks
372    foreach($blocks as $block){
373      $block = preg_replace('/#.*$/','',$block);
374      $block = trim($block);
375      if(empty($block)) continue;
376      $re[]  = $block;
377    }
378    if(preg_match('#('.join('|',$re).')#si',$TEXT)) return true;
379  }
380  return false;
381}
382
383/**
384 * Return the IP of the client
385 *
386 * Honours X-Forwarded-For Proxy Headers
387 *
388 * @author Andreas Gohr <andi@splitbrain.org>
389 */
390function clientIP(){
391  $my = $_SERVER['REMOTE_ADDR'];
392  if($_SERVER['HTTP_X_FORWARDED_FOR']){
393    $my .= ' ('.$_SERVER['HTTP_X_FORWARDED_FOR'].')';
394  }
395  return $my;
396}
397
398/**
399 * Checks if a given page is currently locked.
400 *
401 * removes stale lockfiles
402 *
403 * @author Andreas Gohr <andi@splitbrain.org>
404 */
405function checklock($id){
406  global $conf;
407  $lock = wikiFN($id).'.lock';
408
409  //no lockfile
410  if(!@file_exists($lock)) return false;
411
412  //lockfile expired
413  if((time() - filemtime($lock)) > $conf['locktime']){
414    unlink($lock);
415    return false;
416  }
417
418  //my own lock
419  $ip = io_readFile($lock);
420  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
421    return false;
422  }
423
424  return $ip;
425}
426
427/**
428 * Lock a page for editing
429 *
430 * @author Andreas Gohr <andi@splitbrain.org>
431 */
432function lock($id){
433  $lock = wikiFN($id).'.lock';
434  if($_SERVER['REMOTE_USER']){
435    io_saveFile($lock,$_SERVER['REMOTE_USER']);
436  }else{
437    io_saveFile($lock,clientIP());
438  }
439}
440
441/**
442 * Unlock a page if it was locked by the user
443 *
444 * @author Andreas Gohr <andi@splitbrain.org>
445 * @return bool true if a lock was removed
446 */
447function unlock($id){
448  $lock = wikiFN($id).'.lock';
449  if(@file_exists($lock)){
450    $ip = io_readFile($lock);
451    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
452      @unlink($lock);
453      return true;
454    }
455  }
456  return false;
457}
458
459/**
460 * convert line ending to unix format
461 *
462 * @see    formText() for 2crlf conversion
463 * @author Andreas Gohr <andi@splitbrain.org>
464 */
465function cleanText($text){
466  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
467  return $text;
468}
469
470/**
471 * Prepares text for print in Webforms by encoding special chars.
472 * It also converts line endings to Windows format which is
473 * pseudo standard for webforms.
474 *
475 * @see    cleanText() for 2unix conversion
476 * @author Andreas Gohr <andi@splitbrain.org>
477 */
478function formText($text){
479  $text = preg_replace("/\012/","\015\012",$text);
480  return htmlspecialchars($text);
481}
482
483/**
484 * Returns the specified local text in raw format
485 *
486 * @author Andreas Gohr <andi@splitbrain.org>
487 */
488function rawLocale($id){
489  return io_readFile(localeFN($id));
490}
491
492/**
493 * Returns the raw WikiText
494 *
495 * @author Andreas Gohr <andi@splitbrain.org>
496 */
497function rawWiki($id,$rev=''){
498  return io_readFile(wikiFN($id,$rev));
499}
500
501/**
502 * Returns the pagetemplate contents for the ID's namespace
503 *
504 * @author Andreas Gohr <andi@splitbrain.org>
505 */
506function pageTemplate($id){
507  global $conf;
508  global $INFO;
509  $tpl = io_readFile(dirname(wikiFN($id)).'/_template.txt');
510  $tpl = str_replace('@ID@',$id,$tpl);
511  $tpl = str_replace('@NS@',getNS($id),$tpl);
512  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
513  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
514  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
515  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
516  $tpl = str_replace('@DATE@',date($conf['dformat']),$tpl);
517  return $tpl;
518}
519
520
521/**
522 * Returns the raw Wiki Text in three slices.
523 *
524 * The range parameter needs to have the form "from-to"
525 * and gives the range of the section in bytes - no
526 * UTF-8 awareness is needed.
527 * The returned order is prefix, section and suffix.
528 *
529 * @author Andreas Gohr <andi@splitbrain.org>
530 */
531function rawWikiSlices($range,$id,$rev=''){
532  list($from,$to) = split('-',$range,2);
533  $text = io_readFile(wikiFN($id,$rev));
534  if(!$from) $from = 0;
535  if(!$to)   $to   = strlen($text)+1;
536
537  $slices[0] = substr($text,0,$from-1);
538  $slices[1] = substr($text,$from-1,$to-$from);
539  $slices[2] = substr($text,$to);
540
541  return $slices;
542}
543
544/**
545 * Joins wiki text slices
546 *
547 * function to join the text slices with correct lineendings again.
548 * When the pretty parameter is set to true it adds additional empty
549 * lines between sections if needed (used on saving).
550 *
551 * @author Andreas Gohr <andi@splitbrain.org>
552 */
553function con($pre,$text,$suf,$pretty=false){
554
555  if($pretty){
556    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
557    if($suf && substr($text,-1) != "\n") $text .= "\n";
558  }
559
560  if($pre) $pre .= "\n";
561  if($suf) $text .= "\n";
562  return $pre.$text.$suf;
563}
564
565/**
566 * print debug messages
567 *
568 * little function to print the content of a var
569 *
570 * @author Andreas Gohr <andi@splitbrain.org>
571 */
572function dbg($msg,$hidden=false){
573  (!$hidden) ? print '<pre class="dbg">' : print "<!--\n";
574  print_r($msg);
575  (!$hidden) ? print '</pre>' : print "\n-->";
576}
577
578/**
579 * Add's an entry to the changelog
580 *
581 * @author Andreas Gohr <andi@splitbrain.org>
582 */
583function addLogEntry($date,$id,$summary=""){
584  global $conf;
585  $id     = cleanID($id);//FIXME not needed anymore?
586
587  if(!@is_writable($conf['changelog'])){
588    msg($conf['changelog'].' is not writable!',-1);
589    return;
590  }
591
592  if(!$date) $date = time(); //use current time if none supplied
593  $remote = $_SERVER['REMOTE_ADDR'];
594  $user   = $_SERVER['REMOTE_USER'];
595
596  $logline = join("\t",array($date,$remote,$id,$user,$summary))."\n";
597
598  //FIXME: use adjusted io_saveFile instead
599  $fh = fopen($conf['changelog'],'a');
600  if($fh){
601    fwrite($fh,$logline);
602    fclose($fh);
603  }
604}
605
606/**
607 * returns an array of recently changed files using the
608 * changelog
609 * first   : first entry in array returned
610 * num     : return 'num' entries
611 *
612 * @author Andreas Gohr <andi@splitbrain.org>
613 */
614function getRecents($first,$num,$incdel=false){
615  global $conf;
616  $recent = array();
617  $names  = array();
618
619  if(!$num)
620    return $recent;
621
622  if(!@is_readable($conf['changelog'])){
623    msg($conf['changelog'].' is not readable',-1);
624    return $recent;
625  }
626
627  $loglines = file($conf['changelog']);
628  rsort($loglines); //reverse sort on timestamp
629
630  foreach ($loglines as $line){
631    $line = rtrim($line);        //remove newline
632    if(empty($line)) continue;   //skip empty lines
633    $info = split("\t",$line);   //split into parts
634    //add id if not in yet and file still exists and is allowed to read
635    if(!$names[$info[2]] &&
636       (@file_exists(wikiFN($info[2])) || $incdel) &&
637       (auth_quickaclcheck($info[2]) >= AUTH_READ)
638      ){
639      $names[$info[2]] = 1;
640      if(--$first >= 0) continue;  /* skip "first" entries */
641
642      $recent[$info[2]]['date'] = $info[0];
643      $recent[$info[2]]['ip']   = $info[1];
644      $recent[$info[2]]['user'] = $info[3];
645      $recent[$info[2]]['sum']  = $info[4];
646      $recent[$info[2]]['del']  = !@file_exists(wikiFN($info[2]));
647    }
648    if(count($recent) >= $num){
649      break; //finish if enough items found
650    }
651  }
652  return $recent;
653}
654
655/**
656 * gets additonal informations for a certain pagerevison
657 * from the changelog
658 *
659 * @author Andreas Gohr <andi@splitbrain.org>
660 */
661function getRevisionInfo($id,$rev){
662  global $conf;
663
664  if(!$rev) return(null);
665
666  $info = array();
667  if(!@is_readable($conf['changelog'])){
668    msg($conf['changelog'].' is not readable',-1);
669    return $recent;
670  }
671  $loglines = file($conf['changelog']);
672  $loglines = preg_grep("/$rev\t\d+\.\d+\.\d+\.\d+\t$id\t/",$loglines);
673  $loglines = array_reverse($loglines); //reverse sort on timestamp (shouldn't be needed)
674  $line = split("\t",$loglines[0]);
675  $info['date'] = $line[0];
676  $info['ip']   = $line[1];
677  $info['user'] = $line[3];
678  $info['sum']   = $line[4];
679  return $info;
680}
681
682/**
683 * Saves a wikitext by calling io_saveFile
684 *
685 * @author Andreas Gohr <andi@splitbrain.org>
686 */
687function saveWikiText($id,$text,$summary){
688  global $conf;
689  global $lang;
690  umask($conf['umask']);
691  // ignore if no changes were made
692  if($text == rawWiki($id,'')){
693    return;
694  }
695
696  $file = wikiFN($id);
697  $old  = saveOldRevision($id);
698
699  if (empty($text)){
700    // remove empty file
701    @unlink($file);
702    // remove any meta info
703    $mfiles = metaFiles($id);
704    foreach ($mfiles as $mfile) {
705      if (file_exists($mfile)) @unlink($mfile);
706    }
707    $del = true;
708    //autoset summary on deletion
709    if(empty($summary)) $summary = $lang['deleted'];
710    //remove empty namespaces
711    io_sweepNS($id);
712  }else{
713    // save file (datadir is created in io_saveFile)
714    io_saveFile($file,$text);
715    $del = false;
716  }
717
718  addLogEntry(@filemtime($file),$id,$summary);
719  // send notify mails
720  notify($id,'admin',$old,$summary);
721  notify($id,'subscribers',$old,$summary);
722
723  //purge cache on add by updating the purgefile
724  if($conf['purgeonadd'] && (!$old || $del)){
725    io_saveFile($conf['cachedir'].'/purgefile',time());
726  }
727}
728
729/**
730 * moves the current version to the attic and returns its
731 * revision date
732 *
733 * @author Andreas Gohr <andi@splitbrain.org>
734 */
735function saveOldRevision($id){
736	global $conf;
737  umask($conf['umask']);
738  $oldf = wikiFN($id);
739  if(!@file_exists($oldf)) return '';
740  $date = filemtime($oldf);
741  $newf = wikiFN($id,$date);
742  if(substr($newf,-3)=='.gz'){
743    io_saveFile($newf,rawWiki($id));
744  }else{
745    io_makeFileDir($newf);
746    copy($oldf, $newf);
747  }
748  return $date;
749}
750
751/**
752 * Sends a notify mail on page change
753 *
754 * @param  string $id       The changed page
755 * @param  string $who      Who to notify (admin|subscribers)
756 * @param  int    $rev      Old page revision
757 * @param  string $summary  What changed
758 *
759 * @author Andreas Gohr <andi@splitbrain.org>
760 */
761function notify($id,$who,$rev='',$summary=''){
762  global $lang;
763  global $conf;
764
765  // decide if there is something to do
766  if($who == 'admin'){
767    if(empty($conf['notify'])) return; //notify enabled?
768    $text = rawLocale('mailtext');
769    $to   = $conf['notify'];
770    $bcc  = '';
771  }elseif($who == 'subscribers'){
772    if(!$conf['subscribers']) return; //subscribers enabled?
773    $bcc  = subscriber_addresslist($id);
774    if(empty($bcc)) return;
775    $to   = '';
776    $text = rawLocale('subscribermail');
777  }else{
778    return; //just to be safe
779  }
780
781  $text = str_replace('@DATE@',date($conf['dformat']),$text);
782  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
783  $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text);
784  $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text);
785  $text = str_replace('@NEWPAGE@',wl($id,'',true),$text);
786  $text = str_replace('@PAGE@',$id,$text);
787  $text = str_replace('@TITLE@',$conf['title'],$text);
788  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
789  $text = str_replace('@SUMMARY@',$summary,$text);
790  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
791
792  if($rev){
793    $subject = $lang['mail_changed'].' '.$id;
794    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true),$text);
795    require_once("inc/DifferenceEngine.php");
796    $df  = new Diff(split("\n",rawWiki($id,$rev)),
797                    split("\n",rawWiki($id)));
798    $dformat = new UnifiedDiffFormatter();
799    $diff    = $dformat->format($df);
800  }else{
801    $subject=$lang['mail_newpage'].' '.$id;
802    $text = str_replace('@OLDPAGE@','none',$text);
803    $diff = rawWiki($id);
804  }
805  $text = str_replace('@DIFF@',$diff,$text);
806  $subject = '['.$conf['title'].'] '.$subject;
807
808  mail_send($to,$subject,$text,$conf['mailfrom'],'',$bcc);
809}
810
811/**
812 * Return a list of available page revisons
813 *
814 * @author Andreas Gohr <andi@splitbrain.org>
815 */
816function getRevisions($id){
817  $revd = dirname(wikiFN($id,'foo'));
818  $revs = array();
819  $clid = cleanID($id);
820  if(strrpos($clid,':')) $clid = substr($clid,strrpos($clid,':')+1); //remove path
821  $clid = utf8_encodeFN($clid);
822
823  if (is_dir($revd) && $dh = opendir($revd)) {
824    while (($file = readdir($dh)) !== false) {
825      if (is_dir($revd.'/'.$file)) continue;
826      if (preg_match('/^'.$clid.'\.(\d+)\.txt(\.gz)?$/',$file,$match)){
827        $revs[]=$match[1];
828      }
829    }
830    closedir($dh);
831  }
832  rsort($revs);
833  return $revs;
834}
835
836/**
837 * extracts the query from a google referer
838 *
839 * @todo   should be more generic and support yahoo et al
840 * @author Andreas Gohr <andi@splitbrain.org>
841 */
842function getGoogleQuery(){
843  $url = parse_url($_SERVER['HTTP_REFERER']);
844  if(!$url) return '';
845
846  if(!preg_match("#google\.#i",$url['host'])) return '';
847  $query = array();
848  parse_str($url['query'],$query);
849
850  return $query['q'];
851}
852
853/**
854 * Try to set correct locale
855 *
856 * @deprecated No longer used
857 * @author     Andreas Gohr <andi@splitbrain.org>
858 */
859function setCorrectLocale(){
860  global $conf;
861  global $lang;
862
863  $enc = strtoupper($lang['encoding']);
864  foreach ($lang['locales'] as $loc){
865    //try locale
866    if(@setlocale(LC_ALL,$loc)) return;
867    //try loceale with encoding
868    if(@setlocale(LC_ALL,"$loc.$enc")) return;
869  }
870  //still here? try to set from environment
871  @setlocale(LC_ALL,"");
872}
873
874/**
875 * Return the human readable size of a file
876 *
877 * @param       int    $size   A file size
878 * @param       int    $dec    A number of decimal places
879 * @author      Martin Benjamin <b.martin@cybernet.ch>
880 * @author      Aidan Lister <aidan@php.net>
881 * @version     1.0.0
882 */
883function filesize_h($size, $dec = 1){
884  $sizes = array('B', 'KB', 'MB', 'GB');
885  $count = count($sizes);
886  $i = 0;
887
888  while ($size >= 1024 && ($i < $count - 1)) {
889    $size /= 1024;
890    $i++;
891  }
892
893  return round($size, $dec) . ' ' . $sizes[$i];
894}
895
896/**
897 * Return DokuWikis version
898 *
899 * @author Andreas Gohr <andi@splitbrain.org>
900 */
901function getVersion(){
902  //import version string
903  if(@file_exists('VERSION')){
904    //official release
905    return 'Release '.trim(io_readfile('VERSION'));
906  }elseif(is_dir('_darcs')){
907    //darcs checkout
908    $inv = file('_darcs/inventory');
909    $inv = preg_grep('#andi@splitbrain\.org\*\*\d{14}#',$inv);
910    $cur = array_pop($inv);
911    preg_match('#\*\*(\d{4})(\d{2})(\d{2})#',$cur,$matches);
912    return 'Darcs '.$matches[1].'-'.$matches[2].'-'.$matches[3];
913  }else{
914    return 'snapshot?';
915  }
916}
917
918/**
919 * Run a few sanity checks
920 *
921 * @author Andreas Gohr <andi@splitbrain.org>
922 */
923function check(){
924  global $conf;
925  global $INFO;
926
927  msg('DokuWiki version: '.getVersion(),1);
928
929  if(version_compare(phpversion(),'4.3.0','<')){
930    msg('Your PHP version is too old ('.phpversion().' vs. 4.3.+ recommended)',-1);
931  }elseif(version_compare(phpversion(),'4.3.10','<')){
932    msg('Consider upgrading PHP to 4.3.10 or higher for security reasons (your version: '.phpversion().')',0);
933  }else{
934    msg('PHP version '.phpversion(),1);
935  }
936
937  if(is_writable($conf['changelog'])){
938    msg('Changelog is writable',1);
939  }else{
940    msg('Changelog is not writable',-1);
941  }
942
943  if(is_writable($conf['datadir'])){
944    msg('Datadir is writable',1);
945  }else{
946    msg('Datadir is not writable',-1);
947  }
948
949  if(is_writable($conf['olddir'])){
950    msg('Attic is writable',1);
951  }else{
952    msg('Attic is not writable',-1);
953  }
954
955  if(is_writable($conf['mediadir'])){
956    msg('Mediadir is writable',1);
957  }else{
958    msg('Mediadir is not writable',-1);
959  }
960
961  if(is_writable($conf['cachedir'])){
962    msg('Cachedir is writable',1);
963  }else{
964    msg('Cachedir is not writable',-1);
965  }
966
967  if(is_writable(DOKU_CONF.'users.auth.php')){
968    msg('conf/users.auth.php is writable',1);
969  }else{
970    msg('conf/users.auth.php is not writable',0);
971  }
972
973  if(function_exists('mb_strpos')){
974    if(defined('UTF8_NOMBSTRING')){
975      msg('mb_string extension is available but will not be used',0);
976    }else{
977      msg('mb_string extension is available and will be used',1);
978    }
979  }else{
980    msg('mb_string extension not available - PHP only replacements will be used',0);
981  }
982
983  msg('Your current permission for this page is '.$INFO['perm'],0);
984
985  if(is_writable($INFO['filepath'])){
986    msg('The current page is writable by the webserver',0);
987  }else{
988    msg('The current page is not writable by the webserver',0);
989  }
990
991  if($INFO['writable']){
992    msg('The current page is writable by you',0);
993  }else{
994    msg('The current page is not writable you',0);
995  }
996}
997
998/**
999 * Let us know if a user is tracking a page
1000 *
1001 * @author Andreas Gohr <andi@splitbrain.org>
1002 */
1003function is_subscribed($id,$uid){
1004  $file=metaFN($id,'.mlist');
1005  if (@file_exists($file)) {
1006    $mlist = file($file);
1007    $pos = array_search($uid."\n",$mlist);
1008    return is_int($pos);
1009  }
1010
1011  return false;
1012}
1013
1014/**
1015 * Return a string with the email addresses of all the
1016 * users subscribed to a page
1017 *
1018 * @author Steven Danz <steven-danz@kc.rr.com>
1019 */
1020function subscriber_addresslist($id){
1021  global $conf;
1022
1023  $emails = '';
1024
1025  if (!$conf['subscribers']) return;
1026
1027  $mlist = array();
1028  $file=metaFN($id,'.mlist');
1029  if (file_exists($file)) {
1030    $mlist = file($file);
1031  }
1032  if(count($mlist) > 0) {
1033    foreach ($mlist as $who) {
1034      $who = rtrim($who);
1035      $info = auth_getUserData($who);
1036      $level = auth_aclcheck($id,$who,$info['grps']);
1037      if ($level >= AUTH_READ) {
1038        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1039          if (empty($emails)) {
1040            $emails = $info['mail'];
1041          } else {
1042            $emails = "$emails,".$info['mail'];
1043          }
1044        }
1045      }
1046    }
1047  }
1048
1049  return $emails;
1050}
1051
1052/**
1053 * Removes quoting backslashes
1054 *
1055 * @author Andreas Gohr <andi@splitbrain.org>
1056 */
1057function unslash($string,$char="'"){
1058  return str_replace('\\'.$char,$char,$string);
1059}
1060
1061//Setup VIM: ex: et ts=2 enc=utf-8 :
1062