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