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