xref: /dokuwiki/inc/common.php (revision 5aa52fafe8be8e728c0d2c9ff12c999e80766127)
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    if (isset($info['meta']['last_change'])) { $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']==='e' 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 = noNS($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;'){
372  global $conf;
373  if(is_array($more)){
374    $more = buildURLparams($more,$sep);
375  }else{
376    $more = str_replace(',',$sep,$more);
377  }
378
379  $xlink = DOKU_BASE;
380
381  // external URLs are always direct without rewriting
382  if(preg_match('#^(https?|ftp)://#i',$id)){
383    $xlink .= 'lib/exe/fetch.php';
384    if($more){
385      $xlink .= '?'.$more;
386      $xlink .= $sep.'media='.rawurlencode($id);
387    }else{
388      $xlink .= '?media='.rawurlencode($id);
389    }
390    return $xlink;
391  }
392
393  $id = idfilter($id);
394
395  // decide on scriptname
396  if($direct){
397    if($conf['userewrite'] == 1){
398      $script = '_media';
399    }else{
400      $script = 'lib/exe/fetch.php';
401    }
402  }else{
403    if($conf['userewrite'] == 1){
404      $script = '_detail';
405    }else{
406      $script = 'lib/exe/detail.php';
407    }
408  }
409
410  // build URL based on rewrite mode
411   if($conf['userewrite']){
412     $xlink .= $script.'/'.$id;
413     if($more) $xlink .= '?'.$more;
414   }else{
415     if($more){
416       $xlink .= $script.'?'.$more;
417       $xlink .= $sep.'media='.$id;
418     }else{
419       $xlink .= $script.'?media='.$id;
420     }
421   }
422
423  return $xlink;
424}
425
426
427
428/**
429 * Just builds a link to a script
430 *
431 * @todo   maybe obsolete
432 * @author Andreas Gohr <andi@splitbrain.org>
433 */
434function script($script='doku.php'){
435#  $link = getBaseURL();
436#  $link .= $script;
437#  return $link;
438  return DOKU_BASE.DOKU_SCRIPT;
439}
440
441/**
442 * Spamcheck against wordlist
443 *
444 * Checks the wikitext against a list of blocked expressions
445 * returns true if the text contains any bad words
446 *
447 * @author Andreas Gohr <andi@splitbrain.org>
448 */
449function checkwordblock(){
450  global $TEXT;
451  global $conf;
452
453  if(!$conf['usewordblock']) return false;
454
455  // we prepare the text a tiny bit to prevent spammers circumventing URL checks
456  $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT);
457
458  $wordblocks = getWordblocks();
459  //how many lines to read at once (to work around some PCRE limits)
460  if(version_compare(phpversion(),'4.3.0','<')){
461    //old versions of PCRE define a maximum of parenthesises even if no
462    //backreferences are used - the maximum is 99
463    //this is very bad performancewise and may even be too high still
464    $chunksize = 40;
465  }else{
466    //read file in chunks of 200 - this should work around the
467    //MAX_PATTERN_SIZE in modern PCRE
468    $chunksize = 200;
469  }
470  while($blocks = array_splice($wordblocks,0,$chunksize)){
471    $re = array();
472    #build regexp from blocks
473    foreach($blocks as $block){
474      $block = preg_replace('/#.*$/','',$block);
475      $block = trim($block);
476      if(empty($block)) continue;
477      $re[]  = $block;
478    }
479    if(preg_match('#('.join('|',$re).')#si',$text, $match=array())) {
480      return true;
481    }
482  }
483  return false;
484}
485
486/**
487 * Return the IP of the client
488 *
489 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
490 *
491 * It returns a comma separated list of IPs if the above mentioned
492 * headers are set. If the single parameter is set, it tries to return
493 * a routable public address, prefering the ones suplied in the X
494 * headers
495 *
496 * @param  boolean $single If set only a single IP is returned
497 * @author Andreas Gohr <andi@splitbrain.org>
498 */
499function clientIP($single=false){
500  $ip = array();
501  $ip[] = $_SERVER['REMOTE_ADDR'];
502  if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
503    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']));
504  if(!empty($_SERVER['HTTP_X_REAL_IP']))
505    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP']));
506
507  // remove any non-IP stuff
508  $cnt = count($ip);
509  $match = array();
510  for($i=0; $i<$cnt; $i++){
511    if(preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i],$match)) {
512      $ip[$i] = $match[0];
513    } else {
514      $ip[$i] = '';
515    }
516    if(empty($ip[$i])) unset($ip[$i]);
517  }
518  $ip = array_values(array_unique($ip));
519  if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
520
521  if(!$single) return join(',',$ip);
522
523  // decide which IP to use, trying to avoid local addresses
524  $ip = array_reverse($ip);
525  foreach($ip as $i){
526    if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){
527      continue;
528    }else{
529      return $i;
530    }
531  }
532  // still here? just use the first (last) address
533  return $ip[0];
534}
535
536/**
537 * Checks if a given page is currently locked.
538 *
539 * removes stale lockfiles
540 *
541 * @author Andreas Gohr <andi@splitbrain.org>
542 */
543function checklock($id){
544  global $conf;
545  $lock = wikiLockFN($id);
546
547  //no lockfile
548  if(!@file_exists($lock)) return false;
549
550  //lockfile expired
551  if((time() - filemtime($lock)) > $conf['locktime']){
552    @unlink($lock);
553    return false;
554  }
555
556  //my own lock
557  $ip = io_readFile($lock);
558  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
559    return false;
560  }
561
562  return $ip;
563}
564
565/**
566 * Lock a page for editing
567 *
568 * @author Andreas Gohr <andi@splitbrain.org>
569 */
570function lock($id){
571  $lock = wikiLockFN($id);
572  if($_SERVER['REMOTE_USER']){
573    io_saveFile($lock,$_SERVER['REMOTE_USER']);
574  }else{
575    io_saveFile($lock,clientIP());
576  }
577}
578
579/**
580 * Unlock a page if it was locked by the user
581 *
582 * @author Andreas Gohr <andi@splitbrain.org>
583 * @return bool true if a lock was removed
584 */
585function unlock($id){
586  $lock = wikiLockFN($id);
587  if(@file_exists($lock)){
588    $ip = io_readFile($lock);
589    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
590      @unlink($lock);
591      return true;
592    }
593  }
594  return false;
595}
596
597/**
598 * convert line ending to unix format
599 *
600 * @see    formText() for 2crlf conversion
601 * @author Andreas Gohr <andi@splitbrain.org>
602 */
603function cleanText($text){
604  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
605  return $text;
606}
607
608/**
609 * Prepares text for print in Webforms by encoding special chars.
610 * It also converts line endings to Windows format which is
611 * pseudo standard for webforms.
612 *
613 * @see    cleanText() for 2unix conversion
614 * @author Andreas Gohr <andi@splitbrain.org>
615 */
616function formText($text){
617  $text = preg_replace("/\012/","\015\012",$text);
618  return htmlspecialchars($text);
619}
620
621/**
622 * Returns the specified local text in raw format
623 *
624 * @author Andreas Gohr <andi@splitbrain.org>
625 */
626function rawLocale($id){
627  return io_readFile(localeFN($id));
628}
629
630/**
631 * Returns the raw WikiText
632 *
633 * @author Andreas Gohr <andi@splitbrain.org>
634 */
635function rawWiki($id,$rev=''){
636  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
637}
638
639/**
640 * Returns the pagetemplate contents for the ID's namespace
641 *
642 * @author Andreas Gohr <andi@splitbrain.org>
643 */
644function pageTemplate($data){
645  $id = $data[0];
646  global $conf;
647  global $INFO;
648  $tpl = io_readFile(dirname(wikiFN($id)).'/_template.txt');
649  $tpl = str_replace('@ID@',$id,$tpl);
650  $tpl = str_replace('@NS@',getNS($id),$tpl);
651  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
652  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
653  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
654  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
655  $tpl = str_replace('@DATE@',date($conf['dformat']),$tpl);
656  return $tpl;
657}
658
659
660/**
661 * Returns the raw Wiki Text in three slices.
662 *
663 * The range parameter needs to have the form "from-to"
664 * and gives the range of the section in bytes - no
665 * UTF-8 awareness is needed.
666 * The returned order is prefix, section and suffix.
667 *
668 * @author Andreas Gohr <andi@splitbrain.org>
669 */
670function rawWikiSlices($range,$id,$rev=''){
671  list($from,$to) = split('-',$range,2);
672  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
673  if(!$from) $from = 0;
674  if(!$to)   $to   = strlen($text)+1;
675
676  $slices[0] = substr($text,0,$from-1);
677  $slices[1] = substr($text,$from-1,$to-$from);
678  $slices[2] = substr($text,$to);
679
680  return $slices;
681}
682
683/**
684 * Joins wiki text slices
685 *
686 * function to join the text slices with correct lineendings again.
687 * When the pretty parameter is set to true it adds additional empty
688 * lines between sections if needed (used on saving).
689 *
690 * @author Andreas Gohr <andi@splitbrain.org>
691 */
692function con($pre,$text,$suf,$pretty=false){
693
694  if($pretty){
695    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
696    if($suf && substr($text,-1) != "\n") $text .= "\n";
697  }
698
699  if($pre) $pre .= "\n";
700  if($suf) $text .= "\n";
701  return $pre.$text.$suf;
702}
703
704/**
705 * Saves a wikitext by calling io_writeWikiPage.
706 * Also directs changelog and attic updates.
707 *
708 * @author Andreas Gohr <andi@splitbrain.org>
709 * @author Ben Coburn <btcoburn@silicodon.net>
710 */
711function saveWikiText($id,$text,$summary,$minor=false){
712  /* Note to developers:
713     This code is subtle and delicate. Test the behavior of
714     the attic and changelog with dokuwiki and external edits
715     after any changes. External edits change the wiki page
716     directly without using php or dokuwiki.
717  */
718  global $conf;
719  global $lang;
720  global $REV;
721  // ignore if no changes were made
722  if($text == rawWiki($id,'')){
723    return;
724  }
725
726  $file = wikiFN($id);
727  $old = @filemtime($file); // from page
728  $wasRemoved = empty($text);
729  $wasCreated = !@file_exists($file);
730  $wasReverted = ($REV==true);
731  $newRev = false;
732  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
733  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
734  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
735    // add old revision to the attic if missing
736    saveOldRevision($id);
737    // add a changelog entry if this edit came from outside dokuwiki
738    if ($old>$oldRev) {
739      addLogEntry($old, $id, 'E', $lang['external_edit'], '', array('ExternalEdit'=>true));
740      // remove soon to be stale instructions
741      $cache = new cache_instructions($id, $file);
742      $cache->removeCache();
743    }
744  }
745
746  if ($wasRemoved){
747    // pre-save deleted revision
748    @touch($file);
749    clearstatcache();
750    $newRev = saveOldRevision($id);
751    // remove empty file
752    @unlink($file);
753    // remove old meta info...
754    $mfiles = metaFiles($id);
755    $changelog = metaFN($id, '.changes');
756    foreach ($mfiles as $mfile) {
757      // but keep per-page changelog to preserve page history
758      if (@file_exists($mfile) && $mfile!==$changelog) { @unlink($mfile); }
759    }
760    $del = true;
761    // autoset summary on deletion
762    if(empty($summary)) $summary = $lang['deleted'];
763    // remove empty namespaces
764    io_sweepNS($id, 'datadir');
765    io_sweepNS($id, 'mediadir');
766  }else{
767    // save file (namespace dir is created in io_writeWikiPage)
768    io_writeWikiPage($file, $text, $id);
769    // pre-save the revision, to keep the attic in sync
770    $newRev = saveOldRevision($id);
771    $del = false;
772  }
773
774  // select changelog line type
775  $extra = '';
776  $type = 'E';
777  if ($wasReverted) {
778    $type = 'R';
779    $extra = $REV;
780  }
781  else if ($wasCreated) { $type = 'C'; }
782  else if ($wasRemoved) { $type = 'D'; }
783  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = 'e'; } //minor edits only for logged in users
784
785  addLogEntry($newRev, $id, $type, $summary, $extra);
786  // send notify mails
787  notify($id,'admin',$old,$summary,$minor);
788  notify($id,'subscribers',$old,$summary,$minor);
789
790  // update the purgefile (timestamp of the last time anything within the wiki was changed)
791  io_saveFile($conf['cachedir'].'/purgefile',time());
792}
793
794/**
795 * moves the current version to the attic and returns its
796 * revision date
797 *
798 * @author Andreas Gohr <andi@splitbrain.org>
799 */
800function saveOldRevision($id){
801  global $conf;
802  $oldf = wikiFN($id);
803  if(!@file_exists($oldf)) return '';
804  $date = filemtime($oldf);
805  $newf = wikiFN($id,$date);
806  io_writeWikiPage($newf, rawWiki($id), $id, $date);
807  return $date;
808}
809
810/**
811 * Sends a notify mail on page change
812 *
813 * @param  string  $id       The changed page
814 * @param  string  $who      Who to notify (admin|subscribers)
815 * @param  int     $rev      Old page revision
816 * @param  string  $summary  What changed
817 * @param  boolean $minor    Is this a minor edit?
818 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
819 *
820 * @author Andreas Gohr <andi@splitbrain.org>
821 */
822function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
823  global $lang;
824  global $conf;
825  global $INFO;
826
827  // decide if there is something to do
828  if($who == 'admin'){
829    if(empty($conf['notify'])) return; //notify enabled?
830    $text = rawLocale('mailtext');
831    $to   = $conf['notify'];
832    $bcc  = '';
833  }elseif($who == 'subscribers'){
834    if(!$conf['subscribers']) return; //subscribers enabled?
835    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
836    $bcc  = subscriber_addresslist($id);
837    if(empty($bcc)) return;
838    $to   = '';
839    $text = rawLocale('subscribermail');
840  }elseif($who == 'register'){
841    if(empty($conf['registernotify'])) return;
842    $text = rawLocale('registermail');
843    $to   = $conf['registernotify'];
844    $bcc  = '';
845  }else{
846    return; //just to be safe
847  }
848
849  $text = str_replace('@DATE@',date($conf['dformat']),$text);
850  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
851  $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text);
852  $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text);
853  $text = str_replace('@NEWPAGE@',wl($id,'',true),$text);
854  $text = str_replace('@PAGE@',$id,$text);
855  $text = str_replace('@TITLE@',$conf['title'],$text);
856  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
857  $text = str_replace('@SUMMARY@',$summary,$text);
858  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
859
860  foreach ($replace as $key => $substitution) {
861    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
862  }
863
864  if($who == 'register'){
865    $subject = $lang['mail_new_user'].' '.$summary;
866  }elseif($rev){
867    $subject = $lang['mail_changed'].' '.$id;
868    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true),$text);
869    require_once(DOKU_INC.'inc/DifferenceEngine.php');
870    $df  = new Diff(split("\n",rawWiki($id,$rev)),
871                    split("\n",rawWiki($id)));
872    $dformat = new UnifiedDiffFormatter();
873    $diff    = $dformat->format($df);
874  }else{
875    $subject=$lang['mail_newpage'].' '.$id;
876    $text = str_replace('@OLDPAGE@','none',$text);
877    $diff = rawWiki($id);
878  }
879  $text = str_replace('@DIFF@',$diff,$text);
880  $subject = '['.$conf['title'].'] '.$subject;
881
882  $from = $conf['mailfrom'];
883  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
884  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
885  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
886
887  mail_send($to,$subject,$text,$from,'',$bcc);
888}
889
890/**
891 * extracts the query from a google referer
892 *
893 * @todo   should be more generic and support yahoo et al
894 * @author Andreas Gohr <andi@splitbrain.org>
895 */
896function getGoogleQuery(){
897  $url = parse_url($_SERVER['HTTP_REFERER']);
898  if(!$url) return '';
899
900  if(!preg_match("#google\.#i",$url['host'])) return '';
901  $query = array();
902  parse_str($url['query'],$query);
903
904  return $query['q'];
905}
906
907/**
908 * Try to set correct locale
909 *
910 * @deprecated No longer used
911 * @author     Andreas Gohr <andi@splitbrain.org>
912 */
913function setCorrectLocale(){
914  global $conf;
915  global $lang;
916
917  $enc = strtoupper($lang['encoding']);
918  foreach ($lang['locales'] as $loc){
919    //try locale
920    if(@setlocale(LC_ALL,$loc)) return;
921    //try loceale with encoding
922    if(@setlocale(LC_ALL,"$loc.$enc")) return;
923  }
924  //still here? try to set from environment
925  @setlocale(LC_ALL,"");
926}
927
928/**
929 * Return the human readable size of a file
930 *
931 * @param       int    $size   A file size
932 * @param       int    $dec    A number of decimal places
933 * @author      Martin Benjamin <b.martin@cybernet.ch>
934 * @author      Aidan Lister <aidan@php.net>
935 * @version     1.0.0
936 */
937function filesize_h($size, $dec = 1){
938  $sizes = array('B', 'KB', 'MB', 'GB');
939  $count = count($sizes);
940  $i = 0;
941
942  while ($size >= 1024 && ($i < $count - 1)) {
943    $size /= 1024;
944    $i++;
945  }
946
947  return round($size, $dec) . ' ' . $sizes[$i];
948}
949
950/**
951 * return an obfuscated email address in line with $conf['mailguard'] setting
952 *
953 * @author Harry Fuecks <hfuecks@gmail.com>
954 * @author Christopher Smith <chris@jalakai.co.uk>
955 */
956function obfuscate($email) {
957  global $conf;
958
959  switch ($conf['mailguard']) {
960    case 'visible' :
961      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
962      return strtr($email, $obfuscate);
963
964    case 'hex' :
965      $encode = '';
966      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
967      return $encode;
968
969    case 'none' :
970    default :
971      return $email;
972  }
973}
974
975/**
976 * Let us know if a user is tracking a page
977 *
978 * @author Andreas Gohr <andi@splitbrain.org>
979 */
980function is_subscribed($id,$uid){
981  $file=metaFN($id,'.mlist');
982  if (@file_exists($file)) {
983    $mlist = file($file);
984    $pos = array_search($uid."\n",$mlist);
985    return is_int($pos);
986  }
987
988  return false;
989}
990
991/**
992 * Return a string with the email addresses of all the
993 * users subscribed to a page
994 *
995 * @author Steven Danz <steven-danz@kc.rr.com>
996 */
997function subscriber_addresslist($id){
998  global $conf;
999  global $auth;
1000
1001  $emails = '';
1002
1003  if (!$conf['subscribers']) return;
1004
1005  $mlist = array();
1006  $file=metaFN($id,'.mlist');
1007  if (@file_exists($file)) {
1008    $mlist = file($file);
1009  }
1010  if(count($mlist) > 0) {
1011    foreach ($mlist as $who) {
1012      $who = rtrim($who);
1013      $info = $auth->getUserData($who);
1014      $level = auth_aclcheck($id,$who,$info['grps']);
1015      if ($level >= AUTH_READ) {
1016        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1017          if (empty($emails)) {
1018            $emails = $info['mail'];
1019          } else {
1020            $emails = "$emails,".$info['mail'];
1021          }
1022        }
1023      }
1024    }
1025  }
1026
1027  return $emails;
1028}
1029
1030/**
1031 * Removes quoting backslashes
1032 *
1033 * @author Andreas Gohr <andi@splitbrain.org>
1034 */
1035function unslash($string,$char="'"){
1036  return str_replace('\\'.$char,$char,$string);
1037}
1038
1039//Setup VIM: ex: et ts=2 enc=utf-8 :
1040