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 * Checks if the current page version is newer than the last entry in the page's 1188 * changelog. If so, we assume it has been an external edit and we create an 1189 * attic copy and add a proper changelog line. 1190 * 1191 * This check is only executed when the page is about to be saved again from the 1192 * wiki, triggered in @see saveWikiText() 1193 * 1194 * @param string $id the page ID 1195 */ 1196function detectExternalEdit($id) { 1197 global $lang; 1198 1199 $file = wikiFN($id); 1200 $old = @filemtime($file); // from page 1201 $pagelog = new PageChangeLog($id, 1024); 1202 $oldRev = $pagelog->getRevisions(-1, 1); // from changelog 1203 $oldRev = (int) (empty($oldRev) ? 0 : $oldRev[0]); 1204 1205 if(!file_exists(wikiFN($id, $old)) && file_exists($file) && $old >= $oldRev) { 1206 // add old revision to the attic if missing 1207 saveOldRevision($id); 1208 // add a changelog entry if this edit came from outside dokuwiki 1209 if($old > $oldRev) { 1210 addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=> true)); 1211 // remove soon to be stale instructions 1212 $cache = new cache_instructions($id, $file); 1213 $cache->removeCache(); 1214 } 1215 } 1216} 1217 1218/** 1219 * Saves a wikitext by calling io_writeWikiPage. 1220 * Also directs changelog and attic updates. 1221 * 1222 * @author Andreas Gohr <andi@splitbrain.org> 1223 * @author Ben Coburn <btcoburn@silicodon.net> 1224 * 1225 * @param string $id page id 1226 * @param string $text wikitext being saved 1227 * @param string $summary summary of text update 1228 * @param bool $minor mark this saved version as minor update 1229 */ 1230function saveWikiText($id, $text, $summary, $minor = false) { 1231 /* Note to developers: 1232 This code is subtle and delicate. Test the behavior of 1233 the attic and changelog with dokuwiki and external edits 1234 after any changes. External edits change the wiki page 1235 directly without using php or dokuwiki. 1236 */ 1237 global $conf; 1238 global $lang; 1239 global $REV; 1240 /* @var Input $INPUT */ 1241 global $INPUT; 1242 1243 // prepare data for event 1244 $svdta = array(); 1245 $svdta['id'] = $id; 1246 $svdta['file'] = wikiFN($id); 1247 $svdta['revertFrom'] = $REV; 1248 $svdta['oldRevision'] = @filemtime($svdta['file']); 1249 $svdta['newRevision'] = 0; 1250 $svdta['newContent'] = $text; 1251 $svdta['oldContent'] = rawWiki($id); 1252 $svdta['summary'] = $summary; 1253 $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']); 1254 $svdta['changeInfo'] = ''; 1255 $svdta['changeType'] = DOKU_CHANGE_TYPE_EDIT; 1256 1257 // select changelog line type 1258 if($REV) { 1259 $svdta['changeType'] = DOKU_CHANGE_TYPE_REVERT; 1260 $svdta['changeInfo'] = $REV; 1261 } else if(!file_exists($svdta['file'])) { 1262 $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE; 1263 } else if(trim($text) == '') { 1264 // empty or whitespace only content deletes 1265 $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE; 1266 // autoset summary on deletion 1267 if(blank($svdta['summary'])) $svdta['summary'] = $lang['deleted']; 1268 } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) { 1269 //minor edits only for logged in users 1270 $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT; 1271 } 1272 1273 $event = new Doku_Event('COMMON_WIKIPAGE_SAVE', $svdta); 1274 if(!$event->advise_before()) return; 1275 1276 // if the content has not been changed, no save happens (plugins may override this) 1277 if(!$svdta['contentChanged']) return; 1278 1279 detectExternalEdit($id); 1280 if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) { 1281 // Send "update" event with empty data, so plugins can react to page deletion 1282 $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false); 1283 trigger_event('IO_WIKIPAGE_WRITE', $data); 1284 // pre-save deleted revision 1285 @touch($svdta['file']); 1286 clearstatcache(); 1287 $data['newRevision'] = saveOldRevision($id); 1288 // remove empty file 1289 @unlink($svdta['file']); 1290 // don't remove old meta info as it should be saved, plugins can use IO_WIKIPAGE_WRITE for removing their metadata... 1291 // purge non-persistant meta data 1292 p_purge_metadata($id); 1293 // remove empty namespaces 1294 io_sweepNS($id, 'datadir'); 1295 io_sweepNS($id, 'mediadir'); 1296 } else { 1297 // save file (namespace dir is created in io_writeWikiPage) 1298 io_writeWikiPage($svdta['file'], $text, $id); 1299 // pre-save the revision, to keep the attic in sync 1300 $svdta['newRevision'] = saveOldRevision($id); 1301 } 1302 1303 $event->advise_after(); 1304 1305 addLogEntry($svdta['newRevision'], $svdta['id'], $svdta['changeType'], $svdta['summary'], $svdta['changeInfo']); 1306 // send notify mails 1307 notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor); 1308 notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor); 1309 1310 // update the purgefile (timestamp of the last time anything within the wiki was changed) 1311 io_saveFile($conf['cachedir'].'/purgefile', time()); 1312 1313 // if useheading is enabled, purge the cache of all linking pages 1314 if(useHeading('content')) { 1315 $pages = ft_backlinks($id, true); 1316 foreach($pages as $page) { 1317 $cache = new cache_renderer($page, wikiFN($page), 'xhtml'); 1318 $cache->removeCache(); 1319 } 1320 } 1321} 1322 1323/** 1324 * moves the current version to the attic and returns its 1325 * revision date 1326 * 1327 * @author Andreas Gohr <andi@splitbrain.org> 1328 * 1329 * @param string $id page id 1330 * @return int|string revision timestamp 1331 */ 1332function saveOldRevision($id) { 1333 $oldf = wikiFN($id); 1334 if(!file_exists($oldf)) return ''; 1335 $date = filemtime($oldf); 1336 $newf = wikiFN($id, $date); 1337 io_writeWikiPage($newf, rawWiki($id), $id, $date); 1338 return $date; 1339} 1340 1341/** 1342 * Sends a notify mail on page change or registration 1343 * 1344 * @param string $id The changed page 1345 * @param string $who Who to notify (admin|subscribers|register) 1346 * @param int|string $rev Old page revision 1347 * @param string $summary What changed 1348 * @param boolean $minor Is this a minor edit? 1349 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value 1350 * @return bool 1351 * 1352 * @author Andreas Gohr <andi@splitbrain.org> 1353 */ 1354function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) { 1355 global $conf; 1356 /* @var Input $INPUT */ 1357 global $INPUT; 1358 1359 // decide if there is something to do, eg. whom to mail 1360 if($who == 'admin') { 1361 if(empty($conf['notify'])) return false; //notify enabled? 1362 $tpl = 'mailtext'; 1363 $to = $conf['notify']; 1364 } elseif($who == 'subscribers') { 1365 if(!actionOK('subscribe')) return false; //subscribers enabled? 1366 if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors 1367 $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace); 1368 trigger_event( 1369 'COMMON_NOTIFY_ADDRESSLIST', $data, 1370 array(new Subscription(), 'notifyaddresses') 1371 ); 1372 $to = $data['addresslist']; 1373 if(empty($to)) return false; 1374 $tpl = 'subscr_single'; 1375 } else { 1376 return false; //just to be safe 1377 } 1378 1379 // prepare content 1380 $subscription = new Subscription(); 1381 return $subscription->send_diff($to, $tpl, $id, $rev, $summary); 1382} 1383 1384/** 1385 * extracts the query from a search engine referrer 1386 * 1387 * @author Andreas Gohr <andi@splitbrain.org> 1388 * @author Todd Augsburger <todd@rollerorgans.com> 1389 * 1390 * @return array|string 1391 */ 1392function getGoogleQuery() { 1393 /* @var Input $INPUT */ 1394 global $INPUT; 1395 1396 if(!$INPUT->server->has('HTTP_REFERER')) { 1397 return ''; 1398 } 1399 $url = parse_url($INPUT->server->str('HTTP_REFERER')); 1400 1401 // only handle common SEs 1402 if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return ''; 1403 1404 $query = array(); 1405 // temporary workaround against PHP bug #49733 1406 // see http://bugs.php.net/bug.php?id=49733 1407 if(UTF8_MBSTRING) $enc = mb_internal_encoding(); 1408 parse_str($url['query'], $query); 1409 if(UTF8_MBSTRING) mb_internal_encoding($enc); 1410 1411 $q = ''; 1412 if(isset($query['q'])){ 1413 $q = $query['q']; 1414 }elseif(isset($query['p'])){ 1415 $q = $query['p']; 1416 }elseif(isset($query['query'])){ 1417 $q = $query['query']; 1418 } 1419 $q = trim($q); 1420 1421 if(!$q) return ''; 1422 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY); 1423 return $q; 1424} 1425 1426/** 1427 * Return the human readable size of a file 1428 * 1429 * @param int $size A file size 1430 * @param int $dec A number of decimal places 1431 * @return string human readable size 1432 * 1433 * @author Martin Benjamin <b.martin@cybernet.ch> 1434 * @author Aidan Lister <aidan@php.net> 1435 * @version 1.0.0 1436 */ 1437function filesize_h($size, $dec = 1) { 1438 $sizes = array('B', 'KB', 'MB', 'GB'); 1439 $count = count($sizes); 1440 $i = 0; 1441 1442 while($size >= 1024 && ($i < $count - 1)) { 1443 $size /= 1024; 1444 $i++; 1445 } 1446 1447 return round($size, $dec).' '.$sizes[$i]; 1448} 1449 1450/** 1451 * Return the given timestamp as human readable, fuzzy age 1452 * 1453 * @author Andreas Gohr <gohr@cosmocode.de> 1454 * 1455 * @param int $dt timestamp 1456 * @return string 1457 */ 1458function datetime_h($dt) { 1459 global $lang; 1460 1461 $ago = time() - $dt; 1462 if($ago > 24 * 60 * 60 * 30 * 12 * 2) { 1463 return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12))); 1464 } 1465 if($ago > 24 * 60 * 60 * 30 * 2) { 1466 return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30))); 1467 } 1468 if($ago > 24 * 60 * 60 * 7 * 2) { 1469 return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7))); 1470 } 1471 if($ago > 24 * 60 * 60 * 2) { 1472 return sprintf($lang['days'], round($ago / (24 * 60 * 60))); 1473 } 1474 if($ago > 60 * 60 * 2) { 1475 return sprintf($lang['hours'], round($ago / (60 * 60))); 1476 } 1477 if($ago > 60 * 2) { 1478 return sprintf($lang['minutes'], round($ago / (60))); 1479 } 1480 return sprintf($lang['seconds'], $ago); 1481} 1482 1483/** 1484 * Wraps around strftime but provides support for fuzzy dates 1485 * 1486 * The format default to $conf['dformat']. It is passed to 1487 * strftime - %f can be used to get the value from datetime_h() 1488 * 1489 * @see datetime_h 1490 * @author Andreas Gohr <gohr@cosmocode.de> 1491 * 1492 * @param int|null $dt timestamp when given, null will take current timestamp 1493 * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime() 1494 * @return string 1495 */ 1496function dformat($dt = null, $format = '') { 1497 global $conf; 1498 1499 if(is_null($dt)) $dt = time(); 1500 $dt = (int) $dt; 1501 if(!$format) $format = $conf['dformat']; 1502 1503 $format = str_replace('%f', datetime_h($dt), $format); 1504 return strftime($format, $dt); 1505} 1506 1507/** 1508 * Formats a timestamp as ISO 8601 date 1509 * 1510 * @author <ungu at terong dot com> 1511 * @link http://www.php.net/manual/en/function.date.php#54072 1512 * 1513 * @param int $int_date current date in UNIX timestamp 1514 * @return string 1515 */ 1516function date_iso8601($int_date) { 1517 $date_mod = date('Y-m-d\TH:i:s', $int_date); 1518 $pre_timezone = date('O', $int_date); 1519 $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2); 1520 $date_mod .= $time_zone; 1521 return $date_mod; 1522} 1523 1524/** 1525 * return an obfuscated email address in line with $conf['mailguard'] setting 1526 * 1527 * @author Harry Fuecks <hfuecks@gmail.com> 1528 * @author Christopher Smith <chris@jalakai.co.uk> 1529 * 1530 * @param string $email email address 1531 * @return string 1532 */ 1533function obfuscate($email) { 1534 global $conf; 1535 1536 switch($conf['mailguard']) { 1537 case 'visible' : 1538 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1539 return strtr($email, $obfuscate); 1540 1541 case 'hex' : 1542 $encode = ''; 1543 $len = strlen($email); 1544 for($x = 0; $x < $len; $x++) { 1545 $encode .= '&#x'.bin2hex($email{$x}).';'; 1546 } 1547 return $encode; 1548 1549 case 'none' : 1550 default : 1551 return $email; 1552 } 1553} 1554 1555/** 1556 * Removes quoting backslashes 1557 * 1558 * @author Andreas Gohr <andi@splitbrain.org> 1559 * 1560 * @param string $string 1561 * @param string $char backslashed character 1562 * @return string 1563 */ 1564function unslash($string, $char = "'") { 1565 return str_replace('\\'.$char, $char, $string); 1566} 1567 1568/** 1569 * Convert php.ini shorthands to byte 1570 * 1571 * @author <gilthans dot NO dot SPAM at gmail dot com> 1572 * @link http://de3.php.net/manual/en/ini.core.php#79564 1573 * 1574 * @param string $v shorthands 1575 * @return int|string 1576 */ 1577function php_to_byte($v) { 1578 $l = substr($v, -1); 1579 $ret = substr($v, 0, -1); 1580 switch(strtoupper($l)) { 1581 /** @noinspection PhpMissingBreakStatementInspection */ 1582 case 'P': 1583 $ret *= 1024; 1584 /** @noinspection PhpMissingBreakStatementInspection */ 1585 case 'T': 1586 $ret *= 1024; 1587 /** @noinspection PhpMissingBreakStatementInspection */ 1588 case 'G': 1589 $ret *= 1024; 1590 /** @noinspection PhpMissingBreakStatementInspection */ 1591 case 'M': 1592 $ret *= 1024; 1593 /** @noinspection PhpMissingBreakStatementInspection */ 1594 case 'K': 1595 $ret *= 1024; 1596 break; 1597 default; 1598 $ret *= 10; 1599 break; 1600 } 1601 return $ret; 1602} 1603 1604/** 1605 * Wrapper around preg_quote adding the default delimiter 1606 * 1607 * @param string $string 1608 * @return string 1609 */ 1610function preg_quote_cb($string) { 1611 return preg_quote($string, '/'); 1612} 1613 1614/** 1615 * Shorten a given string by removing data from the middle 1616 * 1617 * You can give the string in two parts, the first part $keep 1618 * will never be shortened. The second part $short will be cut 1619 * in the middle to shorten but only if at least $min chars are 1620 * left to display it. Otherwise it will be left off. 1621 * 1622 * @param string $keep the part to keep 1623 * @param string $short the part to shorten 1624 * @param int $max maximum chars you want for the whole string 1625 * @param int $min minimum number of chars to have left for middle shortening 1626 * @param string $char the shortening character to use 1627 * @return string 1628 */ 1629function shorten($keep, $short, $max, $min = 9, $char = '…') { 1630 $max = $max - utf8_strlen($keep); 1631 if($max < $min) return $keep; 1632 $len = utf8_strlen($short); 1633 if($len <= $max) return $keep.$short; 1634 $half = floor($max / 2); 1635 return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half); 1636} 1637 1638/** 1639 * Return the users real name or e-mail address for use 1640 * in page footer and recent changes pages 1641 * 1642 * @param string|null $username or null when currently logged-in user should be used 1643 * @param bool $textonly true returns only plain text, true allows returning html 1644 * @return string html or plain text(not escaped) of formatted user name 1645 * 1646 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1647 */ 1648function editorinfo($username, $textonly = false) { 1649 return userlink($username, $textonly); 1650} 1651 1652/** 1653 * Returns users realname w/o link 1654 * 1655 * @param string|null $username or null when currently logged-in user should be used 1656 * @param bool $textonly true returns only plain text, true allows returning html 1657 * @return string html or plain text(not escaped) of formatted user name 1658 * 1659 * @triggers COMMON_USER_LINK 1660 */ 1661function userlink($username = null, $textonly = false) { 1662 global $conf, $INFO; 1663 /** @var DokuWiki_Auth_Plugin $auth */ 1664 global $auth; 1665 /** @var Input $INPUT */ 1666 global $INPUT; 1667 1668 // prepare initial event data 1669 $data = array( 1670 'username' => $username, // the unique user name 1671 'name' => '', 1672 'link' => array( //setting 'link' to false disables linking 1673 'target' => '', 1674 'pre' => '', 1675 'suf' => '', 1676 'style' => '', 1677 'more' => '', 1678 'url' => '', 1679 'title' => '', 1680 'class' => '' 1681 ), 1682 'userlink' => '', // formatted user name as will be returned 1683 'textonly' => $textonly 1684 ); 1685 if($username === null) { 1686 $data['username'] = $username = $INPUT->server->str('REMOTE_USER'); 1687 if($textonly){ 1688 $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')'; 1689 }else { 1690 $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> (<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)'; 1691 } 1692 } 1693 1694 $evt = new Doku_Event('COMMON_USER_LINK', $data); 1695 if($evt->advise_before(true)) { 1696 if(empty($data['name'])) { 1697 if($auth) $info = $auth->getUserData($username); 1698 if($conf['showuseras'] != 'loginname' && isset($info) && $info) { 1699 switch($conf['showuseras']) { 1700 case 'username': 1701 case 'username_link': 1702 $data['name'] = $textonly ? $info['name'] : hsc($info['name']); 1703 break; 1704 case 'email': 1705 case 'email_link': 1706 $data['name'] = obfuscate($info['mail']); 1707 break; 1708 } 1709 } else { 1710 $data['name'] = $textonly ? $data['username'] : hsc($data['username']); 1711 } 1712 } 1713 1714 /** @var Doku_Renderer_xhtml $xhtml_renderer */ 1715 static $xhtml_renderer = null; 1716 1717 if(!$data['textonly'] && empty($data['link']['url'])) { 1718 1719 if(in_array($conf['showuseras'], array('email_link', 'username_link'))) { 1720 if(!isset($info)) { 1721 if($auth) $info = $auth->getUserData($username); 1722 } 1723 if(isset($info) && $info) { 1724 if($conf['showuseras'] == 'email_link') { 1725 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']); 1726 } else { 1727 if(is_null($xhtml_renderer)) { 1728 $xhtml_renderer = p_get_renderer('xhtml'); 1729 } 1730 if(empty($xhtml_renderer->interwiki)) { 1731 $xhtml_renderer->interwiki = getInterwiki(); 1732 } 1733 $shortcut = 'user'; 1734 $exists = null; 1735 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists); 1736 $data['link']['class'] .= ' interwiki iw_user'; 1737 if($exists !== null) { 1738 if($exists) { 1739 $data['link']['class'] .= ' wikilink1'; 1740 } else { 1741 $data['link']['class'] .= ' wikilink2'; 1742 $data['link']['rel'] = 'nofollow'; 1743 } 1744 } 1745 } 1746 } else { 1747 $data['textonly'] = true; 1748 } 1749 1750 } else { 1751 $data['textonly'] = true; 1752 } 1753 } 1754 1755 if($data['textonly']) { 1756 $data['userlink'] = $data['name']; 1757 } else { 1758 $data['link']['name'] = $data['name']; 1759 if(is_null($xhtml_renderer)) { 1760 $xhtml_renderer = p_get_renderer('xhtml'); 1761 } 1762 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']); 1763 } 1764 } 1765 $evt->advise_after(); 1766 unset($evt); 1767 1768 return $data['userlink']; 1769} 1770 1771/** 1772 * Returns the path to a image file for the currently chosen license. 1773 * When no image exists, returns an empty string 1774 * 1775 * @author Andreas Gohr <andi@splitbrain.org> 1776 * 1777 * @param string $type - type of image 'badge' or 'button' 1778 * @return string 1779 */ 1780function license_img($type) { 1781 global $license; 1782 global $conf; 1783 if(!$conf['license']) return ''; 1784 if(!is_array($license[$conf['license']])) return ''; 1785 $try = array(); 1786 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1787 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1788 if(substr($conf['license'], 0, 3) == 'cc-') { 1789 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1790 } 1791 foreach($try as $src) { 1792 if(file_exists(DOKU_INC.$src)) return $src; 1793 } 1794 return ''; 1795} 1796 1797/** 1798 * Checks if the given amount of memory is available 1799 * 1800 * If the memory_get_usage() function is not available the 1801 * function just assumes $bytes of already allocated memory 1802 * 1803 * @author Filip Oscadal <webmaster@illusionsoftworks.cz> 1804 * @author Andreas Gohr <andi@splitbrain.org> 1805 * 1806 * @param int $mem Size of memory you want to allocate in bytes 1807 * @param int $bytes already allocated memory (see above) 1808 * @return bool 1809 */ 1810function is_mem_available($mem, $bytes = 1048576) { 1811 $limit = trim(ini_get('memory_limit')); 1812 if(empty($limit)) return true; // no limit set! 1813 1814 // parse limit to bytes 1815 $limit = php_to_byte($limit); 1816 1817 // get used memory if possible 1818 if(function_exists('memory_get_usage')) { 1819 $used = memory_get_usage(); 1820 } else { 1821 $used = $bytes; 1822 } 1823 1824 if($used + $mem > $limit) { 1825 return false; 1826 } 1827 1828 return true; 1829} 1830 1831/** 1832 * Send a HTTP redirect to the browser 1833 * 1834 * Works arround Microsoft IIS cookie sending bug. Exits the script. 1835 * 1836 * @link http://support.microsoft.com/kb/q176113/ 1837 * @author Andreas Gohr <andi@splitbrain.org> 1838 * 1839 * @param string $url url being directed to 1840 */ 1841function send_redirect($url) { 1842 /* @var Input $INPUT */ 1843 global $INPUT; 1844 1845 //are there any undisplayed messages? keep them in session for display 1846 global $MSG; 1847 if(isset($MSG) && count($MSG) && !defined('NOSESSION')) { 1848 //reopen session, store data and close session again 1849 @session_start(); 1850 $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 1851 } 1852 1853 // always close the session 1854 session_write_close(); 1855 1856 // check if running on IIS < 6 with CGI-PHP 1857 if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') && 1858 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) && 1859 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) && 1860 $matches[1] < 6 1861 ) { 1862 header('Refresh: 0;url='.$url); 1863 } else { 1864 header('Location: '.$url); 1865 } 1866 1867 if(defined('DOKU_UNITTEST')) return; // no exits during unit tests 1868 exit; 1869} 1870 1871/** 1872 * Validate a value using a set of valid values 1873 * 1874 * This function checks whether a specified value is set and in the array 1875 * $valid_values. If not, the function returns a default value or, if no 1876 * default is specified, throws an exception. 1877 * 1878 * @param string $param The name of the parameter 1879 * @param array $valid_values A set of valid values; Optionally a default may 1880 * be marked by the key “default”. 1881 * @param array $array The array containing the value (typically $_POST 1882 * or $_GET) 1883 * @param string $exc The text of the raised exception 1884 * 1885 * @throws Exception 1886 * @return mixed 1887 * @author Adrian Lang <lang@cosmocode.de> 1888 */ 1889function valid_input_set($param, $valid_values, $array, $exc = '') { 1890 if(isset($array[$param]) && in_array($array[$param], $valid_values)) { 1891 return $array[$param]; 1892 } elseif(isset($valid_values['default'])) { 1893 return $valid_values['default']; 1894 } else { 1895 throw new Exception($exc); 1896 } 1897} 1898 1899/** 1900 * Read a preference from the DokuWiki cookie 1901 * (remembering both keys & values are urlencoded) 1902 * 1903 * @param string $pref preference key 1904 * @param mixed $default value returned when preference not found 1905 * @return string preference value 1906 */ 1907function get_doku_pref($pref, $default) { 1908 $enc_pref = urlencode($pref); 1909 if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) { 1910 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1911 $cnt = count($parts); 1912 for($i = 0; $i < $cnt; $i += 2) { 1913 if($parts[$i] == $enc_pref) { 1914 return urldecode($parts[$i + 1]); 1915 } 1916 } 1917 } 1918 return $default; 1919} 1920 1921/** 1922 * Add a preference to the DokuWiki cookie 1923 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded) 1924 * Remove it by setting $val to false 1925 * 1926 * @param string $pref preference key 1927 * @param string $val preference value 1928 */ 1929function set_doku_pref($pref, $val) { 1930 global $conf; 1931 $orig = get_doku_pref($pref, false); 1932 $cookieVal = ''; 1933 1934 if($orig && ($orig != $val)) { 1935 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1936 $cnt = count($parts); 1937 // urlencode $pref for the comparison 1938 $enc_pref = rawurlencode($pref); 1939 for($i = 0; $i < $cnt; $i += 2) { 1940 if($parts[$i] == $enc_pref) { 1941 if ($val !== false) { 1942 $parts[$i + 1] = rawurlencode($val); 1943 } else { 1944 unset($parts[$i]); 1945 unset($parts[$i + 1]); 1946 } 1947 break; 1948 } 1949 } 1950 $cookieVal = implode('#', $parts); 1951 } else if (!$orig && $val !== false) { 1952 $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val); 1953 } 1954 1955 if (!empty($cookieVal)) { 1956 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 1957 setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl())); 1958 } 1959} 1960 1961/** 1962 * Strips source mapping declarations from given text #601 1963 * 1964 * @param string &$text reference to the CSS or JavaScript code to clean 1965 */ 1966function stripsourcemaps(&$text){ 1967 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text); 1968} 1969 1970//Setup VIM: ex: et ts=2 : 1971