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