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