[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> common.php (source)

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