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.
 
 
 
 
 

2039 lines
60 KiB

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