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