[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> common.php (source)

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