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