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