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.'); 10 11/** 12 * These constants are used with the recents function 13 */ 14define('RECENTS_SKIP_DELETED', 2); 15define('RECENTS_SKIP_MINORS', 4); 16define('RECENTS_SKIP_SUBSPACES', 8); 17define('RECENTS_MEDIA_CHANGES', 16); 18define('RECENTS_MEDIA_PAGES_MIXED', 32); 19 20/** 21 * Wrapper around htmlspecialchars() 22 * 23 * @author Andreas Gohr <andi@splitbrain.org> 24 * @see htmlspecialchars() 25 * 26 * @param string $string the string being converted 27 * @return string converted string 28 */ 29function hsc($string) { 30 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 31} 32 33/** 34 * Checks if the given input is blank 35 * 36 * This is similar to empty() but will return false for "0". 37 * 38 * @param $in 39 * @param bool $trim Consider a string of whitespace to be blank 40 * @return bool 41 */ 42function blank(&$in, $trim = false) { 43 if(!isset($in)) return true; 44 if(is_null($in)) return true; 45 if(is_array($in)) return empty($in); 46 if($in === "\0") return true; 47 if($trim && trim($in) === '') return true; 48 if(strlen($in) > 0) return false; 49 return empty($in); 50} 51 52/** 53 * print a newline terminated string 54 * 55 * You can give an indention as optional parameter 56 * 57 * @author Andreas Gohr <andi@splitbrain.org> 58 * 59 * @param string $string line of text 60 * @param int $indent number of spaces indention 61 */ 62function ptln($string, $indent = 0) { 63 echo str_repeat(' ', $indent)."$string\n"; 64} 65 66/** 67 * strips control characters (<32) from the given string 68 * 69 * @author Andreas Gohr <andi@splitbrain.org> 70 * 71 * @param string $string being stripped 72 * @return string 73 */ 74function stripctl($string) { 75 return preg_replace('/[\x00-\x1F]+/s', '', $string); 76} 77 78/** 79 * Return a secret token to be used for CSRF attack prevention 80 * 81 * @author Andreas Gohr <andi@splitbrain.org> 82 * @link http://en.wikipedia.org/wiki/Cross-site_request_forgery 83 * @link http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html 84 * 85 * @return string 86 */ 87function getSecurityToken() { 88 /** @var Input $INPUT */ 89 global $INPUT; 90 return PassHash::hmac('md5', session_id().$INPUT->server->str('REMOTE_USER'), auth_cookiesalt()); 91} 92 93/** 94 * Check the secret CSRF token 95 * 96 * @param null|string $token security token or null to read it from request variable 97 * @return bool success if the token matched 98 */ 99function checkSecurityToken($token = null) { 100 /** @var Input $INPUT */ 101 global $INPUT; 102 if(!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check 103 104 if(is_null($token)) $token = $INPUT->str('sectok'); 105 if(getSecurityToken() != $token) { 106 msg('Security Token did not match. Possible CSRF attack.', -1); 107 return false; 108 } 109 return true; 110} 111 112/** 113 * Print a hidden form field with a secret CSRF token 114 * 115 * @author Andreas Gohr <andi@splitbrain.org> 116 * 117 * @param bool $print if true print the field, otherwise html of the field is returned 118 * @return string html of hidden form field 119 */ 120function formSecurityToken($print = true) { 121 $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n"; 122 if($print) echo $ret; 123 return $ret; 124} 125 126/** 127 * Determine basic information for a request of $id 128 * 129 * @author Andreas Gohr <andi@splitbrain.org> 130 * @author Chris Smith <chris@jalakai.co.uk> 131 * 132 * @param string $id pageid 133 * @param bool $htmlClient add info about whether is mobile browser 134 * @return array with info for a request of $id 135 * 136 */ 137function basicinfo($id, $htmlClient=true){ 138 global $USERINFO; 139 /* @var Input $INPUT */ 140 global $INPUT; 141 142 // set info about manager/admin status. 143 $info = array(); 144 $info['isadmin'] = false; 145 $info['ismanager'] = false; 146 if($INPUT->server->has('REMOTE_USER')) { 147 $info['userinfo'] = $USERINFO; 148 $info['perm'] = auth_quickaclcheck($id); 149 $info['client'] = $INPUT->server->str('REMOTE_USER'); 150 151 if($info['perm'] == AUTH_ADMIN) { 152 $info['isadmin'] = true; 153 $info['ismanager'] = true; 154 } elseif(auth_ismanager()) { 155 $info['ismanager'] = true; 156 } 157 158 // if some outside auth were used only REMOTE_USER is set 159 if(!$info['userinfo']['name']) { 160 $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER'); 161 } 162 163 } else { 164 $info['perm'] = auth_aclcheck($id, '', null); 165 $info['client'] = clientIP(true); 166 } 167 168 $info['namespace'] = getNS($id); 169 170 // mobile detection 171 if ($htmlClient) { 172 $info['ismobile'] = clientismobile(); 173 } 174 175 return $info; 176 } 177 178/** 179 * Return info about the current document as associative 180 * array. 181 * 182 * @author Andreas Gohr <andi@splitbrain.org> 183 * 184 * @return array with info about current document 185 */ 186function pageinfo() { 187 global $ID; 188 global $REV; 189 global $RANGE; 190 global $lang; 191 /* @var Input $INPUT */ 192 global $INPUT; 193 194 $info = basicinfo($ID); 195 196 // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml 197 // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary 198 $info['id'] = $ID; 199 $info['rev'] = $REV; 200 201 if($INPUT->server->has('REMOTE_USER')) { 202 $sub = new Subscription(); 203 $info['subscribed'] = $sub->user_subscription(); 204 } else { 205 $info['subscribed'] = false; 206 } 207 208 $info['locked'] = checklock($ID); 209 $info['filepath'] = fullpath(wikiFN($ID)); 210 $info['exists'] = file_exists($info['filepath']); 211 $info['currentrev'] = @filemtime($info['filepath']); 212 if($REV) { 213 //check if current revision was meant 214 if($info['exists'] && ($info['currentrev'] == $REV)) { 215 $REV = ''; 216 } elseif($RANGE) { 217 //section editing does not work with old revisions! 218 $REV = ''; 219 $RANGE = ''; 220 msg($lang['nosecedit'], 0); 221 } else { 222 //really use old revision 223 $info['filepath'] = fullpath(wikiFN($ID, $REV)); 224 $info['exists'] = file_exists($info['filepath']); 225 } 226 } 227 $info['rev'] = $REV; 228 if($info['exists']) { 229 $info['writable'] = (is_writable($info['filepath']) && 230 ($info['perm'] >= AUTH_EDIT)); 231 } else { 232 $info['writable'] = ($info['perm'] >= AUTH_CREATE); 233 } 234 $info['editable'] = ($info['writable'] && empty($info['locked'])); 235 $info['lastmod'] = @filemtime($info['filepath']); 236 237 //load page meta data 238 $info['meta'] = p_get_metadata($ID); 239 240 //who's the editor 241 $pagelog = new PageChangeLog($ID, 1024); 242 if($REV) { 243 $revinfo = $pagelog->getRevisionInfo($REV); 244 } else { 245 if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) { 246 $revinfo = $info['meta']['last_change']; 247 } else { 248 $revinfo = $pagelog->getRevisionInfo($info['lastmod']); 249 // cache most recent changelog line in metadata if missing and still valid 250 if($revinfo !== false) { 251 $info['meta']['last_change'] = $revinfo; 252 p_set_metadata($ID, array('last_change' => $revinfo)); 253 } 254 } 255 } 256 //and check for an external edit 257 if($revinfo !== false && $revinfo['date'] != $info['lastmod']) { 258 // cached changelog line no longer valid 259 $revinfo = false; 260 $info['meta']['last_change'] = $revinfo; 261 p_set_metadata($ID, array('last_change' => $revinfo)); 262 } 263 264 $info['ip'] = $revinfo['ip']; 265 $info['user'] = $revinfo['user']; 266 $info['sum'] = $revinfo['sum']; 267 // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID. 268 // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor']. 269 270 if($revinfo['user']) { 271 $info['editor'] = $revinfo['user']; 272 } else { 273 $info['editor'] = $revinfo['ip']; 274 } 275 276 // draft 277 $draft = getCacheName($info['client'].$ID, '.draft'); 278 if(file_exists($draft)) { 279 if(@filemtime($draft) < @filemtime(wikiFN($ID))) { 280 // remove stale draft 281 @unlink($draft); 282 } else { 283 $info['draft'] = $draft; 284 } 285 } 286 287 return $info; 288} 289 290/** 291 * Return information about the current media item as an associative array. 292 * 293 * @return array with info about current media item 294 */ 295function mediainfo(){ 296 global $NS; 297 global $IMG; 298 299 $info = basicinfo("$NS:*"); 300 $info['image'] = $IMG; 301 302 return $info; 303} 304 305/** 306 * Build an string of URL parameters 307 * 308 * @author Andreas Gohr 309 * 310 * @param array $params array with key-value pairs 311 * @param string $sep series of pairs are separated by this character 312 * @return string query string 313 */ 314function buildURLparams($params, $sep = '&') { 315 $url = ''; 316 $amp = false; 317 foreach($params as $key => $val) { 318 if($amp) $url .= $sep; 319 320 $url .= rawurlencode($key).'='; 321 $url .= rawurlencode((string) $val); 322 $amp = true; 323 } 324 return $url; 325} 326 327/** 328 * Build an string of html tag attributes 329 * 330 * Skips keys starting with '_', values get HTML encoded 331 * 332 * @author Andreas Gohr 333 * 334 * @param array $params array with (attribute name-attribute value) pairs 335 * @param bool $skipempty skip empty string values? 336 * @return string 337 */ 338function buildAttributes($params, $skipempty = false) { 339 $url = ''; 340 $white = false; 341 foreach($params as $key => $val) { 342 if($key{0} == '_') continue; 343 if($val === '' && $skipempty) continue; 344 if($white) $url .= ' '; 345 346 $url .= $key.'="'; 347 $url .= htmlspecialchars($val); 348 $url .= '"'; 349 $white = true; 350 } 351 return $url; 352} 353 354/** 355 * This builds the breadcrumb trail and returns it as array 356 * 357 * @author Andreas Gohr <andi@splitbrain.org> 358 * 359 * @return string[] with the data: array(pageid=>name, ... ) 360 */ 361function breadcrumbs() { 362 // we prepare the breadcrumbs early for quick session closing 363 static $crumbs = null; 364 if($crumbs != null) return $crumbs; 365 366 global $ID; 367 global $ACT; 368 global $conf; 369 370 //first visit? 371 $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array(); 372 //we only save on show and existing wiki documents 373 $file = wikiFN($ID); 374 if($ACT != 'show' || !file_exists($file)) { 375 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 376 return $crumbs; 377 } 378 379 // page names 380 $name = noNSorNS($ID); 381 if(useHeading('navigation')) { 382 // get page title 383 $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE); 384 if($title) { 385 $name = $title; 386 } 387 } 388 389 //remove ID from array 390 if(isset($crumbs[$ID])) { 391 unset($crumbs[$ID]); 392 } 393 394 //add to array 395 $crumbs[$ID] = $name; 396 //reduce size 397 while(count($crumbs) > $conf['breadcrumbs']) { 398 array_shift($crumbs); 399 } 400 //save to session 401 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 402 return $crumbs; 403} 404 405/** 406 * Filter for page IDs 407 * 408 * This is run on a ID before it is outputted somewhere 409 * currently used to replace the colon with something else 410 * on Windows (non-IIS) systems and to have proper URL encoding 411 * 412 * See discussions at https://github.com/splitbrain/dokuwiki/pull/84 and 413 * https://github.com/splitbrain/dokuwiki/pull/173 why we use a whitelist of 414 * unaffected servers instead of blacklisting affected servers here. 415 * 416 * Urlencoding is ommitted when the second parameter is false 417 * 418 * @author Andreas Gohr <andi@splitbrain.org> 419 * 420 * @param string $id pageid being filtered 421 * @param bool $ue apply urlencoding? 422 * @return string 423 */ 424function idfilter($id, $ue = true) { 425 global $conf; 426 /* @var Input $INPUT */ 427 global $INPUT; 428 429 if($conf['useslash'] && $conf['userewrite']) { 430 $id = strtr($id, ':', '/'); 431 } elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && 432 $conf['userewrite'] && 433 strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false 434 ) { 435 $id = strtr($id, ':', ';'); 436 } 437 if($ue) { 438 $id = rawurlencode($id); 439 $id = str_replace('%3A', ':', $id); //keep as colon 440 $id = str_replace('%3B', ';', $id); //keep as semicolon 441 $id = str_replace('%2F', '/', $id); //keep as slash 442 } 443 return $id; 444} 445 446/** 447 * This builds a link to a wikipage 448 * 449 * It handles URL rewriting and adds additional parameters 450 * 451 * @author Andreas Gohr <andi@splitbrain.org> 452 * 453 * @param string $id page id, defaults to start page 454 * @param string|array $urlParameters URL parameters, associative array recommended 455 * @param bool $absolute request an absolute URL instead of relative 456 * @param string $separator parameter separator 457 * @return string 458 */ 459function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&') { 460 global $conf; 461 if(is_array($urlParameters)) { 462 if(isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']); 463 if(isset($urlParameters['at']) && $conf['date_at_format']) $urlParameters['at'] = date($conf['date_at_format'],$urlParameters['at']); 464 $urlParameters = buildURLparams($urlParameters, $separator); 465 } else { 466 $urlParameters = str_replace(',', $separator, $urlParameters); 467 } 468 if($id === '') { 469 $id = $conf['start']; 470 } 471 $id = idfilter($id); 472 if($absolute) { 473 $xlink = DOKU_URL; 474 } else { 475 $xlink = DOKU_BASE; 476 } 477 478 if($conf['userewrite'] == 2) { 479 $xlink .= DOKU_SCRIPT.'/'.$id; 480 if($urlParameters) $xlink .= '?'.$urlParameters; 481 } elseif($conf['userewrite']) { 482 $xlink .= $id; 483 if($urlParameters) $xlink .= '?'.$urlParameters; 484 } elseif($id) { 485 $xlink .= DOKU_SCRIPT.'?id='.$id; 486 if($urlParameters) $xlink .= $separator.$urlParameters; 487 } else { 488 $xlink .= DOKU_SCRIPT; 489 if($urlParameters) $xlink .= '?'.$urlParameters; 490 } 491 492 return $xlink; 493} 494 495/** 496 * This builds a link to an alternate page format 497 * 498 * Handles URL rewriting if enabled. Follows the style of wl(). 499 * 500 * @author Ben Coburn <btcoburn@silicodon.net> 501 * @param string $id page id, defaults to start page 502 * @param string $format the export renderer to use 503 * @param string|array $urlParameters URL parameters, associative array recommended 504 * @param bool $abs request an absolute URL instead of relative 505 * @param string $sep parameter separator 506 * @return string 507 */ 508function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&') { 509 global $conf; 510 if(is_array($urlParameters)) { 511 $urlParameters = buildURLparams($urlParameters, $sep); 512 } else { 513 $urlParameters = str_replace(',', $sep, $urlParameters); 514 } 515 516 $format = rawurlencode($format); 517 $id = idfilter($id); 518 if($abs) { 519 $xlink = DOKU_URL; 520 } else { 521 $xlink = DOKU_BASE; 522 } 523 524 if($conf['userewrite'] == 2) { 525 $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format; 526 if($urlParameters) $xlink .= $sep.$urlParameters; 527 } elseif($conf['userewrite'] == 1) { 528 $xlink .= '_export/'.$format.'/'.$id; 529 if($urlParameters) $xlink .= '?'.$urlParameters; 530 } else { 531 $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id; 532 if($urlParameters) $xlink .= $sep.$urlParameters; 533 } 534 535 return $xlink; 536} 537 538/** 539 * Build a link to a media file 540 * 541 * Will return a link to the detail page if $direct is false 542 * 543 * The $more parameter should always be given as array, the function then 544 * will strip default parameters to produce even cleaner URLs 545 * 546 * @param string $id the media file id or URL 547 * @param mixed $more string or array with additional parameters 548 * @param bool $direct link to detail page if false 549 * @param string $sep URL parameter separator 550 * @param bool $abs Create an absolute URL 551 * @return string 552 */ 553function ml($id = '', $more = '', $direct = true, $sep = '&', $abs = false) { 554 global $conf; 555 $isexternalimage = media_isexternal($id); 556 if(!$isexternalimage) { 557 $id = cleanID($id); 558 } 559 560 if(is_array($more)) { 561 // add token for resized images 562 if(!empty($more['w']) || !empty($more['h']) || $isexternalimage){ 563 $more['tok'] = media_get_token($id,$more['w'],$more['h']); 564 } 565 // strip defaults for shorter URLs 566 if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']); 567 if(empty($more['w'])) unset($more['w']); 568 if(empty($more['h'])) unset($more['h']); 569 if(isset($more['id']) && $direct) unset($more['id']); 570 if(isset($more['rev']) && !$more['rev']) unset($more['rev']); 571 $more = buildURLparams($more, $sep); 572 } else { 573 $matches = array(); 574 if (preg_match_all('/\b(w|h)=(\d*)\b/',$more,$matches,PREG_SET_ORDER) || $isexternalimage){ 575 $resize = array('w'=>0, 'h'=>0); 576 foreach ($matches as $match){ 577 $resize[$match[1]] = $match[2]; 578 } 579 $more .= $more === '' ? '' : $sep; 580 $more .= 'tok='.media_get_token($id,$resize['w'],$resize['h']); 581 } 582 $more = str_replace('cache=cache', '', $more); //skip default 583 $more = str_replace(',,', ',', $more); 584 $more = str_replace(',', $sep, $more); 585 } 586 587 if($abs) { 588 $xlink = DOKU_URL; 589 } else { 590 $xlink = DOKU_BASE; 591 } 592 593 // external URLs are always direct without rewriting 594 if($isexternalimage) { 595 $xlink .= 'lib/exe/fetch.php'; 596 $xlink .= '?'.$more; 597 $xlink .= $sep.'media='.rawurlencode($id); 598 return $xlink; 599 } 600 601 $id = idfilter($id); 602 603 // decide on scriptname 604 if($direct) { 605 if($conf['userewrite'] == 1) { 606 $script = '_media'; 607 } else { 608 $script = 'lib/exe/fetch.php'; 609 } 610 } else { 611 if($conf['userewrite'] == 1) { 612 $script = '_detail'; 613 } else { 614 $script = 'lib/exe/detail.php'; 615 } 616 } 617 618 // build URL based on rewrite mode 619 if($conf['userewrite']) { 620 $xlink .= $script.'/'.$id; 621 if($more) $xlink .= '?'.$more; 622 } else { 623 if($more) { 624 $xlink .= $script.'?'.$more; 625 $xlink .= $sep.'media='.$id; 626 } else { 627 $xlink .= $script.'?media='.$id; 628 } 629 } 630 631 return $xlink; 632} 633 634/** 635 * Returns the URL to the DokuWiki base script 636 * 637 * Consider using wl() instead, unless you absoutely need the doku.php endpoint 638 * 639 * @author Andreas Gohr <andi@splitbrain.org> 640 * 641 * @return string 642 */ 643function script() { 644 return DOKU_BASE.DOKU_SCRIPT; 645} 646 647/** 648 * Spamcheck against wordlist 649 * 650 * Checks the wikitext against a list of blocked expressions 651 * returns true if the text contains any bad words 652 * 653 * Triggers COMMON_WORDBLOCK_BLOCKED 654 * 655 * Action Plugins can use this event to inspect the blocked data 656 * and gain information about the user who was blocked. 657 * 658 * Event data: 659 * data['matches'] - array of matches 660 * data['userinfo'] - information about the blocked user 661 * [ip] - ip address 662 * [user] - username (if logged in) 663 * [mail] - mail address (if logged in) 664 * [name] - real name (if logged in) 665 * 666 * @author Andreas Gohr <andi@splitbrain.org> 667 * @author Michael Klier <chi@chimeric.de> 668 * 669 * @param string $text - optional text to check, if not given the globals are used 670 * @return bool - true if a spam word was found 671 */ 672function checkwordblock($text = '') { 673 global $TEXT; 674 global $PRE; 675 global $SUF; 676 global $SUM; 677 global $conf; 678 global $INFO; 679 /* @var Input $INPUT */ 680 global $INPUT; 681 682 if(!$conf['usewordblock']) return false; 683 684 if(!$text) $text = "$PRE $TEXT $SUF $SUM"; 685 686 // we prepare the text a tiny bit to prevent spammers circumventing URL checks 687 $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i', '\1http://\2 \2\3', $text); 688 689 $wordblocks = getWordblocks(); 690 // how many lines to read at once (to work around some PCRE limits) 691 if(version_compare(phpversion(), '4.3.0', '<')) { 692 // old versions of PCRE define a maximum of parenthesises even if no 693 // backreferences are used - the maximum is 99 694 // this is very bad performancewise and may even be too high still 695 $chunksize = 40; 696 } else { 697 // read file in chunks of 200 - this should work around the 698 // MAX_PATTERN_SIZE in modern PCRE 699 $chunksize = 200; 700 } 701 while($blocks = array_splice($wordblocks, 0, $chunksize)) { 702 $re = array(); 703 // build regexp from blocks 704 foreach($blocks as $block) { 705 $block = preg_replace('/#.*$/', '', $block); 706 $block = trim($block); 707 if(empty($block)) continue; 708 $re[] = $block; 709 } 710 if(count($re) && preg_match('#('.join('|', $re).')#si', $text, $matches)) { 711 // prepare event data 712 $data = array(); 713 $data['matches'] = $matches; 714 $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR'); 715 if($INPUT->server->str('REMOTE_USER')) { 716 $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER'); 717 $data['userinfo']['name'] = $INFO['userinfo']['name']; 718 $data['userinfo']['mail'] = $INFO['userinfo']['mail']; 719 } 720 $callback = create_function('', 'return true;'); 721 return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true); 722 } 723 } 724 return false; 725} 726 727/** 728 * Return the IP of the client 729 * 730 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 731 * 732 * It returns a comma separated list of IPs if the above mentioned 733 * headers are set. If the single parameter is set, it tries to return 734 * a routable public address, prefering the ones suplied in the X 735 * headers 736 * 737 * @author Andreas Gohr <andi@splitbrain.org> 738 * 739 * @param boolean $single If set only a single IP is returned 740 * @return string 741 */ 742function clientIP($single = false) { 743 /* @var Input $INPUT */ 744 global $INPUT; 745 746 $ip = array(); 747 $ip[] = $INPUT->server->str('REMOTE_ADDR'); 748 if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) { 749 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR')))); 750 } 751 if($INPUT->server->str('HTTP_X_REAL_IP')) { 752 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP')))); 753 } 754 755 // some IPv4/v6 regexps borrowed from Feyd 756 // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479 757 $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])'; 758 $hex_digit = '[A-Fa-f0-9]'; 759 $h16 = "{$hex_digit}{1,4}"; 760 $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet"; 761 $ls32 = "(?:$h16:$h16|$IPv4Address)"; 762 $IPv6Address = 763 "(?:(?:{$IPv4Address})|(?:". 764 "(?:$h16:){6}$ls32". 765 "|::(?:$h16:){5}$ls32". 766 "|(?:$h16)?::(?:$h16:){4}$ls32". 767 "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32". 768 "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32". 769 "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32". 770 "|(?:(?:$h16:){0,4}$h16)?::$ls32". 771 "|(?:(?:$h16:){0,5}$h16)?::$h16". 772 "|(?:(?:$h16:){0,6}$h16)?::". 773 ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)"; 774 775 // remove any non-IP stuff 776 $cnt = count($ip); 777 $match = array(); 778 for($i = 0; $i < $cnt; $i++) { 779 if(preg_match("/^$IPv4Address$/", $ip[$i], $match) || preg_match("/^$IPv6Address$/", $ip[$i], $match)) { 780 $ip[$i] = $match[0]; 781 } else { 782 $ip[$i] = ''; 783 } 784 if(empty($ip[$i])) unset($ip[$i]); 785 } 786 $ip = array_values(array_unique($ip)); 787 if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 788 789 if(!$single) return join(',', $ip); 790 791 // decide which IP to use, trying to avoid local addresses 792 $ip = array_reverse($ip); 793 foreach($ip as $i) { 794 if(preg_match('/^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/', $i)) { 795 continue; 796 } else { 797 return $i; 798 } 799 } 800 // still here? just use the first (last) address 801 return $ip[0]; 802} 803 804/** 805 * Check if the browser is on a mobile device 806 * 807 * Adapted from the example code at url below 808 * 809 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code 810 * 811 * @return bool if true, client is mobile browser; otherwise false 812 */ 813function clientismobile() { 814 /* @var Input $INPUT */ 815 global $INPUT; 816 817 if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true; 818 819 if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true; 820 821 if(!$INPUT->server->has('HTTP_USER_AGENT')) return false; 822 823 $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'; 824 825 if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true; 826 827 return false; 828} 829 830/** 831 * Convert one or more comma separated IPs to hostnames 832 * 833 * If $conf['dnslookups'] is disabled it simply returns the input string 834 * 835 * @author Glen Harris <astfgl@iamnota.org> 836 * 837 * @param string $ips comma separated list of IP addresses 838 * @return string a comma separated list of hostnames 839 */ 840function gethostsbyaddrs($ips) { 841 global $conf; 842 if(!$conf['dnslookups']) return $ips; 843 844 $hosts = array(); 845 $ips = explode(',', $ips); 846 847 if(is_array($ips)) { 848 foreach($ips as $ip) { 849 $hosts[] = gethostbyaddr(trim($ip)); 850 } 851 return join(',', $hosts); 852 } else { 853 return gethostbyaddr(trim($ips)); 854 } 855} 856 857/** 858 * Checks if a given page is currently locked. 859 * 860 * removes stale lockfiles 861 * 862 * @author Andreas Gohr <andi@splitbrain.org> 863 * 864 * @param string $id page id 865 * @return bool page is locked? 866 */ 867function checklock($id) { 868 global $conf; 869 /* @var Input $INPUT */ 870 global $INPUT; 871 872 $lock = wikiLockFN($id); 873 874 //no lockfile 875 if(!file_exists($lock)) return false; 876 877 //lockfile expired 878 if((time() - filemtime($lock)) > $conf['locktime']) { 879 @unlink($lock); 880 return false; 881 } 882 883 //my own lock 884 @list($ip, $session) = explode("\n", io_readFile($lock)); 885 if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) { 886 return false; 887 } 888 889 return $ip; 890} 891 892/** 893 * Lock a page for editing 894 * 895 * @author Andreas Gohr <andi@splitbrain.org> 896 * 897 * @param string $id page id to lock 898 */ 899function lock($id) { 900 global $conf; 901 /* @var Input $INPUT */ 902 global $INPUT; 903 904 if($conf['locktime'] == 0) { 905 return; 906 } 907 908 $lock = wikiLockFN($id); 909 if($INPUT->server->str('REMOTE_USER')) { 910 io_saveFile($lock, $INPUT->server->str('REMOTE_USER')); 911 } else { 912 io_saveFile($lock, clientIP()."\n".session_id()); 913 } 914} 915 916/** 917 * Unlock a page if it was locked by the user 918 * 919 * @author Andreas Gohr <andi@splitbrain.org> 920 * 921 * @param string $id page id to unlock 922 * @return bool true if a lock was removed 923 */ 924function unlock($id) { 925 /* @var Input $INPUT */ 926 global $INPUT; 927 928 $lock = wikiLockFN($id); 929 if(file_exists($lock)) { 930 @list($ip, $session) = explode("\n", io_readFile($lock)); 931 if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) { 932 @unlink($lock); 933 return true; 934 } 935 } 936 return false; 937} 938 939/** 940 * convert line ending to unix format 941 * 942 * also makes sure the given text is valid UTF-8 943 * 944 * @see formText() for 2crlf conversion 945 * @author Andreas Gohr <andi@splitbrain.org> 946 * 947 * @param string $text 948 * @return string 949 */ 950function cleanText($text) { 951 $text = preg_replace("/(\015\012)|(\015)/", "\012", $text); 952 953 // if the text is not valid UTF-8 we simply assume latin1 954 // this won't break any worse than it breaks with the wrong encoding 955 // but might actually fix the problem in many cases 956 if(!utf8_check($text)) $text = utf8_encode($text); 957 958 return $text; 959} 960 961/** 962 * Prepares text for print in Webforms by encoding special chars. 963 * It also converts line endings to Windows format which is 964 * pseudo standard for webforms. 965 * 966 * @see cleanText() for 2unix conversion 967 * @author Andreas Gohr <andi@splitbrain.org> 968 * 969 * @param string $text 970 * @return string 971 */ 972function formText($text) { 973 $text = str_replace("\012", "\015\012", $text); 974 return htmlspecialchars($text); 975} 976 977/** 978 * Returns the specified local text in raw format 979 * 980 * @author Andreas Gohr <andi@splitbrain.org> 981 * 982 * @param string $id page id 983 * @param string $ext extension of file being read, default 'txt' 984 * @return string 985 */ 986function rawLocale($id, $ext = 'txt') { 987 return io_readFile(localeFN($id, $ext)); 988} 989 990/** 991 * Returns the raw WikiText 992 * 993 * @author Andreas Gohr <andi@splitbrain.org> 994 * 995 * @param string $id page id 996 * @param string|int $rev timestamp when a revision of wikitext is desired 997 * @return string 998 */ 999function rawWiki($id, $rev = '') { 1000 return io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1001} 1002 1003/** 1004 * Returns the pagetemplate contents for the ID's namespace 1005 * 1006 * @triggers COMMON_PAGETPL_LOAD 1007 * @author Andreas Gohr <andi@splitbrain.org> 1008 * 1009 * @param string $id the id of the page to be created 1010 * @return string parsed pagetemplate content 1011 */ 1012function pageTemplate($id) { 1013 global $conf; 1014 1015 if(is_array($id)) $id = $id[0]; 1016 1017 // prepare initial event data 1018 $data = array( 1019 'id' => $id, // the id of the page to be created 1020 'tpl' => '', // the text used as template 1021 'tplfile' => '', // the file above text was/should be loaded from 1022 'doreplace' => true // should wildcard replacements be done on the text? 1023 ); 1024 1025 $evt = new Doku_Event('COMMON_PAGETPL_LOAD', $data); 1026 if($evt->advise_before(true)) { 1027 // the before event might have loaded the content already 1028 if(empty($data['tpl'])) { 1029 // if the before event did not set a template file, try to find one 1030 if(empty($data['tplfile'])) { 1031 $path = dirname(wikiFN($id)); 1032 if(file_exists($path.'/_template.txt')) { 1033 $data['tplfile'] = $path.'/_template.txt'; 1034 } else { 1035 // search upper namespaces for templates 1036 $len = strlen(rtrim($conf['datadir'], '/')); 1037 while(strlen($path) >= $len) { 1038 if(file_exists($path.'/__template.txt')) { 1039 $data['tplfile'] = $path.'/__template.txt'; 1040 break; 1041 } 1042 $path = substr($path, 0, strrpos($path, '/')); 1043 } 1044 } 1045 } 1046 // load the content 1047 $data['tpl'] = io_readFile($data['tplfile']); 1048 } 1049 if($data['doreplace']) parsePageTemplate($data); 1050 } 1051 $evt->advise_after(); 1052 unset($evt); 1053 1054 return $data['tpl']; 1055} 1056 1057/** 1058 * Performs common page template replacements 1059 * This works on data from COMMON_PAGETPL_LOAD 1060 * 1061 * @author Andreas Gohr <andi@splitbrain.org> 1062 * 1063 * @param array $data array with event data 1064 * @return string 1065 */ 1066function parsePageTemplate(&$data) { 1067 /** 1068 * @var string $id the id of the page to be created 1069 * @var string $tpl the text used as template 1070 * @var string $tplfile the file above text was/should be loaded from 1071 * @var bool $doreplace should wildcard replacements be done on the text? 1072 */ 1073 extract($data); 1074 1075 global $USERINFO; 1076 global $conf; 1077 /* @var Input $INPUT */ 1078 global $INPUT; 1079 1080 // replace placeholders 1081 $file = noNS($id); 1082 $page = strtr($file, $conf['sepchar'], ' '); 1083 1084 $tpl = str_replace( 1085 array( 1086 '@ID@', 1087 '@NS@', 1088 '@FILE@', 1089 '@!FILE@', 1090 '@!FILE!@', 1091 '@PAGE@', 1092 '@!PAGE@', 1093 '@!!PAGE@', 1094 '@!PAGE!@', 1095 '@USER@', 1096 '@NAME@', 1097 '@MAIL@', 1098 '@DATE@', 1099 ), 1100 array( 1101 $id, 1102 getNS($id), 1103 $file, 1104 utf8_ucfirst($file), 1105 utf8_strtoupper($file), 1106 $page, 1107 utf8_ucfirst($page), 1108 utf8_ucwords($page), 1109 utf8_strtoupper($page), 1110 $INPUT->server->str('REMOTE_USER'), 1111 $USERINFO['name'], 1112 $USERINFO['mail'], 1113 $conf['dformat'], 1114 ), $tpl 1115 ); 1116 1117 // we need the callback to work around strftime's char limit 1118 $tpl = preg_replace_callback('/%./', create_function('$m', 'return strftime($m[0]);'), $tpl); 1119 $data['tpl'] = $tpl; 1120 return $tpl; 1121} 1122 1123/** 1124 * Returns the raw Wiki Text in three slices. 1125 * 1126 * The range parameter needs to have the form "from-to" 1127 * and gives the range of the section in bytes - no 1128 * UTF-8 awareness is needed. 1129 * The returned order is prefix, section and suffix. 1130 * 1131 * @author Andreas Gohr <andi@splitbrain.org> 1132 * 1133 * @param string $range in form "from-to" 1134 * @param string $id page id 1135 * @param string $rev optional, the revision timestamp 1136 * @return string[] with three slices 1137 */ 1138function rawWikiSlices($range, $id, $rev = '') { 1139 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1140 1141 // Parse range 1142 list($from, $to) = explode('-', $range, 2); 1143 // Make range zero-based, use defaults if marker is missing 1144 $from = !$from ? 0 : ($from - 1); 1145 $to = !$to ? strlen($text) : ($to - 1); 1146 1147 $slices = array(); 1148 $slices[0] = substr($text, 0, $from); 1149 $slices[1] = substr($text, $from, $to - $from); 1150 $slices[2] = substr($text, $to); 1151 return $slices; 1152} 1153 1154/** 1155 * Joins wiki text slices 1156 * 1157 * function to join the text slices. 1158 * When the pretty parameter is set to true it adds additional empty 1159 * lines between sections if needed (used on saving). 1160 * 1161 * @author Andreas Gohr <andi@splitbrain.org> 1162 * 1163 * @param string $pre prefix 1164 * @param string $text text in the middle 1165 * @param string $suf suffix 1166 * @param bool $pretty add additional empty lines between sections 1167 * @return string 1168 */ 1169function con($pre, $text, $suf, $pretty = false) { 1170 if($pretty) { 1171 if($pre !== '' && substr($pre, -1) !== "\n" && 1172 substr($text, 0, 1) !== "\n" 1173 ) { 1174 $pre .= "\n"; 1175 } 1176 if($suf !== '' && substr($text, -1) !== "\n" && 1177 substr($suf, 0, 1) !== "\n" 1178 ) { 1179 $text .= "\n"; 1180 } 1181 } 1182 1183 return $pre.$text.$suf; 1184} 1185 1186/** 1187 * Saves a wikitext by calling io_writeWikiPage. 1188 * Also directs changelog and attic updates. 1189 * 1190 * @author Andreas Gohr <andi@splitbrain.org> 1191 * @author Ben Coburn <btcoburn@silicodon.net> 1192 * 1193 * @param string $id page id 1194 * @param string $text wikitext being saved 1195 * @param string $summary summary of text update 1196 * @param bool $minor mark this saved version as minor update 1197 */ 1198function saveWikiText($id, $text, $summary, $minor = false) { 1199 /* Note to developers: 1200 This code is subtle and delicate. Test the behavior of 1201 the attic and changelog with dokuwiki and external edits 1202 after any changes. External edits change the wiki page 1203 directly without using php or dokuwiki. 1204 */ 1205 global $conf; 1206 global $lang; 1207 global $REV; 1208 /* @var Input $INPUT */ 1209 global $INPUT; 1210 1211 // ignore if no changes were made 1212 if($text == rawWiki($id, '')) { 1213 return; 1214 } 1215 1216 $file = wikiFN($id); 1217 $old = @filemtime($file); // from page 1218 $wasRemoved = (trim($text) == ''); // check for empty or whitespace only 1219 $wasCreated = !file_exists($file); 1220 $wasReverted = ($REV == true); 1221 $pagelog = new PageChangeLog($id, 1024); 1222 $newRev = false; 1223 $oldRev = $pagelog->getRevisions(-1, 1); // from changelog 1224 $oldRev = (int) (empty($oldRev) ? 0 : $oldRev[0]); 1225 if(!file_exists(wikiFN($id, $old)) && file_exists($file) && $old >= $oldRev) { 1226 // add old revision to the attic if missing 1227 saveOldRevision($id); 1228 // add a changelog entry if this edit came from outside dokuwiki 1229 if($old > $oldRev) { 1230 addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=> true)); 1231 // remove soon to be stale instructions 1232 $cache = new cache_instructions($id, $file); 1233 $cache->removeCache(); 1234 } 1235 } 1236 1237 if($wasRemoved) { 1238 // Send "update" event with empty data, so plugins can react to page deletion 1239 $data = array(array($file, '', false), getNS($id), noNS($id), false); 1240 trigger_event('IO_WIKIPAGE_WRITE', $data); 1241 // pre-save deleted revision 1242 @touch($file); 1243 clearstatcache(); 1244 $newRev = saveOldRevision($id); 1245 // remove empty file 1246 @unlink($file); 1247 // don't remove old meta info as it should be saved, plugins can use IO_WIKIPAGE_WRITE for removing their metadata... 1248 // purge non-persistant meta data 1249 p_purge_metadata($id); 1250 $del = true; 1251 // autoset summary on deletion 1252 if(empty($summary)) $summary = $lang['deleted']; 1253 // remove empty namespaces 1254 io_sweepNS($id, 'datadir'); 1255 io_sweepNS($id, 'mediadir'); 1256 } else { 1257 // save file (namespace dir is created in io_writeWikiPage) 1258 io_writeWikiPage($file, $text, $id); 1259 // pre-save the revision, to keep the attic in sync 1260 $newRev = saveOldRevision($id); 1261 $del = false; 1262 } 1263 1264 // select changelog line type 1265 $extra = ''; 1266 $type = DOKU_CHANGE_TYPE_EDIT; 1267 if($wasReverted) { 1268 $type = DOKU_CHANGE_TYPE_REVERT; 1269 $extra = $REV; 1270 } else if($wasCreated) { 1271 $type = DOKU_CHANGE_TYPE_CREATE; 1272 } else if($wasRemoved) { 1273 $type = DOKU_CHANGE_TYPE_DELETE; 1274 } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) { 1275 $type = DOKU_CHANGE_TYPE_MINOR_EDIT; 1276 } //minor edits only for logged in users 1277 1278 addLogEntry($newRev, $id, $type, $summary, $extra); 1279 // send notify mails 1280 notify($id, 'admin', $old, $summary, $minor); 1281 notify($id, 'subscribers', $old, $summary, $minor); 1282 1283 // update the purgefile (timestamp of the last time anything within the wiki was changed) 1284 io_saveFile($conf['cachedir'].'/purgefile', time()); 1285 1286 // if useheading is enabled, purge the cache of all linking pages 1287 if(useHeading('content')) { 1288 $pages = ft_backlinks($id, true); 1289 foreach($pages as $page) { 1290 $cache = new cache_renderer($page, wikiFN($page), 'xhtml'); 1291 $cache->removeCache(); 1292 } 1293 } 1294} 1295 1296/** 1297 * moves the current version to the attic and returns its 1298 * revision date 1299 * 1300 * @author Andreas Gohr <andi@splitbrain.org> 1301 * 1302 * @param string $id page id 1303 * @return int|string revision timestamp 1304 */ 1305function saveOldRevision($id) { 1306 $oldf = wikiFN($id); 1307 if(!file_exists($oldf)) return ''; 1308 $date = filemtime($oldf); 1309 $newf = wikiFN($id, $date); 1310 io_writeWikiPage($newf, rawWiki($id), $id, $date); 1311 return $date; 1312} 1313 1314/** 1315 * Sends a notify mail on page change or registration 1316 * 1317 * @param string $id The changed page 1318 * @param string $who Who to notify (admin|subscribers|register) 1319 * @param int|string $rev Old page revision 1320 * @param string $summary What changed 1321 * @param boolean $minor Is this a minor edit? 1322 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value 1323 * @return bool 1324 * 1325 * @author Andreas Gohr <andi@splitbrain.org> 1326 */ 1327function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) { 1328 global $conf; 1329 /* @var Input $INPUT */ 1330 global $INPUT; 1331 1332 // decide if there is something to do, eg. whom to mail 1333 if($who == 'admin') { 1334 if(empty($conf['notify'])) return false; //notify enabled? 1335 $tpl = 'mailtext'; 1336 $to = $conf['notify']; 1337 } elseif($who == 'subscribers') { 1338 if(!actionOK('subscribe')) return false; //subscribers enabled? 1339 if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors 1340 $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace); 1341 trigger_event( 1342 'COMMON_NOTIFY_ADDRESSLIST', $data, 1343 array(new Subscription(), 'notifyaddresses') 1344 ); 1345 $to = $data['addresslist']; 1346 if(empty($to)) return false; 1347 $tpl = 'subscr_single'; 1348 } else { 1349 return false; //just to be safe 1350 } 1351 1352 // prepare content 1353 $subscription = new Subscription(); 1354 return $subscription->send_diff($to, $tpl, $id, $rev, $summary); 1355} 1356 1357/** 1358 * extracts the query from a search engine referrer 1359 * 1360 * @author Andreas Gohr <andi@splitbrain.org> 1361 * @author Todd Augsburger <todd@rollerorgans.com> 1362 * 1363 * @return array|string 1364 */ 1365function getGoogleQuery() { 1366 /* @var Input $INPUT */ 1367 global $INPUT; 1368 1369 if(!$INPUT->server->has('HTTP_REFERER')) { 1370 return ''; 1371 } 1372 $url = parse_url($INPUT->server->str('HTTP_REFERER')); 1373 1374 // only handle common SEs 1375 if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return ''; 1376 1377 $query = array(); 1378 // temporary workaround against PHP bug #49733 1379 // see http://bugs.php.net/bug.php?id=49733 1380 if(UTF8_MBSTRING) $enc = mb_internal_encoding(); 1381 parse_str($url['query'], $query); 1382 if(UTF8_MBSTRING) mb_internal_encoding($enc); 1383 1384 $q = ''; 1385 if(isset($query['q'])){ 1386 $q = $query['q']; 1387 }elseif(isset($query['p'])){ 1388 $q = $query['p']; 1389 }elseif(isset($query['query'])){ 1390 $q = $query['query']; 1391 } 1392 $q = trim($q); 1393 1394 if(!$q) return ''; 1395 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY); 1396 return $q; 1397} 1398 1399/** 1400 * Return the human readable size of a file 1401 * 1402 * @param int $size A file size 1403 * @param int $dec A number of decimal places 1404 * @return string human readable size 1405 * 1406 * @author Martin Benjamin <b.martin@cybernet.ch> 1407 * @author Aidan Lister <aidan@php.net> 1408 * @version 1.0.0 1409 */ 1410function filesize_h($size, $dec = 1) { 1411 $sizes = array('B', 'KB', 'MB', 'GB'); 1412 $count = count($sizes); 1413 $i = 0; 1414 1415 while($size >= 1024 && ($i < $count - 1)) { 1416 $size /= 1024; 1417 $i++; 1418 } 1419 1420 return round($size, $dec).' '.$sizes[$i]; 1421} 1422 1423/** 1424 * Return the given timestamp as human readable, fuzzy age 1425 * 1426 * @author Andreas Gohr <gohr@cosmocode.de> 1427 * 1428 * @param int $dt timestamp 1429 * @return string 1430 */ 1431function datetime_h($dt) { 1432 global $lang; 1433 1434 $ago = time() - $dt; 1435 if($ago > 24 * 60 * 60 * 30 * 12 * 2) { 1436 return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12))); 1437 } 1438 if($ago > 24 * 60 * 60 * 30 * 2) { 1439 return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30))); 1440 } 1441 if($ago > 24 * 60 * 60 * 7 * 2) { 1442 return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7))); 1443 } 1444 if($ago > 24 * 60 * 60 * 2) { 1445 return sprintf($lang['days'], round($ago / (24 * 60 * 60))); 1446 } 1447 if($ago > 60 * 60 * 2) { 1448 return sprintf($lang['hours'], round($ago / (60 * 60))); 1449 } 1450 if($ago > 60 * 2) { 1451 return sprintf($lang['minutes'], round($ago / (60))); 1452 } 1453 return sprintf($lang['seconds'], $ago); 1454} 1455 1456/** 1457 * Wraps around strftime but provides support for fuzzy dates 1458 * 1459 * The format default to $conf['dformat']. It is passed to 1460 * strftime - %f can be used to get the value from datetime_h() 1461 * 1462 * @see datetime_h 1463 * @author Andreas Gohr <gohr@cosmocode.de> 1464 * 1465 * @param int|null $dt timestamp when given, null will take current timestamp 1466 * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime() 1467 * @return string 1468 */ 1469function dformat($dt = null, $format = '') { 1470 global $conf; 1471 1472 if(is_null($dt)) $dt = time(); 1473 $dt = (int) $dt; 1474 if(!$format) $format = $conf['dformat']; 1475 1476 $format = str_replace('%f', datetime_h($dt), $format); 1477 return strftime($format, $dt); 1478} 1479 1480/** 1481 * Formats a timestamp as ISO 8601 date 1482 * 1483 * @author <ungu at terong dot com> 1484 * @link http://www.php.net/manual/en/function.date.php#54072 1485 * 1486 * @param int $int_date current date in UNIX timestamp 1487 * @return string 1488 */ 1489function date_iso8601($int_date) { 1490 $date_mod = date('Y-m-d\TH:i:s', $int_date); 1491 $pre_timezone = date('O', $int_date); 1492 $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2); 1493 $date_mod .= $time_zone; 1494 return $date_mod; 1495} 1496 1497/** 1498 * return an obfuscated email address in line with $conf['mailguard'] setting 1499 * 1500 * @author Harry Fuecks <hfuecks@gmail.com> 1501 * @author Christopher Smith <chris@jalakai.co.uk> 1502 * 1503 * @param string $email email address 1504 * @return string 1505 */ 1506function obfuscate($email) { 1507 global $conf; 1508 1509 switch($conf['mailguard']) { 1510 case 'visible' : 1511 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1512 return strtr($email, $obfuscate); 1513 1514 case 'hex' : 1515 $encode = ''; 1516 $len = strlen($email); 1517 for($x = 0; $x < $len; $x++) { 1518 $encode .= '&#x'.bin2hex($email{$x}).';'; 1519 } 1520 return $encode; 1521 1522 case 'none' : 1523 default : 1524 return $email; 1525 } 1526} 1527 1528/** 1529 * Removes quoting backslashes 1530 * 1531 * @author Andreas Gohr <andi@splitbrain.org> 1532 * 1533 * @param string $string 1534 * @param string $char backslashed character 1535 * @return string 1536 */ 1537function unslash($string, $char = "'") { 1538 return str_replace('\\'.$char, $char, $string); 1539} 1540 1541/** 1542 * Convert php.ini shorthands to byte 1543 * 1544 * @author <gilthans dot NO dot SPAM at gmail dot com> 1545 * @link http://de3.php.net/manual/en/ini.core.php#79564 1546 * 1547 * @param string $v shorthands 1548 * @return int|string 1549 */ 1550function php_to_byte($v) { 1551 $l = substr($v, -1); 1552 $ret = substr($v, 0, -1); 1553 switch(strtoupper($l)) { 1554 /** @noinspection PhpMissingBreakStatementInspection */ 1555 case 'P': 1556 $ret *= 1024; 1557 /** @noinspection PhpMissingBreakStatementInspection */ 1558 case 'T': 1559 $ret *= 1024; 1560 /** @noinspection PhpMissingBreakStatementInspection */ 1561 case 'G': 1562 $ret *= 1024; 1563 /** @noinspection PhpMissingBreakStatementInspection */ 1564 case 'M': 1565 $ret *= 1024; 1566 /** @noinspection PhpMissingBreakStatementInspection */ 1567 case 'K': 1568 $ret *= 1024; 1569 break; 1570 default; 1571 $ret *= 10; 1572 break; 1573 } 1574 return $ret; 1575} 1576 1577/** 1578 * Wrapper around preg_quote adding the default delimiter 1579 * 1580 * @param string $string 1581 * @return string 1582 */ 1583function preg_quote_cb($string) { 1584 return preg_quote($string, '/'); 1585} 1586 1587/** 1588 * Shorten a given string by removing data from the middle 1589 * 1590 * You can give the string in two parts, the first part $keep 1591 * will never be shortened. The second part $short will be cut 1592 * in the middle to shorten but only if at least $min chars are 1593 * left to display it. Otherwise it will be left off. 1594 * 1595 * @param string $keep the part to keep 1596 * @param string $short the part to shorten 1597 * @param int $max maximum chars you want for the whole string 1598 * @param int $min minimum number of chars to have left for middle shortening 1599 * @param string $char the shortening character to use 1600 * @return string 1601 */ 1602function shorten($keep, $short, $max, $min = 9, $char = '…') { 1603 $max = $max - utf8_strlen($keep); 1604 if($max < $min) return $keep; 1605 $len = utf8_strlen($short); 1606 if($len <= $max) return $keep.$short; 1607 $half = floor($max / 2); 1608 return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half); 1609} 1610 1611/** 1612 * Return the users real name or e-mail address for use 1613 * in page footer and recent changes pages 1614 * 1615 * @param string|null $username or null when currently logged-in user should be used 1616 * @param bool $textonly true returns only plain text, true allows returning html 1617 * @return string html or plain text(not escaped) of formatted user name 1618 * 1619 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1620 */ 1621function editorinfo($username, $textonly = false) { 1622 return userlink($username, $textonly); 1623} 1624 1625/** 1626 * Returns users realname w/o link 1627 * 1628 * @param string|null $username or null when currently logged-in user should be used 1629 * @param bool $textonly true returns only plain text, true allows returning html 1630 * @return string html or plain text(not escaped) of formatted user name 1631 * 1632 * @triggers COMMON_USER_LINK 1633 */ 1634function userlink($username = null, $textonly = false) { 1635 global $conf, $INFO; 1636 /** @var DokuWiki_Auth_Plugin $auth */ 1637 global $auth; 1638 /** @var Input $INPUT */ 1639 global $INPUT; 1640 1641 // prepare initial event data 1642 $data = array( 1643 'username' => $username, // the unique user name 1644 'name' => '', 1645 'link' => array( //setting 'link' to false disables linking 1646 'target' => '', 1647 'pre' => '', 1648 'suf' => '', 1649 'style' => '', 1650 'more' => '', 1651 'url' => '', 1652 'title' => '', 1653 'class' => '' 1654 ), 1655 'userlink' => '', // formatted user name as will be returned 1656 'textonly' => $textonly 1657 ); 1658 if($username === null) { 1659 $data['username'] = $username = $INPUT->server->str('REMOTE_USER'); 1660 if($textonly){ 1661 $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')'; 1662 }else { 1663 $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> (<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)'; 1664 } 1665 } 1666 1667 $evt = new Doku_Event('COMMON_USER_LINK', $data); 1668 if($evt->advise_before(true)) { 1669 if(empty($data['name'])) { 1670 if($auth) $info = $auth->getUserData($username); 1671 if($conf['showuseras'] != 'loginname' && isset($info) && $info) { 1672 switch($conf['showuseras']) { 1673 case 'username': 1674 case 'username_link': 1675 $data['name'] = $textonly ? $info['name'] : hsc($info['name']); 1676 break; 1677 case 'email': 1678 case 'email_link': 1679 $data['name'] = obfuscate($info['mail']); 1680 break; 1681 } 1682 } else { 1683 $data['name'] = $textonly ? $data['username'] : hsc($data['username']); 1684 } 1685 } 1686 1687 /** @var Doku_Renderer_xhtml $xhtml_renderer */ 1688 static $xhtml_renderer = null; 1689 1690 if(!$data['textonly'] && empty($data['link']['url'])) { 1691 1692 if(in_array($conf['showuseras'], array('email_link', 'username_link'))) { 1693 if(!isset($info)) { 1694 if($auth) $info = $auth->getUserData($username); 1695 } 1696 if(isset($info) && $info) { 1697 if($conf['showuseras'] == 'email_link') { 1698 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']); 1699 } else { 1700 if(is_null($xhtml_renderer)) { 1701 $xhtml_renderer = p_get_renderer('xhtml'); 1702 } 1703 if(empty($xhtml_renderer->interwiki)) { 1704 $xhtml_renderer->interwiki = getInterwiki(); 1705 } 1706 $shortcut = 'user'; 1707 $exists = null; 1708 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists); 1709 $data['link']['class'] .= ' interwiki iw_user'; 1710 if($exists !== null) { 1711 if($exists) { 1712 $data['link']['class'] .= ' wikilink1'; 1713 } else { 1714 $data['link']['class'] .= ' wikilink2'; 1715 $data['link']['rel'] = 'nofollow'; 1716 } 1717 } 1718 } 1719 } else { 1720 $data['textonly'] = true; 1721 } 1722 1723 } else { 1724 $data['textonly'] = true; 1725 } 1726 } 1727 1728 if($data['textonly']) { 1729 $data['userlink'] = $data['name']; 1730 } else { 1731 $data['link']['name'] = $data['name']; 1732 if(is_null($xhtml_renderer)) { 1733 $xhtml_renderer = p_get_renderer('xhtml'); 1734 } 1735 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']); 1736 } 1737 } 1738 $evt->advise_after(); 1739 unset($evt); 1740 1741 return $data['userlink']; 1742} 1743 1744/** 1745 * Returns the path to a image file for the currently chosen license. 1746 * When no image exists, returns an empty string 1747 * 1748 * @author Andreas Gohr <andi@splitbrain.org> 1749 * 1750 * @param string $type - type of image 'badge' or 'button' 1751 * @return string 1752 */ 1753function license_img($type) { 1754 global $license; 1755 global $conf; 1756 if(!$conf['license']) return ''; 1757 if(!is_array($license[$conf['license']])) return ''; 1758 $try = array(); 1759 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1760 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1761 if(substr($conf['license'], 0, 3) == 'cc-') { 1762 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1763 } 1764 foreach($try as $src) { 1765 if(file_exists(DOKU_INC.$src)) return $src; 1766 } 1767 return ''; 1768} 1769 1770/** 1771 * Checks if the given amount of memory is available 1772 * 1773 * If the memory_get_usage() function is not available the 1774 * function just assumes $bytes of already allocated memory 1775 * 1776 * @author Filip Oscadal <webmaster@illusionsoftworks.cz> 1777 * @author Andreas Gohr <andi@splitbrain.org> 1778 * 1779 * @param int $mem Size of memory you want to allocate in bytes 1780 * @param int $bytes already allocated memory (see above) 1781 * @return bool 1782 */ 1783function is_mem_available($mem, $bytes = 1048576) { 1784 $limit = trim(ini_get('memory_limit')); 1785 if(empty($limit)) return true; // no limit set! 1786 1787 // parse limit to bytes 1788 $limit = php_to_byte($limit); 1789 1790 // get used memory if possible 1791 if(function_exists('memory_get_usage')) { 1792 $used = memory_get_usage(); 1793 } else { 1794 $used = $bytes; 1795 } 1796 1797 if($used + $mem > $limit) { 1798 return false; 1799 } 1800 1801 return true; 1802} 1803 1804/** 1805 * Send a HTTP redirect to the browser 1806 * 1807 * Works arround Microsoft IIS cookie sending bug. Exits the script. 1808 * 1809 * @link http://support.microsoft.com/kb/q176113/ 1810 * @author Andreas Gohr <andi@splitbrain.org> 1811 * 1812 * @param string $url url being directed to 1813 */ 1814function send_redirect($url) { 1815 /* @var Input $INPUT */ 1816 global $INPUT; 1817 1818 //are there any undisplayed messages? keep them in session for display 1819 global $MSG; 1820 if(isset($MSG) && count($MSG) && !defined('NOSESSION')) { 1821 //reopen session, store data and close session again 1822 @session_start(); 1823 $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 1824 } 1825 1826 // always close the session 1827 session_write_close(); 1828 1829 // check if running on IIS < 6 with CGI-PHP 1830 if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') && 1831 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) && 1832 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) && 1833 $matches[1] < 6 1834 ) { 1835 header('Refresh: 0;url='.$url); 1836 } else { 1837 header('Location: '.$url); 1838 } 1839 1840 if(defined('DOKU_UNITTEST')) return; // no exits during unit tests 1841 exit; 1842} 1843 1844/** 1845 * Validate a value using a set of valid values 1846 * 1847 * This function checks whether a specified value is set and in the array 1848 * $valid_values. If not, the function returns a default value or, if no 1849 * default is specified, throws an exception. 1850 * 1851 * @param string $param The name of the parameter 1852 * @param array $valid_values A set of valid values; Optionally a default may 1853 * be marked by the key “default”. 1854 * @param array $array The array containing the value (typically $_POST 1855 * or $_GET) 1856 * @param string $exc The text of the raised exception 1857 * 1858 * @throws Exception 1859 * @return mixed 1860 * @author Adrian Lang <lang@cosmocode.de> 1861 */ 1862function valid_input_set($param, $valid_values, $array, $exc = '') { 1863 if(isset($array[$param]) && in_array($array[$param], $valid_values)) { 1864 return $array[$param]; 1865 } elseif(isset($valid_values['default'])) { 1866 return $valid_values['default']; 1867 } else { 1868 throw new Exception($exc); 1869 } 1870} 1871 1872/** 1873 * Read a preference from the DokuWiki cookie 1874 * (remembering both keys & values are urlencoded) 1875 * 1876 * @param string $pref preference key 1877 * @param mixed $default value returned when preference not found 1878 * @return string preference value 1879 */ 1880function get_doku_pref($pref, $default) { 1881 $enc_pref = urlencode($pref); 1882 if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) { 1883 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1884 $cnt = count($parts); 1885 for($i = 0; $i < $cnt; $i += 2) { 1886 if($parts[$i] == $enc_pref) { 1887 return urldecode($parts[$i + 1]); 1888 } 1889 } 1890 } 1891 return $default; 1892} 1893 1894/** 1895 * Add a preference to the DokuWiki cookie 1896 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded) 1897 * Remove it by setting $val to false 1898 * 1899 * @param string $pref preference key 1900 * @param string $val preference value 1901 */ 1902function set_doku_pref($pref, $val) { 1903 global $conf; 1904 $orig = get_doku_pref($pref, false); 1905 $cookieVal = ''; 1906 1907 if($orig && ($orig != $val)) { 1908 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1909 $cnt = count($parts); 1910 // urlencode $pref for the comparison 1911 $enc_pref = rawurlencode($pref); 1912 for($i = 0; $i < $cnt; $i += 2) { 1913 if($parts[$i] == $enc_pref) { 1914 if ($val !== false) { 1915 $parts[$i + 1] = rawurlencode($val); 1916 } else { 1917 unset($parts[$i]); 1918 unset($parts[$i + 1]); 1919 } 1920 break; 1921 } 1922 } 1923 $cookieVal = implode('#', $parts); 1924 } else if (!$orig && $val !== false) { 1925 $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val); 1926 } 1927 1928 if (!empty($cookieVal)) { 1929 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 1930 setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl())); 1931 } 1932} 1933 1934/** 1935 * Strips source mapping declarations from given text #601 1936 * 1937 * @param string &$text reference to the CSS or JavaScript code to clean 1938 */ 1939function stripsourcemaps(&$text){ 1940 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text); 1941} 1942 1943//Setup VIM: ex: et ts=2 : 1944