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