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