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