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