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