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