[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> common.php (source)

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