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