xref: /dokuwiki/inc/common.php (revision 9a3b7d9f166ec2227b91614228c308f50d3e259f)
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    // Send "update" event with empty data, so plugins can react to page deletion
791    $data = array(array($file, '', false), getNS($id), noNS($id), false);
792    trigger_event('IO_WIKIPAGE_WRITE', $data);
793    // pre-save deleted revision
794    @touch($file);
795    clearstatcache();
796    $newRev = saveOldRevision($id);
797    // remove empty file
798    @unlink($file);
799    // remove old meta info...
800    $mfiles = metaFiles($id);
801    $changelog = metaFN($id, '.changes');
802    foreach ($mfiles as $mfile) {
803      // but keep per-page changelog to preserve page history
804      if (@file_exists($mfile) && $mfile!==$changelog) { @unlink($mfile); }
805    }
806    $del = true;
807    // autoset summary on deletion
808    if(empty($summary)) $summary = $lang['deleted'];
809    // remove empty namespaces
810    io_sweepNS($id, 'datadir');
811    io_sweepNS($id, 'mediadir');
812  }else{
813    // save file (namespace dir is created in io_writeWikiPage)
814    io_writeWikiPage($file, $text, $id);
815    // pre-save the revision, to keep the attic in sync
816    $newRev = saveOldRevision($id);
817    $del = false;
818  }
819
820  // select changelog line type
821  $extra = '';
822  $type = DOKU_CHANGE_TYPE_EDIT;
823  if ($wasReverted) {
824    $type = DOKU_CHANGE_TYPE_REVERT;
825    $extra = $REV;
826  }
827  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
828  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
829  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
830
831  addLogEntry($newRev, $id, $type, $summary, $extra);
832  // send notify mails
833  notify($id,'admin',$old,$summary,$minor);
834  notify($id,'subscribers',$old,$summary,$minor);
835
836  // update the purgefile (timestamp of the last time anything within the wiki was changed)
837  io_saveFile($conf['cachedir'].'/purgefile',time());
838}
839
840/**
841 * moves the current version to the attic and returns its
842 * revision date
843 *
844 * @author Andreas Gohr <andi@splitbrain.org>
845 */
846function saveOldRevision($id){
847  global $conf;
848  $oldf = wikiFN($id);
849  if(!@file_exists($oldf)) return '';
850  $date = filemtime($oldf);
851  $newf = wikiFN($id,$date);
852  io_writeWikiPage($newf, rawWiki($id), $id, $date);
853  return $date;
854}
855
856/**
857 * Sends a notify mail on page change
858 *
859 * @param  string  $id       The changed page
860 * @param  string  $who      Who to notify (admin|subscribers)
861 * @param  int     $rev      Old page revision
862 * @param  string  $summary  What changed
863 * @param  boolean $minor    Is this a minor edit?
864 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
865 *
866 * @author Andreas Gohr <andi@splitbrain.org>
867 */
868function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
869  global $lang;
870  global $conf;
871  global $INFO;
872
873  // decide if there is something to do
874  if($who == 'admin'){
875    if(empty($conf['notify'])) return; //notify enabled?
876    $text = rawLocale('mailtext');
877    $to   = $conf['notify'];
878    $bcc  = '';
879  }elseif($who == 'subscribers'){
880    if(!$conf['subscribers']) return; //subscribers enabled?
881    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
882    $bcc  = subscriber_addresslist($id);
883    if(empty($bcc)) return;
884    $to   = '';
885    $text = rawLocale('subscribermail');
886  }elseif($who == 'register'){
887    if(empty($conf['registernotify'])) return;
888    $text = rawLocale('registermail');
889    $to   = $conf['registernotify'];
890    $bcc  = '';
891  }else{
892    return; //just to be safe
893  }
894
895  $text = str_replace('@DATE@',date($conf['dformat']),$text);
896  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
897  $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text);
898  $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text);
899  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
900  $text = str_replace('@PAGE@',$id,$text);
901  $text = str_replace('@TITLE@',$conf['title'],$text);
902  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
903  $text = str_replace('@SUMMARY@',$summary,$text);
904  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
905
906  foreach ($replace as $key => $substitution) {
907    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
908  }
909
910  if($who == 'register'){
911    $subject = $lang['mail_new_user'].' '.$summary;
912  }elseif($rev){
913    $subject = $lang['mail_changed'].' '.$id;
914    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
915    require_once(DOKU_INC.'inc/DifferenceEngine.php');
916    $df  = new Diff(split("\n",rawWiki($id,$rev)),
917                    split("\n",rawWiki($id)));
918    $dformat = new UnifiedDiffFormatter();
919    $diff    = $dformat->format($df);
920  }else{
921    $subject=$lang['mail_newpage'].' '.$id;
922    $text = str_replace('@OLDPAGE@','none',$text);
923    $diff = rawWiki($id);
924  }
925  $text = str_replace('@DIFF@',$diff,$text);
926  $subject = '['.$conf['title'].'] '.$subject;
927
928  $from = $conf['mailfrom'];
929  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
930  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
931  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
932
933  mail_send($to,$subject,$text,$from,'',$bcc);
934}
935
936/**
937 * extracts the query from a search engine referrer
938 *
939 * @author Andreas Gohr <andi@splitbrain.org>
940 * @author Todd Augsburger <todd@rollerorgans.com>
941 */
942function getGoogleQuery(){
943  $url = parse_url($_SERVER['HTTP_REFERER']);
944  if(!$url) return '';
945
946  $query = array();
947  parse_str($url['query'],$query);
948  if(isset($query['q']))
949    return $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
950  elseif(isset($query['p']))
951    return $query['p'];        // yahoo
952  elseif(isset($query['query']))
953    return $query['query'];    // lycos, netscape, clusty, hotbot
954  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
955    return urldecode(ltrim($url['path'],'/'));
956
957  return '';
958}
959
960/**
961 * Try to set correct locale
962 *
963 * @deprecated No longer used
964 * @author     Andreas Gohr <andi@splitbrain.org>
965 */
966function setCorrectLocale(){
967  global $conf;
968  global $lang;
969
970  $enc = strtoupper($lang['encoding']);
971  foreach ($lang['locales'] as $loc){
972    //try locale
973    if(@setlocale(LC_ALL,$loc)) return;
974    //try loceale with encoding
975    if(@setlocale(LC_ALL,"$loc.$enc")) return;
976  }
977  //still here? try to set from environment
978  @setlocale(LC_ALL,"");
979}
980
981/**
982 * Return the human readable size of a file
983 *
984 * @param       int    $size   A file size
985 * @param       int    $dec    A number of decimal places
986 * @author      Martin Benjamin <b.martin@cybernet.ch>
987 * @author      Aidan Lister <aidan@php.net>
988 * @version     1.0.0
989 */
990function filesize_h($size, $dec = 1){
991  $sizes = array('B', 'KB', 'MB', 'GB');
992  $count = count($sizes);
993  $i = 0;
994
995  while ($size >= 1024 && ($i < $count - 1)) {
996    $size /= 1024;
997    $i++;
998  }
999
1000  return round($size, $dec) . ' ' . $sizes[$i];
1001}
1002
1003/**
1004 * return an obfuscated email address in line with $conf['mailguard'] setting
1005 *
1006 * @author Harry Fuecks <hfuecks@gmail.com>
1007 * @author Christopher Smith <chris@jalakai.co.uk>
1008 */
1009function obfuscate($email) {
1010  global $conf;
1011
1012  switch ($conf['mailguard']) {
1013    case 'visible' :
1014      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1015      return strtr($email, $obfuscate);
1016
1017    case 'hex' :
1018      $encode = '';
1019      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1020      return $encode;
1021
1022    case 'none' :
1023    default :
1024      return $email;
1025  }
1026}
1027
1028/**
1029 * Let us know if a user is tracking a page
1030 *
1031 * @author Andreas Gohr <andi@splitbrain.org>
1032 */
1033function is_subscribed($id,$uid){
1034  $file=metaFN($id,'.mlist');
1035  if (@file_exists($file)) {
1036    $mlist = file($file);
1037    $pos = array_search($uid."\n",$mlist);
1038    return is_int($pos);
1039  }
1040
1041  return false;
1042}
1043
1044/**
1045 * Return a string with the email addresses of all the
1046 * users subscribed to a page
1047 *
1048 * @author Steven Danz <steven-danz@kc.rr.com>
1049 */
1050function subscriber_addresslist($id){
1051  global $conf;
1052  global $auth;
1053
1054  $emails = '';
1055
1056  if (!$conf['subscribers']) return;
1057
1058  $mlist = array();
1059  $file=metaFN($id,'.mlist');
1060  if (@file_exists($file)) {
1061    $mlist = file($file);
1062  }
1063  if(count($mlist) > 0) {
1064    foreach ($mlist as $who) {
1065      $who = rtrim($who);
1066      $info = $auth->getUserData($who);
1067      if($info === false) continue;
1068      $level = auth_aclcheck($id,$who,$info['grps']);
1069      if ($level >= AUTH_READ) {
1070        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1071          if (empty($emails)) {
1072            $emails = $info['mail'];
1073          } else {
1074            $emails = "$emails,".$info['mail'];
1075          }
1076        }
1077      }
1078    }
1079  }
1080
1081  return $emails;
1082}
1083
1084/**
1085 * Removes quoting backslashes
1086 *
1087 * @author Andreas Gohr <andi@splitbrain.org>
1088 */
1089function unslash($string,$char="'"){
1090  return str_replace('\\'.$char,$char,$string);
1091}
1092
1093//Setup VIM: ex: et ts=2 enc=utf-8 :
1094