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