[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> pageutils.php (source)

   1  <?php
   2  /**
   3   * Utilities for handling pagenames
   4   *
   5   * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
   6   * @author     Andreas Gohr <andi@splitbrain.org>
   7   * @todo       Combine similar functions like {wiki,media,meta}FN()
   8   */
   9  
  10  use dokuwiki\ChangeLog\MediaChangeLog;
  11  use dokuwiki\ChangeLog\PageChangeLog;
  12  use dokuwiki\File\MediaResolver;
  13  use dokuwiki\File\PageResolver;
  14  
  15  /**
  16   * Fetch the an ID from request
  17   *
  18   * Uses either standard $_REQUEST variable or extracts it from
  19   * the full request URI when userewrite is set to 2
  20   *
  21   * For $param='id' $conf['start'] is returned if no id was found.
  22   * If the second parameter is true (default) the ID is cleaned.
  23   *
  24   * @author Andreas Gohr <andi@splitbrain.org>
  25   *
  26   * @param string $param  the $_REQUEST variable name, default 'id'
  27   * @param bool   $clean  if true, ID is cleaned
  28   * @return string
  29   */
  30  function getID($param='id',$clean=true){
  31      /** @var Input $INPUT */
  32      global $INPUT;
  33      global $conf;
  34      global $ACT;
  35  
  36      $id = $INPUT->str($param);
  37  
  38      //construct page id from request URI
  39      if(empty($id) && $conf['userewrite'] == 2){
  40          $request = $INPUT->server->str('REQUEST_URI');
  41          $script = '';
  42  
  43          //get the script URL
  44          if($conf['basedir']){
  45              $relpath = '';
  46              if($param != 'id') {
  47                  $relpath = 'lib/exe/';
  48              }
  49              $script = $conf['basedir'] . $relpath .
  50                  \dokuwiki\Utf8\PhpString::basename($INPUT->server->str('SCRIPT_FILENAME'));
  51  
  52          }elseif($INPUT->server->str('PATH_INFO')){
  53              $request = $INPUT->server->str('PATH_INFO');
  54          }elseif($INPUT->server->str('SCRIPT_NAME')){
  55              $script = $INPUT->server->str('SCRIPT_NAME');
  56          }elseif($INPUT->server->str('DOCUMENT_ROOT') && $INPUT->server->str('SCRIPT_FILENAME')){
  57              $script = preg_replace ('/^'.preg_quote($INPUT->server->str('DOCUMENT_ROOT'),'/').'/','',
  58                      $INPUT->server->str('SCRIPT_FILENAME'));
  59              $script = '/'.$script;
  60          }
  61  
  62          //clean script and request (fixes a windows problem)
  63          $script  = preg_replace('/\/\/+/','/',$script);
  64          $request = preg_replace('/\/\/+/','/',$request);
  65  
  66          //remove script URL and Querystring to gain the id
  67          if(preg_match('/^'.preg_quote($script,'/').'(.*)/',$request, $match)){
  68              $id = preg_replace ('/\?.*/','',$match[1]);
  69          }
  70          $id = urldecode($id);
  71          //strip leading slashes
  72          $id = preg_replace('!^/+!','',$id);
  73      }
  74  
  75      // Namespace autolinking from URL
  76      if(substr($id,-1) == ':' || ($conf['useslash'] && substr($id,-1) == '/')){
  77          if(page_exists($id.$conf['start'])){
  78              // start page inside namespace
  79              $id = $id.$conf['start'];
  80          }elseif(page_exists($id.noNS(cleanID($id)))){
  81              // page named like the NS inside the NS
  82              $id = $id.noNS(cleanID($id));
  83          }elseif(page_exists($id)){
  84              // page like namespace exists
  85              $id = substr($id,0,-1);
  86          }else{
  87              // fall back to default
  88              $id = $id.$conf['start'];
  89          }
  90          if (isset($ACT) && $ACT === 'show') {
  91              $urlParameters = $_GET;
  92              if (isset($urlParameters['id'])) {
  93                  unset($urlParameters['id']);
  94              }
  95              send_redirect(wl($id, $urlParameters, true, '&'));
  96          }
  97      }
  98      if($clean) $id = cleanID($id);
  99      if($id === '' && $param=='id') $id = $conf['start'];
 100  
 101      return $id;
 102  }
 103  
 104  /**
 105   * Remove unwanted chars from ID
 106   *
 107   * Cleans a given ID to only use allowed characters. Accented characters are
 108   * converted to unaccented ones
 109   *
 110   * @author Andreas Gohr <andi@splitbrain.org>
 111   *
 112   * @param  string  $raw_id    The pageid to clean
 113   * @param  boolean $ascii     Force ASCII
 114   * @return string cleaned id
 115   */
 116  function cleanID($raw_id,$ascii=false){
 117      global $conf;
 118      static $sepcharpat = null;
 119  
 120      global $cache_cleanid;
 121      $cache = & $cache_cleanid;
 122  
 123      // check if it's already in the memory cache
 124      if (!$ascii && isset($cache[(string)$raw_id])) {
 125          return $cache[(string)$raw_id];
 126      }
 127  
 128      $sepchar = $conf['sepchar'];
 129      if($sepcharpat == null) // build string only once to save clock cycles
 130          $sepcharpat = '#\\'.$sepchar.'+#';
 131  
 132      $id = trim((string)$raw_id);
 133      $id = \dokuwiki\Utf8\PhpString::strtolower($id);
 134  
 135      //alternative namespace seperator
 136      if($conf['useslash']){
 137          $id = strtr($id,';/','::');
 138      }else{
 139          $id = strtr($id,';/',':'.$sepchar);
 140      }
 141  
 142      if($conf['deaccent'] == 2 || $ascii) $id = \dokuwiki\Utf8\Clean::romanize($id);
 143      if($conf['deaccent'] || $ascii) $id = \dokuwiki\Utf8\Clean::deaccent($id,-1);
 144  
 145      //remove specials
 146      $id = \dokuwiki\Utf8\Clean::stripspecials($id,$sepchar,'\*');
 147  
 148      if($ascii) $id = \dokuwiki\Utf8\Clean::strip($id);
 149  
 150      //clean up
 151      $id = preg_replace($sepcharpat,$sepchar,$id);
 152      $id = preg_replace('#:+#',':',$id);
 153      $id = trim($id,':._-');
 154      $id = preg_replace('#:[:\._\-]+#',':',$id);
 155      $id = preg_replace('#[:\._\-]+:#',':',$id);
 156  
 157      if (!$ascii) $cache[(string)$raw_id] = $id;
 158      return($id);
 159  }
 160  
 161  /**
 162   * Return namespacepart of a wiki ID
 163   *
 164   * @author Andreas Gohr <andi@splitbrain.org>
 165   *
 166   * @param string $id
 167   * @return string|false the namespace part or false if the given ID has no namespace (root)
 168   */
 169  function getNS($id){
 170      $pos = strrpos((string)$id,':');
 171      if($pos!==false){
 172          return substr((string)$id,0,$pos);
 173      }
 174      return false;
 175  }
 176  
 177  /**
 178   * Returns the ID without the namespace
 179   *
 180   * @author Andreas Gohr <andi@splitbrain.org>
 181   *
 182   * @param string $id
 183   * @return string
 184   */
 185  function noNS($id) {
 186      $pos = strrpos($id, ':');
 187      if ($pos!==false) {
 188          return substr($id, $pos+1);
 189      } else {
 190          return $id;
 191      }
 192  }
 193  
 194  /**
 195   * Returns the current namespace
 196   *
 197   * @author Nathan Fritz <fritzn@crown.edu>
 198   *
 199   * @param string $id
 200   * @return string
 201   */
 202  function curNS($id) {
 203      return noNS(getNS($id));
 204  }
 205  
 206  /**
 207   * Returns the ID without the namespace or current namespace for 'start' pages
 208   *
 209   * @author Nathan Fritz <fritzn@crown.edu>
 210   *
 211   * @param string $id
 212   * @return string
 213   */
 214  function noNSorNS($id) {
 215      global $conf;
 216  
 217      $p = noNS($id);
 218      if ($p === $conf['start'] || $p === false || $p === '') {
 219          $p = curNS($id);
 220          if ($p === false || $p === '') {
 221              return $conf['start'];
 222          }
 223      }
 224      return $p;
 225  }
 226  
 227  /**
 228   * Creates a XHTML valid linkid from a given headline title
 229   *
 230   * @param string  $title   The headline title
 231   * @param array|bool   $check   Existing IDs
 232   * @return string the title
 233   *
 234   * @author Andreas Gohr <andi@splitbrain.org>
 235   */
 236  function sectionID($title,&$check) {
 237      $title = str_replace(array(':','.'),'',cleanID($title));
 238      $new = ltrim($title,'0123456789_-');
 239      if(empty($new)){
 240          $title = 'section'.preg_replace('/[^0-9]+/','',$title); //keep numbers from headline
 241      }else{
 242          $title = $new;
 243      }
 244  
 245      if(is_array($check)){
 246          $suffix=0;
 247          $candidateTitle = $title;
 248          while(in_array($candidateTitle, $check)){
 249            $candidateTitle = $title . ++$suffix;
 250          }
 251          $check []= $candidateTitle;
 252          return $candidateTitle;
 253      } else {
 254        return $title;
 255      }
 256  }
 257  
 258  /**
 259   * Wiki page existence check
 260   *
 261   * parameters as for wikiFN
 262   *
 263   * @author Chris Smith <chris@jalakai.co.uk>
 264   *
 265   * @param string $id page id
 266   * @param string|int $rev empty or revision timestamp
 267   * @param bool $clean flag indicating that $id should be cleaned (see wikiFN as well)
 268   * @param bool $date_at
 269   * @return bool exists?
 270   */
 271  function page_exists($id, $rev = '', $clean = true, $date_at = false) {
 272      if ($rev !== '' && $date_at) {
 273          $pagelog = new PageChangeLog($id);
 274          $pagelog_rev = $pagelog->getLastRevisionAt($rev);
 275          if ($pagelog_rev !== false)
 276              $rev = $pagelog_rev;
 277      }
 278      return file_exists(wikiFN($id, $rev, $clean));
 279  }
 280  
 281  /**
 282   * Media existence check
 283   *
 284   * @param string $id page id
 285   * @param string|int $rev empty or revision timestamp
 286   * @param bool $clean flag indicating that $id should be cleaned (see mediaFN as well)
 287   * @param bool $date_at
 288   * @return bool exists?
 289   */
 290  function media_exists($id, $rev = '', $clean = true, $date_at = false)
 291  {
 292      if ($rev !== '' && $date_at) {
 293          $changeLog = new MediaChangeLog($id);
 294          $changelog_rev = $changeLog->getLastRevisionAt($rev);
 295          if ($changelog_rev !== false) {
 296              $rev = $changelog_rev;
 297          }
 298      }
 299      return file_exists(mediaFN($id, $rev, $clean));
 300  }
 301  
 302  /**
 303   * returns the full path to the datafile specified by ID and optional revision
 304   *
 305   * The filename is URL encoded to protect Unicode chars
 306   *
 307   * @param  $raw_id  string   id of wikipage
 308   * @param  $rev     int|string   page revision, empty string for current
 309   * @param  $clean   bool     flag indicating that $raw_id should be cleaned.  Only set to false
 310   *                           when $id is guaranteed to have been cleaned already.
 311   * @return string full path
 312   *
 313   * @author Andreas Gohr <andi@splitbrain.org>
 314   */
 315  function wikiFN($raw_id,$rev='',$clean=true){
 316      global $conf;
 317  
 318      global $cache_wikifn;
 319      $cache = & $cache_wikifn;
 320  
 321      $id = $raw_id;
 322  
 323      if ($clean) $id = cleanID($id);
 324      $id = str_replace(':','/',$id);
 325  
 326      if (isset($cache[$id]) && isset($cache[$id][$rev])) {
 327          return $cache[$id][$rev];
 328      }
 329  
 330      if(empty($rev)){
 331          $fn = $conf['datadir'].'/'.utf8_encodeFN($id).'.txt';
 332      }else{
 333          $fn = $conf['olddir'].'/'.utf8_encodeFN($id).'.'.$rev.'.txt';
 334          if($conf['compression']){
 335              //test for extensions here, we want to read both compressions
 336              if (file_exists($fn . '.gz')){
 337                  $fn .= '.gz';
 338              }else if(file_exists($fn . '.bz2')){
 339                  $fn .= '.bz2';
 340              }else{
 341                  //file doesnt exist yet, so we take the configured extension
 342                  $fn .= '.' . $conf['compression'];
 343              }
 344          }
 345      }
 346  
 347      if (!isset($cache[$id])) { $cache[$id] = array(); }
 348      $cache[$id][$rev] = $fn;
 349      return $fn;
 350  }
 351  
 352  /**
 353   * Returns the full path to the file for locking the page while editing.
 354   *
 355   * @author Ben Coburn <btcoburn@silicodon.net>
 356   *
 357   * @param string $id page id
 358   * @return string full path
 359   */
 360  function wikiLockFN($id) {
 361      global $conf;
 362      return $conf['lockdir'].'/'.md5(cleanID($id)).'.lock';
 363  }
 364  
 365  
 366  /**
 367   * returns the full path to the meta file specified by ID and extension
 368   *
 369   * @author Steven Danz <steven-danz@kc.rr.com>
 370   *
 371   * @param string $id   page id
 372   * @param string $ext  file extension
 373   * @return string full path
 374   */
 375  function metaFN($id,$ext){
 376      global $conf;
 377      $id = cleanID($id);
 378      $id = str_replace(':','/',$id);
 379      $fn = $conf['metadir'].'/'.utf8_encodeFN($id).$ext;
 380      return $fn;
 381  }
 382  
 383  /**
 384   * returns the full path to the media's meta file specified by ID and extension
 385   *
 386   * @author Kate Arzamastseva <pshns@ukr.net>
 387   *
 388   * @param string $id   media id
 389   * @param string $ext  extension of media
 390   * @return string
 391   */
 392  function mediaMetaFN($id,$ext){
 393      global $conf;
 394      $id = cleanID($id);
 395      $id = str_replace(':','/',$id);
 396      $fn = $conf['mediametadir'].'/'.utf8_encodeFN($id).$ext;
 397      return $fn;
 398  }
 399  
 400  /**
 401   * returns an array of full paths to all metafiles of a given ID
 402   *
 403   * @author Esther Brunner <esther@kaffeehaus.ch>
 404   * @author Michael Hamann <michael@content-space.de>
 405   *
 406   * @param string $id page id
 407   * @return array
 408   */
 409  function metaFiles($id){
 410      $basename = metaFN($id, '');
 411      $files    = glob($basename.'.*', GLOB_MARK);
 412      // filter files like foo.bar.meta when $id == 'foo'
 413      return    $files ? preg_grep('/^'.preg_quote($basename, '/').'\.[^.\/]*$/u', $files) : array();
 414  }
 415  
 416  /**
 417   * returns the full path to the mediafile specified by ID
 418   *
 419   * The filename is URL encoded to protect Unicode chars
 420   *
 421   * @author Andreas Gohr <andi@splitbrain.org>
 422   * @author Kate Arzamastseva <pshns@ukr.net>
 423   *
 424   * @param string     $id  media id
 425   * @param string|int $rev empty string or revision timestamp
 426   * @param bool $clean
 427   *
 428   * @return string full path
 429   */
 430  function mediaFN($id, $rev='', $clean=true){
 431      global $conf;
 432      if ($clean) $id = cleanID($id);
 433      $id = str_replace(':','/',$id);
 434      if(empty($rev)){
 435          $fn = $conf['mediadir'].'/'.utf8_encodeFN($id);
 436      }else{
 437          $ext = mimetype($id);
 438          $name = substr($id,0, -1*strlen($ext[0])-1);
 439          $fn = $conf['mediaolddir'].'/'.utf8_encodeFN($name .'.'.( (int) $rev ).'.'.$ext[0]);
 440      }
 441      return $fn;
 442  }
 443  
 444  /**
 445   * Returns the full filepath to a localized file if local
 446   * version isn't found the english one is returned
 447   *
 448   * @param  string $id  The id of the local file
 449   * @param  string $ext The file extension (usually txt)
 450   * @return string full filepath to localized file
 451   *
 452   * @author Andreas Gohr <andi@splitbrain.org>
 453   */
 454  function localeFN($id,$ext='txt'){
 455      global $conf;
 456      $file = DOKU_CONF.'lang/'.$conf['lang'].'/'.$id.'.'.$ext;
 457      if(!file_exists($file)){
 458          $file = DOKU_INC.'inc/lang/'.$conf['lang'].'/'.$id.'.'.$ext;
 459          if(!file_exists($file)){
 460              //fall back to english
 461              $file = DOKU_INC.'inc/lang/en/'.$id.'.'.$ext;
 462          }
 463      }
 464      return $file;
 465  }
 466  
 467  /**
 468   * Resolve relative paths in IDs
 469   *
 470   * Do not call directly use resolve_mediaid or resolve_pageid
 471   * instead
 472   *
 473   * Partyly based on a cleanPath function found at
 474   * http://php.net/manual/en/function.realpath.php#57016
 475   *
 476   * @deprecated 2020-09-30
 477   * @param string $ns     namespace which is context of id
 478   * @param string $id     relative id
 479   * @param bool   $clean  flag indicating that id should be cleaned
 480   * @return string
 481   */
 482  function resolve_id($ns,$id,$clean=true){
 483      global $conf;
 484      dbg_deprecated(\dokuwiki\File\Resolver::class.' and its children');
 485  
 486      // some pre cleaning for useslash:
 487      if($conf['useslash']) $id = str_replace('/',':',$id);
 488  
 489      // if the id starts with a dot we need to handle the
 490      // relative stuff
 491      if($id && $id[0] == '.'){
 492          // normalize initial dots without a colon
 493          $id = preg_replace('/^((\.+:)*)(\.+)(?=[^:\.])/','\1\3:',$id);
 494          // prepend the current namespace
 495          $id = $ns.':'.$id;
 496  
 497          // cleanup relatives
 498          $result = array();
 499          $pathA  = explode(':', $id);
 500          if (!$pathA[0]) $result[] = '';
 501          foreach ($pathA AS $key => $dir) {
 502              if ($dir == '..') {
 503                  if (end($result) == '..') {
 504                      $result[] = '..';
 505                  } elseif (!array_pop($result)) {
 506                      $result[] = '..';
 507                  }
 508              } elseif ($dir && $dir != '.') {
 509                  $result[] = $dir;
 510              }
 511          }
 512          if (!end($pathA)) $result[] = '';
 513          $id = implode(':', $result);
 514      }elseif($ns !== false && strpos($id,':') === false){
 515          //if link contains no namespace. add current namespace (if any)
 516          $id = $ns.':'.$id;
 517      }
 518  
 519      if($clean) $id = cleanID($id);
 520      return $id;
 521  }
 522  
 523  /**
 524   * Returns a full media id
 525   *
 526   * @param string $ns namespace which is context of id
 527   * @param string &$media (reference) relative media id, updated to resolved id
 528   * @param bool &$exists (reference) updated with existance of media
 529   * @param int|string $rev
 530   * @param bool $date_at
 531   * @deprecated 2020-09-30
 532   */
 533  function resolve_mediaid($ns,&$media,&$exists,$rev='',$date_at=false){
 534      dbg_deprecated(MediaResolver::class);
 535      $resolver = new MediaResolver("$ns:deprecated");
 536      $media = $resolver->resolveId($media, $rev, $date_at);
 537      $exists = media_exists($media, $rev, false, $date_at);
 538  }
 539  
 540  /**
 541   * Returns a full page id
 542   *
 543   * @deprecated 2020-09-30
 544   * @param string $ns namespace which is context of id
 545   * @param string &$page (reference) relative page id, updated to resolved id
 546   * @param bool &$exists (reference) updated with existance of media
 547   * @param string $rev
 548   * @param bool $date_at
 549   */
 550  function resolve_pageid($ns,&$page,&$exists,$rev='',$date_at=false )
 551  {
 552      dbg_deprecated(PageResolver::class);
 553  
 554      global $ID;
 555      if(getNS($ID) == $ns) {
 556          $context = $ID; // this is usually the case
 557      } else {
 558          $context = "$ns:deprecated"; // only used when a different context namespace was given
 559      }
 560  
 561      $resolver = new PageResolver($context);
 562      $page = $resolver->resolveId($page, $rev, $date_at);
 563      $exists = page_exists($page, $rev, false, $date_at);
 564  }
 565  
 566  /**
 567   * Returns the name of a cachefile from given data
 568   *
 569   * The needed directory is created by this function!
 570   *
 571   * @author Andreas Gohr <andi@splitbrain.org>
 572   *
 573   * @param string $data  This data is used to create a unique md5 name
 574   * @param string $ext   This is appended to the filename if given
 575   * @return string       The filename of the cachefile
 576   */
 577  function getCacheName($data,$ext=''){
 578      global $conf;
 579      $md5  = md5($data);
 580      $file = $conf['cachedir'].'/'.$md5[0].'/'.$md5.$ext;
 581      io_makeFileDir($file);
 582      return $file;
 583  }
 584  
 585  /**
 586   * Checks a pageid against $conf['hidepages']
 587   *
 588   * @author Andreas Gohr <gohr@cosmocode.de>
 589   *
 590   * @param string $id page id
 591   * @return bool
 592   */
 593  function isHiddenPage($id){
 594      $data = array(
 595          'id' => $id,
 596          'hidden' => false
 597      );
 598      \dokuwiki\Extension\Event::createAndTrigger('PAGEUTILS_ID_HIDEPAGE', $data, '_isHiddenPage');
 599      return $data['hidden'];
 600  }
 601  
 602  /**
 603   * callback checks if page is hidden
 604   *
 605   * @param array $data event data    - see isHiddenPage()
 606   */
 607  function _isHiddenPage(&$data) {
 608      global $conf;
 609      global $ACT;
 610  
 611      if ($data['hidden']) return;
 612      if(empty($conf['hidepages'])) return;
 613      if($ACT == 'admin') return;
 614  
 615      if(preg_match('/'.$conf['hidepages'].'/ui',':'.$data['id'])){
 616          $data['hidden'] = true;
 617      }
 618  }
 619  
 620  /**
 621   * Reverse of isHiddenPage
 622   *
 623   * @author Andreas Gohr <gohr@cosmocode.de>
 624   *
 625   * @param string $id page id
 626   * @return bool
 627   */
 628  function isVisiblePage($id){
 629      return !isHiddenPage($id);
 630  }
 631  
 632  /**
 633   * Format an id for output to a user
 634   *
 635   * Namespaces are denoted by a trailing “:*”. The root namespace is
 636   * “*”. Output is escaped.
 637   *
 638   * @author Adrian Lang <lang@cosmocode.de>
 639   *
 640   * @param string $id page id
 641   * @return string
 642   */
 643  function prettyprint_id($id) {
 644      if (!$id || $id === ':') {
 645          return '*';
 646      }
 647      if ((substr($id, -1, 1) === ':')) {
 648          $id .= '*';
 649      }
 650      return hsc($id);
 651  }
 652  
 653  /**
 654   * Encode a UTF-8 filename to use on any filesystem
 655   *
 656   * Uses the 'fnencode' option to determine encoding
 657   *
 658   * When the second parameter is true the string will
 659   * be encoded only if non ASCII characters are detected -
 660   * This makes it safe to run it multiple times on the
 661   * same string (default is true)
 662   *
 663   * @author Andreas Gohr <andi@splitbrain.org>
 664   * @see    urlencode
 665   *
 666   * @param string $file file name
 667   * @param bool   $safe if true, only encoded when non ASCII characters detected
 668   * @return string
 669   */
 670  function utf8_encodeFN($file,$safe=true){
 671      global $conf;
 672      if($conf['fnencode'] == 'utf-8') return $file;
 673  
 674      if($safe && preg_match('#^[a-zA-Z0-9/_\-\.%]+$#',$file)){
 675          return $file;
 676      }
 677  
 678      if($conf['fnencode'] == 'safe'){
 679          return SafeFN::encode($file);
 680      }
 681  
 682      $file = urlencode($file);
 683      $file = str_replace('%2F','/',$file);
 684      return $file;
 685  }
 686  
 687  /**
 688   * Decode a filename back to UTF-8
 689   *
 690   * Uses the 'fnencode' option to determine encoding
 691   *
 692   * @author Andreas Gohr <andi@splitbrain.org>
 693   * @see    urldecode
 694   *
 695   * @param string $file file name
 696   * @return string
 697   */
 698  function utf8_decodeFN($file){
 699      global $conf;
 700      if($conf['fnencode'] == 'utf-8') return $file;
 701  
 702      if($conf['fnencode'] == 'safe'){
 703          return SafeFN::decode($file);
 704      }
 705  
 706      return urldecode($file);
 707  }
 708  
 709  /**
 710   * Find a page in the current namespace (determined from $ID) or any
 711   * higher namespace that can be accessed by the current user,
 712   * this condition can be overriden by an optional parameter.
 713   *
 714   * Used for sidebars, but can be used other stuff as well
 715   *
 716   * @todo   add event hook
 717   *
 718   * @param  string $page the pagename you're looking for
 719   * @param bool $useacl only return pages readable by the current user, false to ignore ACLs
 720   * @return false|string the full page id of the found page, false if any
 721   */
 722  function page_findnearest($page, $useacl = true){
 723      if ((string) $page === '') return false;
 724      global $ID;
 725  
 726      $ns = $ID;
 727      do {
 728          $ns = getNS($ns);
 729          $pageid = cleanID("$ns:$page");
 730          if(page_exists($pageid) && (!$useacl || auth_quickaclcheck($pageid) >= AUTH_READ)){
 731              return $pageid;
 732          }
 733      } while($ns !== false);
 734  
 735      return false;
 736  }