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