xref: /dokuwiki/inc/common.php (revision e7835c601a9d31a48299c5cf122de0df6f61a08a)
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((string)$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 *
414 * The $more parameter should always be given as array, the function then
415 * will strip default parameters to produce even cleaner URLs
416 *
417 * @param string  $id     - the media file id or URL
418 * @param mixed   $more   - string or array with additional parameters
419 * @param boolean $direct - link to detail page if false
420 * @param string  $sep    - URL parameter separator
421 * @param boolean $abs    - Create an absolute URL
422 */
423function ml($id='',$more='',$direct=true,$sep='&amp;',$abs=false){
424  global $conf;
425  if(is_array($more)){
426    // strip defaults for shorter URLs
427    if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
428    if(!$more['w']) unset($more['w']);
429    if(!$more['h']) unset($more['h']);
430    if(isset($more['id']) && $direct) unset($more['id']);
431    $more = buildURLparams($more,$sep);
432  }else{
433    $more = str_replace('cache=cache','',$more); //skip default
434    $more = str_replace(',,',',',$more);
435    $more = str_replace(',',$sep,$more);
436  }
437
438  if($abs){
439    $xlink = DOKU_URL;
440  }else{
441    $xlink = DOKU_BASE;
442  }
443
444  // external URLs are always direct without rewriting
445  if(preg_match('#^(https?|ftp)://#i',$id)){
446    $xlink .= 'lib/exe/fetch.php';
447    if($more){
448      $xlink .= '?'.$more;
449      $xlink .= $sep.'media='.rawurlencode($id);
450    }else{
451      $xlink .= '?media='.rawurlencode($id);
452    }
453    return $xlink;
454  }
455
456  $id = idfilter($id);
457
458  // decide on scriptname
459  if($direct){
460    if($conf['userewrite'] == 1){
461      $script = '_media';
462    }else{
463      $script = 'lib/exe/fetch.php';
464    }
465  }else{
466    if($conf['userewrite'] == 1){
467      $script = '_detail';
468    }else{
469      $script = 'lib/exe/detail.php';
470    }
471  }
472
473  // build URL based on rewrite mode
474   if($conf['userewrite']){
475     $xlink .= $script.'/'.$id;
476     if($more) $xlink .= '?'.$more;
477   }else{
478     if($more){
479       $xlink .= $script.'?'.$more;
480       $xlink .= $sep.'media='.$id;
481     }else{
482       $xlink .= $script.'?media='.$id;
483     }
484   }
485
486  return $xlink;
487}
488
489
490
491/**
492 * Just builds a link to a script
493 *
494 * @todo   maybe obsolete
495 * @author Andreas Gohr <andi@splitbrain.org>
496 */
497function script($script='doku.php'){
498#  $link = getBaseURL();
499#  $link .= $script;
500#  return $link;
501  return DOKU_BASE.DOKU_SCRIPT;
502}
503
504/**
505 * Spamcheck against wordlist
506 *
507 * Checks the wikitext against a list of blocked expressions
508 * returns true if the text contains any bad words
509 *
510 * @author Andreas Gohr <andi@splitbrain.org>
511 */
512function checkwordblock(){
513  global $TEXT;
514  global $conf;
515
516  if(!$conf['usewordblock']) return false;
517
518  // we prepare the text a tiny bit to prevent spammers circumventing URL checks
519  $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT);
520
521  $wordblocks = getWordblocks();
522  //how many lines to read at once (to work around some PCRE limits)
523  if(version_compare(phpversion(),'4.3.0','<')){
524    //old versions of PCRE define a maximum of parenthesises even if no
525    //backreferences are used - the maximum is 99
526    //this is very bad performancewise and may even be too high still
527    $chunksize = 40;
528  }else{
529    //read file in chunks of 200 - this should work around the
530    //MAX_PATTERN_SIZE in modern PCRE
531    $chunksize = 200;
532  }
533  while($blocks = array_splice($wordblocks,0,$chunksize)){
534    $re = array();
535    #build regexp from blocks
536    foreach($blocks as $block){
537      $block = preg_replace('/#.*$/','',$block);
538      $block = trim($block);
539      if(empty($block)) continue;
540      $re[]  = $block;
541    }
542    if(count($re) && preg_match('#('.join('|',$re).')#si',$text)) {
543      return true;
544    }
545  }
546  return false;
547}
548
549/**
550 * Return the IP of the client
551 *
552 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
553 *
554 * It returns a comma separated list of IPs if the above mentioned
555 * headers are set. If the single parameter is set, it tries to return
556 * a routable public address, prefering the ones suplied in the X
557 * headers
558 *
559 * @param  boolean $single If set only a single IP is returned
560 * @author Andreas Gohr <andi@splitbrain.org>
561 */
562function clientIP($single=false){
563  $ip = array();
564  $ip[] = $_SERVER['REMOTE_ADDR'];
565  if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
566    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']));
567  if(!empty($_SERVER['HTTP_X_REAL_IP']))
568    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP']));
569
570  // remove any non-IP stuff
571  $cnt = count($ip);
572  $match = array();
573  for($i=0; $i<$cnt; $i++){
574    if(preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i],$match)) {
575      $ip[$i] = $match[0];
576    } else {
577      $ip[$i] = '';
578    }
579    if(empty($ip[$i])) unset($ip[$i]);
580  }
581  $ip = array_values(array_unique($ip));
582  if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
583
584  if(!$single) return join(',',$ip);
585
586  // decide which IP to use, trying to avoid local addresses
587  $ip = array_reverse($ip);
588  foreach($ip as $i){
589    if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){
590      continue;
591    }else{
592      return $i;
593    }
594  }
595  // still here? just use the first (last) address
596  return $ip[0];
597}
598
599/**
600 * Convert one or more comma separated IPs to hostnames
601 *
602 * @author Glen Harris <astfgl@iamnota.org>
603 * @returns a comma separated list of hostnames
604 */
605function gethostsbyaddrs($ips){
606  $hosts = array();
607  $ips = explode(',',$ips);
608
609  if(is_array($ips)) {
610    foreach($ips as $ip){
611      $hosts[] = gethostbyaddr(trim($ip));
612    }
613    return join(',',$hosts);
614  } else {
615    return gethostbyaddr(trim($ips));
616  }
617}
618
619/**
620 * Checks if a given page is currently locked.
621 *
622 * removes stale lockfiles
623 *
624 * @author Andreas Gohr <andi@splitbrain.org>
625 */
626function checklock($id){
627  global $conf;
628  $lock = wikiLockFN($id);
629
630  //no lockfile
631  if(!@file_exists($lock)) return false;
632
633  //lockfile expired
634  if((time() - filemtime($lock)) > $conf['locktime']){
635    @unlink($lock);
636    return false;
637  }
638
639  //my own lock
640  $ip = io_readFile($lock);
641  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
642    return false;
643  }
644
645  return $ip;
646}
647
648/**
649 * Lock a page for editing
650 *
651 * @author Andreas Gohr <andi@splitbrain.org>
652 */
653function lock($id){
654  $lock = wikiLockFN($id);
655  if($_SERVER['REMOTE_USER']){
656    io_saveFile($lock,$_SERVER['REMOTE_USER']);
657  }else{
658    io_saveFile($lock,clientIP());
659  }
660}
661
662/**
663 * Unlock a page if it was locked by the user
664 *
665 * @author Andreas Gohr <andi@splitbrain.org>
666 * @return bool true if a lock was removed
667 */
668function unlock($id){
669  $lock = wikiLockFN($id);
670  if(@file_exists($lock)){
671    $ip = io_readFile($lock);
672    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
673      @unlink($lock);
674      return true;
675    }
676  }
677  return false;
678}
679
680/**
681 * convert line ending to unix format
682 *
683 * @see    formText() for 2crlf conversion
684 * @author Andreas Gohr <andi@splitbrain.org>
685 */
686function cleanText($text){
687  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
688  return $text;
689}
690
691/**
692 * Prepares text for print in Webforms by encoding special chars.
693 * It also converts line endings to Windows format which is
694 * pseudo standard for webforms.
695 *
696 * @see    cleanText() for 2unix conversion
697 * @author Andreas Gohr <andi@splitbrain.org>
698 */
699function formText($text){
700  $text = str_replace("\012","\015\012",$text);
701  return htmlspecialchars($text);
702}
703
704/**
705 * Returns the specified local text in raw format
706 *
707 * @author Andreas Gohr <andi@splitbrain.org>
708 */
709function rawLocale($id){
710  return io_readFile(localeFN($id));
711}
712
713/**
714 * Returns the raw WikiText
715 *
716 * @author Andreas Gohr <andi@splitbrain.org>
717 */
718function rawWiki($id,$rev=''){
719  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
720}
721
722/**
723 * Returns the pagetemplate contents for the ID's namespace
724 *
725 * @author Andreas Gohr <andi@splitbrain.org>
726 */
727function pageTemplate($data){
728  $id = $data[0];
729  global $conf;
730  global $INFO;
731
732  $path = dirname(wikiFN($id));
733
734  if(@file_exists($path.'/_template.txt')){
735    $tpl = io_readFile($path.'/_template.txt');
736  }else{
737    // search upper namespaces for templates
738    $len = strlen(rtrim($conf['datadir'],'/'));
739    while (strlen($path) >= $len){
740      if(@file_exists($path.'/__template.txt')){
741        $tpl = io_readFile($path.'/__template.txt');
742        break;
743      }
744      $path = substr($path, 0, strrpos($path, '/'));
745    }
746  }
747  if(!$tpl) return '';
748
749  // replace placeholders
750  $tpl = str_replace('@ID@',$id,$tpl);
751  $tpl = str_replace('@NS@',getNS($id),$tpl);
752  $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl);
753  $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl);
754  $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl);
755  $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl);
756  $tpl = str_replace('@DATE@',$conf['dformat'],$tpl);
757  // we need the callback to work around strftime's char limit
758  $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl);
759
760  return $tpl;
761}
762
763
764/**
765 * Returns the raw Wiki Text in three slices.
766 *
767 * The range parameter needs to have the form "from-to"
768 * and gives the range of the section in bytes - no
769 * UTF-8 awareness is needed.
770 * The returned order is prefix, section and suffix.
771 *
772 * @author Andreas Gohr <andi@splitbrain.org>
773 */
774function rawWikiSlices($range,$id,$rev=''){
775  list($from,$to) = split('-',$range,2);
776  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
777  if(!$from) $from = 0;
778  if(!$to)   $to   = strlen($text)+1;
779
780  $slices[0] = substr($text,0,$from-1);
781  $slices[1] = substr($text,$from-1,$to-$from);
782  $slices[2] = substr($text,$to);
783
784  return $slices;
785}
786
787/**
788 * Joins wiki text slices
789 *
790 * function to join the text slices with correct lineendings again.
791 * When the pretty parameter is set to true it adds additional empty
792 * lines between sections if needed (used on saving).
793 *
794 * @author Andreas Gohr <andi@splitbrain.org>
795 */
796function con($pre,$text,$suf,$pretty=false){
797
798  if($pretty){
799    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
800    if($suf && substr($text,-1) != "\n") $text .= "\n";
801  }
802
803  // Avoid double newline above section when saving section edit
804  //if($pre) $pre .= "\n";
805  if($suf) $text .= "\n";
806  return $pre.$text.$suf;
807}
808
809/**
810 * Saves a wikitext by calling io_writeWikiPage.
811 * Also directs changelog and attic updates.
812 *
813 * @author Andreas Gohr <andi@splitbrain.org>
814 * @author Ben Coburn <btcoburn@silicodon.net>
815 */
816function saveWikiText($id,$text,$summary,$minor=false){
817  /* Note to developers:
818     This code is subtle and delicate. Test the behavior of
819     the attic and changelog with dokuwiki and external edits
820     after any changes. External edits change the wiki page
821     directly without using php or dokuwiki.
822  */
823  global $conf;
824  global $lang;
825  global $REV;
826  // ignore if no changes were made
827  if($text == rawWiki($id,'')){
828    return;
829  }
830
831  $file = wikiFN($id);
832  $old = @filemtime($file); // from page
833  $wasRemoved = empty($text);
834  $wasCreated = !@file_exists($file);
835  $wasReverted = ($REV==true);
836  $newRev = false;
837  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
838  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
839  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
840    // add old revision to the attic if missing
841    saveOldRevision($id);
842    // add a changelog entry if this edit came from outside dokuwiki
843    if ($old>$oldRev) {
844      addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
845      // remove soon to be stale instructions
846      $cache = new cache_instructions($id, $file);
847      $cache->removeCache();
848    }
849  }
850
851  if ($wasRemoved){
852    // Send "update" event with empty data, so plugins can react to page deletion
853    $data = array(array($file, '', false), getNS($id), noNS($id), false);
854    trigger_event('IO_WIKIPAGE_WRITE', $data);
855    // pre-save deleted revision
856    @touch($file);
857    clearstatcache();
858    $newRev = saveOldRevision($id);
859    // remove empty file
860    @unlink($file);
861    // remove old meta info...
862    $mfiles = metaFiles($id);
863    $changelog = metaFN($id, '.changes');
864    $metadata  = metaFN($id, '.meta');
865    foreach ($mfiles as $mfile) {
866      // but keep per-page changelog to preserve page history and keep meta data
867      if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); }
868    }
869    // purge meta data
870    p_purge_metadata($id);
871    $del = true;
872    // autoset summary on deletion
873    if(empty($summary)) $summary = $lang['deleted'];
874    // remove empty namespaces
875    io_sweepNS($id, 'datadir');
876    io_sweepNS($id, 'mediadir');
877  }else{
878    // save file (namespace dir is created in io_writeWikiPage)
879    io_writeWikiPage($file, $text, $id);
880    // pre-save the revision, to keep the attic in sync
881    $newRev = saveOldRevision($id);
882    $del = false;
883  }
884
885  // select changelog line type
886  $extra = '';
887  $type = DOKU_CHANGE_TYPE_EDIT;
888  if ($wasReverted) {
889    $type = DOKU_CHANGE_TYPE_REVERT;
890    $extra = $REV;
891  }
892  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
893  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
894  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
895
896  addLogEntry($newRev, $id, $type, $summary, $extra);
897  // send notify mails
898  notify($id,'admin',$old,$summary,$minor);
899  notify($id,'subscribers',$old,$summary,$minor);
900
901  // update the purgefile (timestamp of the last time anything within the wiki was changed)
902  io_saveFile($conf['cachedir'].'/purgefile',time());
903}
904
905/**
906 * moves the current version to the attic and returns its
907 * revision date
908 *
909 * @author Andreas Gohr <andi@splitbrain.org>
910 */
911function saveOldRevision($id){
912  global $conf;
913  $oldf = wikiFN($id);
914  if(!@file_exists($oldf)) return '';
915  $date = filemtime($oldf);
916  $newf = wikiFN($id,$date);
917  io_writeWikiPage($newf, rawWiki($id), $id, $date);
918  return $date;
919}
920
921/**
922 * Sends a notify mail on page change
923 *
924 * @param  string  $id       The changed page
925 * @param  string  $who      Who to notify (admin|subscribers)
926 * @param  int     $rev      Old page revision
927 * @param  string  $summary  What changed
928 * @param  boolean $minor    Is this a minor edit?
929 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
930 *
931 * @author Andreas Gohr <andi@splitbrain.org>
932 */
933function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
934  global $lang;
935  global $conf;
936  global $INFO;
937
938  // decide if there is something to do
939  if($who == 'admin'){
940    if(empty($conf['notify'])) return; //notify enabled?
941    $text = rawLocale('mailtext');
942    $to   = $conf['notify'];
943    $bcc  = '';
944  }elseif($who == 'subscribers'){
945    if(!$conf['subscribers']) return; //subscribers enabled?
946    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
947    $bcc  = subscriber_addresslist($id);
948    if(empty($bcc)) return;
949    $to   = '';
950    $text = rawLocale('subscribermail');
951  }elseif($who == 'register'){
952    if(empty($conf['registernotify'])) return;
953    $text = rawLocale('registermail');
954    $to   = $conf['registernotify'];
955    $bcc  = '';
956  }else{
957    return; //just to be safe
958  }
959
960  $ip   = clientIP();
961  $text = str_replace('@DATE@',strftime($conf['dformat']),$text);
962  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
963  $text = str_replace('@IPADDRESS@',$ip,$text);
964  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
965  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
966  $text = str_replace('@PAGE@',$id,$text);
967  $text = str_replace('@TITLE@',$conf['title'],$text);
968  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
969  $text = str_replace('@SUMMARY@',$summary,$text);
970  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
971
972  foreach ($replace as $key => $substitution) {
973    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
974  }
975
976  if($who == 'register'){
977    $subject = $lang['mail_new_user'].' '.$summary;
978  }elseif($rev){
979    $subject = $lang['mail_changed'].' '.$id;
980    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
981    require_once(DOKU_INC.'inc/DifferenceEngine.php');
982    $df  = new Diff(split("\n",rawWiki($id,$rev)),
983                    split("\n",rawWiki($id)));
984    $dformat = new UnifiedDiffFormatter();
985    $diff    = $dformat->format($df);
986  }else{
987    $subject=$lang['mail_newpage'].' '.$id;
988    $text = str_replace('@OLDPAGE@','none',$text);
989    $diff = rawWiki($id);
990  }
991  $text = str_replace('@DIFF@',$diff,$text);
992  $subject = '['.$conf['title'].'] '.$subject;
993
994  $from = $conf['mailfrom'];
995  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
996  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
997  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
998
999  mail_send($to,$subject,$text,$from,'',$bcc);
1000}
1001
1002/**
1003 * extracts the query from a search engine referrer
1004 *
1005 * @author Andreas Gohr <andi@splitbrain.org>
1006 * @author Todd Augsburger <todd@rollerorgans.com>
1007 */
1008function getGoogleQuery(){
1009  $url = parse_url($_SERVER['HTTP_REFERER']);
1010  if(!$url) return '';
1011
1012  $query = array();
1013  parse_str($url['query'],$query);
1014  if(isset($query['q']))
1015    $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
1016  elseif(isset($query['p']))
1017    $q = $query['p'];        // yahoo
1018  elseif(isset($query['query']))
1019    $q = $query['query'];    // lycos, netscape, clusty, hotbot
1020  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
1021    $q = urldecode(ltrim($url['path'],'/'));
1022
1023  if(!$q) return '';
1024  $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY);
1025  return $q;
1026}
1027
1028/**
1029 * Try to set correct locale
1030 *
1031 * @deprecated No longer used
1032 * @author     Andreas Gohr <andi@splitbrain.org>
1033 */
1034function setCorrectLocale(){
1035  global $conf;
1036  global $lang;
1037
1038  $enc = strtoupper($lang['encoding']);
1039  foreach ($lang['locales'] as $loc){
1040    //try locale
1041    if(@setlocale(LC_ALL,$loc)) return;
1042    //try loceale with encoding
1043    if(@setlocale(LC_ALL,"$loc.$enc")) return;
1044  }
1045  //still here? try to set from environment
1046  @setlocale(LC_ALL,"");
1047}
1048
1049/**
1050 * Return the human readable size of a file
1051 *
1052 * @param       int    $size   A file size
1053 * @param       int    $dec    A number of decimal places
1054 * @author      Martin Benjamin <b.martin@cybernet.ch>
1055 * @author      Aidan Lister <aidan@php.net>
1056 * @version     1.0.0
1057 */
1058function filesize_h($size, $dec = 1){
1059  $sizes = array('B', 'KB', 'MB', 'GB');
1060  $count = count($sizes);
1061  $i = 0;
1062
1063  while ($size >= 1024 && ($i < $count - 1)) {
1064    $size /= 1024;
1065    $i++;
1066  }
1067
1068  return round($size, $dec) . ' ' . $sizes[$i];
1069}
1070
1071/**
1072 * return an obfuscated email address in line with $conf['mailguard'] setting
1073 *
1074 * @author Harry Fuecks <hfuecks@gmail.com>
1075 * @author Christopher Smith <chris@jalakai.co.uk>
1076 */
1077function obfuscate($email) {
1078  global $conf;
1079
1080  switch ($conf['mailguard']) {
1081    case 'visible' :
1082      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1083      return strtr($email, $obfuscate);
1084
1085    case 'hex' :
1086      $encode = '';
1087      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1088      return $encode;
1089
1090    case 'none' :
1091    default :
1092      return $email;
1093  }
1094}
1095
1096/**
1097 * Let us know if a user is tracking a page or a namespace
1098 *
1099 * @author Andreas Gohr <andi@splitbrain.org>
1100 */
1101function is_subscribed($id,$uid,$ns=false){
1102  if(!$ns) {
1103    $file=metaFN($id,'.mlist');
1104  } else {
1105    if(!getNS($id)) {
1106      $file = metaFN(getNS($id),'.mlist');
1107    } else {
1108      $file = metaFN(getNS($id),'/.mlist');
1109    }
1110  }
1111  if (@file_exists($file)) {
1112    $mlist = file($file);
1113    $pos = array_search($uid."\n",$mlist);
1114    return is_int($pos);
1115  }
1116
1117  return false;
1118}
1119
1120/**
1121 * Return a string with the email addresses of all the
1122 * users subscribed to a page
1123 *
1124 * @author Steven Danz <steven-danz@kc.rr.com>
1125 */
1126function subscriber_addresslist($id){
1127  global $conf;
1128  global $auth;
1129
1130  if (!$conf['subscribers']) return '';
1131
1132  $users = array();
1133  $emails = array();
1134
1135  // load the page mlist file content
1136  $mlist = array();
1137  $file=metaFN($id,'.mlist');
1138  if (@file_exists($file)) {
1139    $mlist = file($file);
1140    foreach ($mlist as $who) {
1141      $who = rtrim($who);
1142      $users[$who] = true;
1143    }
1144  }
1145
1146  // load also the namespace mlist file content
1147  $ns = getNS($id);
1148  while ($ns) {
1149    $nsfile = metaFN($ns,'/.mlist');
1150    if (@file_exists($nsfile)) {
1151      $mlist = file($nsfile);
1152      foreach ($mlist as $who) {
1153        $who = rtrim($who);
1154        $users[$who] = true;
1155      }
1156    }
1157    $ns = getNS($ns);
1158  }
1159  // root namespace
1160  $nsfile = metaFN('','.mlist');
1161  if (@file_exists($nsfile)) {
1162    $mlist = file($nsfile);
1163    foreach ($mlist as $who) {
1164      $who = rtrim($who);
1165      $users[$who] = true;
1166    }
1167  }
1168  if(!empty($users)) {
1169    foreach (array_keys($users) as $who) {
1170      $info = $auth->getUserData($who);
1171      if($info === false) continue;
1172      $level = auth_aclcheck($id,$who,$info['grps']);
1173      if ($level >= AUTH_READ) {
1174        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1175          $emails[] = $info['mail'];
1176        }
1177      }
1178    }
1179  }
1180
1181  return implode(',',$emails);
1182}
1183
1184/**
1185 * Removes quoting backslashes
1186 *
1187 * @author Andreas Gohr <andi@splitbrain.org>
1188 */
1189function unslash($string,$char="'"){
1190  return str_replace('\\'.$char,$char,$string);
1191}
1192
1193/**
1194 * Convert php.ini shorthands to byte
1195 *
1196 * @author <gilthans dot NO dot SPAM at gmail dot com>
1197 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1198 */
1199function php_to_byte($v){
1200    $l = substr($v, -1);
1201    $ret = substr($v, 0, -1);
1202    switch(strtoupper($l)){
1203        case 'P':
1204            $ret *= 1024;
1205        case 'T':
1206            $ret *= 1024;
1207        case 'G':
1208            $ret *= 1024;
1209        case 'M':
1210            $ret *= 1024;
1211        case 'K':
1212            $ret *= 1024;
1213        break;
1214    }
1215    return $ret;
1216}
1217
1218/**
1219 * Wrapper around preg_quote adding the default delimiter
1220 */
1221function preg_quote_cb($string){
1222    return preg_quote($string,'/');
1223}
1224
1225/**
1226 * Shorten a given string by removing data from the middle
1227 *
1228 * You can give the string in two parts, teh first part $keep
1229 * will never be shortened. The second part $short will be cut
1230 * in the middle to shorten but only if at least $min chars are
1231 * left to display it. Otherwise it will be left off.
1232 *
1233 * @param string $keep   the part to keep
1234 * @param string $short  the part to shorten
1235 * @param int    $max    maximum chars you want for the whole string
1236 * @param int    $min    minimum number of chars to have left for middle shortening
1237 * @param string $char   the shortening character to use
1238 */
1239function shorten($keep,$short,$max,$min=9,$char='⌇'){
1240    $max = $max - utf8_strlen($keep);
1241	 if($max < $min) return $keep;
1242    $len = utf8_strlen($short);
1243    if($len <= $max) return $keep.$short;
1244    $half = floor($max/2);
1245    return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half);
1246}
1247
1248//Setup VIM: ex: et ts=2 enc=utf-8 :
1249