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