xref: /dokuwiki/inc/common.php (revision 2d5ccb39b065724c19bf34e24edfe52ebce90e74)
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
814  // decide if there is something to do
815  if($who == 'admin'){
816    if(empty($conf['notify'])) return; //notify enabled?
817    $text = rawLocale('mailtext');
818    $to   = $conf['notify'];
819    $bcc  = '';
820  }elseif($who == 'subscribers'){
821    if(!$conf['subscribers']) return; //subscribers enabled?
822    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
823    $bcc  = subscriber_addresslist($id);
824    if(empty($bcc)) return;
825    $to   = '';
826    $text = rawLocale('subscribermail');
827  }elseif($who == 'register'){
828    if(empty($conf['registernotify'])) return;
829    $text = rawLocale('registermail');
830    $to   = $conf['registernotify'];
831    $bcc  = '';
832  }else{
833    return; //just to be safe
834  }
835
836  $text = str_replace('@DATE@',date($conf['dformat']),$text);
837  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
838  $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text);
839  $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text);
840  $text = str_replace('@NEWPAGE@',wl($id,'',true),$text);
841  $text = str_replace('@PAGE@',$id,$text);
842  $text = str_replace('@TITLE@',$conf['title'],$text);
843  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
844  $text = str_replace('@SUMMARY@',$summary,$text);
845  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
846
847  foreach ($replace as $key => $substitution) {
848    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
849  }
850
851  if($who == 'register'){
852    $subject = $lang['mail_new_user'].' '.$summary;
853  }elseif($rev){
854    $subject = $lang['mail_changed'].' '.$id;
855    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true),$text);
856    require_once(DOKU_INC.'inc/DifferenceEngine.php');
857    $df  = new Diff(split("\n",rawWiki($id,$rev)),
858                    split("\n",rawWiki($id)));
859    $dformat = new UnifiedDiffFormatter();
860    $diff    = $dformat->format($df);
861  }else{
862    $subject=$lang['mail_newpage'].' '.$id;
863    $text = str_replace('@OLDPAGE@','none',$text);
864    $diff = rawWiki($id);
865  }
866  $text = str_replace('@DIFF@',$diff,$text);
867  $subject = '['.$conf['title'].'] '.$subject;
868
869  mail_send($to,$subject,$text,$conf['mailfrom'],'',$bcc);
870}
871
872/**
873 * extracts the query from a google referer
874 *
875 * @todo   should be more generic and support yahoo et al
876 * @author Andreas Gohr <andi@splitbrain.org>
877 */
878function getGoogleQuery(){
879  $url = parse_url($_SERVER['HTTP_REFERER']);
880  if(!$url) return '';
881
882  if(!preg_match("#google\.#i",$url['host'])) return '';
883  $query = array();
884  parse_str($url['query'],$query);
885
886  return $query['q'];
887}
888
889/**
890 * Try to set correct locale
891 *
892 * @deprecated No longer used
893 * @author     Andreas Gohr <andi@splitbrain.org>
894 */
895function setCorrectLocale(){
896  global $conf;
897  global $lang;
898
899  $enc = strtoupper($lang['encoding']);
900  foreach ($lang['locales'] as $loc){
901    //try locale
902    if(@setlocale(LC_ALL,$loc)) return;
903    //try loceale with encoding
904    if(@setlocale(LC_ALL,"$loc.$enc")) return;
905  }
906  //still here? try to set from environment
907  @setlocale(LC_ALL,"");
908}
909
910/**
911 * Return the human readable size of a file
912 *
913 * @param       int    $size   A file size
914 * @param       int    $dec    A number of decimal places
915 * @author      Martin Benjamin <b.martin@cybernet.ch>
916 * @author      Aidan Lister <aidan@php.net>
917 * @version     1.0.0
918 */
919function filesize_h($size, $dec = 1){
920  $sizes = array('B', 'KB', 'MB', 'GB');
921  $count = count($sizes);
922  $i = 0;
923
924  while ($size >= 1024 && ($i < $count - 1)) {
925    $size /= 1024;
926    $i++;
927  }
928
929  return round($size, $dec) . ' ' . $sizes[$i];
930}
931
932/**
933 * return an obfuscated email address in line with $conf['mailguard'] setting
934 *
935 * @author Harry Fuecks <hfuecks@gmail.com>
936 * @author Christopher Smith <chris@jalakai.co.uk>
937 */
938function obfuscate($email) {
939  global $conf;
940
941  switch ($conf['mailguard']) {
942    case 'visible' :
943      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
944      return strtr($email, $obfuscate);
945
946    case 'hex' :
947      $encode = '';
948      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
949      return $encode;
950
951    case 'none' :
952    default :
953      return $email;
954  }
955}
956
957/**
958 * Let us know if a user is tracking a page
959 *
960 * @author Andreas Gohr <andi@splitbrain.org>
961 */
962function is_subscribed($id,$uid){
963  $file=metaFN($id,'.mlist');
964  if (@file_exists($file)) {
965    $mlist = file($file);
966    $pos = array_search($uid."\n",$mlist);
967    return is_int($pos);
968  }
969
970  return false;
971}
972
973/**
974 * Return a string with the email addresses of all the
975 * users subscribed to a page
976 *
977 * @author Steven Danz <steven-danz@kc.rr.com>
978 */
979function subscriber_addresslist($id){
980  global $conf;
981  global $auth;
982
983  $emails = '';
984
985  if (!$conf['subscribers']) return;
986
987  $mlist = array();
988  $file=metaFN($id,'.mlist');
989  if (@file_exists($file)) {
990    $mlist = file($file);
991  }
992  if(count($mlist) > 0) {
993    foreach ($mlist as $who) {
994      $who = rtrim($who);
995      $info = $auth->getUserData($who);
996      $level = auth_aclcheck($id,$who,$info['grps']);
997      if ($level >= AUTH_READ) {
998        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
999          if (empty($emails)) {
1000            $emails = $info['mail'];
1001          } else {
1002            $emails = "$emails,".$info['mail'];
1003          }
1004        }
1005      }
1006    }
1007  }
1008
1009  return $emails;
1010}
1011
1012/**
1013 * Removes quoting backslashes
1014 *
1015 * @author Andreas Gohr <andi@splitbrain.org>
1016 */
1017function unslash($string,$char="'"){
1018  return str_replace('\\'.$char,$char,$string);
1019}
1020
1021//Setup VIM: ex: et ts=2 enc=utf-8 :
1022