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