xref: /dokuwiki/inc/common.php (revision 30d7d7185531fa4b68ece7f3b11ca855e1fd5b5a)
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
9if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../').'/');
10require_once(DOKU_CONF.'dokuwiki.php');
11require_once(DOKU_INC.'inc/io.php');
12require_once(DOKU_INC.'inc/changelog.php');
13require_once(DOKU_INC.'inc/utf8.php');
14require_once(DOKU_INC.'inc/mail.php');
15require_once(DOKU_INC.'inc/parserutils.php');
16require_once(DOKU_INC.'inc/infoutils.php');
17
18/**
19 * These constants are used with the recents function
20 */
21define('RECENTS_SKIP_DELETED',2);
22define('RECENTS_SKIP_MINORS',4);
23define('RECENTS_SKIP_SUBSPACES',8);
24
25/**
26 * Wrapper around htmlspecialchars()
27 *
28 * @author Andreas Gohr <andi@splitbrain.org>
29 * @see    htmlspecialchars()
30 */
31function hsc($string){
32  return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
33}
34
35/**
36 * print a newline terminated string
37 *
38 * You can give an indention as optional parameter
39 *
40 * @author Andreas Gohr <andi@splitbrain.org>
41 */
42function ptln($string,$intend=0){
43  for($i=0; $i<$intend; $i++) print ' ';
44  echo "$string\n";
45}
46
47/**
48 * strips control characters (<32) from the given string
49 *
50 * @author Andreas Gohr <andi@splitbrain.org>
51 */
52function stripctl($string){
53  return preg_replace('/[\x00-\x1F]+/s','',$string);
54}
55
56/**
57 * Return info about the current document as associative
58 * array.
59 *
60 * @author Andreas Gohr <andi@splitbrain.org>
61 */
62function pageinfo(){
63  global $ID;
64  global $REV;
65  global $USERINFO;
66  global $conf;
67
68  // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
69  // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
70  $info['id'] = $ID;
71  $info['rev'] = $REV;
72
73  if($_SERVER['REMOTE_USER']){
74    $info['userinfo']   = $USERINFO;
75    $info['perm']       = auth_quickaclcheck($ID);
76    $info['subscribed'] = is_subscribed($ID,$_SERVER['REMOTE_USER']);
77    $info['client']     = $_SERVER['REMOTE_USER'];
78
79    // set info about manager/admin status
80    $info['isadmin']   = false;
81    $info['ismanager'] = false;
82    if($info['perm'] == AUTH_ADMIN){
83      $info['isadmin']   = true;
84      $info['ismanager'] = true;
85    }elseif(auth_ismanager()){
86      $info['ismanager'] = true;
87    }
88
89    // if some outside auth were used only REMOTE_USER is set
90    if(!$info['userinfo']['name']){
91      $info['userinfo']['name'] = $_SERVER['REMOTE_USER'];
92    }
93
94  }else{
95    $info['perm']       = auth_aclcheck($ID,'',null);
96    $info['subscribed'] = false;
97    $info['client']     = clientIP(true);
98  }
99
100  $info['namespace'] = getNS($ID);
101  $info['locked']    = checklock($ID);
102  $info['filepath']  = realpath(wikiFN($ID));
103  $info['exists']    = @file_exists($info['filepath']);
104  if($REV){
105    //check if current revision was meant
106    if($info['exists'] && (@filemtime($info['filepath'])==$REV)){
107      $REV = '';
108    }else{
109      //really use old revision
110      $info['filepath'] = realpath(wikiFN($ID,$REV));
111      $info['exists']   = @file_exists($info['filepath']);
112    }
113  }
114  $info['rev'] = $REV;
115  if($info['exists']){
116    $info['writable'] = (is_writable($info['filepath']) &&
117                         ($info['perm'] >= AUTH_EDIT));
118  }else{
119    $info['writable'] = ($info['perm'] >= AUTH_CREATE);
120  }
121  $info['editable']  = ($info['writable'] && empty($info['lock']));
122  $info['lastmod']   = @filemtime($info['filepath']);
123
124  //load page meta data
125  $info['meta'] = p_get_metadata($ID);
126
127  //who's the editor
128  if($REV){
129    $revinfo = getRevisionInfo($ID, $REV, 1024);
130  }else{
131    $revinfo = isset($info['meta']['last_change']) ? $info['meta']['last_change'] : getRevisionInfo($ID,$info['lastmod'],1024);
132  }
133
134  $info['ip']     = $revinfo['ip'];
135  $info['user']   = $revinfo['user'];
136  $info['sum']    = $revinfo['sum'];
137  // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
138  // Use $INFO['meta']['last_change']['type']==='e' in place of $info['minor'].
139
140  if($revinfo['user']){
141    $info['editor'] = $revinfo['user'];
142  }else{
143    $info['editor'] = $revinfo['ip'];
144  }
145
146  // draft
147  $draft = getCacheName($info['client'].$ID,'.draft');
148  if(@file_exists($draft)){
149    if(@filemtime($draft) < @filemtime(wikiFN($ID))){
150      // remove stale draft
151      @unlink($draft);
152    }else{
153      $info['draft'] = $draft;
154    }
155  }
156
157  return $info;
158}
159
160/**
161 * Build an string of URL parameters
162 *
163 * @author Andreas Gohr
164 */
165function buildURLparams($params, $sep='&amp;'){
166  $url = '';
167  $amp = false;
168  foreach($params as $key => $val){
169    if($amp) $url .= $sep;
170
171    $url .= $key.'=';
172    $url .= rawurlencode($val);
173    $amp = true;
174  }
175  return $url;
176}
177
178/**
179 * Build an string of html tag attributes
180 *
181 * Skips keys starting with '_', values get HTML encoded
182 *
183 * @author Andreas Gohr
184 */
185function buildAttributes($params){
186  $url = '';
187  foreach($params as $key => $val){
188    if($key{0} == '_') continue;
189
190    $url .= $key.'="';
191    $url .= htmlspecialchars ($val);
192    $url .= '" ';
193  }
194  return $url;
195}
196
197
198/**
199 * This builds the breadcrumb trail and returns it as array
200 *
201 * @author Andreas Gohr <andi@splitbrain.org>
202 */
203function breadcrumbs(){
204  // we prepare the breadcrumbs early for quick session closing
205  static $crumbs = null;
206  if($crumbs != null) return $crumbs;
207
208  global $ID;
209  global $ACT;
210  global $conf;
211  $crumbs = $_SESSION[DOKU_COOKIE]['bc'];
212
213  //first visit?
214  if (!is_array($crumbs)){
215    $crumbs = array();
216  }
217  //we only save on show and existing wiki documents
218  $file = wikiFN($ID);
219  if($ACT != 'show' || !@file_exists($file)){
220    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
221    return $crumbs;
222  }
223
224  // page names
225  $name = noNS($ID);
226  if ($conf['useheading']) {
227    // get page title
228    $title = p_get_first_heading($ID);
229    if ($title) {
230      $name = $title;
231    }
232  }
233
234  //remove ID from array
235  if (isset($crumbs[$ID])) {
236    unset($crumbs[$ID]);
237  }
238
239  //add to array
240  $crumbs[$ID] = $name;
241  //reduce size
242  while(count($crumbs) > $conf['breadcrumbs']){
243    array_shift($crumbs);
244  }
245  //save to session
246  $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
247  return $crumbs;
248}
249
250/**
251 * Filter for page IDs
252 *
253 * This is run on a ID before it is outputted somewhere
254 * currently used to replace the colon with something else
255 * on Windows systems and to have proper URL encoding
256 *
257 * Urlencoding is ommitted when the second parameter is false
258 *
259 * @author Andreas Gohr <andi@splitbrain.org>
260 */
261function idfilter($id,$ue=true){
262  global $conf;
263  if ($conf['useslash'] && $conf['userewrite']){
264    $id = strtr($id,':','/');
265  }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
266      $conf['userewrite']) {
267    $id = strtr($id,':',';');
268  }
269  if($ue){
270    $id = rawurlencode($id);
271    $id = str_replace('%3A',':',$id); //keep as colon
272    $id = str_replace('%2F','/',$id); //keep as slash
273  }
274  return $id;
275}
276
277/**
278 * This builds a link to a wikipage
279 *
280 * It handles URL rewriting and adds additional parameter if
281 * given in $more
282 *
283 * @author Andreas Gohr <andi@splitbrain.org>
284 */
285function wl($id='',$more='',$abs=false,$sep='&amp;'){
286  global $conf;
287  if(is_array($more)){
288    $more = buildURLparams($more,$sep);
289  }else{
290    $more = str_replace(',',$sep,$more);
291  }
292
293  $id    = idfilter($id);
294  if($abs){
295    $xlink = DOKU_URL;
296  }else{
297    $xlink = DOKU_BASE;
298  }
299
300  if($conf['userewrite'] == 2){
301    $xlink .= DOKU_SCRIPT.'/'.$id;
302    if($more) $xlink .= '?'.$more;
303  }elseif($conf['userewrite']){
304    $xlink .= $id;
305    if($more) $xlink .= '?'.$more;
306  }else{
307    $xlink .= DOKU_SCRIPT.'?id='.$id;
308    if($more) $xlink .= $sep.$more;
309  }
310
311  return $xlink;
312}
313
314/**
315 * This builds a link to an alternate page format
316 *
317 * Handles URL rewriting if enabled. Follows the style of wl().
318 *
319 * @author Ben Coburn <btcoburn@silicodon.net>
320 */
321function exportlink($id='',$format='raw',$more='',$abs=false,$sep='&amp;'){
322  global $conf;
323  if(is_array($more)){
324    $more = buildURLparams($more,$sep);
325  }else{
326    $more = str_replace(',',$sep,$more);
327  }
328
329  $format = rawurlencode($format);
330  $id = idfilter($id);
331  if($abs){
332    $xlink = DOKU_URL;
333  }else{
334    $xlink = DOKU_BASE;
335  }
336
337  if($conf['userewrite'] == 2){
338    $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
339    if($more) $xlink .= $sep.$more;
340  }elseif($conf['userewrite'] == 1){
341    $xlink .= '_export/'.$format.'/'.$id;
342    if($more) $xlink .= '?'.$more;
343  }else{
344    $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
345    if($more) $xlink .= $sep.$more;
346  }
347
348  return $xlink;
349}
350
351/**
352 * Build a link to a media file
353 *
354 * Will return a link to the detail page if $direct is false
355 */
356function ml($id='',$more='',$direct=true,$sep='&amp;'){
357  global $conf;
358  if(is_array($more)){
359    $more = buildURLparams($more,$sep);
360  }else{
361    $more = str_replace(',',$sep,$more);
362  }
363
364  $xlink = DOKU_BASE;
365
366  // external URLs are always direct without rewriting
367  if(preg_match('#^(https?|ftp)://#i',$id)){
368    $xlink .= 'lib/exe/fetch.php';
369    if($more){
370      $xlink .= '?'.$more;
371      $xlink .= $sep.'media='.rawurlencode($id);
372    }else{
373      $xlink .= '?media='.rawurlencode($id);
374    }
375    return $xlink;
376  }
377
378  $id = idfilter($id);
379
380  // decide on scriptname
381  if($direct){
382    if($conf['userewrite'] == 1){
383      $script = '_media';
384    }else{
385      $script = 'lib/exe/fetch.php';
386    }
387  }else{
388    if($conf['userewrite'] == 1){
389      $script = '_detail';
390    }else{
391      $script = 'lib/exe/detail.php';
392    }
393  }
394
395  // build URL based on rewrite mode
396   if($conf['userewrite']){
397     $xlink .= $script.'/'.$id;
398     if($more) $xlink .= '?'.$more;
399   }else{
400     if($more){
401       $xlink .= $script.'?'.$more;
402       $xlink .= $sep.'media='.$id;
403     }else{
404       $xlink .= $script.'?media='.$id;
405     }
406   }
407
408  return $xlink;
409}
410
411
412
413/**
414 * Just builds a link to a script
415 *
416 * @todo   maybe obsolete
417 * @author Andreas Gohr <andi@splitbrain.org>
418 */
419function script($script='doku.php'){
420#  $link = getBaseURL();
421#  $link .= $script;
422#  return $link;
423  return DOKU_BASE.DOKU_SCRIPT;
424}
425
426/**
427 * Spamcheck against wordlist
428 *
429 * Checks the wikitext against a list of blocked expressions
430 * returns true if the text contains any bad words
431 *
432 * @author Andreas Gohr <andi@splitbrain.org>
433 */
434function checkwordblock(){
435  global $TEXT;
436  global $conf;
437
438  if(!$conf['usewordblock']) return false;
439
440  // we prepare the text a tiny bit to prevent spammers circumventing URL checks
441  $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT);
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 200 - this should work around the
452    //MAX_PATTERN_SIZE in modern PCRE
453    $chunksize = 200;
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(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
488    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']));
489  if(!empty($_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  $match = array();
495  for($i=0; $i<$cnt; $i++){
496    if(preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i],$match)) {
497      $ip[$i] = $match[0];
498    } else {
499      $ip[$i] = '';
500    }
501    if(empty($ip[$i])) unset($ip[$i]);
502  }
503  $ip = array_values(array_unique($ip));
504  if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
505
506  if(!$single) return join(',',$ip);
507
508  // decide which IP to use, trying to avoid local addresses
509  $ip = array_reverse($ip);
510  foreach($ip as $i){
511    if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){
512      continue;
513    }else{
514      return $i;
515    }
516  }
517  // still here? just use the first (last) address
518  return $ip[0];
519}
520
521/**
522 * Checks if a given page is currently locked.
523 *
524 * removes stale lockfiles
525 *
526 * @author Andreas Gohr <andi@splitbrain.org>
527 */
528function checklock($id){
529  global $conf;
530  $lock = wikiLockFN($id);
531
532  //no lockfile
533  if(!@file_exists($lock)) return false;
534
535  //lockfile expired
536  if((time() - filemtime($lock)) > $conf['locktime']){
537    @unlink($lock);
538    return false;
539  }
540
541  //my own lock
542  $ip = io_readFile($lock);
543  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
544    return false;
545  }
546
547  return $ip;
548}
549
550/**
551 * Lock a page for editing
552 *
553 * @author Andreas Gohr <andi@splitbrain.org>
554 */
555function lock($id){
556  $lock = wikiLockFN($id);
557  if($_SERVER['REMOTE_USER']){
558    io_saveFile($lock,$_SERVER['REMOTE_USER']);
559  }else{
560    io_saveFile($lock,clientIP());
561  }
562}
563
564/**
565 * Unlock a page if it was locked by the user
566 *
567 * @author Andreas Gohr <andi@splitbrain.org>
568 * @return bool true if a lock was removed
569 */
570function unlock($id){
571  $lock = wikiLockFN($id);
572  if(@file_exists($lock)){
573    $ip = io_readFile($lock);
574    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
575      @unlink($lock);
576      return true;
577    }
578  }
579  return false;
580}
581
582/**
583 * convert line ending to unix format
584 *
585 * @see    formText() for 2crlf conversion
586 * @author Andreas Gohr <andi@splitbrain.org>
587 */
588function cleanText($text){
589  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
590  return $text;
591}
592
593/**
594 * Prepares text for print in Webforms by encoding special chars.
595 * It also converts line endings to Windows format which is
596 * pseudo standard for webforms.
597 *
598 * @see    cleanText() for 2unix conversion
599 * @author Andreas Gohr <andi@splitbrain.org>
600 */
601function formText($text){
602  $text = preg_replace("/\012/","\015\012",$text);
603  return htmlspecialchars($text);
604}
605
606/**
607 * Returns the specified local text in raw format
608 *
609 * @author Andreas Gohr <andi@splitbrain.org>
610 */
611function rawLocale($id){
612  return io_readFile(localeFN($id));
613}
614
615/**
616 * Returns the raw WikiText
617 *
618 * @author Andreas Gohr <andi@splitbrain.org>
619 */
620function rawWiki($id,$rev=''){
621  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
622}
623
624/**
625 * Returns the pagetemplate contents for the ID's namespace
626 *
627 * @author Andreas Gohr <andi@splitbrain.org>
628 */
629function pageTemplate($data){
630  $id = $data[0];
631  global $conf;
632  global $INFO;
633  $tpl = io_readFile(dirname(wikiFN($id)).'/_template.txt');
634  $tpl = str_replace('@ID@',$id,$tpl);
635  $tpl = str_replace('@NS@',getNS($id),$tpl);
636  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
637  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
638  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
639  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
640  $tpl = str_replace('@DATE@',date($conf['dformat']),$tpl);
641  return $tpl;
642}
643
644
645/**
646 * Returns the raw Wiki Text in three slices.
647 *
648 * The range parameter needs to have the form "from-to"
649 * and gives the range of the section in bytes - no
650 * UTF-8 awareness is needed.
651 * The returned order is prefix, section and suffix.
652 *
653 * @author Andreas Gohr <andi@splitbrain.org>
654 */
655function rawWikiSlices($range,$id,$rev=''){
656  list($from,$to) = split('-',$range,2);
657  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
658  if(!$from) $from = 0;
659  if(!$to)   $to   = strlen($text)+1;
660
661  $slices[0] = substr($text,0,$from-1);
662  $slices[1] = substr($text,$from-1,$to-$from);
663  $slices[2] = substr($text,$to);
664
665  return $slices;
666}
667
668/**
669 * Joins wiki text slices
670 *
671 * function to join the text slices with correct lineendings again.
672 * When the pretty parameter is set to true it adds additional empty
673 * lines between sections if needed (used on saving).
674 *
675 * @author Andreas Gohr <andi@splitbrain.org>
676 */
677function con($pre,$text,$suf,$pretty=false){
678
679  if($pretty){
680    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
681    if($suf && substr($text,-1) != "\n") $text .= "\n";
682  }
683
684  if($pre) $pre .= "\n";
685  if($suf) $text .= "\n";
686  return $pre.$text.$suf;
687}
688
689/**
690 * Saves a wikitext by calling io_writeWikiPage.
691 * Also directs changelog and attic updates.
692 *
693 * @author Andreas Gohr <andi@splitbrain.org>
694 * @author Ben Coburn <btcoburn@silicodon.net>
695 */
696function saveWikiText($id,$text,$summary,$minor=false){
697  /* Note to developers:
698     This code is subtle and delicate. Test the behavior of
699     the attic and changelog with dokuwiki and external edits
700     after any changes. External edits change the wiki page
701     directly without using php or dokuwiki.
702  */
703  global $conf;
704  global $lang;
705  global $REV;
706  // ignore if no changes were made
707  if($text == rawWiki($id,'')){
708    return;
709  }
710
711  $file = wikiFN($id);
712  $old = @filemtime($file); // from page
713  $wasRemoved = empty($text);
714  $wasCreated = !@file_exists($file);
715  $wasReverted = ($REV==true);
716  $newRev = false;
717  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
718  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
719  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
720    // add old revision to the attic if missing
721    saveOldRevision($id);
722    // add a changelog entry if this edit came from outside dokuwiki
723    if ($old>$oldRev) {
724      addLogEntry($old, $id);
725      // send notify mails
726      notify($id,'admin',$oldRev,'',false);
727      notify($id,'subscribers',$oldRev,'',false);
728      // remove soon to be stale instructions
729      $cache = new cache_instructions($id, $file);
730      $cache->removeCache();
731    }
732  }
733
734  if ($wasRemoved){
735    // pre-save deleted revision
736    @touch($file);
737    clearstatcache();
738    $newRev = saveOldRevision($id);
739    // remove empty file
740    @unlink($file);
741    // remove old meta info...
742    $mfiles = metaFiles($id);
743    $changelog = metaFN($id, '.changes');
744    foreach ($mfiles as $mfile) {
745      // but keep per-page changelog to preserve page history
746      if (@file_exists($mfile) && $mfile!==$changelog) { @unlink($mfile); }
747    }
748    $del = true;
749    // autoset summary on deletion
750    if(empty($summary)) $summary = $lang['deleted'];
751    // remove empty namespaces
752    io_sweepNS($id, 'datadir');
753    io_sweepNS($id, 'mediadir');
754  }else{
755    // save file (namespace dir is created in io_writeWikiPage)
756    io_writeWikiPage($file, $text, $id);
757    // pre-save the revision, to keep the attic in sync
758    $newRev = saveOldRevision($id);
759    $del = false;
760  }
761
762  // select changelog line type
763  $extra = '';
764  $type = 'E';
765  if ($wasReverted) {
766    $type = 'R';
767    $extra = $REV;
768  }
769  else if ($wasCreated) { $type = 'C'; }
770  else if ($wasRemoved) { $type = 'D'; }
771  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = 'e'; } //minor edits only for logged in users
772
773  addLogEntry($newRev, $id, $type, $summary, $extra);
774  // send notify mails
775  notify($id,'admin',$old,$summary,$minor);
776  notify($id,'subscribers',$old,$summary,$minor);
777
778  // update the purgefile (timestamp of the last time anything within the wiki was changed)
779  io_saveFile($conf['cachedir'].'/purgefile',time());
780}
781
782/**
783 * moves the current version to the attic and returns its
784 * revision date
785 *
786 * @author Andreas Gohr <andi@splitbrain.org>
787 */
788function saveOldRevision($id){
789  global $conf;
790  $oldf = wikiFN($id);
791  if(!@file_exists($oldf)) return '';
792  $date = filemtime($oldf);
793  $newf = wikiFN($id,$date);
794  io_writeWikiPage($newf, rawWiki($id), $id, $date);
795  return $date;
796}
797
798/**
799 * Sends a notify mail on page change
800 *
801 * @param  string  $id       The changed page
802 * @param  string  $who      Who to notify (admin|subscribers)
803 * @param  int     $rev      Old page revision
804 * @param  string  $summary  What changed
805 * @param  boolean $minor    Is this a minor edit?
806 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
807 *
808 * @author Andreas Gohr <andi@splitbrain.org>
809 */
810function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
811  global $lang;
812  global $conf;
813  global $INFO;
814
815  // decide if there is something to do
816  if($who == 'admin'){
817    if(empty($conf['notify'])) return; //notify enabled?
818    $text = rawLocale('mailtext');
819    $to   = $conf['notify'];
820    $bcc  = '';
821  }elseif($who == 'subscribers'){
822    if(!$conf['subscribers']) return; //subscribers enabled?
823    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
824    $bcc  = subscriber_addresslist($id);
825    if(empty($bcc)) return;
826    $to   = '';
827    $text = rawLocale('subscribermail');
828  }elseif($who == 'register'){
829    if(empty($conf['registernotify'])) return;
830    $text = rawLocale('registermail');
831    $to   = $conf['registernotify'];
832    $bcc  = '';
833  }else{
834    return; //just to be safe
835  }
836
837  $text = str_replace('@DATE@',date($conf['dformat']),$text);
838  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
839  $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text);
840  $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text);
841  $text = str_replace('@NEWPAGE@',wl($id,'',true),$text);
842  $text = str_replace('@PAGE@',$id,$text);
843  $text = str_replace('@TITLE@',$conf['title'],$text);
844  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
845  $text = str_replace('@SUMMARY@',$summary,$text);
846  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
847
848  foreach ($replace as $key => $substitution) {
849    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
850  }
851
852  if($who == 'register'){
853    $subject = $lang['mail_new_user'].' '.$summary;
854  }elseif($rev){
855    $subject = $lang['mail_changed'].' '.$id;
856    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true),$text);
857    require_once(DOKU_INC.'inc/DifferenceEngine.php');
858    $df  = new Diff(split("\n",rawWiki($id,$rev)),
859                    split("\n",rawWiki($id)));
860    $dformat = new UnifiedDiffFormatter();
861    $diff    = $dformat->format($df);
862  }else{
863    $subject=$lang['mail_newpage'].' '.$id;
864    $text = str_replace('@OLDPAGE@','none',$text);
865    $diff = rawWiki($id);
866  }
867  $text = str_replace('@DIFF@',$diff,$text);
868  $subject = '['.$conf['title'].'] '.$subject;
869
870  $from = $conf['mailfrom'];
871  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
872  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
873  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
874
875  mail_send($to,$subject,$text,$from,'',$bcc);
876}
877
878/**
879 * extracts the query from a google referer
880 *
881 * @todo   should be more generic and support yahoo et al
882 * @author Andreas Gohr <andi@splitbrain.org>
883 */
884function getGoogleQuery(){
885  $url = parse_url($_SERVER['HTTP_REFERER']);
886  if(!$url) return '';
887
888  if(!preg_match("#google\.#i",$url['host'])) return '';
889  $query = array();
890  parse_str($url['query'],$query);
891
892  return $query['q'];
893}
894
895/**
896 * Try to set correct locale
897 *
898 * @deprecated No longer used
899 * @author     Andreas Gohr <andi@splitbrain.org>
900 */
901function setCorrectLocale(){
902  global $conf;
903  global $lang;
904
905  $enc = strtoupper($lang['encoding']);
906  foreach ($lang['locales'] as $loc){
907    //try locale
908    if(@setlocale(LC_ALL,$loc)) return;
909    //try loceale with encoding
910    if(@setlocale(LC_ALL,"$loc.$enc")) return;
911  }
912  //still here? try to set from environment
913  @setlocale(LC_ALL,"");
914}
915
916/**
917 * Return the human readable size of a file
918 *
919 * @param       int    $size   A file size
920 * @param       int    $dec    A number of decimal places
921 * @author      Martin Benjamin <b.martin@cybernet.ch>
922 * @author      Aidan Lister <aidan@php.net>
923 * @version     1.0.0
924 */
925function filesize_h($size, $dec = 1){
926  $sizes = array('B', 'KB', 'MB', 'GB');
927  $count = count($sizes);
928  $i = 0;
929
930  while ($size >= 1024 && ($i < $count - 1)) {
931    $size /= 1024;
932    $i++;
933  }
934
935  return round($size, $dec) . ' ' . $sizes[$i];
936}
937
938/**
939 * return an obfuscated email address in line with $conf['mailguard'] setting
940 *
941 * @author Harry Fuecks <hfuecks@gmail.com>
942 * @author Christopher Smith <chris@jalakai.co.uk>
943 */
944function obfuscate($email) {
945  global $conf;
946
947  switch ($conf['mailguard']) {
948    case 'visible' :
949      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
950      return strtr($email, $obfuscate);
951
952    case 'hex' :
953      $encode = '';
954      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
955      return $encode;
956
957    case 'none' :
958    default :
959      return $email;
960  }
961}
962
963/**
964 * Let us know if a user is tracking a page
965 *
966 * @author Andreas Gohr <andi@splitbrain.org>
967 */
968function is_subscribed($id,$uid){
969  $file=metaFN($id,'.mlist');
970  if (@file_exists($file)) {
971    $mlist = file($file);
972    $pos = array_search($uid."\n",$mlist);
973    return is_int($pos);
974  }
975
976  return false;
977}
978
979/**
980 * Return a string with the email addresses of all the
981 * users subscribed to a page
982 *
983 * @author Steven Danz <steven-danz@kc.rr.com>
984 */
985function subscriber_addresslist($id){
986  global $conf;
987  global $auth;
988
989  $emails = '';
990
991  if (!$conf['subscribers']) return;
992
993  $mlist = array();
994  $file=metaFN($id,'.mlist');
995  if (@file_exists($file)) {
996    $mlist = file($file);
997  }
998  if(count($mlist) > 0) {
999    foreach ($mlist as $who) {
1000      $who = rtrim($who);
1001      $info = $auth->getUserData($who);
1002      $level = auth_aclcheck($id,$who,$info['grps']);
1003      if ($level >= AUTH_READ) {
1004        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1005          if (empty($emails)) {
1006            $emails = $info['mail'];
1007          } else {
1008            $emails = "$emails,".$info['mail'];
1009          }
1010        }
1011      }
1012    }
1013  }
1014
1015  return $emails;
1016}
1017
1018/**
1019 * Removes quoting backslashes
1020 *
1021 * @author Andreas Gohr <andi@splitbrain.org>
1022 */
1023function unslash($string,$char="'"){
1024  return str_replace('\\'.$char,$char,$string);
1025}
1026
1027//Setup VIM: ex: et ts=2 enc=utf-8 :
1028