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 visible wiki documents 386 $file = wikiFN($ID); 387 if($ACT != 'show' || isHiddenPage($ID) || !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 = function () { 734 return true; 735 }; 736 return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true); 737 } 738 } 739 return false; 740} 741 742/** 743 * Return the IP of the client 744 * 745 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 746 * 747 * It returns a comma separated list of IPs if the above mentioned 748 * headers are set. If the single parameter is set, it tries to return 749 * a routable public address, prefering the ones suplied in the X 750 * headers 751 * 752 * @author Andreas Gohr <andi@splitbrain.org> 753 * 754 * @param boolean $single If set only a single IP is returned 755 * @return string 756 */ 757function clientIP($single = false) { 758 /* @var Input $INPUT */ 759 global $INPUT; 760 761 $ip = array(); 762 $ip[] = $INPUT->server->str('REMOTE_ADDR'); 763 if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) { 764 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR')))); 765 } 766 if($INPUT->server->str('HTTP_X_REAL_IP')) { 767 $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP')))); 768 } 769 770 // some IPv4/v6 regexps borrowed from Feyd 771 // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479 772 $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])'; 773 $hex_digit = '[A-Fa-f0-9]'; 774 $h16 = "{$hex_digit}{1,4}"; 775 $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet"; 776 $ls32 = "(?:$h16:$h16|$IPv4Address)"; 777 $IPv6Address = 778 "(?:(?:{$IPv4Address})|(?:". 779 "(?:$h16:){6}$ls32". 780 "|::(?:$h16:){5}$ls32". 781 "|(?:$h16)?::(?:$h16:){4}$ls32". 782 "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32". 783 "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32". 784 "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32". 785 "|(?:(?:$h16:){0,4}$h16)?::$ls32". 786 "|(?:(?:$h16:){0,5}$h16)?::$h16". 787 "|(?:(?:$h16:){0,6}$h16)?::". 788 ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)"; 789 790 // remove any non-IP stuff 791 $cnt = count($ip); 792 $match = array(); 793 for($i = 0; $i < $cnt; $i++) { 794 if(preg_match("/^$IPv4Address$/", $ip[$i], $match) || preg_match("/^$IPv6Address$/", $ip[$i], $match)) { 795 $ip[$i] = $match[0]; 796 } else { 797 $ip[$i] = ''; 798 } 799 if(empty($ip[$i])) unset($ip[$i]); 800 } 801 $ip = array_values(array_unique($ip)); 802 if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 803 804 if(!$single) return join(',', $ip); 805 806 // decide which IP to use, trying to avoid local addresses 807 $ip = array_reverse($ip); 808 foreach($ip as $i) { 809 if(preg_match('/^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/', $i)) { 810 continue; 811 } else { 812 return $i; 813 } 814 } 815 // still here? just use the first (last) address 816 return $ip[0]; 817} 818 819/** 820 * Check if the browser is on a mobile device 821 * 822 * Adapted from the example code at url below 823 * 824 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code 825 * 826 * @return bool if true, client is mobile browser; otherwise false 827 */ 828function clientismobile() { 829 /* @var Input $INPUT */ 830 global $INPUT; 831 832 if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true; 833 834 if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true; 835 836 if(!$INPUT->server->has('HTTP_USER_AGENT')) return false; 837 838 $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'; 839 840 if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true; 841 842 return false; 843} 844 845/** 846 * check if a given link is interwiki link 847 * 848 * @param string $link the link, e.g. "wiki>page" 849 * @return bool 850 */ 851function link_isinterwiki($link){ 852 if (preg_match('/^[a-zA-Z0-9\.]+>/u',$link)) return true; 853 return false; 854} 855 856/** 857 * Convert one or more comma separated IPs to hostnames 858 * 859 * If $conf['dnslookups'] is disabled it simply returns the input string 860 * 861 * @author Glen Harris <astfgl@iamnota.org> 862 * 863 * @param string $ips comma separated list of IP addresses 864 * @return string a comma separated list of hostnames 865 */ 866function gethostsbyaddrs($ips) { 867 global $conf; 868 if(!$conf['dnslookups']) return $ips; 869 870 $hosts = array(); 871 $ips = explode(',', $ips); 872 873 if(is_array($ips)) { 874 foreach($ips as $ip) { 875 $hosts[] = gethostbyaddr(trim($ip)); 876 } 877 return join(',', $hosts); 878 } else { 879 return gethostbyaddr(trim($ips)); 880 } 881} 882 883/** 884 * Checks if a given page is currently locked. 885 * 886 * removes stale lockfiles 887 * 888 * @author Andreas Gohr <andi@splitbrain.org> 889 * 890 * @param string $id page id 891 * @return bool page is locked? 892 */ 893function checklock($id) { 894 global $conf; 895 /* @var Input $INPUT */ 896 global $INPUT; 897 898 $lock = wikiLockFN($id); 899 900 //no lockfile 901 if(!file_exists($lock)) return false; 902 903 //lockfile expired 904 if((time() - filemtime($lock)) > $conf['locktime']) { 905 @unlink($lock); 906 return false; 907 } 908 909 //my own lock 910 @list($ip, $session) = explode("\n", io_readFile($lock)); 911 if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) { 912 return false; 913 } 914 915 return $ip; 916} 917 918/** 919 * Lock a page for editing 920 * 921 * @author Andreas Gohr <andi@splitbrain.org> 922 * 923 * @param string $id page id to lock 924 */ 925function lock($id) { 926 global $conf; 927 /* @var Input $INPUT */ 928 global $INPUT; 929 930 if($conf['locktime'] == 0) { 931 return; 932 } 933 934 $lock = wikiLockFN($id); 935 if($INPUT->server->str('REMOTE_USER')) { 936 io_saveFile($lock, $INPUT->server->str('REMOTE_USER')); 937 } else { 938 io_saveFile($lock, clientIP()."\n".session_id()); 939 } 940} 941 942/** 943 * Unlock a page if it was locked by the user 944 * 945 * @author Andreas Gohr <andi@splitbrain.org> 946 * 947 * @param string $id page id to unlock 948 * @return bool true if a lock was removed 949 */ 950function unlock($id) { 951 /* @var Input $INPUT */ 952 global $INPUT; 953 954 $lock = wikiLockFN($id); 955 if(file_exists($lock)) { 956 @list($ip, $session) = explode("\n", io_readFile($lock)); 957 if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) { 958 @unlink($lock); 959 return true; 960 } 961 } 962 return false; 963} 964 965/** 966 * convert line ending to unix format 967 * 968 * also makes sure the given text is valid UTF-8 969 * 970 * @see formText() for 2crlf conversion 971 * @author Andreas Gohr <andi@splitbrain.org> 972 * 973 * @param string $text 974 * @return string 975 */ 976function cleanText($text) { 977 $text = preg_replace("/(\015\012)|(\015)/", "\012", $text); 978 979 // if the text is not valid UTF-8 we simply assume latin1 980 // this won't break any worse than it breaks with the wrong encoding 981 // but might actually fix the problem in many cases 982 if(!utf8_check($text)) $text = utf8_encode($text); 983 984 return $text; 985} 986 987/** 988 * Prepares text for print in Webforms by encoding special chars. 989 * It also converts line endings to Windows format which is 990 * pseudo standard for webforms. 991 * 992 * @see cleanText() for 2unix conversion 993 * @author Andreas Gohr <andi@splitbrain.org> 994 * 995 * @param string $text 996 * @return string 997 */ 998function formText($text) { 999 $text = str_replace("\012", "\015\012", $text); 1000 return htmlspecialchars($text); 1001} 1002 1003/** 1004 * Returns the specified local text in raw format 1005 * 1006 * @author Andreas Gohr <andi@splitbrain.org> 1007 * 1008 * @param string $id page id 1009 * @param string $ext extension of file being read, default 'txt' 1010 * @return string 1011 */ 1012function rawLocale($id, $ext = 'txt') { 1013 return io_readFile(localeFN($id, $ext)); 1014} 1015 1016/** 1017 * Returns the raw WikiText 1018 * 1019 * @author Andreas Gohr <andi@splitbrain.org> 1020 * 1021 * @param string $id page id 1022 * @param string|int $rev timestamp when a revision of wikitext is desired 1023 * @return string 1024 */ 1025function rawWiki($id, $rev = '') { 1026 return io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1027} 1028 1029/** 1030 * Returns the pagetemplate contents for the ID's namespace 1031 * 1032 * @triggers COMMON_PAGETPL_LOAD 1033 * @author Andreas Gohr <andi@splitbrain.org> 1034 * 1035 * @param string $id the id of the page to be created 1036 * @return string parsed pagetemplate content 1037 */ 1038function pageTemplate($id) { 1039 global $conf; 1040 1041 if(is_array($id)) $id = $id[0]; 1042 1043 // prepare initial event data 1044 $data = array( 1045 'id' => $id, // the id of the page to be created 1046 'tpl' => '', // the text used as template 1047 'tplfile' => '', // the file above text was/should be loaded from 1048 'doreplace' => true // should wildcard replacements be done on the text? 1049 ); 1050 1051 $evt = new Doku_Event('COMMON_PAGETPL_LOAD', $data); 1052 if($evt->advise_before(true)) { 1053 // the before event might have loaded the content already 1054 if(empty($data['tpl'])) { 1055 // if the before event did not set a template file, try to find one 1056 if(empty($data['tplfile'])) { 1057 $path = dirname(wikiFN($id)); 1058 if(file_exists($path.'/_template.txt')) { 1059 $data['tplfile'] = $path.'/_template.txt'; 1060 } else { 1061 // search upper namespaces for templates 1062 $len = strlen(rtrim($conf['datadir'], '/')); 1063 while(strlen($path) >= $len) { 1064 if(file_exists($path.'/__template.txt')) { 1065 $data['tplfile'] = $path.'/__template.txt'; 1066 break; 1067 } 1068 $path = substr($path, 0, strrpos($path, '/')); 1069 } 1070 } 1071 } 1072 // load the content 1073 $data['tpl'] = io_readFile($data['tplfile']); 1074 } 1075 if($data['doreplace']) parsePageTemplate($data); 1076 } 1077 $evt->advise_after(); 1078 unset($evt); 1079 1080 return $data['tpl']; 1081} 1082 1083/** 1084 * Performs common page template replacements 1085 * This works on data from COMMON_PAGETPL_LOAD 1086 * 1087 * @author Andreas Gohr <andi@splitbrain.org> 1088 * 1089 * @param array $data array with event data 1090 * @return string 1091 */ 1092function parsePageTemplate(&$data) { 1093 /** 1094 * @var string $id the id of the page to be created 1095 * @var string $tpl the text used as template 1096 * @var string $tplfile the file above text was/should be loaded from 1097 * @var bool $doreplace should wildcard replacements be done on the text? 1098 */ 1099 extract($data); 1100 1101 global $USERINFO; 1102 global $conf; 1103 /* @var Input $INPUT */ 1104 global $INPUT; 1105 1106 // replace placeholders 1107 $file = noNS($id); 1108 $page = strtr($file, $conf['sepchar'], ' '); 1109 1110 $tpl = str_replace( 1111 array( 1112 '@ID@', 1113 '@NS@', 1114 '@CURNS@', 1115 '@FILE@', 1116 '@!FILE@', 1117 '@!FILE!@', 1118 '@PAGE@', 1119 '@!PAGE@', 1120 '@!!PAGE@', 1121 '@!PAGE!@', 1122 '@USER@', 1123 '@NAME@', 1124 '@MAIL@', 1125 '@DATE@', 1126 ), 1127 array( 1128 $id, 1129 getNS($id), 1130 curNS($id), 1131 $file, 1132 utf8_ucfirst($file), 1133 utf8_strtoupper($file), 1134 $page, 1135 utf8_ucfirst($page), 1136 utf8_ucwords($page), 1137 utf8_strtoupper($page), 1138 $INPUT->server->str('REMOTE_USER'), 1139 $USERINFO['name'], 1140 $USERINFO['mail'], 1141 $conf['dformat'], 1142 ), $tpl 1143 ); 1144 1145 // we need the callback to work around strftime's char limit 1146 $tpl = preg_replace_callback( 1147 '/%./', 1148 function ($m) { 1149 return strftime($m[0]); 1150 }, 1151 $tpl 1152 ); 1153 $data['tpl'] = $tpl; 1154 return $tpl; 1155} 1156 1157/** 1158 * Returns the raw Wiki Text in three slices. 1159 * 1160 * The range parameter needs to have the form "from-to" 1161 * and gives the range of the section in bytes - no 1162 * UTF-8 awareness is needed. 1163 * The returned order is prefix, section and suffix. 1164 * 1165 * @author Andreas Gohr <andi@splitbrain.org> 1166 * 1167 * @param string $range in form "from-to" 1168 * @param string $id page id 1169 * @param string $rev optional, the revision timestamp 1170 * @return string[] with three slices 1171 */ 1172function rawWikiSlices($range, $id, $rev = '') { 1173 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev); 1174 1175 // Parse range 1176 list($from, $to) = explode('-', $range, 2); 1177 // Make range zero-based, use defaults if marker is missing 1178 $from = !$from ? 0 : ($from - 1); 1179 $to = !$to ? strlen($text) : ($to - 1); 1180 1181 $slices = array(); 1182 $slices[0] = substr($text, 0, $from); 1183 $slices[1] = substr($text, $from, $to - $from); 1184 $slices[2] = substr($text, $to); 1185 return $slices; 1186} 1187 1188/** 1189 * Joins wiki text slices 1190 * 1191 * function to join the text slices. 1192 * When the pretty parameter is set to true it adds additional empty 1193 * lines between sections if needed (used on saving). 1194 * 1195 * @author Andreas Gohr <andi@splitbrain.org> 1196 * 1197 * @param string $pre prefix 1198 * @param string $text text in the middle 1199 * @param string $suf suffix 1200 * @param bool $pretty add additional empty lines between sections 1201 * @return string 1202 */ 1203function con($pre, $text, $suf, $pretty = false) { 1204 if($pretty) { 1205 if($pre !== '' && substr($pre, -1) !== "\n" && 1206 substr($text, 0, 1) !== "\n" 1207 ) { 1208 $pre .= "\n"; 1209 } 1210 if($suf !== '' && substr($text, -1) !== "\n" && 1211 substr($suf, 0, 1) !== "\n" 1212 ) { 1213 $text .= "\n"; 1214 } 1215 } 1216 1217 return $pre.$text.$suf; 1218} 1219 1220/** 1221 * Checks if the current page version is newer than the last entry in the page's 1222 * changelog. If so, we assume it has been an external edit and we create an 1223 * attic copy and add a proper changelog line. 1224 * 1225 * This check is only executed when the page is about to be saved again from the 1226 * wiki, triggered in @see saveWikiText() 1227 * 1228 * @param string $id the page ID 1229 */ 1230function detectExternalEdit($id) { 1231 global $lang; 1232 1233 $fileLastMod = wikiFN($id); 1234 $lastMod = @filemtime($fileLastMod); // from page 1235 $pagelog = new PageChangeLog($id, 1024); 1236 $lastRev = $pagelog->getRevisions(-1, 1); // from changelog 1237 $lastRev = (int) (empty($lastRev) ? 0 : $lastRev[0]); 1238 1239 if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) { 1240 // add old revision to the attic if missing 1241 saveOldRevision($id); 1242 // add a changelog entry if this edit came from outside dokuwiki 1243 if($lastMod > $lastRev) { 1244 $fileLastRev = wikiFN($id, $lastRev); 1245 $revinfo = $pagelog->getRevisionInfo($lastRev); 1246 if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) { 1247 $filesize_old = 0; 1248 } else { 1249 $filesize_old = io_getSizeFile($fileLastRev); 1250 } 1251 $filesize_new = filesize($fileLastMod); 1252 $sizechange = $filesize_new - $filesize_old; 1253 1254 addLogEntry($lastMod, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=> true), $sizechange); 1255 // remove soon to be stale instructions 1256 $cache = new cache_instructions($id, $fileLastMod); 1257 $cache->removeCache(); 1258 } 1259 } 1260} 1261 1262/** 1263 * Saves a wikitext by calling io_writeWikiPage. 1264 * Also directs changelog and attic updates. 1265 * 1266 * @author Andreas Gohr <andi@splitbrain.org> 1267 * @author Ben Coburn <btcoburn@silicodon.net> 1268 * 1269 * @param string $id page id 1270 * @param string $text wikitext being saved 1271 * @param string $summary summary of text update 1272 * @param bool $minor mark this saved version as minor update 1273 */ 1274function saveWikiText($id, $text, $summary, $minor = false) { 1275 /* Note to developers: 1276 This code is subtle and delicate. Test the behavior of 1277 the attic and changelog with dokuwiki and external edits 1278 after any changes. External edits change the wiki page 1279 directly without using php or dokuwiki. 1280 */ 1281 global $conf; 1282 global $lang; 1283 global $REV; 1284 /* @var Input $INPUT */ 1285 global $INPUT; 1286 1287 // prepare data for event 1288 $svdta = array(); 1289 $svdta['id'] = $id; 1290 $svdta['file'] = wikiFN($id); 1291 $svdta['revertFrom'] = $REV; 1292 $svdta['oldRevision'] = @filemtime($svdta['file']); 1293 $svdta['newRevision'] = 0; 1294 $svdta['newContent'] = $text; 1295 $svdta['oldContent'] = rawWiki($id); 1296 $svdta['summary'] = $summary; 1297 $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']); 1298 $svdta['changeInfo'] = ''; 1299 $svdta['changeType'] = DOKU_CHANGE_TYPE_EDIT; 1300 $svdta['sizechange'] = null; 1301 1302 // select changelog line type 1303 if($REV) { 1304 $svdta['changeType'] = DOKU_CHANGE_TYPE_REVERT; 1305 $svdta['changeInfo'] = $REV; 1306 } else if(!file_exists($svdta['file'])) { 1307 $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE; 1308 } else if(trim($text) == '') { 1309 // empty or whitespace only content deletes 1310 $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE; 1311 // autoset summary on deletion 1312 if(blank($svdta['summary'])) { 1313 $svdta['summary'] = $lang['deleted']; 1314 } 1315 } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) { 1316 //minor edits only for logged in users 1317 $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT; 1318 } 1319 1320 $event = new Doku_Event('COMMON_WIKIPAGE_SAVE', $svdta); 1321 if(!$event->advise_before()) return; 1322 1323 // if the content has not been changed, no save happens (plugins may override this) 1324 if(!$svdta['contentChanged']) return; 1325 1326 detectExternalEdit($id); 1327 1328 if( 1329 $svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE || 1330 ($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file'])) 1331 ) { 1332 $filesize_old = 0; 1333 } else { 1334 $filesize_old = filesize($svdta['file']); 1335 } 1336 if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) { 1337 // Send "update" event with empty data, so plugins can react to page deletion 1338 $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false); 1339 trigger_event('IO_WIKIPAGE_WRITE', $data); 1340 // pre-save deleted revision 1341 @touch($svdta['file']); 1342 clearstatcache(); 1343 $svdta['newRevision'] = saveOldRevision($id); 1344 // remove empty file 1345 @unlink($svdta['file']); 1346 $filesize_new = 0; 1347 // don't remove old meta info as it should be saved, plugins can use IO_WIKIPAGE_WRITE for removing their metadata... 1348 // purge non-persistant meta data 1349 p_purge_metadata($id); 1350 // remove empty namespaces 1351 io_sweepNS($id, 'datadir'); 1352 io_sweepNS($id, 'mediadir'); 1353 } else { 1354 // save file (namespace dir is created in io_writeWikiPage) 1355 io_writeWikiPage($svdta['file'], $svdta['newContent'], $id); 1356 // pre-save the revision, to keep the attic in sync 1357 $svdta['newRevision'] = saveOldRevision($id); 1358 $filesize_new = filesize($svdta['file']); 1359 } 1360 $svdta['sizechange'] = $filesize_new - $filesize_old; 1361 1362 $event->advise_after(); 1363 1364 addLogEntry($svdta['newRevision'], $svdta['id'], $svdta['changeType'], $svdta['summary'], $svdta['changeInfo'], null, $svdta['sizechange']); 1365 1366 // send notify mails 1367 notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor); 1368 notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor); 1369 1370 // update the purgefile (timestamp of the last time anything within the wiki was changed) 1371 io_saveFile($conf['cachedir'].'/purgefile', time()); 1372 1373 // if useheading is enabled, purge the cache of all linking pages 1374 if(useHeading('content')) { 1375 $pages = ft_backlinks($id, true); 1376 foreach($pages as $page) { 1377 $cache = new cache_renderer($page, wikiFN($page), 'xhtml'); 1378 $cache->removeCache(); 1379 } 1380 } 1381} 1382 1383/** 1384 * moves the current version to the attic and returns its 1385 * revision date 1386 * 1387 * @author Andreas Gohr <andi@splitbrain.org> 1388 * 1389 * @param string $id page id 1390 * @return int|string revision timestamp 1391 */ 1392function saveOldRevision($id) { 1393 $oldf = wikiFN($id); 1394 if(!file_exists($oldf)) return ''; 1395 $date = filemtime($oldf); 1396 $newf = wikiFN($id, $date); 1397 io_writeWikiPage($newf, rawWiki($id), $id, $date); 1398 return $date; 1399} 1400 1401/** 1402 * Sends a notify mail on page change or registration 1403 * 1404 * @param string $id The changed page 1405 * @param string $who Who to notify (admin|subscribers|register) 1406 * @param int|string $rev Old page revision 1407 * @param string $summary What changed 1408 * @param boolean $minor Is this a minor edit? 1409 * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value 1410 * @return bool 1411 * 1412 * @author Andreas Gohr <andi@splitbrain.org> 1413 */ 1414function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array()) { 1415 global $conf; 1416 /* @var Input $INPUT */ 1417 global $INPUT; 1418 1419 // decide if there is something to do, eg. whom to mail 1420 if($who == 'admin') { 1421 if(empty($conf['notify'])) return false; //notify enabled? 1422 $tpl = 'mailtext'; 1423 $to = $conf['notify']; 1424 } elseif($who == 'subscribers') { 1425 if(!actionOK('subscribe')) return false; //subscribers enabled? 1426 if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors 1427 $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace); 1428 trigger_event( 1429 'COMMON_NOTIFY_ADDRESSLIST', $data, 1430 array(new Subscription(), 'notifyaddresses') 1431 ); 1432 $to = $data['addresslist']; 1433 if(empty($to)) return false; 1434 $tpl = 'subscr_single'; 1435 } else { 1436 return false; //just to be safe 1437 } 1438 1439 // prepare content 1440 $subscription = new Subscription(); 1441 return $subscription->send_diff($to, $tpl, $id, $rev, $summary); 1442} 1443 1444/** 1445 * extracts the query from a search engine referrer 1446 * 1447 * @author Andreas Gohr <andi@splitbrain.org> 1448 * @author Todd Augsburger <todd@rollerorgans.com> 1449 * 1450 * @return array|string 1451 */ 1452function getGoogleQuery() { 1453 /* @var Input $INPUT */ 1454 global $INPUT; 1455 1456 if(!$INPUT->server->has('HTTP_REFERER')) { 1457 return ''; 1458 } 1459 $url = parse_url($INPUT->server->str('HTTP_REFERER')); 1460 1461 // only handle common SEs 1462 if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return ''; 1463 1464 $query = array(); 1465 // temporary workaround against PHP bug #49733 1466 // see http://bugs.php.net/bug.php?id=49733 1467 if(UTF8_MBSTRING) $enc = mb_internal_encoding(); 1468 parse_str($url['query'], $query); 1469 if(UTF8_MBSTRING) mb_internal_encoding($enc); 1470 1471 $q = ''; 1472 if(isset($query['q'])){ 1473 $q = $query['q']; 1474 }elseif(isset($query['p'])){ 1475 $q = $query['p']; 1476 }elseif(isset($query['query'])){ 1477 $q = $query['query']; 1478 } 1479 $q = trim($q); 1480 1481 if(!$q) return ''; 1482 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY); 1483 return $q; 1484} 1485 1486/** 1487 * Return the human readable size of a file 1488 * 1489 * @param int $size A file size 1490 * @param int $dec A number of decimal places 1491 * @return string human readable size 1492 * 1493 * @author Martin Benjamin <b.martin@cybernet.ch> 1494 * @author Aidan Lister <aidan@php.net> 1495 * @version 1.0.0 1496 */ 1497function filesize_h($size, $dec = 1) { 1498 $sizes = array('B', 'KB', 'MB', 'GB'); 1499 $count = count($sizes); 1500 $i = 0; 1501 1502 while($size >= 1024 && ($i < $count - 1)) { 1503 $size /= 1024; 1504 $i++; 1505 } 1506 1507 return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space 1508} 1509 1510/** 1511 * Return the given timestamp as human readable, fuzzy age 1512 * 1513 * @author Andreas Gohr <gohr@cosmocode.de> 1514 * 1515 * @param int $dt timestamp 1516 * @return string 1517 */ 1518function datetime_h($dt) { 1519 global $lang; 1520 1521 $ago = time() - $dt; 1522 if($ago > 24 * 60 * 60 * 30 * 12 * 2) { 1523 return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12))); 1524 } 1525 if($ago > 24 * 60 * 60 * 30 * 2) { 1526 return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30))); 1527 } 1528 if($ago > 24 * 60 * 60 * 7 * 2) { 1529 return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7))); 1530 } 1531 if($ago > 24 * 60 * 60 * 2) { 1532 return sprintf($lang['days'], round($ago / (24 * 60 * 60))); 1533 } 1534 if($ago > 60 * 60 * 2) { 1535 return sprintf($lang['hours'], round($ago / (60 * 60))); 1536 } 1537 if($ago > 60 * 2) { 1538 return sprintf($lang['minutes'], round($ago / (60))); 1539 } 1540 return sprintf($lang['seconds'], $ago); 1541} 1542 1543/** 1544 * Wraps around strftime but provides support for fuzzy dates 1545 * 1546 * The format default to $conf['dformat']. It is passed to 1547 * strftime - %f can be used to get the value from datetime_h() 1548 * 1549 * @see datetime_h 1550 * @author Andreas Gohr <gohr@cosmocode.de> 1551 * 1552 * @param int|null $dt timestamp when given, null will take current timestamp 1553 * @param string $format empty default to $conf['dformat'], or provide format as recognized by strftime() 1554 * @return string 1555 */ 1556function dformat($dt = null, $format = '') { 1557 global $conf; 1558 1559 if(is_null($dt)) $dt = time(); 1560 $dt = (int) $dt; 1561 if(!$format) $format = $conf['dformat']; 1562 1563 $format = str_replace('%f', datetime_h($dt), $format); 1564 return strftime($format, $dt); 1565} 1566 1567/** 1568 * Formats a timestamp as ISO 8601 date 1569 * 1570 * @author <ungu at terong dot com> 1571 * @link http://php.net/manual/en/function.date.php#54072 1572 * 1573 * @param int $int_date current date in UNIX timestamp 1574 * @return string 1575 */ 1576function date_iso8601($int_date) { 1577 $date_mod = date('Y-m-d\TH:i:s', $int_date); 1578 $pre_timezone = date('O', $int_date); 1579 $time_zone = substr($pre_timezone, 0, 3).":".substr($pre_timezone, 3, 2); 1580 $date_mod .= $time_zone; 1581 return $date_mod; 1582} 1583 1584/** 1585 * return an obfuscated email address in line with $conf['mailguard'] setting 1586 * 1587 * @author Harry Fuecks <hfuecks@gmail.com> 1588 * @author Christopher Smith <chris@jalakai.co.uk> 1589 * 1590 * @param string $email email address 1591 * @return string 1592 */ 1593function obfuscate($email) { 1594 global $conf; 1595 1596 switch($conf['mailguard']) { 1597 case 'visible' : 1598 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1599 return strtr($email, $obfuscate); 1600 1601 case 'hex' : 1602 $encode = ''; 1603 $len = strlen($email); 1604 for($x = 0; $x < $len; $x++) { 1605 $encode .= '&#x'.bin2hex($email{$x}).';'; 1606 } 1607 return $encode; 1608 1609 case 'none' : 1610 default : 1611 return $email; 1612 } 1613} 1614 1615/** 1616 * Removes quoting backslashes 1617 * 1618 * @author Andreas Gohr <andi@splitbrain.org> 1619 * 1620 * @param string $string 1621 * @param string $char backslashed character 1622 * @return string 1623 */ 1624function unslash($string, $char = "'") { 1625 return str_replace('\\'.$char, $char, $string); 1626} 1627 1628/** 1629 * Convert php.ini shorthands to byte 1630 * 1631 * @author <gilthans dot NO dot SPAM at gmail dot com> 1632 * @link http://php.net/manual/en/ini.core.php#79564 1633 * 1634 * @param string $v shorthands 1635 * @return int|string 1636 */ 1637function php_to_byte($v) { 1638 $l = substr($v, -1); 1639 $ret = substr($v, 0, -1); 1640 switch(strtoupper($l)) { 1641 /** @noinspection PhpMissingBreakStatementInspection */ 1642 case 'P': 1643 $ret *= 1024; 1644 /** @noinspection PhpMissingBreakStatementInspection */ 1645 case 'T': 1646 $ret *= 1024; 1647 /** @noinspection PhpMissingBreakStatementInspection */ 1648 case 'G': 1649 $ret *= 1024; 1650 /** @noinspection PhpMissingBreakStatementInspection */ 1651 case 'M': 1652 $ret *= 1024; 1653 /** @noinspection PhpMissingBreakStatementInspection */ 1654 case 'K': 1655 $ret *= 1024; 1656 break; 1657 default; 1658 $ret *= 10; 1659 break; 1660 } 1661 return $ret; 1662} 1663 1664/** 1665 * Wrapper around preg_quote adding the default delimiter 1666 * 1667 * @param string $string 1668 * @return string 1669 */ 1670function preg_quote_cb($string) { 1671 return preg_quote($string, '/'); 1672} 1673 1674/** 1675 * Shorten a given string by removing data from the middle 1676 * 1677 * You can give the string in two parts, the first part $keep 1678 * will never be shortened. The second part $short will be cut 1679 * in the middle to shorten but only if at least $min chars are 1680 * left to display it. Otherwise it will be left off. 1681 * 1682 * @param string $keep the part to keep 1683 * @param string $short the part to shorten 1684 * @param int $max maximum chars you want for the whole string 1685 * @param int $min minimum number of chars to have left for middle shortening 1686 * @param string $char the shortening character to use 1687 * @return string 1688 */ 1689function shorten($keep, $short, $max, $min = 9, $char = '…') { 1690 $max = $max - utf8_strlen($keep); 1691 if($max < $min) return $keep; 1692 $len = utf8_strlen($short); 1693 if($len <= $max) return $keep.$short; 1694 $half = floor($max / 2); 1695 return $keep.utf8_substr($short, 0, $half - 1).$char.utf8_substr($short, $len - $half); 1696} 1697 1698/** 1699 * Return the users real name or e-mail address for use 1700 * in page footer and recent changes pages 1701 * 1702 * @param string|null $username or null when currently logged-in user should be used 1703 * @param bool $textonly true returns only plain text, true allows returning html 1704 * @return string html or plain text(not escaped) of formatted user name 1705 * 1706 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1707 */ 1708function editorinfo($username, $textonly = false) { 1709 return userlink($username, $textonly); 1710} 1711 1712/** 1713 * Returns users realname w/o link 1714 * 1715 * @param string|null $username or null when currently logged-in user should be used 1716 * @param bool $textonly true returns only plain text, true allows returning html 1717 * @return string html or plain text(not escaped) of formatted user name 1718 * 1719 * @triggers COMMON_USER_LINK 1720 */ 1721function userlink($username = null, $textonly = false) { 1722 global $conf, $INFO; 1723 /** @var DokuWiki_Auth_Plugin $auth */ 1724 global $auth; 1725 /** @var Input $INPUT */ 1726 global $INPUT; 1727 1728 // prepare initial event data 1729 $data = array( 1730 'username' => $username, // the unique user name 1731 'name' => '', 1732 'link' => array( //setting 'link' to false disables linking 1733 'target' => '', 1734 'pre' => '', 1735 'suf' => '', 1736 'style' => '', 1737 'more' => '', 1738 'url' => '', 1739 'title' => '', 1740 'class' => '' 1741 ), 1742 'userlink' => '', // formatted user name as will be returned 1743 'textonly' => $textonly 1744 ); 1745 if($username === null) { 1746 $data['username'] = $username = $INPUT->server->str('REMOTE_USER'); 1747 if($textonly){ 1748 $data['name'] = $INFO['userinfo']['name']. ' (' . $INPUT->server->str('REMOTE_USER') . ')'; 1749 }else { 1750 $data['name'] = '<bdi>' . hsc($INFO['userinfo']['name']) . '</bdi> (<bdi>' . hsc($INPUT->server->str('REMOTE_USER')) . '</bdi>)'; 1751 } 1752 } 1753 1754 $evt = new Doku_Event('COMMON_USER_LINK', $data); 1755 if($evt->advise_before(true)) { 1756 if(empty($data['name'])) { 1757 if($auth) $info = $auth->getUserData($username); 1758 if($conf['showuseras'] != 'loginname' && isset($info) && $info) { 1759 switch($conf['showuseras']) { 1760 case 'username': 1761 case 'username_link': 1762 $data['name'] = $textonly ? $info['name'] : hsc($info['name']); 1763 break; 1764 case 'email': 1765 case 'email_link': 1766 $data['name'] = obfuscate($info['mail']); 1767 break; 1768 } 1769 } else { 1770 $data['name'] = $textonly ? $data['username'] : hsc($data['username']); 1771 } 1772 } 1773 1774 /** @var Doku_Renderer_xhtml $xhtml_renderer */ 1775 static $xhtml_renderer = null; 1776 1777 if(!$data['textonly'] && empty($data['link']['url'])) { 1778 1779 if(in_array($conf['showuseras'], array('email_link', 'username_link'))) { 1780 if(!isset($info)) { 1781 if($auth) $info = $auth->getUserData($username); 1782 } 1783 if(isset($info) && $info) { 1784 if($conf['showuseras'] == 'email_link') { 1785 $data['link']['url'] = 'mailto:' . obfuscate($info['mail']); 1786 } else { 1787 if(is_null($xhtml_renderer)) { 1788 $xhtml_renderer = p_get_renderer('xhtml'); 1789 } 1790 if(empty($xhtml_renderer->interwiki)) { 1791 $xhtml_renderer->interwiki = getInterwiki(); 1792 } 1793 $shortcut = 'user'; 1794 $exists = null; 1795 $data['link']['url'] = $xhtml_renderer->_resolveInterWiki($shortcut, $username, $exists); 1796 $data['link']['class'] .= ' interwiki iw_user'; 1797 if($exists !== null) { 1798 if($exists) { 1799 $data['link']['class'] .= ' wikilink1'; 1800 } else { 1801 $data['link']['class'] .= ' wikilink2'; 1802 $data['link']['rel'] = 'nofollow'; 1803 } 1804 } 1805 } 1806 } else { 1807 $data['textonly'] = true; 1808 } 1809 1810 } else { 1811 $data['textonly'] = true; 1812 } 1813 } 1814 1815 if($data['textonly']) { 1816 $data['userlink'] = $data['name']; 1817 } else { 1818 $data['link']['name'] = $data['name']; 1819 if(is_null($xhtml_renderer)) { 1820 $xhtml_renderer = p_get_renderer('xhtml'); 1821 } 1822 $data['userlink'] = $xhtml_renderer->_formatLink($data['link']); 1823 } 1824 } 1825 $evt->advise_after(); 1826 unset($evt); 1827 1828 return $data['userlink']; 1829} 1830 1831/** 1832 * Returns the path to a image file for the currently chosen license. 1833 * When no image exists, returns an empty string 1834 * 1835 * @author Andreas Gohr <andi@splitbrain.org> 1836 * 1837 * @param string $type - type of image 'badge' or 'button' 1838 * @return string 1839 */ 1840function license_img($type) { 1841 global $license; 1842 global $conf; 1843 if(!$conf['license']) return ''; 1844 if(!is_array($license[$conf['license']])) return ''; 1845 $try = array(); 1846 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1847 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1848 if(substr($conf['license'], 0, 3) == 'cc-') { 1849 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1850 } 1851 foreach($try as $src) { 1852 if(file_exists(DOKU_INC.$src)) return $src; 1853 } 1854 return ''; 1855} 1856 1857/** 1858 * Checks if the given amount of memory is available 1859 * 1860 * If the memory_get_usage() function is not available the 1861 * function just assumes $bytes of already allocated memory 1862 * 1863 * @author Filip Oscadal <webmaster@illusionsoftworks.cz> 1864 * @author Andreas Gohr <andi@splitbrain.org> 1865 * 1866 * @param int $mem Size of memory you want to allocate in bytes 1867 * @param int $bytes already allocated memory (see above) 1868 * @return bool 1869 */ 1870function is_mem_available($mem, $bytes = 1048576) { 1871 $limit = trim(ini_get('memory_limit')); 1872 if(empty($limit)) return true; // no limit set! 1873 1874 // parse limit to bytes 1875 $limit = php_to_byte($limit); 1876 1877 // get used memory if possible 1878 if(function_exists('memory_get_usage')) { 1879 $used = memory_get_usage(); 1880 } else { 1881 $used = $bytes; 1882 } 1883 1884 if($used + $mem > $limit) { 1885 return false; 1886 } 1887 1888 return true; 1889} 1890 1891/** 1892 * Send a HTTP redirect to the browser 1893 * 1894 * Works arround Microsoft IIS cookie sending bug. Exits the script. 1895 * 1896 * @link http://support.microsoft.com/kb/q176113/ 1897 * @author Andreas Gohr <andi@splitbrain.org> 1898 * 1899 * @param string $url url being directed to 1900 */ 1901function send_redirect($url) { 1902 $url = stripctl($url); // defend against HTTP Response Splitting 1903 1904 /* @var Input $INPUT */ 1905 global $INPUT; 1906 1907 //are there any undisplayed messages? keep them in session for display 1908 global $MSG; 1909 if(isset($MSG) && count($MSG) && !defined('NOSESSION')) { 1910 //reopen session, store data and close session again 1911 @session_start(); 1912 $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 1913 } 1914 1915 // always close the session 1916 session_write_close(); 1917 1918 // check if running on IIS < 6 with CGI-PHP 1919 if($INPUT->server->has('SERVER_SOFTWARE') && $INPUT->server->has('GATEWAY_INTERFACE') && 1920 (strpos($INPUT->server->str('GATEWAY_INTERFACE'), 'CGI') !== false) && 1921 (preg_match('|^Microsoft-IIS/(\d)\.\d$|', trim($INPUT->server->str('SERVER_SOFTWARE')), $matches)) && 1922 $matches[1] < 6 1923 ) { 1924 header('Refresh: 0;url='.$url); 1925 } else { 1926 header('Location: '.$url); 1927 } 1928 1929 // no exits during unit tests 1930 if(defined('DOKU_UNITTEST')) { 1931 // pass info about the redirect back to the test suite 1932 $testRequest = TestRequest::getRunning(); 1933 if($testRequest !== null) { 1934 $testRequest->addData('send_redirect', $url); 1935 } 1936 return; 1937 } 1938 1939 exit; 1940} 1941 1942/** 1943 * Validate a value using a set of valid values 1944 * 1945 * This function checks whether a specified value is set and in the array 1946 * $valid_values. If not, the function returns a default value or, if no 1947 * default is specified, throws an exception. 1948 * 1949 * @param string $param The name of the parameter 1950 * @param array $valid_values A set of valid values; Optionally a default may 1951 * be marked by the key “default”. 1952 * @param array $array The array containing the value (typically $_POST 1953 * or $_GET) 1954 * @param string $exc The text of the raised exception 1955 * 1956 * @throws Exception 1957 * @return mixed 1958 * @author Adrian Lang <lang@cosmocode.de> 1959 */ 1960function valid_input_set($param, $valid_values, $array, $exc = '') { 1961 if(isset($array[$param]) && in_array($array[$param], $valid_values)) { 1962 return $array[$param]; 1963 } elseif(isset($valid_values['default'])) { 1964 return $valid_values['default']; 1965 } else { 1966 throw new Exception($exc); 1967 } 1968} 1969 1970/** 1971 * Read a preference from the DokuWiki cookie 1972 * (remembering both keys & values are urlencoded) 1973 * 1974 * @param string $pref preference key 1975 * @param mixed $default value returned when preference not found 1976 * @return string preference value 1977 */ 1978function get_doku_pref($pref, $default) { 1979 $enc_pref = urlencode($pref); 1980 if(isset($_COOKIE['DOKU_PREFS']) && strpos($_COOKIE['DOKU_PREFS'], $enc_pref) !== false) { 1981 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 1982 $cnt = count($parts); 1983 for($i = 0; $i < $cnt; $i += 2) { 1984 if($parts[$i] == $enc_pref) { 1985 return urldecode($parts[$i + 1]); 1986 } 1987 } 1988 } 1989 return $default; 1990} 1991 1992/** 1993 * Add a preference to the DokuWiki cookie 1994 * (remembering $_COOKIE['DOKU_PREFS'] is urlencoded) 1995 * Remove it by setting $val to false 1996 * 1997 * @param string $pref preference key 1998 * @param string $val preference value 1999 */ 2000function set_doku_pref($pref, $val) { 2001 global $conf; 2002 $orig = get_doku_pref($pref, false); 2003 $cookieVal = ''; 2004 2005 if($orig && ($orig != $val)) { 2006 $parts = explode('#', $_COOKIE['DOKU_PREFS']); 2007 $cnt = count($parts); 2008 // urlencode $pref for the comparison 2009 $enc_pref = rawurlencode($pref); 2010 for($i = 0; $i < $cnt; $i += 2) { 2011 if($parts[$i] == $enc_pref) { 2012 if ($val !== false) { 2013 $parts[$i + 1] = rawurlencode($val); 2014 } else { 2015 unset($parts[$i]); 2016 unset($parts[$i + 1]); 2017 } 2018 break; 2019 } 2020 } 2021 $cookieVal = implode('#', $parts); 2022 } else if (!$orig && $val !== false) { 2023 $cookieVal = ($_COOKIE['DOKU_PREFS'] ? $_COOKIE['DOKU_PREFS'].'#' : '').rawurlencode($pref).'#'.rawurlencode($val); 2024 } 2025 2026 if (!empty($cookieVal)) { 2027 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 2028 setcookie('DOKU_PREFS', $cookieVal, time()+365*24*3600, $cookieDir, '', ($conf['securecookie'] && is_ssl())); 2029 } 2030} 2031 2032/** 2033 * Strips source mapping declarations from given text #601 2034 * 2035 * @param string &$text reference to the CSS or JavaScript code to clean 2036 */ 2037function stripsourcemaps(&$text){ 2038 $text = preg_replace('/^(\/\/|\/\*)[@#]\s+sourceMappingURL=.*?(\*\/)?$/im', '\\1\\2', $text); 2039} 2040 2041/** 2042 * Returns the contents of a given SVG file for embedding 2043 * 2044 * Inlining SVGs saves on HTTP requests and more importantly allows for styling them through 2045 * CSS. However it should used with small SVGs only. The $maxsize setting ensures only small 2046 * files are embedded. 2047 * 2048 * This strips unneeded headers, comments and newline. The result is not a vaild standalone SVG! 2049 * 2050 * @param string $file full path to the SVG file 2051 * @param int $maxsize maximum allowed size for the SVG to be embedded 2052 * @return string|false the SVG content, false if the file couldn't be loaded 2053 */ 2054function inlineSVG($file, $maxsize = 2048) { 2055 $file = trim($file); 2056 if($file === '') return false; 2057 if(!file_exists($file)) return false; 2058 if(filesize($file) > $maxsize) return false; 2059 if(!is_readable($file)) return false; 2060 $content = file_get_contents($file); 2061 $content = preg_replace('/<!--.*?(-->)/s','', $content); // comments 2062 $content = preg_replace('/<\?xml .*?\?>/i', '', $content); // xml header 2063 $content = preg_replace('/<!DOCTYPE .*?>/i', '', $content); // doc type 2064 $content = preg_replace('/>\s+</s', '><', $content); // newlines between tags 2065 $content = trim($content); 2066 if(substr($content, 0, 5) !== '<svg ') return false; 2067 return $content; 2068} 2069 2070//Setup VIM: ex: et ts=2 : 2071