xref: /dokuwiki/inc/common.php (revision a249961def6b81c4fb6048789d91c8c03fbb881b)
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 * Convert one or more comma separated IPs to hostnames
581 *
582 * @author Glen Harris <astfgl@iamnota.org>
583 * @returns a comma separated list of hostnames
584 */
585function gethostsbyaddrs($ips){
586  $hosts = array();
587  $ips = explode(',',$ips);
588
589  if(is_array($ips)) {
590    foreach($ips as $ip){
591      $hosts[] = gethostbyaddr(trim($ip));
592    }
593    return join(',',$hosts);
594  } else {
595    return gethostbyaddr(trim($ips));
596  }
597}
598
599/**
600 * Checks if a given page is currently locked.
601 *
602 * removes stale lockfiles
603 *
604 * @author Andreas Gohr <andi@splitbrain.org>
605 */
606function checklock($id){
607  global $conf;
608  $lock = wikiLockFN($id);
609
610  //no lockfile
611  if(!@file_exists($lock)) return false;
612
613  //lockfile expired
614  if((time() - filemtime($lock)) > $conf['locktime']){
615    @unlink($lock);
616    return false;
617  }
618
619  //my own lock
620  $ip = io_readFile($lock);
621  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
622    return false;
623  }
624
625  return $ip;
626}
627
628/**
629 * Lock a page for editing
630 *
631 * @author Andreas Gohr <andi@splitbrain.org>
632 */
633function lock($id){
634  $lock = wikiLockFN($id);
635  if($_SERVER['REMOTE_USER']){
636    io_saveFile($lock,$_SERVER['REMOTE_USER']);
637  }else{
638    io_saveFile($lock,clientIP());
639  }
640}
641
642/**
643 * Unlock a page if it was locked by the user
644 *
645 * @author Andreas Gohr <andi@splitbrain.org>
646 * @return bool true if a lock was removed
647 */
648function unlock($id){
649  $lock = wikiLockFN($id);
650  if(@file_exists($lock)){
651    $ip = io_readFile($lock);
652    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
653      @unlink($lock);
654      return true;
655    }
656  }
657  return false;
658}
659
660/**
661 * convert line ending to unix format
662 *
663 * @see    formText() for 2crlf conversion
664 * @author Andreas Gohr <andi@splitbrain.org>
665 */
666function cleanText($text){
667  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
668  return $text;
669}
670
671/**
672 * Prepares text for print in Webforms by encoding special chars.
673 * It also converts line endings to Windows format which is
674 * pseudo standard for webforms.
675 *
676 * @see    cleanText() for 2unix conversion
677 * @author Andreas Gohr <andi@splitbrain.org>
678 */
679function formText($text){
680  $text = str_replace("\012","\015\012",$text);
681  return htmlspecialchars($text);
682}
683
684/**
685 * Returns the specified local text in raw format
686 *
687 * @author Andreas Gohr <andi@splitbrain.org>
688 */
689function rawLocale($id){
690  return io_readFile(localeFN($id));
691}
692
693/**
694 * Returns the raw WikiText
695 *
696 * @author Andreas Gohr <andi@splitbrain.org>
697 */
698function rawWiki($id,$rev=''){
699  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
700}
701
702/**
703 * Returns the pagetemplate contents for the ID's namespace
704 *
705 * @author Andreas Gohr <andi@splitbrain.org>
706 */
707function pageTemplate($data){
708  $id = $data[0];
709  global $conf;
710  global $INFO;
711  $tpl = io_readFile(dirname(wikiFN($id)).'/_template.txt');
712  $tpl = str_replace('@ID@',$id,$tpl);
713  $tpl = str_replace('@NS@',getNS($id),$tpl);
714  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
715  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
716  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
717  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
718  $tpl = str_replace('@DATE@',date($conf['dformat']),$tpl);
719  return $tpl;
720}
721
722
723/**
724 * Returns the raw Wiki Text in three slices.
725 *
726 * The range parameter needs to have the form "from-to"
727 * and gives the range of the section in bytes - no
728 * UTF-8 awareness is needed.
729 * The returned order is prefix, section and suffix.
730 *
731 * @author Andreas Gohr <andi@splitbrain.org>
732 */
733function rawWikiSlices($range,$id,$rev=''){
734  list($from,$to) = split('-',$range,2);
735  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
736  if(!$from) $from = 0;
737  if(!$to)   $to   = strlen($text)+1;
738
739  $slices[0] = substr($text,0,$from-1);
740  $slices[1] = substr($text,$from-1,$to-$from);
741  $slices[2] = substr($text,$to);
742
743  return $slices;
744}
745
746/**
747 * Joins wiki text slices
748 *
749 * function to join the text slices with correct lineendings again.
750 * When the pretty parameter is set to true it adds additional empty
751 * lines between sections if needed (used on saving).
752 *
753 * @author Andreas Gohr <andi@splitbrain.org>
754 */
755function con($pre,$text,$suf,$pretty=false){
756
757  if($pretty){
758    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
759    if($suf && substr($text,-1) != "\n") $text .= "\n";
760  }
761
762  if($pre) $pre .= "\n";
763  if($suf) $text .= "\n";
764  return $pre.$text.$suf;
765}
766
767/**
768 * Saves a wikitext by calling io_writeWikiPage.
769 * Also directs changelog and attic updates.
770 *
771 * @author Andreas Gohr <andi@splitbrain.org>
772 * @author Ben Coburn <btcoburn@silicodon.net>
773 */
774function saveWikiText($id,$text,$summary,$minor=false){
775  /* Note to developers:
776     This code is subtle and delicate. Test the behavior of
777     the attic and changelog with dokuwiki and external edits
778     after any changes. External edits change the wiki page
779     directly without using php or dokuwiki.
780  */
781  global $conf;
782  global $lang;
783  global $REV;
784  // ignore if no changes were made
785  if($text == rawWiki($id,'')){
786    return;
787  }
788
789  $file = wikiFN($id);
790  $old = @filemtime($file); // from page
791  $wasRemoved = empty($text);
792  $wasCreated = !@file_exists($file);
793  $wasReverted = ($REV==true);
794  $newRev = false;
795  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
796  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
797  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
798    // add old revision to the attic if missing
799    saveOldRevision($id);
800    // add a changelog entry if this edit came from outside dokuwiki
801    if ($old>$oldRev) {
802      addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
803      // remove soon to be stale instructions
804      $cache = new cache_instructions($id, $file);
805      $cache->removeCache();
806    }
807  }
808
809  if ($wasRemoved){
810    // Send "update" event with empty data, so plugins can react to page deletion
811    $data = array(array($file, '', false), getNS($id), noNS($id), false);
812    trigger_event('IO_WIKIPAGE_WRITE', $data);
813    // pre-save deleted revision
814    @touch($file);
815    clearstatcache();
816    $newRev = saveOldRevision($id);
817    // remove empty file
818    @unlink($file);
819    // remove old meta info...
820    $mfiles = metaFiles($id);
821    $changelog = metaFN($id, '.changes');
822    foreach ($mfiles as $mfile) {
823      // but keep per-page changelog to preserve page history
824      if (@file_exists($mfile) && $mfile!==$changelog) { @unlink($mfile); }
825    }
826    $del = true;
827    // autoset summary on deletion
828    if(empty($summary)) $summary = $lang['deleted'];
829    // remove empty namespaces
830    io_sweepNS($id, 'datadir');
831    io_sweepNS($id, 'mediadir');
832  }else{
833    // save file (namespace dir is created in io_writeWikiPage)
834    io_writeWikiPage($file, $text, $id);
835    // pre-save the revision, to keep the attic in sync
836    $newRev = saveOldRevision($id);
837    $del = false;
838  }
839
840  // select changelog line type
841  $extra = '';
842  $type = DOKU_CHANGE_TYPE_EDIT;
843  if ($wasReverted) {
844    $type = DOKU_CHANGE_TYPE_REVERT;
845    $extra = $REV;
846  }
847  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
848  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
849  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
850
851  addLogEntry($newRev, $id, $type, $summary, $extra);
852  // send notify mails
853  notify($id,'admin',$old,$summary,$minor);
854  notify($id,'subscribers',$old,$summary,$minor);
855
856  // update the purgefile (timestamp of the last time anything within the wiki was changed)
857  io_saveFile($conf['cachedir'].'/purgefile',time());
858}
859
860/**
861 * moves the current version to the attic and returns its
862 * revision date
863 *
864 * @author Andreas Gohr <andi@splitbrain.org>
865 */
866function saveOldRevision($id){
867  global $conf;
868  $oldf = wikiFN($id);
869  if(!@file_exists($oldf)) return '';
870  $date = filemtime($oldf);
871  $newf = wikiFN($id,$date);
872  io_writeWikiPage($newf, rawWiki($id), $id, $date);
873  return $date;
874}
875
876/**
877 * Sends a notify mail on page change
878 *
879 * @param  string  $id       The changed page
880 * @param  string  $who      Who to notify (admin|subscribers)
881 * @param  int     $rev      Old page revision
882 * @param  string  $summary  What changed
883 * @param  boolean $minor    Is this a minor edit?
884 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
885 *
886 * @author Andreas Gohr <andi@splitbrain.org>
887 */
888function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
889  global $lang;
890  global $conf;
891  global $INFO;
892
893  // decide if there is something to do
894  if($who == 'admin'){
895    if(empty($conf['notify'])) return; //notify enabled?
896    $text = rawLocale('mailtext');
897    $to   = $conf['notify'];
898    $bcc  = '';
899  }elseif($who == 'subscribers'){
900    if(!$conf['subscribers']) return; //subscribers enabled?
901    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
902    $bcc  = subscriber_addresslist($id);
903    if(empty($bcc)) return;
904    $to   = '';
905    $text = rawLocale('subscribermail');
906  }elseif($who == 'register'){
907    if(empty($conf['registernotify'])) return;
908    $text = rawLocale('registermail');
909    $to   = $conf['registernotify'];
910    $bcc  = '';
911  }else{
912    return; //just to be safe
913  }
914
915  $ip   = clientIP();
916  $text = str_replace('@DATE@',date($conf['dformat']),$text);
917  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
918  $text = str_replace('@IPADDRESS@',$ip,$text);
919  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
920  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
921  $text = str_replace('@PAGE@',$id,$text);
922  $text = str_replace('@TITLE@',$conf['title'],$text);
923  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
924  $text = str_replace('@SUMMARY@',$summary,$text);
925  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
926
927  foreach ($replace as $key => $substitution) {
928    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
929  }
930
931  if($who == 'register'){
932    $subject = $lang['mail_new_user'].' '.$summary;
933  }elseif($rev){
934    $subject = $lang['mail_changed'].' '.$id;
935    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
936    require_once(DOKU_INC.'inc/DifferenceEngine.php');
937    $df  = new Diff(split("\n",rawWiki($id,$rev)),
938                    split("\n",rawWiki($id)));
939    $dformat = new UnifiedDiffFormatter();
940    $diff    = $dformat->format($df);
941  }else{
942    $subject=$lang['mail_newpage'].' '.$id;
943    $text = str_replace('@OLDPAGE@','none',$text);
944    $diff = rawWiki($id);
945  }
946  $text = str_replace('@DIFF@',$diff,$text);
947  $subject = '['.$conf['title'].'] '.$subject;
948
949  $from = $conf['mailfrom'];
950  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
951  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
952  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
953
954  mail_send($to,$subject,$text,$from,'',$bcc);
955}
956
957/**
958 * extracts the query from a search engine referrer
959 *
960 * @author Andreas Gohr <andi@splitbrain.org>
961 * @author Todd Augsburger <todd@rollerorgans.com>
962 */
963function getGoogleQuery(){
964  $url = parse_url($_SERVER['HTTP_REFERER']);
965  if(!$url) return '';
966
967  $query = array();
968  parse_str($url['query'],$query);
969  if(isset($query['q']))
970    return $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
971  elseif(isset($query['p']))
972    return $query['p'];        // yahoo
973  elseif(isset($query['query']))
974    return $query['query'];    // lycos, netscape, clusty, hotbot
975  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
976    return urldecode(ltrim($url['path'],'/'));
977
978  return '';
979}
980
981/**
982 * Try to set correct locale
983 *
984 * @deprecated No longer used
985 * @author     Andreas Gohr <andi@splitbrain.org>
986 */
987function setCorrectLocale(){
988  global $conf;
989  global $lang;
990
991  $enc = strtoupper($lang['encoding']);
992  foreach ($lang['locales'] as $loc){
993    //try locale
994    if(@setlocale(LC_ALL,$loc)) return;
995    //try loceale with encoding
996    if(@setlocale(LC_ALL,"$loc.$enc")) return;
997  }
998  //still here? try to set from environment
999  @setlocale(LC_ALL,"");
1000}
1001
1002/**
1003 * Return the human readable size of a file
1004 *
1005 * @param       int    $size   A file size
1006 * @param       int    $dec    A number of decimal places
1007 * @author      Martin Benjamin <b.martin@cybernet.ch>
1008 * @author      Aidan Lister <aidan@php.net>
1009 * @version     1.0.0
1010 */
1011function filesize_h($size, $dec = 1){
1012  $sizes = array('B', 'KB', 'MB', 'GB');
1013  $count = count($sizes);
1014  $i = 0;
1015
1016  while ($size >= 1024 && ($i < $count - 1)) {
1017    $size /= 1024;
1018    $i++;
1019  }
1020
1021  return round($size, $dec) . ' ' . $sizes[$i];
1022}
1023
1024/**
1025 * return an obfuscated email address in line with $conf['mailguard'] setting
1026 *
1027 * @author Harry Fuecks <hfuecks@gmail.com>
1028 * @author Christopher Smith <chris@jalakai.co.uk>
1029 */
1030function obfuscate($email) {
1031  global $conf;
1032
1033  switch ($conf['mailguard']) {
1034    case 'visible' :
1035      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1036      return strtr($email, $obfuscate);
1037
1038    case 'hex' :
1039      $encode = '';
1040      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1041      return $encode;
1042
1043    case 'none' :
1044    default :
1045      return $email;
1046  }
1047}
1048
1049/**
1050 * Let us know if a user is tracking a page
1051 *
1052 * @author Andreas Gohr <andi@splitbrain.org>
1053 */
1054function is_subscribed($id,$uid){
1055  $file=metaFN($id,'.mlist');
1056  if (@file_exists($file)) {
1057    $mlist = file($file);
1058    $pos = array_search($uid."\n",$mlist);
1059    return is_int($pos);
1060  }
1061
1062  return false;
1063}
1064
1065/**
1066 * Return a string with the email addresses of all the
1067 * users subscribed to a page
1068 *
1069 * @author Steven Danz <steven-danz@kc.rr.com>
1070 */
1071function subscriber_addresslist($id){
1072  global $conf;
1073  global $auth;
1074
1075  $emails = '';
1076
1077  if (!$conf['subscribers']) return;
1078
1079  $mlist = array();
1080  $file=metaFN($id,'.mlist');
1081  if (@file_exists($file)) {
1082    $mlist = file($file);
1083  }
1084  if(count($mlist) > 0) {
1085    foreach ($mlist as $who) {
1086      $who = rtrim($who);
1087      $info = $auth->getUserData($who);
1088      if($info === false) continue;
1089      $level = auth_aclcheck($id,$who,$info['grps']);
1090      if ($level >= AUTH_READ) {
1091        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1092          if (empty($emails)) {
1093            $emails = $info['mail'];
1094          } else {
1095            $emails = "$emails,".$info['mail'];
1096          }
1097        }
1098      }
1099    }
1100  }
1101
1102  return $emails;
1103}
1104
1105/**
1106 * Removes quoting backslashes
1107 *
1108 * @author Andreas Gohr <andi@splitbrain.org>
1109 */
1110function unslash($string,$char="'"){
1111  return str_replace('\\'.$char,$char,$string);
1112}
1113
1114//Setup VIM: ex: et ts=2 enc=utf-8 :
1115