PageRenderTime 53ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/inc/common.php

http://github.com/splitbrain/dokuwiki
PHP | 2142 lines | 1189 code | 194 blank | 759 comment | 320 complexity | 426d1fc2cf2034fdf84f45747744adcb MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, GPL-2.0

Large files files are truncated, but you can click here to view the full file

  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\Cache\CacheInstructions;
  9. use dokuwiki\Cache\CacheRenderer;
  10. use dokuwiki\ChangeLog\PageChangeLog;
  11. use dokuwiki\Subscriptions\PageSubscriptionSender;
  12. use dokuwiki\Subscriptions\SubscriberManager;
  13. use dokuwiki\Extension\AuthPlugin;
  14. use dokuwiki\Extension\Event;
  15. /**
  16. * These constants are used with the recents function
  17. */
  18. define('RECENTS_SKIP_DELETED', 2);
  19. define('RECENTS_SKIP_MINORS', 4);
  20. define('RECENTS_SKIP_SUBSPACES', 8);
  21. define('RECENTS_MEDIA_CHANGES', 16);
  22. define('RECENTS_MEDIA_PAGES_MIXED', 32);
  23. define('RECENTS_ONLY_CREATION', 64);
  24. /**
  25. * Wrapper around htmlspecialchars()
  26. *
  27. * @author Andreas Gohr <andi@splitbrain.org>
  28. * @see htmlspecialchars()
  29. *
  30. * @param string $string the string being converted
  31. * @return string converted string
  32. */
  33. function hsc($string) {
  34. return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
  35. }
  36. /**
  37. * Checks if the given input is blank
  38. *
  39. * This is similar to empty() but will return false for "0".
  40. *
  41. * Please note: when you pass uninitialized variables, they will implicitly be created
  42. * with a NULL value without warning.
  43. *
  44. * To avoid this it's recommended to guard the call with isset like this:
  45. *
  46. * (isset($foo) && !blank($foo))
  47. * (!isset($foo) || blank($foo))
  48. *
  49. * @param $in
  50. * @param bool $trim Consider a string of whitespace to be blank
  51. * @return bool
  52. */
  53. function blank(&$in, $trim = false) {
  54. if(is_null($in)) return true;
  55. if(is_array($in)) return empty($in);
  56. if($in === "\0") return true;
  57. if($trim && trim($in) === '') return true;
  58. if(strlen($in) > 0) return false;
  59. return empty($in);
  60. }
  61. /**
  62. * print a newline terminated string
  63. *
  64. * You can give an indention as optional parameter
  65. *
  66. * @author Andreas Gohr <andi@splitbrain.org>
  67. *
  68. * @param string $string line of text
  69. * @param int $indent number of spaces indention
  70. */
  71. function ptln($string, $indent = 0) {
  72. echo str_repeat(' ', $indent)."$string\n";
  73. }
  74. /**
  75. * strips control characters (<32) from the given string
  76. *
  77. * @author Andreas Gohr <andi@splitbrain.org>
  78. *
  79. * @param string $string being stripped
  80. * @return string
  81. */
  82. function stripctl($string) {
  83. return preg_replace('/[\x00-\x1F]+/s', '', $string);
  84. }
  85. /**
  86. * Return a secret token to be used for CSRF attack prevention
  87. *
  88. * @author Andreas Gohr <andi@splitbrain.org>
  89. * @link http://en.wikipedia.org/wiki/Cross-site_request_forgery
  90. * @link http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html
  91. *
  92. * @return string
  93. */
  94. function getSecurityToken() {
  95. /** @var Input $INPUT */
  96. global $INPUT;
  97. $user = $INPUT->server->str('REMOTE_USER');
  98. $session = session_id();
  99. // CSRF checks are only for logged in users - do not generate for anonymous
  100. if(trim($user) == '' || trim($session) == '') return '';
  101. return \dokuwiki\PassHash::hmac('md5', $session.$user, auth_cookiesalt());
  102. }
  103. /**
  104. * Check the secret CSRF token
  105. *
  106. * @param null|string $token security token or null to read it from request variable
  107. * @return bool success if the token matched
  108. */
  109. function checkSecurityToken($token = null) {
  110. /** @var Input $INPUT */
  111. global $INPUT;
  112. if(!$INPUT->server->str('REMOTE_USER')) return true; // no logged in user, no need for a check
  113. if(is_null($token)) $token = $INPUT->str('sectok');
  114. if(getSecurityToken() != $token) {
  115. msg('Security Token did not match. Possible CSRF attack.', -1);
  116. return false;
  117. }
  118. return true;
  119. }
  120. /**
  121. * Print a hidden form field with a secret CSRF token
  122. *
  123. * @author Andreas Gohr <andi@splitbrain.org>
  124. *
  125. * @param bool $print if true print the field, otherwise html of the field is returned
  126. * @return string html of hidden form field
  127. */
  128. function formSecurityToken($print = true) {
  129. $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n";
  130. if($print) echo $ret;
  131. return $ret;
  132. }
  133. /**
  134. * Determine basic information for a request of $id
  135. *
  136. * @author Andreas Gohr <andi@splitbrain.org>
  137. * @author Chris Smith <chris@jalakai.co.uk>
  138. *
  139. * @param string $id pageid
  140. * @param bool $htmlClient add info about whether is mobile browser
  141. * @return array with info for a request of $id
  142. *
  143. */
  144. function basicinfo($id, $htmlClient=true){
  145. global $USERINFO;
  146. /* @var Input $INPUT */
  147. global $INPUT;
  148. // set info about manager/admin status.
  149. $info = array();
  150. $info['isadmin'] = false;
  151. $info['ismanager'] = false;
  152. if($INPUT->server->has('REMOTE_USER')) {
  153. $info['userinfo'] = $USERINFO;
  154. $info['perm'] = auth_quickaclcheck($id);
  155. $info['client'] = $INPUT->server->str('REMOTE_USER');
  156. if($info['perm'] == AUTH_ADMIN) {
  157. $info['isadmin'] = true;
  158. $info['ismanager'] = true;
  159. } elseif(auth_ismanager()) {
  160. $info['ismanager'] = true;
  161. }
  162. // if some outside auth were used only REMOTE_USER is set
  163. if(!$info['userinfo']['name']) {
  164. $info['userinfo']['name'] = $INPUT->server->str('REMOTE_USER');
  165. }
  166. } else {
  167. $info['perm'] = auth_aclcheck($id, '', null);
  168. $info['client'] = clientIP(true);
  169. }
  170. $info['namespace'] = getNS($id);
  171. // mobile detection
  172. if ($htmlClient) {
  173. $info['ismobile'] = clientismobile();
  174. }
  175. return $info;
  176. }
  177. /**
  178. * Return info about the current document as associative
  179. * array.
  180. *
  181. * @author Andreas Gohr <andi@splitbrain.org>
  182. *
  183. * @return array with info about current document
  184. */
  185. function pageinfo() {
  186. global $ID;
  187. global $REV;
  188. global $RANGE;
  189. global $lang;
  190. /* @var Input $INPUT */
  191. global $INPUT;
  192. $info = basicinfo($ID);
  193. // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml
  194. // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary
  195. $info['id'] = $ID;
  196. $info['rev'] = $REV;
  197. $subManager = new SubscriberManager();
  198. $info['subscribed'] = $subManager->userSubscription();
  199. $info['locked'] = checklock($ID);
  200. $info['filepath'] = wikiFN($ID);
  201. $info['exists'] = file_exists($info['filepath']);
  202. $info['currentrev'] = @filemtime($info['filepath']);
  203. if($REV) {
  204. //check if current revision was meant
  205. if($info['exists'] && ($info['currentrev'] == $REV)) {
  206. $REV = '';
  207. } elseif($RANGE) {
  208. //section editing does not work with old revisions!
  209. $REV = '';
  210. $RANGE = '';
  211. msg($lang['nosecedit'], 0);
  212. } else {
  213. //really use old revision
  214. $info['filepath'] = wikiFN($ID, $REV);
  215. $info['exists'] = file_exists($info['filepath']);
  216. }
  217. }
  218. $info['rev'] = $REV;
  219. if($info['exists']) {
  220. $info['writable'] = (is_writable($info['filepath']) &&
  221. ($info['perm'] >= AUTH_EDIT));
  222. } else {
  223. $info['writable'] = ($info['perm'] >= AUTH_CREATE);
  224. }
  225. $info['editable'] = ($info['writable'] && empty($info['locked']));
  226. $info['lastmod'] = @filemtime($info['filepath']);
  227. //load page meta data
  228. $info['meta'] = p_get_metadata($ID);
  229. //who's the editor
  230. $pagelog = new PageChangeLog($ID, 1024);
  231. if($REV) {
  232. $revinfo = $pagelog->getRevisionInfo($REV);
  233. } else {
  234. if(!empty($info['meta']['last_change']) && is_array($info['meta']['last_change'])) {
  235. $revinfo = $info['meta']['last_change'];
  236. } else {
  237. $revinfo = $pagelog->getRevisionInfo($info['lastmod']);
  238. // cache most recent changelog line in metadata if missing and still valid
  239. if($revinfo !== false) {
  240. $info['meta']['last_change'] = $revinfo;
  241. p_set_metadata($ID, array('last_change' => $revinfo));
  242. }
  243. }
  244. }
  245. //and check for an external edit
  246. if($revinfo !== false && $revinfo['date'] != $info['lastmod']) {
  247. // cached changelog line no longer valid
  248. $revinfo = false;
  249. $info['meta']['last_change'] = $revinfo;
  250. p_set_metadata($ID, array('last_change' => $revinfo));
  251. }
  252. if($revinfo !== false){
  253. $info['ip'] = $revinfo['ip'];
  254. $info['user'] = $revinfo['user'];
  255. $info['sum'] = $revinfo['sum'];
  256. // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID.
  257. // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor'].
  258. if($revinfo['user']) {
  259. $info['editor'] = $revinfo['user'];
  260. } else {
  261. $info['editor'] = $revinfo['ip'];
  262. }
  263. }else{
  264. $info['ip'] = null;
  265. $info['user'] = null;
  266. $info['sum'] = null;
  267. $info['editor'] = null;
  268. }
  269. // draft
  270. $draft = new \dokuwiki\Draft($ID, $info['client']);
  271. if ($draft->isDraftAvailable()) {
  272. $info['draft'] = $draft->getDraftFilename();
  273. }
  274. return $info;
  275. }
  276. /**
  277. * Initialize and/or fill global $JSINFO with some basic info to be given to javascript
  278. */
  279. function jsinfo() {
  280. global $JSINFO, $ID, $INFO, $ACT;
  281. if (!is_array($JSINFO)) {
  282. $JSINFO = [];
  283. }
  284. //export minimal info to JS, plugins can add more
  285. $JSINFO['id'] = $ID;
  286. $JSINFO['namespace'] = isset($INFO) ? (string) $INFO['namespace'] : '';
  287. $JSINFO['ACT'] = act_clean($ACT);
  288. $JSINFO['useHeadingNavigation'] = (int) useHeading('navigation');
  289. $JSINFO['useHeadingContent'] = (int) useHeading('content');
  290. }
  291. /**
  292. * Return information about the current media item as an associative array.
  293. *
  294. * @return array with info about current media item
  295. */
  296. function mediainfo(){
  297. global $NS;
  298. global $IMG;
  299. $info = basicinfo("$NS:*");
  300. $info['image'] = $IMG;
  301. return $info;
  302. }
  303. /**
  304. * Build an string of URL parameters
  305. *
  306. * @author Andreas Gohr
  307. *
  308. * @param array $params array with key-value pairs
  309. * @param string $sep series of pairs are separated by this character
  310. * @return string query string
  311. */
  312. function buildURLparams($params, $sep = '&amp;') {
  313. $url = '';
  314. $amp = false;
  315. foreach($params as $key => $val) {
  316. if($amp) $url .= $sep;
  317. $url .= rawurlencode($key).'=';
  318. $url .= rawurlencode((string) $val);
  319. $amp = true;
  320. }
  321. return $url;
  322. }
  323. /**
  324. * Build an string of html tag attributes
  325. *
  326. * Skips keys starting with '_', values get HTML encoded
  327. *
  328. * @author Andreas Gohr
  329. *
  330. * @param array $params array with (attribute name-attribute value) pairs
  331. * @param bool $skipEmptyStrings skip empty string values?
  332. * @return string
  333. */
  334. function buildAttributes($params, $skipEmptyStrings = false) {
  335. $url = '';
  336. $white = false;
  337. foreach($params as $key => $val) {
  338. if($key[0] == '_') continue;
  339. if($val === '' && $skipEmptyStrings) continue;
  340. if($white) $url .= ' ';
  341. $url .= $key.'="';
  342. $url .= htmlspecialchars($val);
  343. $url .= '"';
  344. $white = true;
  345. }
  346. return $url;
  347. }
  348. /**
  349. * This builds the breadcrumb trail and returns it as array
  350. *
  351. * @author Andreas Gohr <andi@splitbrain.org>
  352. *
  353. * @return string[] with the data: array(pageid=>name, ... )
  354. */
  355. function breadcrumbs() {
  356. // we prepare the breadcrumbs early for quick session closing
  357. static $crumbs = null;
  358. if($crumbs != null) return $crumbs;
  359. global $ID;
  360. global $ACT;
  361. global $conf;
  362. global $INFO;
  363. //first visit?
  364. $crumbs = isset($_SESSION[DOKU_COOKIE]['bc']) ? $_SESSION[DOKU_COOKIE]['bc'] : array();
  365. //we only save on show and existing visible readable wiki documents
  366. $file = wikiFN($ID);
  367. if($ACT != 'show' || $INFO['perm'] < AUTH_READ || isHiddenPage($ID) || !file_exists($file)) {
  368. $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
  369. return $crumbs;
  370. }
  371. // page names
  372. $name = noNSorNS($ID);
  373. if(useHeading('navigation')) {
  374. // get page title
  375. $title = p_get_first_heading($ID, METADATA_RENDER_USING_SIMPLE_CACHE);
  376. if($title) {
  377. $name = $title;
  378. }
  379. }
  380. //remove ID from array
  381. if(isset($crumbs[$ID])) {
  382. unset($crumbs[$ID]);
  383. }
  384. //add to array
  385. $crumbs[$ID] = $name;
  386. //reduce size
  387. while(count($crumbs) > $conf['breadcrumbs']) {
  388. array_shift($crumbs);
  389. }
  390. //save to session
  391. $_SESSION[DOKU_COOKIE]['bc'] = $crumbs;
  392. return $crumbs;
  393. }
  394. /**
  395. * Filter for page IDs
  396. *
  397. * This is run on a ID before it is outputted somewhere
  398. * currently used to replace the colon with something else
  399. * on Windows (non-IIS) systems and to have proper URL encoding
  400. *
  401. * See discussions at https://github.com/splitbrain/dokuwiki/pull/84 and
  402. * https://github.com/splitbrain/dokuwiki/pull/173 why we use a whitelist of
  403. * unaffected servers instead of blacklisting affected servers here.
  404. *
  405. * Urlencoding is ommitted when the second parameter is false
  406. *
  407. * @author Andreas Gohr <andi@splitbrain.org>
  408. *
  409. * @param string $id pageid being filtered
  410. * @param bool $ue apply urlencoding?
  411. * @return string
  412. */
  413. function idfilter($id, $ue = true) {
  414. global $conf;
  415. /* @var Input $INPUT */
  416. global $INPUT;
  417. if($conf['useslash'] && $conf['userewrite']) {
  418. $id = strtr($id, ':', '/');
  419. } elseif(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' &&
  420. $conf['userewrite'] &&
  421. strpos($INPUT->server->str('SERVER_SOFTWARE'), 'Microsoft-IIS') === false
  422. ) {
  423. $id = strtr($id, ':', ';');
  424. }
  425. if($ue) {
  426. $id = rawurlencode($id);
  427. $id = str_replace('%3A', ':', $id); //keep as colon
  428. $id = str_replace('%3B', ';', $id); //keep as semicolon
  429. $id = str_replace('%2F', '/', $id); //keep as slash
  430. }
  431. return $id;
  432. }
  433. /**
  434. * This builds a link to a wikipage
  435. *
  436. * It handles URL rewriting and adds additional parameters
  437. *
  438. * @author Andreas Gohr <andi@splitbrain.org>
  439. *
  440. * @param string $id page id, defaults to start page
  441. * @param string|array $urlParameters URL parameters, associative array recommended
  442. * @param bool $absolute request an absolute URL instead of relative
  443. * @param string $separator parameter separator
  444. * @return string
  445. */
  446. function wl($id = '', $urlParameters = '', $absolute = false, $separator = '&amp;') {
  447. global $conf;
  448. if(is_array($urlParameters)) {
  449. if(isset($urlParameters['rev']) && !$urlParameters['rev']) unset($urlParameters['rev']);
  450. if(isset($urlParameters['at']) && $conf['date_at_format']) {
  451. $urlParameters['at'] = date($conf['date_at_format'], $urlParameters['at']);
  452. }
  453. $urlParameters = buildURLparams($urlParameters, $separator);
  454. } else {
  455. $urlParameters = str_replace(',', $separator, $urlParameters);
  456. }
  457. if($id === '') {
  458. $id = $conf['start'];
  459. }
  460. $id = idfilter($id);
  461. if($absolute) {
  462. $xlink = DOKU_URL;
  463. } else {
  464. $xlink = DOKU_BASE;
  465. }
  466. if($conf['userewrite'] == 2) {
  467. $xlink .= DOKU_SCRIPT.'/'.$id;
  468. if($urlParameters) $xlink .= '?'.$urlParameters;
  469. } elseif($conf['userewrite']) {
  470. $xlink .= $id;
  471. if($urlParameters) $xlink .= '?'.$urlParameters;
  472. } elseif($id !== '') {
  473. $xlink .= DOKU_SCRIPT.'?id='.$id;
  474. if($urlParameters) $xlink .= $separator.$urlParameters;
  475. } else {
  476. $xlink .= DOKU_SCRIPT;
  477. if($urlParameters) $xlink .= '?'.$urlParameters;
  478. }
  479. return $xlink;
  480. }
  481. /**
  482. * This builds a link to an alternate page format
  483. *
  484. * Handles URL rewriting if enabled. Follows the style of wl().
  485. *
  486. * @author Ben Coburn <btcoburn@silicodon.net>
  487. * @param string $id page id, defaults to start page
  488. * @param string $format the export renderer to use
  489. * @param string|array $urlParameters URL parameters, associative array recommended
  490. * @param bool $abs request an absolute URL instead of relative
  491. * @param string $sep parameter separator
  492. * @return string
  493. */
  494. function exportlink($id = '', $format = 'raw', $urlParameters = '', $abs = false, $sep = '&amp;') {
  495. global $conf;
  496. if(is_array($urlParameters)) {
  497. $urlParameters = buildURLparams($urlParameters, $sep);
  498. } else {
  499. $urlParameters = str_replace(',', $sep, $urlParameters);
  500. }
  501. $format = rawurlencode($format);
  502. $id = idfilter($id);
  503. if($abs) {
  504. $xlink = DOKU_URL;
  505. } else {
  506. $xlink = DOKU_BASE;
  507. }
  508. if($conf['userewrite'] == 2) {
  509. $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format;
  510. if($urlParameters) $xlink .= $sep.$urlParameters;
  511. } elseif($conf['userewrite'] == 1) {
  512. $xlink .= '_export/'.$format.'/'.$id;
  513. if($urlParameters) $xlink .= '?'.$urlParameters;
  514. } else {
  515. $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id;
  516. if($urlParameters) $xlink .= $sep.$urlParameters;
  517. }
  518. return $xlink;
  519. }
  520. /**
  521. * Build a link to a media file
  522. *
  523. * Will return a link to the detail page if $direct is false
  524. *
  525. * The $more parameter should always be given as array, the function then
  526. * will strip default parameters to produce even cleaner URLs
  527. *
  528. * @param string $id the media file id or URL
  529. * @param mixed $more string or array with additional parameters
  530. * @param bool $direct link to detail page if false
  531. * @param string $sep URL parameter separator
  532. * @param bool $abs Create an absolute URL
  533. * @return string
  534. */
  535. function ml($id = '', $more = '', $direct = true, $sep = '&amp;', $abs = false) {
  536. global $conf;
  537. $isexternalimage = media_isexternal($id);
  538. if(!$isexternalimage) {
  539. $id = cleanID($id);
  540. }
  541. if(is_array($more)) {
  542. // add token for resized images
  543. if(!empty($more['w']) || !empty($more['h']) || $isexternalimage){
  544. $more['tok'] = media_get_token($id,$more['w'],$more['h']);
  545. }
  546. // strip defaults for shorter URLs
  547. if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
  548. if(empty($more['w'])) unset($more['w']);
  549. if(empty($more['h'])) unset($more['h']);
  550. if(isset($more['id']) && $direct) unset($more['id']);
  551. if(isset($more['rev']) && !$more['rev']) unset($more['rev']);
  552. $more = buildURLparams($more, $sep);
  553. } else {
  554. $matches = array();
  555. if (preg_match_all('/\b(w|h)=(\d*)\b/',$more,$matches,PREG_SET_ORDER) || $isexternalimage){
  556. $resize = array('w'=>0, 'h'=>0);
  557. foreach ($matches as $match){
  558. $resize[$match[1]] = $match[2];
  559. }
  560. $more .= $more === '' ? '' : $sep;
  561. $more .= 'tok='.media_get_token($id,$resize['w'],$resize['h']);
  562. }
  563. $more = str_replace('cache=cache', '', $more); //skip default
  564. $more = str_replace(',,', ',', $more);
  565. $more = str_replace(',', $sep, $more);
  566. }
  567. if($abs) {
  568. $xlink = DOKU_URL;
  569. } else {
  570. $xlink = DOKU_BASE;
  571. }
  572. // external URLs are always direct without rewriting
  573. if($isexternalimage) {
  574. $xlink .= 'lib/exe/fetch.php';
  575. $xlink .= '?'.$more;
  576. $xlink .= $sep.'media='.rawurlencode($id);
  577. return $xlink;
  578. }
  579. $id = idfilter($id);
  580. // decide on scriptname
  581. if($direct) {
  582. if($conf['userewrite'] == 1) {
  583. $script = '_media';
  584. } else {
  585. $script = 'lib/exe/fetch.php';
  586. }
  587. } else {
  588. if($conf['userewrite'] == 1) {
  589. $script = '_detail';
  590. } else {
  591. $script = 'lib/exe/detail.php';
  592. }
  593. }
  594. // build URL based on rewrite mode
  595. if($conf['userewrite']) {
  596. $xlink .= $script.'/'.$id;
  597. if($more) $xlink .= '?'.$more;
  598. } else {
  599. if($more) {
  600. $xlink .= $script.'?'.$more;
  601. $xlink .= $sep.'media='.$id;
  602. } else {
  603. $xlink .= $script.'?media='.$id;
  604. }
  605. }
  606. return $xlink;
  607. }
  608. /**
  609. * Returns the URL to the DokuWiki base script
  610. *
  611. * Consider using wl() instead, unless you absoutely need the doku.php endpoint
  612. *
  613. * @author Andreas Gohr <andi@splitbrain.org>
  614. *
  615. * @return string
  616. */
  617. function script() {
  618. return DOKU_BASE.DOKU_SCRIPT;
  619. }
  620. /**
  621. * Spamcheck against wordlist
  622. *
  623. * Checks the wikitext against a list of blocked expressions
  624. * returns true if the text contains any bad words
  625. *
  626. * Triggers COMMON_WORDBLOCK_BLOCKED
  627. *
  628. * Action Plugins can use this event to inspect the blocked data
  629. * and gain information about the user who was blocked.
  630. *
  631. * Event data:
  632. * data['matches'] - array of matches
  633. * data['userinfo'] - information about the blocked user
  634. * [ip] - ip address
  635. * [user] - username (if logged in)
  636. * [mail] - mail address (if logged in)
  637. * [name] - real name (if logged in)
  638. *
  639. * @author Andreas Gohr <andi@splitbrain.org>
  640. * @author Michael Klier <chi@chimeric.de>
  641. *
  642. * @param string $text - optional text to check, if not given the globals are used
  643. * @return bool - true if a spam word was found
  644. */
  645. function checkwordblock($text = '') {
  646. global $TEXT;
  647. global $PRE;
  648. global $SUF;
  649. global $SUM;
  650. global $conf;
  651. global $INFO;
  652. /* @var Input $INPUT */
  653. global $INPUT;
  654. if(!$conf['usewordblock']) return false;
  655. if(!$text) $text = "$PRE $TEXT $SUF $SUM";
  656. // we prepare the text a tiny bit to prevent spammers circumventing URL checks
  657. // phpcs:disable Generic.Files.LineLength.TooLong
  658. $text = preg_replace(
  659. '!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i',
  660. '\1http://\2 \2\3',
  661. $text
  662. );
  663. // phpcs:enable
  664. $wordblocks = getWordblocks();
  665. // how many lines to read at once (to work around some PCRE limits)
  666. if(version_compare(phpversion(), '4.3.0', '<')) {
  667. // old versions of PCRE define a maximum of parenthesises even if no
  668. // backreferences are used - the maximum is 99
  669. // this is very bad performancewise and may even be too high still
  670. $chunksize = 40;
  671. } else {
  672. // read file in chunks of 200 - this should work around the
  673. // MAX_PATTERN_SIZE in modern PCRE
  674. $chunksize = 200;
  675. }
  676. while($blocks = array_splice($wordblocks, 0, $chunksize)) {
  677. $re = array();
  678. // build regexp from blocks
  679. foreach($blocks as $block) {
  680. $block = preg_replace('/#.*$/', '', $block);
  681. $block = trim($block);
  682. if(empty($block)) continue;
  683. $re[] = $block;
  684. }
  685. if(count($re) && preg_match('#('.join('|', $re).')#si', $text, $matches)) {
  686. // prepare event data
  687. $data = array();
  688. $data['matches'] = $matches;
  689. $data['userinfo']['ip'] = $INPUT->server->str('REMOTE_ADDR');
  690. if($INPUT->server->str('REMOTE_USER')) {
  691. $data['userinfo']['user'] = $INPUT->server->str('REMOTE_USER');
  692. $data['userinfo']['name'] = $INFO['userinfo']['name'];
  693. $data['userinfo']['mail'] = $INFO['userinfo']['mail'];
  694. }
  695. $callback = function () {
  696. return true;
  697. };
  698. return Event::createAndTrigger('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true);
  699. }
  700. }
  701. return false;
  702. }
  703. /**
  704. * Return the IP of the client
  705. *
  706. * Honours X-Forwarded-For and X-Real-IP Proxy Headers
  707. *
  708. * It returns a comma separated list of IPs if the above mentioned
  709. * headers are set. If the single parameter is set, it tries to return
  710. * a routable public address, prefering the ones suplied in the X
  711. * headers
  712. *
  713. * @author Andreas Gohr <andi@splitbrain.org>
  714. *
  715. * @param boolean $single If set only a single IP is returned
  716. * @return string
  717. */
  718. function clientIP($single = false) {
  719. /* @var Input $INPUT */
  720. global $INPUT, $conf;
  721. $ip = array();
  722. $ip[] = $INPUT->server->str('REMOTE_ADDR');
  723. if($INPUT->server->str('HTTP_X_FORWARDED_FOR')) {
  724. $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))));
  725. }
  726. if($INPUT->server->str('HTTP_X_REAL_IP')) {
  727. $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP'))));
  728. }
  729. // some IPv4/v6 regexps borrowed from Feyd
  730. // see: http://forums.devnetwork.net/viewtopic.php?f=38&t=53479
  731. $dec_octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|[0-9])';
  732. $hex_digit = '[A-Fa-f0-9]';
  733. $h16 = "{$hex_digit}{1,4}";
  734. $IPv4Address = "$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet";
  735. $ls32 = "(?:$h16:$h16|$IPv4Address)";
  736. $IPv6Address =
  737. "(?:(?:{$IPv4Address})|(?:".
  738. "(?:$h16:){6}$ls32".
  739. "|::(?:$h16:){5}$ls32".
  740. "|(?:$h16)?::(?:$h16:){4}$ls32".
  741. "|(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32".
  742. "|(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32".
  743. "|(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32".
  744. "|(?:(?:$h16:){0,4}$h16)?::$ls32".
  745. "|(?:(?:$h16:){0,5}$h16)?::$h16".
  746. "|(?:(?:$h16:){0,6}$h16)?::".
  747. ")(?:\\/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))?)";
  748. // remove any non-IP stuff
  749. $cnt = count($ip);
  750. $match = array();
  751. for($i = 0; $i < $cnt; $i++) {
  752. if(preg_match("/^$IPv4Address$/", $ip[$i], $match) || preg_match("/^$IPv6Address$/", $ip[$i], $match)) {
  753. $ip[$i] = $match[0];
  754. } else {
  755. $ip[$i] = '';
  756. }
  757. if(empty($ip[$i])) unset($ip[$i]);
  758. }
  759. $ip = array_values(array_unique($ip));
  760. if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
  761. if(!$single) return join(',', $ip);
  762. // skip trusted local addresses
  763. foreach($ip as $i) {
  764. if(!empty($conf['trustedproxy']) && preg_match('/'.$conf['trustedproxy'].'/', $i)) {
  765. continue;
  766. } else {
  767. return $i;
  768. }
  769. }
  770. // still here? just use the last address
  771. // this case all ips in the list are trusted
  772. return $ip[count($ip)-1];
  773. }
  774. /**
  775. * Check if the browser is on a mobile device
  776. *
  777. * Adapted from the example code at url below
  778. *
  779. * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code
  780. *
  781. * @deprecated 2018-04-27 you probably want media queries instead anyway
  782. * @return bool if true, client is mobile browser; otherwise false
  783. */
  784. function clientismobile() {
  785. /* @var Input $INPUT */
  786. global $INPUT;
  787. if($INPUT->server->has('HTTP_X_WAP_PROFILE')) return true;
  788. if(preg_match('/wap\.|\.wap/i', $INPUT->server->str('HTTP_ACCEPT'))) return true;
  789. if(!$INPUT->server->has('HTTP_USER_AGENT')) return false;
  790. $uamatches = join(
  791. '|',
  792. [
  793. 'midp', 'j2me', 'avantg', 'docomo', 'novarra', 'palmos', 'palmsource', '240x320', 'opwv',
  794. 'chtml', 'pda', 'windows ce', 'mmp\/', 'blackberry', 'mib\/', 'symbian', 'wireless', 'nokia',
  795. 'hand', 'mobi', 'phone', 'cdm', 'up\.b', 'audio', 'SIE\-', 'SEC\-', 'samsung', 'HTC', 'mot\-',
  796. 'mitsu', 'sagem', 'sony', 'alcatel', 'lg', 'erics', 'vx', 'NEC', 'philips', 'mmm', 'xx',
  797. 'panasonic', 'sharp', 'wap', 'sch', 'rover', 'pocket', 'benq', 'java', 'pt', 'pg', 'vox',
  798. 'amoi', 'bird', 'compal', 'kg', 'voda', 'sany', 'kdd', 'dbt', 'sendo', 'sgh', 'gradi', 'jb',
  799. '\d\d\di', 'moto'
  800. ]
  801. );
  802. if(preg_match("/$uamatches/i", $INPUT->server->str('HTTP_USER_AGENT'))) return true;
  803. return false;
  804. }
  805. /**
  806. * check if a given link is interwiki link
  807. *
  808. * @param string $link the link, e.g. "wiki>page"
  809. * @return bool
  810. */
  811. function link_isinterwiki($link){
  812. if (preg_match('/^[a-zA-Z0-9\.]+>/u',$link)) return true;
  813. return false;
  814. }
  815. /**
  816. * Convert one or more comma separated IPs to hostnames
  817. *
  818. * If $conf['dnslookups'] is disabled it simply returns the input string
  819. *
  820. * @author Glen Harris <astfgl@iamnota.org>
  821. *
  822. * @param string $ips comma separated list of IP addresses
  823. * @return string a comma separated list of hostnames
  824. */
  825. function gethostsbyaddrs($ips) {
  826. global $conf;
  827. if(!$conf['dnslookups']) return $ips;
  828. $hosts = array();
  829. $ips = explode(',', $ips);
  830. if(is_array($ips)) {
  831. foreach($ips as $ip) {
  832. $hosts[] = gethostbyaddr(trim($ip));
  833. }
  834. return join(',', $hosts);
  835. } else {
  836. return gethostbyaddr(trim($ips));
  837. }
  838. }
  839. /**
  840. * Checks if a given page is currently locked.
  841. *
  842. * removes stale lockfiles
  843. *
  844. * @author Andreas Gohr <andi@splitbrain.org>
  845. *
  846. * @param string $id page id
  847. * @return bool page is locked?
  848. */
  849. function checklock($id) {
  850. global $conf;
  851. /* @var Input $INPUT */
  852. global $INPUT;
  853. $lock = wikiLockFN($id);
  854. //no lockfile
  855. if(!file_exists($lock)) return false;
  856. //lockfile expired
  857. if((time() - filemtime($lock)) > $conf['locktime']) {
  858. @unlink($lock);
  859. return false;
  860. }
  861. //my own lock
  862. @list($ip, $session) = explode("\n", io_readFile($lock));
  863. if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || (session_id() && $session == session_id())) {
  864. return false;
  865. }
  866. return $ip;
  867. }
  868. /**
  869. * Lock a page for editing
  870. *
  871. * @author Andreas Gohr <andi@splitbrain.org>
  872. *
  873. * @param string $id page id to lock
  874. */
  875. function lock($id) {
  876. global $conf;
  877. /* @var Input $INPUT */
  878. global $INPUT;
  879. if($conf['locktime'] == 0) {
  880. return;
  881. }
  882. $lock = wikiLockFN($id);
  883. if($INPUT->server->str('REMOTE_USER')) {
  884. io_saveFile($lock, $INPUT->server->str('REMOTE_USER'));
  885. } else {
  886. io_saveFile($lock, clientIP()."\n".session_id());
  887. }
  888. }
  889. /**
  890. * Unlock a page if it was locked by the user
  891. *
  892. * @author Andreas Gohr <andi@splitbrain.org>
  893. *
  894. * @param string $id page id to unlock
  895. * @return bool true if a lock was removed
  896. */
  897. function unlock($id) {
  898. /* @var Input $INPUT */
  899. global $INPUT;
  900. $lock = wikiLockFN($id);
  901. if(file_exists($lock)) {
  902. @list($ip, $session) = explode("\n", io_readFile($lock));
  903. if($ip == $INPUT->server->str('REMOTE_USER') || $ip == clientIP() || $session == session_id()) {
  904. @unlink($lock);
  905. return true;
  906. }
  907. }
  908. return false;
  909. }
  910. /**
  911. * convert line ending to unix format
  912. *
  913. * also makes sure the given text is valid UTF-8
  914. *
  915. * @see formText() for 2crlf conversion
  916. * @author Andreas Gohr <andi@splitbrain.org>
  917. *
  918. * @param string $text
  919. * @return string
  920. */
  921. function cleanText($text) {
  922. $text = preg_replace("/(\015\012)|(\015)/", "\012", $text);
  923. // if the text is not valid UTF-8 we simply assume latin1
  924. // this won't break any worse than it breaks with the wrong encoding
  925. // but might actually fix the problem in many cases
  926. if(!\dokuwiki\Utf8\Clean::isUtf8($text)) $text = utf8_encode($text);
  927. return $text;
  928. }
  929. /**
  930. * Prepares text for print in Webforms by encoding special chars.
  931. * It also converts line endings to Windows format which is
  932. * pseudo standard for webforms.
  933. *
  934. * @see cleanText() for 2unix conversion
  935. * @author Andreas Gohr <andi@splitbrain.org>
  936. *
  937. * @param string $text
  938. * @return string
  939. */
  940. function formText($text) {
  941. $text = str_replace("\012", "\015\012", $text);
  942. return htmlspecialchars($text);
  943. }
  944. /**
  945. * Returns the specified local text in raw format
  946. *
  947. * @author Andreas Gohr <andi@splitbrain.org>
  948. *
  949. * @param string $id page id
  950. * @param string $ext extension of file being read, default 'txt'
  951. * @return string
  952. */
  953. function rawLocale($id, $ext = 'txt') {
  954. return io_readFile(localeFN($id, $ext));
  955. }
  956. /**
  957. * Returns the raw WikiText
  958. *
  959. * @author Andreas Gohr <andi@splitbrain.org>
  960. *
  961. * @param string $id page id
  962. * @param string|int $rev timestamp when a revision of wikitext is desired
  963. * @return string
  964. */
  965. function rawWiki($id, $rev = '') {
  966. return io_readWikiPage(wikiFN($id, $rev), $id, $rev);
  967. }
  968. /**
  969. * Returns the pagetemplate contents for the ID's namespace
  970. *
  971. * @triggers COMMON_PAGETPL_LOAD
  972. * @author Andreas Gohr <andi@splitbrain.org>
  973. *
  974. * @param string $id the id of the page to be created
  975. * @return string parsed pagetemplate content
  976. */
  977. function pageTemplate($id) {
  978. global $conf;
  979. if(is_array($id)) $id = $id[0];
  980. // prepare initial event data
  981. $data = array(
  982. 'id' => $id, // the id of the page to be created
  983. 'tpl' => '', // the text used as template
  984. 'tplfile' => '', // the file above text was/should be loaded from
  985. 'doreplace' => true // should wildcard replacements be done on the text?
  986. );
  987. $evt = new Event('COMMON_PAGETPL_LOAD', $data);
  988. if($evt->advise_before(true)) {
  989. // the before event might have loaded the content already
  990. if(empty($data['tpl'])) {
  991. // if the before event did not set a template file, try to find one
  992. if(empty($data['tplfile'])) {
  993. $path = dirname(wikiFN($id));
  994. if(file_exists($path.'/_template.txt')) {
  995. $data['tplfile'] = $path.'/_template.txt';
  996. } else {
  997. // search upper namespaces for templates
  998. $len = strlen(rtrim($conf['datadir'], '/'));
  999. while(strlen($path) >= $len) {
  1000. if(file_exists($path.'/__template.txt')) {
  1001. $data['tplfile'] = $path.'/__template.txt';
  1002. break;
  1003. }
  1004. $path = substr($path, 0, strrpos($path, '/'));
  1005. }
  1006. }
  1007. }
  1008. // load the content
  1009. $data['tpl'] = io_readFile($data['tplfile']);
  1010. }
  1011. if($data['doreplace']) parsePageTemplate($data);
  1012. }
  1013. $evt->advise_after();
  1014. unset($evt);
  1015. return $data['tpl'];
  1016. }
  1017. /**
  1018. * Performs common page template replacements
  1019. * This works on data from COMMON_PAGETPL_LOAD
  1020. *
  1021. * @author Andreas Gohr <andi@splitbrain.org>
  1022. *
  1023. * @param array $data array with event data
  1024. * @return string
  1025. */
  1026. function parsePageTemplate(&$data) {
  1027. /**
  1028. * @var string $id the id of the page to be created
  1029. * @var string $tpl the text used as template
  1030. * @var string $tplfile the file above text was/should be loaded from
  1031. * @var bool $doreplace should wildcard replacements be done on the text?
  1032. */
  1033. extract($data);
  1034. global $USERINFO;
  1035. global $conf;
  1036. /* @var Input $INPUT */
  1037. global $INPUT;
  1038. // replace placeholders
  1039. $file = noNS($id);
  1040. $page = strtr($file, $conf['sepchar'], ' ');
  1041. $tpl = str_replace(
  1042. array(
  1043. '@ID@',
  1044. '@NS@',
  1045. '@CURNS@',
  1046. '@!CURNS@',
  1047. '@!!CURNS@',
  1048. '@!CURNS!@',
  1049. '@FILE@',
  1050. '@!FILE@',
  1051. '@!FILE!@',
  1052. '@PAGE@',
  1053. '@!PAGE@',
  1054. '@!!PAGE@',
  1055. '@!PAGE!@',
  1056. '@USER@',
  1057. '@NAME@',
  1058. '@MAIL@',
  1059. '@DATE@',
  1060. ),
  1061. array(
  1062. $id,
  1063. getNS($id),
  1064. curNS($id),
  1065. \dokuwiki\Utf8\PhpString::ucfirst(curNS($id)),
  1066. \dokuwiki\Utf8\PhpString::ucwords(curNS($id)),
  1067. \dokuwiki\Utf8\PhpString::strtoupper(curNS($id)),
  1068. $file,
  1069. \dokuwiki\Utf8\PhpString::ucfirst($file),
  1070. \dokuwiki\Utf8\PhpString::strtoupper($file),
  1071. $page,
  1072. \dokuwiki\Utf8\PhpString::ucfirst($page),
  1073. \dokuwiki\Utf8\PhpString::ucwords($page),
  1074. \dokuwiki\Utf8\PhpString::strtoupper($page),
  1075. $INPUT->server->str('REMOTE_USER'),
  1076. $USERINFO ? $USERINFO['name'] : '',
  1077. $USERINFO ? $USERINFO['mail'] : '',
  1078. $conf['dformat'],
  1079. ), $tpl
  1080. );
  1081. // we need the callback to work around strftime's char limit
  1082. $tpl = preg_replace_callback(
  1083. '/%./',
  1084. function ($m) {
  1085. return strftime($m[0]);
  1086. },
  1087. $tpl
  1088. );
  1089. $data['tpl'] = $tpl;
  1090. return $tpl;
  1091. }
  1092. /**
  1093. * Returns the raw Wiki Text in three slices.
  1094. *
  1095. * The range parameter needs to have the form "from-to"
  1096. * and gives the range of the section in bytes - no
  1097. * UTF-8 awareness is needed.
  1098. * The returned order is prefix, section and suffix.
  1099. *
  1100. * @author Andreas Gohr <andi@splitbrain.org>
  1101. *
  1102. * @param string $range in form "from-to"
  1103. * @param string $id page id
  1104. * @param string $rev optional, the revision timestamp
  1105. * @return string[] with three slices
  1106. */
  1107. function rawWikiSlices($range, $id, $rev = '') {
  1108. $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev);
  1109. // Parse range
  1110. list($from, $to) = explode('-', $range, 2);
  1111. // Make range zero-based, use defaults if marker is missing
  1112. $from = !$from ? 0 : ($from - 1);
  1113. $to = !$to ? strlen($text) : ($to - 1);
  1114. $slices = array();
  1115. $slices[0] = substr($text, 0, $from);
  1116. $slices[1] = substr($text, $from, $to - $from);
  1117. $slices[2] = substr($text, $to);
  1118. return $slices;
  1119. }
  1120. /**
  1121. * Joins wiki text slices
  1122. *
  1123. * function to join the text slices.
  1124. * When the pretty parameter is set to true it adds additional empty
  1125. * lines between sections if needed (used on saving).
  1126. *
  1127. * @author Andreas Gohr <andi@splitbrain.org>
  1128. *
  1129. * @param string $pre prefix
  1130. * @param string $text text in the middle
  1131. * @param string $suf suffix
  1132. * @param bool $pretty add additional empty lines between sections
  1133. * @return string
  1134. */
  1135. function con($pre, $text, $suf, $pretty = false) {
  1136. if($pretty) {
  1137. if($pre !== '' && substr($pre, -1) !== "\n" &&
  1138. substr($text, 0, 1) !== "\n"
  1139. ) {
  1140. $pre .= "\n";
  1141. }
  1142. if($suf !== '' && substr($text, -1) !== "\n" &&
  1143. substr($suf, 0, 1) !== "\n"
  1144. ) {
  1145. $text .= "\n";
  1146. }
  1147. }
  1148. return $pre.$text.$suf;
  1149. }
  1150. /**
  1151. * Checks if the current page version is newer than the last entry in the page's
  1152. * changelog. If so, we assume it has been an external edit and we create an
  1153. * attic copy and add a proper changelog line.
  1154. *
  1155. * This check is only executed when the page is about to be saved again from the
  1156. * wiki, triggered in @see saveWikiText()
  1157. *
  1158. * @param string $id the page ID
  1159. */
  1160. function detectExternalEdit($id) {
  1161. global $lang;
  1162. $fileLastMod = wikiFN($id);
  1163. $lastMod = @filemtime($fileLastMod); // from page
  1164. $pagelog = new PageChangeLog($id, 1024);
  1165. $lastRev = $pagelog->getRevisions(-1, 1); // from changelog
  1166. $lastRev = (int) (empty($lastRev) ? 0 : $lastRev[0]);
  1167. if(!file_exists(wikiFN($id, $lastMod)) && file_exists($fileLastMod) && $lastMod >= $lastRev) {
  1168. // add old revision to the attic if missing
  1169. saveOldRevision($id);
  1170. // add a changelog entry if this edit came from outside dokuwiki
  1171. if($lastMod > $lastRev) {
  1172. $fileLastRev = wikiFN($id, $lastRev);
  1173. $revinfo = $pagelog->getRevisionInfo($lastRev);
  1174. if(empty($lastRev) || !file_exists($fileLastRev) || $revinfo['type'] == DOKU_CHANGE_TYPE_DELETE) {
  1175. $filesize_old = 0;
  1176. } else {
  1177. $filesize_old = io_getSizeFile($fileLastRev);
  1178. }
  1179. $filesize_new = filesize($fileLastMod);
  1180. $sizechange = $filesize_new - $filesize_old;
  1181. addLogEntry(
  1182. $lastMod,
  1183. $id,
  1184. DOKU_CHANGE_TYPE_EDIT,
  1185. $lang['external_edit'],
  1186. '',
  1187. array('ExternalEdit' => true),
  1188. $sizechange
  1189. );
  1190. // remove soon to be stale instructions
  1191. $cache = new CacheInstructions($id, $fileLastMod);
  1192. $cache->removeCache();
  1193. }
  1194. }
  1195. }
  1196. /**
  1197. * Saves a wikitext by calling io_writeWikiPage.
  1198. * Also directs changelog and attic updates.
  1199. *
  1200. * @author Andreas Gohr <andi@splitbrain.org>
  1201. * @author Ben Coburn <btcoburn@silicodon.net>
  1202. *
  1203. * @param string $id page id
  1204. * @param string $text wikitext being saved
  1205. * @param string $summary summary of text update
  1206. * @param bool $minor mark this saved version as minor update
  1207. */
  1208. function saveWikiText($id, $text, $summary, $minor = false) {
  1209. /* Note to developers:
  1210. This code is subtle and delicate. Test the behavior of
  1211. the attic and changelog with dokuwiki and external edits
  1212. after any changes. External edits change the wiki page
  1213. directly without using php or dokuwiki.
  1214. */
  1215. global $conf;
  1216. global $lang;
  1217. global $REV;
  1218. /* @var Input $INPUT */
  1219. global $INPUT;
  1220. // prepare data for event
  1221. $svdta = array();
  1222. $svdta['id'] = $id;
  1223. $svdta['file'] = wikiFN($id);
  1224. $svdta['revertFrom'] = $REV;
  1225. $svdta['oldRevision'] = @filemtime($svdta['file']);
  1226. $svdta['newRevision'] = 0;
  1227. $svdta['newContent'] = $text;
  1228. $svdta['oldContent'] = rawWiki($id);
  1229. $svdta['summary'] = $summary;
  1230. $svdta['contentChanged'] = ($svdta['newContent'] != $svdta['oldContent']);
  1231. $svdta['changeInfo'] = '';
  1232. $svdta['changeType'] = DOKU_CHANGE_TYPE_EDIT;
  1233. $svdta['sizechange'] = null;
  1234. // select changelog line type
  1235. if($REV) {
  1236. $svdta['changeType'] = DOKU_CHANGE_TYPE_REVERT;
  1237. $svdta['changeInfo'] = $REV;
  1238. } else if(!file_exists($svdta['file'])) {
  1239. $svdta['changeType'] = DOKU_CHANGE_TYPE_CREATE;
  1240. } else if(trim($text) == '') {
  1241. // empty or whitespace only content deletes
  1242. $svdta['changeType'] = DOKU_CHANGE_TYPE_DELETE;
  1243. // autoset summary on deletion
  1244. if(blank($svdta['summary'])) {
  1245. $svdta['summary'] = $lang['deleted'];
  1246. }
  1247. } else if($minor && $conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
  1248. //minor edits only for logged in users
  1249. $svdta['changeType'] = DOKU_CHANGE_TYPE_MINOR_EDIT;
  1250. }
  1251. $event = new Event('COMMON_WIKIPAGE_SAVE', $svdta);
  1252. if(!$event->advise_before()) return;
  1253. // if the content has not been changed, no save happens (plugins may override this)
  1254. if(!$svdta['contentChanged']) return;
  1255. detectExternalEdit($id);
  1256. if(
  1257. $svdta['changeType'] == DOKU_CHANGE_TYPE_CREATE ||
  1258. ($svdta['changeType'] == DOKU_CHANGE_TYPE_REVERT && !file_exists($svdta['file']))
  1259. ) {
  1260. $filesize_old = 0;
  1261. } else {
  1262. $filesize_old = filesize($svdta['file']);
  1263. }
  1264. if($svdta['changeType'] == DOKU_CHANGE_TYPE_DELETE) {
  1265. // Send "update" event with empty data, so plugins can react to page deletion
  1266. $data = array(array($svdta['file'], '', false), getNS($id), noNS($id), false);
  1267. Event::createAndTrigger('IO_WIKIPAGE_WRITE', $data);
  1268. // pre-save deleted revision
  1269. @touch($svdta['file']);
  1270. clearstatcache();
  1271. $svdta['newRevision'] = saveOldRevision($id);
  1272. // remove empty file
  1273. @unlink($svdta['file']);
  1274. $filesize_new = 0;
  1275. // don't remove old meta info as it should be saved, plugins can use
  1276. // IO_WIKIPAGE_WRITE for removing their metadata...
  1277. // purge non-persistant meta data
  1278. p_purge_metadata($id);
  1279. // remove empty namespaces
  1280. io_sweepNS($id, 'datadir');
  1281. io_sweepNS($id, 'mediadir');
  1282. } else {
  1283. // save file (namespace dir is created in io_writeWikiPage)
  1284. io_writeWikiPage($svdta['file'], $svdta['newContent'], $id);
  1285. // pre-save the revision, to keep the attic in sync
  1286. $svdta['newRevision'] = saveOldRevision($id);
  1287. $filesize_new = filesize($svdta['file']);
  1288. }
  1289. $svdta['sizechange'] = $filesize_new - $filesize_old;
  1290. $event->advise_after();
  1291. addLogEntry(
  1292. $svdta['newRevision'],
  1293. $svdta['id'],
  1294. $svdta['changeType'],
  1295. $svdta['summary'],
  1296. $svdta['changeInfo'],
  1297. null,
  1298. $svdta['sizechange']
  1299. );
  1300. // send notify mails
  1301. notify($svdta['id'], 'admin', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
  1302. notify($svdta['id'], 'subscribers', $svdta['oldRevision'], $svdta['summary'], $minor, $svdta['newRevision']);
  1303. // update the purgefile (timestamp of the last time anything within the wiki was changed)
  1304. io_saveFile($conf['cachedir'].'/purgefile', time());
  1305. // if useheading is enabled, purge the cache of all linking pages
  1306. if(useHeading('content')) {
  1307. $pages = ft_backlinks($id, true);
  1308. foreach($pages as $page) {
  1309. $cache = new CacheRenderer($page, wikiFN($page), 'xhtml');
  1310. $cache->removeCache();
  1311. }
  1312. }
  1313. }
  1314. /**
  1315. * moves the current version to the attic and returns its
  1316. * revision date
  1317. *
  1318. * @author Andreas Gohr <andi@splitbrain.org>
  1319. *
  1320. * @param string $id page id
  1321. * @return int|string revision timestamp
  1322. */
  1323. function saveOldRevision($id) {
  1324. $oldf = wikiFN($id);
  1325. if(!file_exists($oldf)) return '';
  1326. $date = filemtime($oldf);
  1327. $newf = wikiFN($id, $date);
  1328. io_writeWikiPage($newf, rawWiki($id), $id, $date);
  1329. return $date;
  1330. }
  1331. /**
  1332. * Sends a notify mail on page change or registration
  1333. *
  1334. * @param string $id The changed page
  1335. * @param string $who Who to notify (admin|subscribers|register)
  1336. * @param int|string $rev Old page revision
  1337. * @param string $summary What changed
  1338. * @param boolean $minor Is this a minor edit?
  1339. * @param string[] $replace Additional string substitutions, @KEY@ to be replaced by value
  1340. * @param int|string $current_rev New page revision
  1341. * @return bool
  1342. *
  1343. * @author Andreas Gohr <andi@splitbrain.org>
  1344. */
  1345. function notify($id, $who, $rev = '', $summary = '', $minor = false, $replace = array(), $current_rev = false) {
  1346. global $conf;
  1347. /* @var Input $INPUT */
  1348. global $INPUT;
  1349. // decide if there is something to do, eg. whom to mail
  1350. if($who == 'admin') {
  1351. if(empty($conf['notify'])) return false; //notify enabled?
  1352. $tpl = 'mailtext';
  1353. $to = $conf['notify'];
  1354. } elseif($who == 'subscribers') {
  1355. if(!actionOK('subscribe')) return false; //subscribers enabled?
  1356. if($conf['useacl'] && $INPUT->server->str('REMOTE_USER') && $minor) return false; //skip minors
  1357. $data = array('id' => $id, 'addresslist' => '', 'self' => false, 'replacements' => $replace);
  1358. Event::createAndTrigger(
  1359. 'COMMON_NOTIFY_ADDRESSLIST', $data,
  1360. array(new SubscriberManager(), 'notifyAddresses')
  1361. );
  1362. $to = $data['addresslist'];
  1363. if(empty($to)) return false;
  1364. $tpl = 'subscr_single';
  1365. } else {
  1366. return false; //just to be safe
  1367. }
  1368. // prepare content
  1369. $subscription = new PageSubscriptionSender();
  1370. return $subscription->sendPageDiff($to, $tpl, $id, $rev, $summary, $current_rev);
  1371. }
  1372. /**
  1373. * extracts the query from a search engine referrer
  1374. *
  1375. * @author Andreas Gohr <andi@splitbrain.org>
  1376. * @author Todd Augsburger <todd@rollerorgans.com>
  1377. *
  1378. * @return array|string
  1379. */
  1380. function getGoogleQuery() {
  1381. /* @var Input $INPUT */
  1382. global $INPUT;
  1383. if(!$INPUT->server->has('HTTP_REFERER')) {
  1384. return '';
  1385. }
  1386. $url = parse_url($INPUT->server->str('HTTP_REFERER'));
  1387. // only handle common SEs
  1388. if(!preg_match('/(google|bing|yahoo|ask|duckduckgo|babylon|aol|yandex)/',$url['host'])) return '';
  1389. $query = array();
  1390. parse_str($url['query'], $query);
  1391. $q = '';
  1392. if(isset($query['q'])){
  1393. $q = $query['q'];
  1394. }elseif(isset($query['p'])){
  1395. $q = $query['p'];
  1396. }elseif(isset($query['query'])){
  1397. $q = $query['query'];
  1398. }
  1399. $q = trim($q);
  1400. if(!$q) return '';
  1401. // ignore if query includes a full URL
  1402. if(strpos($q, '//') !== false) return '';
  1403. $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/', $q, -1, PREG_SPLIT_NO_EMPTY);
  1404. return $q;
  1405. }
  1406. /**
  1407. * Return the human readable size of a file
  1408. *
  1409. * @param int $size A file size
  1410. * @param int $dec A number of decimal places
  1411. * @return string human readable size
  1412. *
  1413. * @author Martin Benjamin <b.martin@cybernet.ch>
  1414. * @author Aidan Lister <aidan@php.net>
  1415. * @version 1.0.0
  1416. */
  1417. function filesize_h($size, $dec = 1) {
  1418. $sizes = array('B', 'KB', 'MB', 'GB');
  1419. $count = count($sizes);
  1420. $i = 0;
  1421. while($size >= 1024 && ($i < $count - 1)) {
  1422. $size /= 1024;
  1423. $i++;
  1424. }
  1425. return round($size, $dec)."\xC2\xA0".$sizes[$i]; //non-breaking space
  1426. }
  1427. /**
  1428. * Return the given timestamp as human readable, fuzzy age
  1429. *
  1430. * @author Andreas Gohr <gohr@cosmocode.de>
  1431. *
  1432. * @param int $dt timestamp
  1433. * @return string
  1434. */
  1435. function datetime_h($dt) {
  1436. global $lang;
  1437. $ago = time() - $dt;
  1438. if($ago > 24 * 60 * 60 * 30 * 12 * 2) {
  1439. return sprintf($lang['years'], round($ago / (24 * 60 * 60 * 30 * 12)));
  1440. }
  1441. if($ago > 24 * 60 * 60 * 30 * 2) {
  1442. return sprintf($lang['months'], round($ago / (24 * 60 * 60 * 30)));
  1443. }
  1444. if($ago > 24 * 60 * 60 * 7 * 2) {
  1445. return sprintf($lang['weeks'], round($ago / (24 * 60 * 60 * 7)));
  1446. }
  1447. if($ago > 24 * 60 * 60 * 2) {
  1448. return sprintf($lang['days'], round($ago / (24 * 60 * 60)));
  1449. }
  1450. if($ago > 60 * 60 * 2) {
  1451. return sprintf($lang['hours'], round($ago / (60 * 60)));
  1452. }
  1453. if($ago > 60 * 2) {
  1454. return sprintf($lang['minutes'], round($ago / (60)));
  1455. }
  1456. return sprintf($lang['seconds'], $ago);
  1457. }
  1458. /**
  1459. * Wraps around strftime but provides support for fuzzy dates
  1460. *
  1461. * The format default to $conf['dformat']. It is passed to
  1462. * strftime - %f can be used to get the value from datetime_h()
  1463. *
  1464. * @see datetime_h
  1465. * @author Andreas Gohr <gohr@cosmocode.de>
  1466. *
  1467. * @param int|null $dt timestamp when given, null will take current timestamp
  1468. * @param string $forma…

Large files files are truncated, but you can click here to view the full file