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