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