xref: /dokuwiki/inc/common.php (revision 202b74945886b65fcf2ff266e6622ee9e007e883)
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')) die('meh.');
10require_once(DOKU_INC.'inc/io.php');
11require_once(DOKU_INC.'inc/changelog.php');
12require_once(DOKU_INC.'inc/utf8.php');
13require_once(DOKU_INC.'inc/mail.php');
14require_once(DOKU_INC.'inc/parserutils.php');
15require_once(DOKU_INC.'inc/infoutils.php');
16
17/**
18 * These constants are used with the recents function
19 */
20define('RECENTS_SKIP_DELETED',2);
21define('RECENTS_SKIP_MINORS',4);
22define('RECENTS_SKIP_SUBSPACES',8);
23define('RECENTS_MEDIA_CHANGES',16);
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(!$_SERVER['REMOTE_USER']) return true; // no logged in user, no need for a check
72
73  if(is_null($token)) $token = $_REQUEST['sectok'];
74  if(getSecurityToken() != $token){
75    msg('Security Token did not match. Possible CSRF attack.',-1);
76    return false;
77  }
78  return true;
79}
80
81/**
82 * Print a hidden form field with a secret CSRF token
83 *
84 * @author  Andreas Gohr <andi@splitbrain.org>
85 */
86function formSecurityToken($print=true){
87  $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n";
88  if($print){
89    echo $ret;
90  }else{
91    return $ret;
92  }
93}
94
95/**
96 * Return info about the current document as associative
97 * array.
98 *
99 * @author Andreas Gohr <andi@splitbrain.org>
100 */
101function pageinfo(){
102  global $ID;
103  global $REV;
104  global $RANGE;
105  global $USERINFO;
106  global $conf;
107  global $lang;
108
109  // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
110  // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
111  $info['id'] = $ID;
112  $info['rev'] = $REV;
113
114    // set info about manager/admin status.
115    $info['isadmin']   = false;
116    $info['ismanager'] = false;
117  if(isset($_SERVER['REMOTE_USER'])){
118    $info['userinfo']     = $USERINFO;
119    $info['perm']         = auth_quickaclcheck($ID);
120    $info['subscribed']   = is_subscribed($ID,$_SERVER['REMOTE_USER'],false);
121    $info['subscribedns'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],true);
122    $info['client']       = $_SERVER['REMOTE_USER'];
123
124    if($info['perm'] == AUTH_ADMIN){
125      $info['isadmin']   = true;
126      $info['ismanager'] = true;
127    }elseif(auth_ismanager()){
128      $info['ismanager'] = true;
129    }
130
131    // if some outside auth were used only REMOTE_USER is set
132    if(!$info['userinfo']['name']){
133      $info['userinfo']['name'] = $_SERVER['REMOTE_USER'];
134    }
135
136  }else{
137    $info['perm']       = auth_aclcheck($ID,'',null);
138    $info['subscribed'] = false;
139    $info['client']     = clientIP(true);
140  }
141
142  $info['namespace'] = getNS($ID);
143  $info['locked']    = checklock($ID);
144  $info['filepath']  = fullpath(wikiFN($ID));
145  $info['exists']    = @file_exists($info['filepath']);
146  if($REV){
147    //check if current revision was meant
148    if($info['exists'] && (@filemtime($info['filepath'])==$REV)){
149      $REV = '';
150    }elseif($RANGE){
151      //section editing does not work with old revisions!
152      $REV   = '';
153      $RANGE = '';
154      msg($lang['nosecedit'],0);
155    }else{
156      //really use old revision
157      $info['filepath'] = fullpath(wikiFN($ID,$REV));
158      $info['exists']   = @file_exists($info['filepath']);
159    }
160  }
161  $info['rev'] = $REV;
162  if($info['exists']){
163    $info['writable'] = (is_writable($info['filepath']) &&
164                         ($info['perm'] >= AUTH_EDIT));
165  }else{
166    $info['writable'] = ($info['perm'] >= AUTH_CREATE);
167  }
168  $info['editable']  = ($info['writable'] && empty($info['lock']));
169  $info['lastmod']   = @filemtime($info['filepath']);
170
171  //load page meta data
172  $info['meta'] = p_get_metadata($ID);
173
174  //who's the editor
175  if($REV){
176    $revinfo = getRevisionInfo($ID, $REV, 1024);
177  }else{
178    if (is_array($info['meta']['last_change'])) {
179       $revinfo = $info['meta']['last_change'];
180    } else {
181      $revinfo = getRevisionInfo($ID, $info['lastmod'], 1024);
182      // cache most recent changelog line in metadata if missing and still valid
183      if ($revinfo!==false) {
184        $info['meta']['last_change'] = $revinfo;
185        p_set_metadata($ID, array('last_change' => $revinfo));
186      }
187    }
188  }
189  //and check for an external edit
190  if($revinfo!==false && $revinfo['date']!=$info['lastmod']){
191    // cached changelog line no longer valid
192    $revinfo = false;
193    $info['meta']['last_change'] = $revinfo;
194    p_set_metadata($ID, array('last_change' => $revinfo));
195  }
196
197  $info['ip']     = $revinfo['ip'];
198  $info['user']   = $revinfo['user'];
199  $info['sum']    = $revinfo['sum'];
200  // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
201  // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
202
203  if($revinfo['user']){
204    $info['editor'] = $revinfo['user'];
205  }else{
206    $info['editor'] = $revinfo['ip'];
207  }
208
209  // draft
210  $draft = getCacheName($info['client'].$ID,'.draft');
211  if(@file_exists($draft)){
212    if(@filemtime($draft) < @filemtime(wikiFN($ID))){
213      // remove stale draft
214      @unlink($draft);
215    }else{
216      $info['draft'] = $draft;
217    }
218  }
219
220  // mobile detection
221  $info['ismobile'] = clientismobile();
222
223  return $info;
224}
225
226/**
227 * Build an string of URL parameters
228 *
229 * @author Andreas Gohr
230 */
231function buildURLparams($params, $sep='&amp;'){
232  $url = '';
233  $amp = false;
234  foreach($params as $key => $val){
235    if($amp) $url .= $sep;
236
237    $url .= $key.'=';
238    $url .= rawurlencode((string)$val);
239    $amp = true;
240  }
241  return $url;
242}
243
244/**
245 * Build an string of html tag attributes
246 *
247 * Skips keys starting with '_', values get HTML encoded
248 *
249 * @author Andreas Gohr
250 */
251function buildAttributes($params,$skipempty=false){
252  $url = '';
253  foreach($params as $key => $val){
254    if($key{0} == '_') continue;
255    if($val === '' && $skipempty) continue;
256
257    $url .= $key.'="';
258    $url .= htmlspecialchars ($val);
259    $url .= '" ';
260  }
261  return $url;
262}
263
264
265/**
266 * This builds the breadcrumb trail and returns it as array
267 *
268 * @author Andreas Gohr <andi@splitbrain.org>
269 */
270function breadcrumbs(){
271  // we prepare the breadcrumbs early for quick session closing
272  static $crumbs = null;
273  if($crumbs != null) return $crumbs;
274
275  global $ID;
276  global $ACT;
277  global $conf;
278
279  //first visit?
280  $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array();
281  //we only save on show and existing wiki documents
282  $file = wikiFN($ID);
283  if($ACT != 'show' || !@file_exists($file)){
284    $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
285    return $crumbs;
286  }
287
288  // page names
289  $name = noNSorNS($ID);
290  if (useHeading('navigation')) {
291    // get page title
292    $title = p_get_first_heading($ID,true);
293    if ($title) {
294      $name = $title;
295    }
296  }
297
298  //remove ID from array
299  if (isset($crumbs[$ID])) {
300    unset($crumbs[$ID]);
301  }
302
303  //add to array
304  $crumbs[$ID] = $name;
305  //reduce size
306  while(count($crumbs) > $conf['breadcrumbs']){
307    array_shift($crumbs);
308  }
309  //save to session
310  $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
311  return $crumbs;
312}
313
314/**
315 * Filter for page IDs
316 *
317 * This is run on a ID before it is outputted somewhere
318 * currently used to replace the colon with something else
319 * on Windows systems and to have proper URL encoding
320 *
321 * Urlencoding is ommitted when the second parameter is false
322 *
323 * @author Andreas Gohr <andi@splitbrain.org>
324 */
325function idfilter($id,$ue=true){
326  global $conf;
327  if ($conf['useslash'] && $conf['userewrite']){
328    $id = strtr($id,':','/');
329  }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
330      $conf['userewrite']) {
331    $id = strtr($id,':',';');
332  }
333  if($ue){
334    $id = rawurlencode($id);
335    $id = str_replace('%3A',':',$id); //keep as colon
336    $id = str_replace('%2F','/',$id); //keep as slash
337  }
338  return $id;
339}
340
341/**
342 * This builds a link to a wikipage
343 *
344 * It handles URL rewriting and adds additional parameter if
345 * given in $more
346 *
347 * @author Andreas Gohr <andi@splitbrain.org>
348 */
349function wl($id='',$more='',$abs=false,$sep='&amp;'){
350  global $conf;
351  if(is_array($more)){
352    $more = buildURLparams($more,$sep);
353  }else{
354    $more = str_replace(',',$sep,$more);
355  }
356
357  $id    = idfilter($id);
358  if($abs){
359    $xlink = DOKU_URL;
360  }else{
361    $xlink = DOKU_BASE;
362  }
363
364  if($conf['userewrite'] == 2){
365    $xlink .= DOKU_SCRIPT.'/'.$id;
366    if($more) $xlink .= '?'.$more;
367  }elseif($conf['userewrite']){
368    $xlink .= $id;
369    if($more) $xlink .= '?'.$more;
370  }elseif($id){
371    $xlink .= DOKU_SCRIPT.'?id='.$id;
372    if($more) $xlink .= $sep.$more;
373  }else{
374    $xlink .= DOKU_SCRIPT;
375    if($more) $xlink .= '?'.$more;
376  }
377
378  return $xlink;
379}
380
381/**
382 * This builds a link to an alternate page format
383 *
384 * Handles URL rewriting if enabled. Follows the style of wl().
385 *
386 * @author Ben Coburn <btcoburn@silicodon.net>
387 */
388function exportlink($id='',$format='raw',$more='',$abs=false,$sep='&amp;'){
389  global $conf;
390  if(is_array($more)){
391    $more = buildURLparams($more,$sep);
392  }else{
393    $more = str_replace(',',$sep,$more);
394  }
395
396  $format = rawurlencode($format);
397  $id = idfilter($id);
398  if($abs){
399    $xlink = DOKU_URL;
400  }else{
401    $xlink = DOKU_BASE;
402  }
403
404  if($conf['userewrite'] == 2){
405    $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
406    if($more) $xlink .= $sep.$more;
407  }elseif($conf['userewrite'] == 1){
408    $xlink .= '_export/'.$format.'/'.$id;
409    if($more) $xlink .= '?'.$more;
410  }else{
411    $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
412    if($more) $xlink .= $sep.$more;
413  }
414
415  return $xlink;
416}
417
418/**
419 * Build a link to a media file
420 *
421 * Will return a link to the detail page if $direct is false
422 *
423 * The $more parameter should always be given as array, the function then
424 * will strip default parameters to produce even cleaner URLs
425 *
426 * @param string  $id     - the media file id or URL
427 * @param mixed   $more   - string or array with additional parameters
428 * @param boolean $direct - link to detail page if false
429 * @param string  $sep    - URL parameter separator
430 * @param boolean $abs    - Create an absolute URL
431 */
432function ml($id='',$more='',$direct=true,$sep='&amp;',$abs=false){
433  global $conf;
434  if(is_array($more)){
435    // strip defaults for shorter URLs
436    if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
437    if(!$more['w']) unset($more['w']);
438    if(!$more['h']) unset($more['h']);
439    if(isset($more['id']) && $direct) unset($more['id']);
440    $more = buildURLparams($more,$sep);
441  }else{
442    $more = str_replace('cache=cache','',$more); //skip default
443    $more = str_replace(',,',',',$more);
444    $more = str_replace(',',$sep,$more);
445  }
446
447  if($abs){
448    $xlink = DOKU_URL;
449  }else{
450    $xlink = DOKU_BASE;
451  }
452
453  // external URLs are always direct without rewriting
454  if(preg_match('#^(https?|ftp)://#i',$id)){
455    $xlink .= 'lib/exe/fetch.php';
456    // add hash:
457    $xlink .= '?hash='.substr(md5(auth_cookiesalt().$id),0,6);
458    if($more){
459      $xlink .= $sep.$more;
460      $xlink .= $sep.'media='.rawurlencode($id);
461    }else{
462      $xlink .= $sep.'media='.rawurlencode($id);
463    }
464    return $xlink;
465  }
466
467  $id = idfilter($id);
468
469  // decide on scriptname
470  if($direct){
471    if($conf['userewrite'] == 1){
472      $script = '_media';
473    }else{
474      $script = 'lib/exe/fetch.php';
475    }
476  }else{
477    if($conf['userewrite'] == 1){
478      $script = '_detail';
479    }else{
480      $script = 'lib/exe/detail.php';
481    }
482  }
483
484  // build URL based on rewrite mode
485   if($conf['userewrite']){
486     $xlink .= $script.'/'.$id;
487     if($more) $xlink .= '?'.$more;
488   }else{
489     if($more){
490       $xlink .= $script.'?'.$more;
491       $xlink .= $sep.'media='.$id;
492     }else{
493       $xlink .= $script.'?media='.$id;
494     }
495   }
496
497  return $xlink;
498}
499
500
501
502/**
503 * Just builds a link to a script
504 *
505 * @todo   maybe obsolete
506 * @author Andreas Gohr <andi@splitbrain.org>
507 */
508function script($script='doku.php'){
509#  $link = getBaseURL();
510#  $link .= $script;
511#  return $link;
512  return DOKU_BASE.DOKU_SCRIPT;
513}
514
515/**
516 * Spamcheck against wordlist
517 *
518 * Checks the wikitext against a list of blocked expressions
519 * returns true if the text contains any bad words
520 *
521 * Triggers COMMON_WORDBLOCK_BLOCKED
522 *
523 *  Action Plugins can use this event to inspect the blocked data
524 *  and gain information about the user who was blocked.
525 *
526 *  Event data:
527 *    data['matches']  - array of matches
528 *    data['userinfo'] - information about the blocked user
529 *      [ip]           - ip address
530 *      [user]         - username (if logged in)
531 *      [mail]         - mail address (if logged in)
532 *      [name]         - real name (if logged in)
533 *
534 * @author Andreas Gohr <andi@splitbrain.org>
535 * @author Michael Klier <chi@chimeric.de>
536 * @param  string $text - optional text to check, if not given the globals are used
537 * @return bool         - true if a spam word was found
538 */
539function checkwordblock($text=''){
540  global $TEXT;
541  global $PRE;
542  global $SUF;
543  global $conf;
544  global $INFO;
545
546  if(!$conf['usewordblock']) return false;
547
548  if(!$text) $text = "$PRE $TEXT $SUF";
549
550  // we prepare the text a tiny bit to prevent spammers circumventing URL checks
551  $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$text);
552
553  $wordblocks = getWordblocks();
554  //how many lines to read at once (to work around some PCRE limits)
555  if(version_compare(phpversion(),'4.3.0','<')){
556    //old versions of PCRE define a maximum of parenthesises even if no
557    //backreferences are used - the maximum is 99
558    //this is very bad performancewise and may even be too high still
559    $chunksize = 40;
560  }else{
561    //read file in chunks of 200 - this should work around the
562    //MAX_PATTERN_SIZE in modern PCRE
563    $chunksize = 200;
564  }
565  while($blocks = array_splice($wordblocks,0,$chunksize)){
566    $re = array();
567    #build regexp from blocks
568    foreach($blocks as $block){
569      $block = preg_replace('/#.*$/','',$block);
570      $block = trim($block);
571      if(empty($block)) continue;
572      $re[]  = $block;
573    }
574    if(count($re) && preg_match('#('.join('|',$re).')#si',$text,$matches)) {
575      //prepare event data
576      $data['matches'] = $matches;
577      $data['userinfo']['ip'] = $_SERVER['REMOTE_ADDR'];
578      if($_SERVER['REMOTE_USER']) {
579          $data['userinfo']['user'] = $_SERVER['REMOTE_USER'];
580          $data['userinfo']['name'] = $INFO['userinfo']['name'];
581          $data['userinfo']['mail'] = $INFO['userinfo']['mail'];
582      }
583      $callback = create_function('', 'return true;');
584      return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true);
585    }
586  }
587  return false;
588}
589
590/**
591 * Return the IP of the client
592 *
593 * Honours X-Forwarded-For and X-Real-IP Proxy Headers
594 *
595 * It returns a comma separated list of IPs if the above mentioned
596 * headers are set. If the single parameter is set, it tries to return
597 * a routable public address, prefering the ones suplied in the X
598 * headers
599 *
600 * @param  boolean $single If set only a single IP is returned
601 * @author Andreas Gohr <andi@splitbrain.org>
602 */
603function clientIP($single=false){
604  $ip = array();
605  $ip[] = $_SERVER['REMOTE_ADDR'];
606  if(!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
607    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']));
608  if(!empty($_SERVER['HTTP_X_REAL_IP']))
609    $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP']));
610
611  // some IPv4/v6 regexps borrowed from Feyd
612  // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479
613  $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])';
614  $hex_digit = '[A-Fa-f0-9]';
615  $h16 = "{$hex_digit}{1,4}";
616  $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet";
617  $ls32 = "(?:$h16:$h16|$IPv4Address)";
618  $IPv6Address =
619    "(?:(?:{$IPv4Address})|(?:".
620    "(?:$h16:){6}$ls32" .
621    "|::(?:$h16:){5}$ls32" .
622    "|(?:$h16)?::(?:$h16:){4}$ls32" .
623    "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32" .
624    "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32" .
625    "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32" .
626    "|(?:(?:$h16:){0,4}$h16)?::$ls32" .
627    "|(?:(?:$h16:){0,5}$h16)?::$h16" .
628    "|(?:(?:$h16:){0,6}$h16)?::" .
629    ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)";
630
631  // remove any non-IP stuff
632  $cnt = count($ip);
633  $match = array();
634  for($i=0; $i<$cnt; $i++){
635    if(preg_match("/^$IPv4Address$/",$ip[$i],$match) || preg_match("/^$IPv6Address$/",$ip[$i],$match)) {
636      $ip[$i] = $match[0];
637    } else {
638      $ip[$i] = '';
639    }
640    if(empty($ip[$i])) unset($ip[$i]);
641  }
642  $ip = array_values(array_unique($ip));
643  if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
644
645  if(!$single) return join(',',$ip);
646
647  // decide which IP to use, trying to avoid local addresses
648  $ip = array_reverse($ip);
649  foreach($ip as $i){
650    if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){
651      continue;
652    }else{
653      return $i;
654    }
655  }
656  // still here? just use the first (last) address
657  return $ip[0];
658}
659
660/**
661 * Check if the browser is on a mobile device
662 *
663 * Adapted from the example code at url below
664 *
665 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
666 */
667function clientismobile(){
668
669    if(isset($_SERVER['HTTP_X_WAP_PROFILE'])) return true;
670
671    if(preg_match('/wap\.|\.wap/i',$_SERVER['HTTP_ACCEPT'])) return true;
672
673    if(!isset($_SERVER['HTTP_USER_AGENT'])) return false;
674
675    $uamatches = 'midp|j2me|avantg|docomo|novarra|palmos|palmsource|240x320|opwv|chtml|pda|windows ce|mmp\/|blackberry|mib\/|symbian|wireless|nokia|hand|mobi|phone|cdm|up\.b|audio|SIE\-|SEC\-|samsung|HTC|mot\-|mitsu|sagem|sony|alcatel|lg|erics|vx|NEC|philips|mmm|xx|panasonic|sharp|wap|sch|rover|pocket|benq|java|pt|pg|vox|amoi|bird|compal|kg|voda|sany|kdd|dbt|sendo|sgh|gradi|jb|\d\d\di|moto';
676
677    if(preg_match("/$uamatches/i",$_SERVER['HTTP_USER_AGENT'])) return true;
678
679    return false;
680}
681
682
683/**
684 * Convert one or more comma separated IPs to hostnames
685 *
686 * @author Glen Harris <astfgl@iamnota.org>
687 * @returns a comma separated list of hostnames
688 */
689function gethostsbyaddrs($ips){
690  $hosts = array();
691  $ips = explode(',',$ips);
692
693  if(is_array($ips)) {
694    foreach($ips as $ip){
695      $hosts[] = gethostbyaddr(trim($ip));
696    }
697    return join(',',$hosts);
698  } else {
699    return gethostbyaddr(trim($ips));
700  }
701}
702
703/**
704 * Checks if a given page is currently locked.
705 *
706 * removes stale lockfiles
707 *
708 * @author Andreas Gohr <andi@splitbrain.org>
709 */
710function checklock($id){
711  global $conf;
712  $lock = wikiLockFN($id);
713
714  //no lockfile
715  if(!@file_exists($lock)) return false;
716
717  //lockfile expired
718  if((time() - filemtime($lock)) > $conf['locktime']){
719    @unlink($lock);
720    return false;
721  }
722
723  //my own lock
724  $ip = io_readFile($lock);
725  if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
726    return false;
727  }
728
729  return $ip;
730}
731
732/**
733 * Lock a page for editing
734 *
735 * @author Andreas Gohr <andi@splitbrain.org>
736 */
737function lock($id){
738  $lock = wikiLockFN($id);
739  if($_SERVER['REMOTE_USER']){
740    io_saveFile($lock,$_SERVER['REMOTE_USER']);
741  }else{
742    io_saveFile($lock,clientIP());
743  }
744}
745
746/**
747 * Unlock a page if it was locked by the user
748 *
749 * @author Andreas Gohr <andi@splitbrain.org>
750 * @return bool true if a lock was removed
751 */
752function unlock($id){
753  $lock = wikiLockFN($id);
754  if(@file_exists($lock)){
755    $ip = io_readFile($lock);
756    if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){
757      @unlink($lock);
758      return true;
759    }
760  }
761  return false;
762}
763
764/**
765 * convert line ending to unix format
766 *
767 * @see    formText() for 2crlf conversion
768 * @author Andreas Gohr <andi@splitbrain.org>
769 */
770function cleanText($text){
771  $text = preg_replace("/(\015\012)|(\015)/","\012",$text);
772  return $text;
773}
774
775/**
776 * Prepares text for print in Webforms by encoding special chars.
777 * It also converts line endings to Windows format which is
778 * pseudo standard for webforms.
779 *
780 * @see    cleanText() for 2unix conversion
781 * @author Andreas Gohr <andi@splitbrain.org>
782 */
783function formText($text){
784  $text = str_replace("\012","\015\012",$text);
785  return htmlspecialchars($text);
786}
787
788/**
789 * Returns the specified local text in raw format
790 *
791 * @author Andreas Gohr <andi@splitbrain.org>
792 */
793function rawLocale($id){
794  return io_readFile(localeFN($id));
795}
796
797/**
798 * Returns the raw WikiText
799 *
800 * @author Andreas Gohr <andi@splitbrain.org>
801 */
802function rawWiki($id,$rev=''){
803  return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
804}
805
806/**
807 * Returns the pagetemplate contents for the ID's namespace
808 *
809 * @author Andreas Gohr <andi@splitbrain.org>
810 */
811function pageTemplate($data){
812  $id = $data[0];
813  global $conf;
814  global $INFO;
815
816  $path = dirname(wikiFN($id));
817
818  if(@file_exists($path.'/_template.txt')){
819    $tpl = io_readFile($path.'/_template.txt');
820  }else{
821    // search upper namespaces for templates
822    $len = strlen(rtrim($conf['datadir'],'/'));
823    while (strlen($path) >= $len){
824      if(@file_exists($path.'/__template.txt')){
825        $tpl = io_readFile($path.'/__template.txt');
826        break;
827      }
828      $path = substr($path, 0, strrpos($path, '/'));
829    }
830  }
831  if(!$tpl) return '';
832
833  // replace placeholders
834  $file = noNS($id);
835  $page = strtr($file,'_',' ');
836
837  $tpl = str_replace(array(
838                        '@ID@',
839                        '@NS@',
840                        '@FILE@',
841                        '@!FILE@',
842                        '@!FILE!@',
843                        '@PAGE@',
844                        '@!PAGE@',
845                        '@!!PAGE@',
846                        '@!PAGE!@',
847                        '@USER@',
848                        '@NAME@',
849                        '@MAIL@',
850                        '@DATE@',
851                     ),
852                     array(
853                        $id,
854                        getNS($id),
855                        $file,
856                        utf8_ucfirst($file),
857                        utf8_strtoupper($file),
858                        $page,
859                        utf8_ucfirst($page),
860                        utf8_ucwords($page),
861                        utf8_strtoupper($page),
862                        $_SERVER['REMOTE_USER'],
863                        $INFO['userinfo']['name'],
864                        $INFO['userinfo']['mail'],
865                        $conf['dformat'],
866                     ), $tpl);
867
868  // we need the callback to work around strftime's char limit
869  $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl);
870
871  return $tpl;
872}
873
874
875/**
876 * Returns the raw Wiki Text in three slices.
877 *
878 * The range parameter needs to have the form "from-to"
879 * and gives the range of the section in bytes - no
880 * UTF-8 awareness is needed.
881 * The returned order is prefix, section and suffix.
882 *
883 * @author Andreas Gohr <andi@splitbrain.org>
884 */
885function rawWikiSlices($range,$id,$rev=''){
886  list($from,$to) = explode('-',$range,2);
887  $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
888  if(!$from) $from = 0;
889  if(!$to)   $to   = strlen($text)+1;
890
891  $slices[0] = substr($text,0,$from-1);
892  $slices[1] = substr($text,$from-1,$to-$from);
893  $slices[2] = substr($text,$to);
894
895  return $slices;
896}
897
898/**
899 * Joins wiki text slices
900 *
901 * function to join the text slices with correct lineendings again.
902 * When the pretty parameter is set to true it adds additional empty
903 * lines between sections if needed (used on saving).
904 *
905 * @author Andreas Gohr <andi@splitbrain.org>
906 */
907function con($pre,$text,$suf,$pretty=false){
908
909  if($pretty){
910    if($pre && substr($pre,-1) != "\n") $pre .= "\n";
911    if($suf && substr($text,-1) != "\n") $text .= "\n";
912  }
913
914  // Avoid double newline above section when saving section edit
915  //if($pre) $pre .= "\n";
916  if($suf) $text .= "\n";
917  return $pre.$text.$suf;
918}
919
920/**
921 * Saves a wikitext by calling io_writeWikiPage.
922 * Also directs changelog and attic updates.
923 *
924 * @author Andreas Gohr <andi@splitbrain.org>
925 * @author Ben Coburn <btcoburn@silicodon.net>
926 */
927function saveWikiText($id,$text,$summary,$minor=false){
928  /* Note to developers:
929     This code is subtle and delicate. Test the behavior of
930     the attic and changelog with dokuwiki and external edits
931     after any changes. External edits change the wiki page
932     directly without using php or dokuwiki.
933  */
934  global $conf;
935  global $lang;
936  global $REV;
937  // ignore if no changes were made
938  if($text == rawWiki($id,'')){
939    return;
940  }
941
942  $file = wikiFN($id);
943  $old = @filemtime($file); // from page
944  $wasRemoved = empty($text);
945  $wasCreated = !@file_exists($file);
946  $wasReverted = ($REV==true);
947  $newRev = false;
948  $oldRev = getRevisions($id, -1, 1, 1024); // from changelog
949  $oldRev = (int)(empty($oldRev)?0:$oldRev[0]);
950  if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) {
951    // add old revision to the attic if missing
952    saveOldRevision($id);
953    // add a changelog entry if this edit came from outside dokuwiki
954    if ($old>$oldRev) {
955      addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true));
956      // remove soon to be stale instructions
957      $cache = new cache_instructions($id, $file);
958      $cache->removeCache();
959    }
960  }
961
962  if ($wasRemoved){
963    // Send "update" event with empty data, so plugins can react to page deletion
964    $data = array(array($file, '', false), getNS($id), noNS($id), false);
965    trigger_event('IO_WIKIPAGE_WRITE', $data);
966    // pre-save deleted revision
967    @touch($file);
968    clearstatcache();
969    $newRev = saveOldRevision($id);
970    // remove empty file
971    @unlink($file);
972    // remove old meta info...
973    $mfiles = metaFiles($id);
974    $changelog = metaFN($id, '.changes');
975    $metadata  = metaFN($id, '.meta');
976    foreach ($mfiles as $mfile) {
977      // but keep per-page changelog to preserve page history and keep meta data
978      if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); }
979    }
980    // purge meta data
981    p_purge_metadata($id);
982    $del = true;
983    // autoset summary on deletion
984    if(empty($summary)) $summary = $lang['deleted'];
985    // remove empty namespaces
986    io_sweepNS($id, 'datadir');
987    io_sweepNS($id, 'mediadir');
988  }else{
989    // save file (namespace dir is created in io_writeWikiPage)
990    io_writeWikiPage($file, $text, $id);
991    // pre-save the revision, to keep the attic in sync
992    $newRev = saveOldRevision($id);
993    $del = false;
994  }
995
996  // select changelog line type
997  $extra = '';
998  $type = DOKU_CHANGE_TYPE_EDIT;
999  if ($wasReverted) {
1000    $type = DOKU_CHANGE_TYPE_REVERT;
1001    $extra = $REV;
1002  }
1003  else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; }
1004  else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; }
1005  else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users
1006
1007  addLogEntry($newRev, $id, $type, $summary, $extra);
1008  // send notify mails
1009  notify($id,'admin',$old,$summary,$minor);
1010  notify($id,'subscribers',$old,$summary,$minor);
1011
1012  // update the purgefile (timestamp of the last time anything within the wiki was changed)
1013  io_saveFile($conf['cachedir'].'/purgefile',time());
1014
1015  // if useheading is enabled, purge the cache of all linking pages
1016  if(useHeading('content')){
1017    require_once(DOKU_INC.'inc/fulltext.php');
1018    $pages = ft_backlinks($id);
1019    foreach ($pages as $page) {
1020      $cache = new cache_renderer($page, wikiFN($page), 'xhtml');
1021      $cache->removeCache();
1022    }
1023  }
1024}
1025
1026/**
1027 * moves the current version to the attic and returns its
1028 * revision date
1029 *
1030 * @author Andreas Gohr <andi@splitbrain.org>
1031 */
1032function saveOldRevision($id){
1033  global $conf;
1034  $oldf = wikiFN($id);
1035  if(!@file_exists($oldf)) return '';
1036  $date = filemtime($oldf);
1037  $newf = wikiFN($id,$date);
1038  io_writeWikiPage($newf, rawWiki($id), $id, $date);
1039  return $date;
1040}
1041
1042/**
1043 * Sends a notify mail on page change or registration
1044 *
1045 * @param  string  $id       The changed page
1046 * @param  string  $who      Who to notify (admin|subscribers|register)
1047 * @param  int     $rev      Old page revision
1048 * @param  string  $summary  What changed
1049 * @param  boolean $minor    Is this a minor edit?
1050 * @param  array   $replace  Additional string substitutions, @KEY@ to be replaced by value
1051 *
1052 * @author Andreas Gohr <andi@splitbrain.org>
1053 */
1054function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){
1055  global $lang;
1056  global $conf;
1057  global $INFO;
1058
1059  // decide if there is something to do
1060  if($who == 'admin'){
1061    if(empty($conf['notify'])) return; //notify enabled?
1062    $text = rawLocale('mailtext');
1063    $to   = $conf['notify'];
1064    $bcc  = '';
1065  }elseif($who == 'subscribers'){
1066    if(!$conf['subscribers']) return; //subscribers enabled?
1067    if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors
1068    $bcc  = subscriber_addresslist($id,false);
1069    if(empty($bcc)) return;
1070    $to   = '';
1071    $text = rawLocale('subscribermail');
1072  }elseif($who == 'register'){
1073    if(empty($conf['registernotify'])) return;
1074    $text = rawLocale('registermail');
1075    $to   = $conf['registernotify'];
1076    $bcc  = '';
1077  }else{
1078    return; //just to be safe
1079  }
1080
1081  $ip   = clientIP();
1082  $text = str_replace('@DATE@',dformat(),$text);
1083  $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text);
1084  $text = str_replace('@IPADDRESS@',$ip,$text);
1085  $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text);
1086  $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text);
1087  $text = str_replace('@PAGE@',$id,$text);
1088  $text = str_replace('@TITLE@',$conf['title'],$text);
1089  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
1090  $text = str_replace('@SUMMARY@',$summary,$text);
1091  $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text);
1092
1093  foreach ($replace as $key => $substitution) {
1094    $text = str_replace('@'.strtoupper($key).'@',$substitution, $text);
1095  }
1096
1097  if($who == 'register'){
1098    $subject = $lang['mail_new_user'].' '.$summary;
1099  }elseif($rev){
1100    $subject = $lang['mail_changed'].' '.$id;
1101    $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text);
1102    require_once(DOKU_INC.'inc/DifferenceEngine.php');
1103    $df  = new Diff(explode("\n",rawWiki($id,$rev)),
1104                    explode("\n",rawWiki($id)));
1105    $dformat = new UnifiedDiffFormatter();
1106    $diff    = $dformat->format($df);
1107  }else{
1108    $subject=$lang['mail_newpage'].' '.$id;
1109    $text = str_replace('@OLDPAGE@','none',$text);
1110    $diff = rawWiki($id);
1111  }
1112  $text = str_replace('@DIFF@',$diff,$text);
1113  $subject = '['.$conf['title'].'] '.$subject;
1114
1115  $from = $conf['mailfrom'];
1116  $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from);
1117  $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from);
1118  $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from);
1119
1120  mail_send($to,$subject,$text,$from,'',$bcc);
1121}
1122
1123/**
1124 * extracts the query from a search engine referrer
1125 *
1126 * @author Andreas Gohr <andi@splitbrain.org>
1127 * @author Todd Augsburger <todd@rollerorgans.com>
1128 */
1129function getGoogleQuery(){
1130  if (!isset($_SERVER['HTTP_REFERER'])) {
1131    return '';
1132  }
1133  $url = parse_url($_SERVER['HTTP_REFERER']);
1134
1135  $query = array();
1136  parse_str($url['query'],$query);
1137  $q = '';
1138  if(isset($query['q']))
1139    $q = $query['q'];        // google, live/msn, aol, ask, altavista, alltheweb, gigablast
1140  elseif(isset($query['p']))
1141    $q = $query['p'];        // yahoo
1142  elseif(isset($query['query']))
1143    $q = $query['query'];    // lycos, netscape, clusty, hotbot
1144  elseif(preg_match("#a9\.com#i",$url['host'])) // a9
1145    $q = urldecode(ltrim($url['path'],'/'));
1146
1147  if($q === '') return '';
1148  $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY);
1149  return $q;
1150}
1151
1152/**
1153 * Try to set correct locale
1154 *
1155 * @deprecated No longer used
1156 * @author     Andreas Gohr <andi@splitbrain.org>
1157 */
1158function setCorrectLocale(){
1159  global $conf;
1160  global $lang;
1161
1162  $enc = strtoupper($lang['encoding']);
1163  foreach ($lang['locales'] as $loc){
1164    //try locale
1165    if(@setlocale(LC_ALL,$loc)) return;
1166    //try loceale with encoding
1167    if(@setlocale(LC_ALL,"$loc.$enc")) return;
1168  }
1169  //still here? try to set from environment
1170  @setlocale(LC_ALL,"");
1171}
1172
1173/**
1174 * Return the human readable size of a file
1175 *
1176 * @param       int    $size   A file size
1177 * @param       int    $dec    A number of decimal places
1178 * @author      Martin Benjamin <b.martin@cybernet.ch>
1179 * @author      Aidan Lister <aidan@php.net>
1180 * @version     1.0.0
1181 */
1182function filesize_h($size, $dec = 1){
1183  $sizes = array('B', 'KB', 'MB', 'GB');
1184  $count = count($sizes);
1185  $i = 0;
1186
1187  while ($size >= 1024 && ($i < $count - 1)) {
1188    $size /= 1024;
1189    $i++;
1190  }
1191
1192  return round($size, $dec) . ' ' . $sizes[$i];
1193}
1194
1195/**
1196 * Return the given timestamp as human readable, fuzzy age
1197 *
1198 * @author Andreas Gohr <gohr@cosmocode.de>
1199 */
1200function datetime_h($dt){
1201  global $lang;
1202
1203  $ago = time() - $dt;
1204  if($ago > 24*60*60*30*12*2){
1205    return sprintf($lang['years'], round($ago/(24*60*60*30*12)));
1206  }
1207  if($ago > 24*60*60*30*2){
1208    return sprintf($lang['months'], round($ago/(24*60*60*30)));
1209  }
1210  if($ago > 24*60*60*7*2){
1211    return sprintf($lang['weeks'], round($ago/(24*60*60*7)));
1212  }
1213  if($ago > 24*60*60*2){
1214    return sprintf($lang['days'], round($ago/(24*60*60)));
1215  }
1216  if($ago > 60*60*2){
1217    return sprintf($lang['hours'], round($ago/(60*60)));
1218  }
1219  if($ago > 60*2){
1220    return sprintf($lang['minutes'], round($ago/(60)));
1221  }
1222  return sprintf($lang['seconds'], $ago);
1223
1224}
1225
1226/**
1227 * Wraps around strftime but provides support for fuzzy dates
1228 *
1229 * The format default to $conf['dformat']. It is passed to
1230 * strftime - %f can be used to get the value from datetime_h()
1231 *
1232 * @see datetime_h
1233 * @author Andreas Gohr <gohr@cosmocode.de>
1234 */
1235function dformat($dt=null,$format=''){
1236  global $conf;
1237
1238  if(is_null($dt)) $dt = time();
1239  $dt = (int) $dt;
1240  if(!$format) $format = $conf['dformat'];
1241
1242  $format = str_replace('%f',datetime_h($dt),$format);
1243  return strftime($format,$dt);
1244}
1245
1246/**
1247 * return an obfuscated email address in line with $conf['mailguard'] setting
1248 *
1249 * @author Harry Fuecks <hfuecks@gmail.com>
1250 * @author Christopher Smith <chris@jalakai.co.uk>
1251 */
1252function obfuscate($email) {
1253  global $conf;
1254
1255  switch ($conf['mailguard']) {
1256    case 'visible' :
1257      $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
1258      return strtr($email, $obfuscate);
1259
1260    case 'hex' :
1261      $encode = '';
1262      for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';';
1263      return $encode;
1264
1265    case 'none' :
1266    default :
1267      return $email;
1268  }
1269}
1270
1271/**
1272 * Let us know if a user is tracking a page or a namespace
1273 *
1274 * @author Andreas Gohr <andi@splitbrain.org>
1275 */
1276function is_subscribed($id,$uid,$ns=false){
1277  if(!$ns) {
1278    $file=metaFN($id,'.mlist');
1279  } else {
1280    if(!getNS($id)) {
1281      $file = metaFN(getNS($id),'.mlist');
1282    } else {
1283      $file = metaFN(getNS($id),'/.mlist');
1284    }
1285  }
1286  if (@file_exists($file)) {
1287    $mlist = file($file);
1288    $pos = array_search($uid."\n",$mlist);
1289    return is_int($pos);
1290  }
1291
1292  return false;
1293}
1294
1295/**
1296 * Return a string with the email addresses of all the
1297 * users subscribed to a page
1298 *
1299 * @author Steven Danz <steven-danz@kc.rr.com>
1300 */
1301function subscriber_addresslist($id,$self=true){
1302  global $conf;
1303  global $auth;
1304
1305  if (!$conf['subscribers']) return '';
1306
1307  $users = array();
1308  $emails = array();
1309
1310  // load the page mlist file content
1311  $mlist = array();
1312  $file=metaFN($id,'.mlist');
1313  if (@file_exists($file)) {
1314    $mlist = file($file);
1315    foreach ($mlist as $who) {
1316      $who = rtrim($who);
1317      if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
1318      $users[$who] = true;
1319    }
1320  }
1321
1322  // load also the namespace mlist file content
1323  $ns = getNS($id);
1324  while ($ns) {
1325    $nsfile = metaFN($ns,'/.mlist');
1326    if (@file_exists($nsfile)) {
1327      $mlist = file($nsfile);
1328      foreach ($mlist as $who) {
1329        $who = rtrim($who);
1330        if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
1331        $users[$who] = true;
1332      }
1333    }
1334    $ns = getNS($ns);
1335  }
1336  // root namespace
1337  $nsfile = metaFN('','.mlist');
1338  if (@file_exists($nsfile)) {
1339    $mlist = file($nsfile);
1340    foreach ($mlist as $who) {
1341      $who = rtrim($who);
1342      if(!$self && $who == $_SERVER['REMOTE_USER']) continue;
1343      $users[$who] = true;
1344    }
1345  }
1346  if(!empty($users)) {
1347    foreach (array_keys($users) as $who) {
1348      $info = $auth->getUserData($who);
1349      if($info === false) continue;
1350      $level = auth_aclcheck($id,$who,$info['grps']);
1351      if ($level >= AUTH_READ) {
1352        if (strcasecmp($info['mail'],$conf['notify']) != 0) {
1353          $emails[] = $info['mail'];
1354        }
1355      }
1356    }
1357  }
1358
1359  return implode(',',$emails);
1360}
1361
1362/**
1363 * Removes quoting backslashes
1364 *
1365 * @author Andreas Gohr <andi@splitbrain.org>
1366 */
1367function unslash($string,$char="'"){
1368  return str_replace('\\'.$char,$char,$string);
1369}
1370
1371/**
1372 * Convert php.ini shorthands to byte
1373 *
1374 * @author <gilthans dot NO dot SPAM at gmail dot com>
1375 * @link   http://de3.php.net/manual/en/ini.core.php#79564
1376 */
1377function php_to_byte($v){
1378    $l = substr($v, -1);
1379    $ret = substr($v, 0, -1);
1380    switch(strtoupper($l)){
1381        case 'P':
1382            $ret *= 1024;
1383        case 'T':
1384            $ret *= 1024;
1385        case 'G':
1386            $ret *= 1024;
1387        case 'M':
1388            $ret *= 1024;
1389        case 'K':
1390            $ret *= 1024;
1391        break;
1392    }
1393    return $ret;
1394}
1395
1396/**
1397 * Wrapper around preg_quote adding the default delimiter
1398 */
1399function preg_quote_cb($string){
1400    return preg_quote($string,'/');
1401}
1402
1403/**
1404 * Shorten a given string by removing data from the middle
1405 *
1406 * You can give the string in two parts, the first part $keep
1407 * will never be shortened. The second part $short will be cut
1408 * in the middle to shorten but only if at least $min chars are
1409 * left to display it. Otherwise it will be left off.
1410 *
1411 * @param string $keep   the part to keep
1412 * @param string $short  the part to shorten
1413 * @param int    $max    maximum chars you want for the whole string
1414 * @param int    $min    minimum number of chars to have left for middle shortening
1415 * @param string $char   the shortening character to use
1416 */
1417function shorten($keep,$short,$max,$min=9,$char='…'){
1418    $max = $max - utf8_strlen($keep);
1419   if($max < $min) return $keep;
1420    $len = utf8_strlen($short);
1421    if($len <= $max) return $keep.$short;
1422    $half = floor($max/2);
1423    return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half);
1424}
1425
1426/**
1427 * Return the users realname or e-mail address for use
1428 * in page footer and recent changes pages
1429 *
1430 * @author Andy Webber <dokuwiki AT andywebber DOT com>
1431 */
1432function editorinfo($username){
1433    global $conf;
1434    global $auth;
1435
1436    switch($conf['showuseras']){
1437      case 'username':
1438      case 'email':
1439      case 'email_link':
1440        if($auth) $info = $auth->getUserData($username);
1441        break;
1442      default:
1443        return hsc($username);
1444    }
1445
1446    if(isset($info) && $info) {
1447        switch($conf['showuseras']){
1448          case 'username':
1449            return hsc($info['name']);
1450          case 'email':
1451            return obfuscate($info['mail']);
1452          case 'email_link':
1453            $mail=obfuscate($info['mail']);
1454            return '<a href="mailto:'.$mail.'">'.$mail.'</a>';
1455          default:
1456            return hsc($username);
1457        }
1458    } else {
1459        return hsc($username);
1460    }
1461}
1462
1463/**
1464 * Returns the path to a image file for the currently chosen license.
1465 * When no image exists, returns an empty string
1466 *
1467 * @author Andreas Gohr <andi@splitbrain.org>
1468 * @param  string $type - type of image 'badge' or 'button'
1469 */
1470function license_img($type){
1471    global $license;
1472    global $conf;
1473    if(!$conf['license']) return '';
1474    if(!is_array($license[$conf['license']])) return '';
1475    $lic = $license[$conf['license']];
1476    $try = array();
1477    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png';
1478    $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif';
1479    if(substr($conf['license'],0,3) == 'cc-'){
1480        $try[] = 'lib/images/license/'.$type.'/cc.png';
1481    }
1482    foreach($try as $src){
1483        if(@file_exists(DOKU_INC.$src)) return $src;
1484    }
1485    return '';
1486}
1487
1488/**
1489 * Checks if the given amount of memory is available
1490 *
1491 * If the memory_get_usage() function is not available the
1492 * function just assumes $bytes of already allocated memory
1493 *
1494 * @param  int $mem  Size of memory you want to allocate in bytes
1495 * @param  int $used already allocated memory (see above)
1496 * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
1497 * @author Andreas Gohr <andi@splitbrain.org>
1498 */
1499function is_mem_available($mem,$bytes=1048576){
1500  $limit = trim(ini_get('memory_limit'));
1501  if(empty($limit)) return true; // no limit set!
1502
1503  // parse limit to bytes
1504  $limit = php_to_byte($limit);
1505
1506  // get used memory if possible
1507  if(function_exists('memory_get_usage')){
1508    $used = memory_get_usage();
1509  }
1510
1511  if($used+$mem > $limit){
1512    return false;
1513  }
1514
1515  return true;
1516}
1517
1518/**
1519 * Send a HTTP redirect to the browser
1520 *
1521 * Works arround Microsoft IIS cookie sending bug. Exits the script.
1522 *
1523 * @link   http://support.microsoft.com/kb/q176113/
1524 * @author Andreas Gohr <andi@splitbrain.org>
1525 */
1526function send_redirect($url){
1527    // always close the session
1528    session_write_close();
1529
1530    // check if running on IIS < 6 with CGI-PHP
1531    if( isset($_SERVER['SERVER_SOFTWARE']) && isset($_SERVER['GATEWAY_INTERFACE']) &&
1532        (strpos($_SERVER['GATEWAY_INTERFACE'],'CGI') !== false) &&
1533        (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($_SERVER['SERVER_SOFTWARE']), $matches)) &&
1534        $matches[1] < 6 ){
1535        header('Refresh: 0;url='.$url);
1536    }else{
1537        header('Location: '.$url);
1538    }
1539    exit;
1540}
1541
1542//Setup VIM: ex: et ts=2 enc=utf-8 :
1543