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