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