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