You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

772 lines
20 KiB

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