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