feed.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <?php
  2. /**
  3. * XML feed export
  4. *
  5. * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
  6. * @author Andreas Gohr <andi@splitbrain.org>
  7. *
  8. * @global array $conf
  9. * @global Input $INPUT
  10. */
  11. use dokuwiki\Cache\Cache;
  12. use dokuwiki\ChangeLog\MediaChangeLog;
  13. use dokuwiki\ChangeLog\PageChangeLog;
  14. use dokuwiki\Extension\AuthPlugin;
  15. use dokuwiki\Extension\Event;
  16. if (!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/');
  17. require_once(DOKU_INC . 'inc/init.php');
  18. //close session
  19. session_write_close();
  20. //feed disabled?
  21. if (!actionOK('rss')) {
  22. http_status(404);
  23. echo '<error>RSS feed is disabled.</error>';
  24. exit;
  25. }
  26. // get params
  27. $opt = rss_parseOptions();
  28. // the feed is dynamic - we need a cache for each combo
  29. // (but most people just use the default feed so it's still effective)
  30. $key = join('', array_values($opt)) . '$' . $INPUT->server->str('REMOTE_USER')
  31. . '$' . $INPUT->server->str('HTTP_HOST') . $INPUT->server->str('SERVER_PORT');
  32. $cache = new Cache($key, '.feed');
  33. // prepare cache depends
  34. $depends['files'] = getConfigFiles('main');
  35. $depends['age'] = $conf['rss_update'];
  36. $depends['purge'] = $INPUT->bool('purge');
  37. // check cacheage and deliver if nothing has changed since last
  38. // time or the update interval has not passed, also handles conditional requests
  39. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  40. header('Pragma: public');
  41. header('Content-Type: application/xml; charset=utf-8');
  42. header('X-Robots-Tag: noindex');
  43. if ($cache->useCache($depends)) {
  44. http_conditionalRequest($cache->getTime());
  45. if ($conf['allowdebug']) header("X-CacheUsed: $cache->cache");
  46. print $cache->retrieveCache();
  47. exit;
  48. } else {
  49. http_conditionalRequest(time());
  50. }
  51. // create new feed
  52. $rss = new UniversalFeedCreator();
  53. $rss->title = $conf['title'] . (($opt['namespace']) ? ' ' . $opt['namespace'] : '');
  54. $rss->link = DOKU_URL;
  55. $rss->syndicationURL = DOKU_URL . 'feed.php';
  56. $rss->cssStyleSheet = DOKU_URL . 'lib/exe/css.php?s=feed';
  57. $image = new FeedImage();
  58. $image->title = $conf['title'];
  59. $image->url = tpl_getMediaFile([':wiki:favicon.ico', ':favicon.ico', 'images/favicon.ico'], true);
  60. $image->link = DOKU_URL;
  61. $rss->image = $image;
  62. $data = null;
  63. $modes = [
  64. 'list' => 'rssListNamespace',
  65. 'search' => 'rssSearch',
  66. 'recent' => 'rssRecentChanges'
  67. ];
  68. if (isset($modes[$opt['feed_mode']])) {
  69. $data = $modes[$opt['feed_mode']]($opt);
  70. } else {
  71. $eventData = [
  72. 'opt' => &$opt,
  73. 'data' => &$data,
  74. ];
  75. $event = new Event('FEED_MODE_UNKNOWN', $eventData);
  76. if ($event->advise_before(true)) {
  77. echo sprintf('<error>Unknown feed mode %s</error>', hsc($opt['feed_mode']));
  78. exit;
  79. }
  80. $event->advise_after();
  81. }
  82. rss_buildItems($rss, $data, $opt);
  83. $feed = $rss->createFeed($opt['feed_type']);
  84. // save cachefile
  85. $cache->storeCache($feed);
  86. // finally deliver
  87. print $feed;
  88. // ---------------------------------------------------------------- //
  89. /**
  90. * Get URL parameters and config options and return an initialized option array
  91. *
  92. * @author Andreas Gohr <andi@splitbrain.org>
  93. */
  94. function rss_parseOptions()
  95. {
  96. global $conf;
  97. global $INPUT;
  98. $opt = [];
  99. foreach (
  100. [
  101. // Basic feed properties
  102. // Plugins may probably want to add new values to these
  103. // properties for implementing own feeds
  104. // One of: list, search, recent
  105. 'feed_mode' => ['str', 'mode', 'recent'],
  106. // One of: diff, page, rev, current
  107. 'link_to' => ['str', 'linkto', $conf['rss_linkto']],
  108. // One of: abstract, diff, htmldiff, html
  109. 'item_content' => ['str', 'content', $conf['rss_content']],
  110. // Special feed properties
  111. // These are only used by certain feed_modes
  112. // String, used for feed title, in list and rc mode
  113. 'namespace' => ['str', 'ns', null],
  114. // Positive integer, only used in rc mode
  115. 'items' => ['int', 'num', $conf['recent']],
  116. // Boolean, only used in rc mode
  117. 'show_minor' => ['bool', 'minor', false],
  118. // Boolean, only used in rc mode
  119. 'only_new' => ['bool', 'onlynewpages', false],
  120. // String, only used in list mode
  121. 'sort' => ['str', 'sort', 'natural'],
  122. // String, only used in search mode
  123. 'search_query' => ['str', 'q', null],
  124. // One of: pages, media, both
  125. 'content_type' => ['str', 'view', $conf['rss_media']]
  126. ] as $name => $val
  127. ) {
  128. $opt[$name] = $INPUT->{$val[0]}($val[1], $val[2], true);
  129. }
  130. $opt['items'] = max(0, (int) $opt['items']);
  131. $opt['show_minor'] = (bool) $opt['show_minor'];
  132. $opt['only_new'] = (bool) $opt['only_new'];
  133. $opt['sort'] = valid_input_set('sort', ['default' => 'natural', 'date'], $opt);
  134. $opt['guardmail'] = ($conf['mailguard'] != '' && $conf['mailguard'] != 'none');
  135. $type = $INPUT->valid(
  136. 'type',
  137. ['rss', 'rss2', 'atom', 'atom1', 'rss1'],
  138. $conf['rss_type']
  139. );
  140. switch ($type) {
  141. case 'rss':
  142. $opt['feed_type'] = 'RSS0.91';
  143. $opt['mime_type'] = 'text/xml';
  144. break;
  145. case 'rss2':
  146. $opt['feed_type'] = 'RSS2.0';
  147. $opt['mime_type'] = 'text/xml';
  148. break;
  149. case 'atom':
  150. $opt['feed_type'] = 'ATOM0.3';
  151. $opt['mime_type'] = 'application/xml';
  152. break;
  153. case 'atom1':
  154. $opt['feed_type'] = 'ATOM1.0';
  155. $opt['mime_type'] = 'application/atom+xml';
  156. break;
  157. default:
  158. $opt['feed_type'] = 'RSS1.0';
  159. $opt['mime_type'] = 'application/xml';
  160. }
  161. $eventData = [
  162. 'opt' => &$opt,
  163. ];
  164. Event::createAndTrigger('FEED_OPTS_POSTPROCESS', $eventData);
  165. return $opt;
  166. }
  167. /**
  168. * Add recent changed pages to a feed object
  169. *
  170. * @param FeedCreator $rss the FeedCreator Object
  171. * @param array $data the items to add
  172. * @param array $opt the feed options
  173. * @author Andreas Gohr <andi@splitbrain.org>
  174. */
  175. function rss_buildItems(&$rss, &$data, $opt)
  176. {
  177. global $conf;
  178. global $lang;
  179. /* @var AuthPlugin $auth */
  180. global $auth;
  181. $eventData = [
  182. 'rss' => &$rss,
  183. 'data' => &$data,
  184. 'opt' => &$opt,
  185. ];
  186. $event = new Event('FEED_DATA_PROCESS', $eventData);
  187. if ($event->advise_before(false)) {
  188. foreach ($data as $ditem) {
  189. if (!is_array($ditem)) {
  190. // not an array? then only a list of IDs was given
  191. $ditem = ['id' => $ditem];
  192. }
  193. $item = new FeedItem();
  194. $id = $ditem['id'];
  195. if (empty($ditem['media'])) {
  196. $meta = p_get_metadata($id);
  197. } else {
  198. $meta = [];
  199. }
  200. // add date
  201. if (isset($ditem['date'])) {
  202. $date = $ditem['date'];
  203. } elseif ($ditem['media']) {
  204. $date = @filemtime(mediaFN($id));
  205. } elseif (file_exists(wikiFN($id))) {
  206. $date = @filemtime(wikiFN($id));
  207. } elseif ($meta['date']['modified']) {
  208. $date = $meta['date']['modified'];
  209. } else {
  210. $date = 0;
  211. }
  212. if ($date) $item->date = date('r', $date);
  213. // add title
  214. if ($conf['useheading'] && $meta['title'] ?? '') {
  215. $item->title = $meta['title'];
  216. } else {
  217. $item->title = $ditem['id'];
  218. }
  219. if ($conf['rss_show_summary'] && !empty($ditem['sum'])) {
  220. $item->title .= ' - ' . strip_tags($ditem['sum']);
  221. }
  222. // add item link
  223. switch ($opt['link_to']) {
  224. case 'page':
  225. if (isset($ditem['media'])) {
  226. $item->link = media_managerURL(
  227. [
  228. 'image' => $id,
  229. 'ns' => getNS($id),
  230. 'rev' => $date
  231. ],
  232. '&',
  233. true
  234. );
  235. } else {
  236. $item->link = wl($id, 'rev=' . $date, true, '&');
  237. }
  238. break;
  239. case 'rev':
  240. if ($ditem['media']) {
  241. $item->link = media_managerURL(
  242. [
  243. 'image' => $id,
  244. 'ns' => getNS($id),
  245. 'rev' => $date,
  246. 'tab_details' => 'history'
  247. ],
  248. '&',
  249. true
  250. );
  251. } else {
  252. $item->link = wl($id, 'do=revisions&rev=' . $date, true, '&');
  253. }
  254. break;
  255. case 'current':
  256. if ($ditem['media']) {
  257. $item->link = media_managerURL(
  258. [
  259. 'image' => $id,
  260. 'ns' => getNS($id)
  261. ],
  262. '&',
  263. true
  264. );
  265. } else {
  266. $item->link = wl($id, '', true, '&');
  267. }
  268. break;
  269. case 'diff':
  270. default:
  271. if ($ditem['media']) {
  272. $item->link = media_managerURL(
  273. [
  274. 'image' => $id,
  275. 'ns' => getNS($id),
  276. 'rev' => $date,
  277. 'tab_details' => 'history',
  278. 'mediado' => 'diff'
  279. ],
  280. '&',
  281. true
  282. );
  283. } else {
  284. $item->link = wl($id, 'rev=' . $date . '&do=diff', true, '&');
  285. }
  286. }
  287. // add item content
  288. switch ($opt['item_content']) {
  289. case 'diff':
  290. case 'htmldiff':
  291. if ($ditem['media']) {
  292. $medialog = new MediaChangeLog($id);
  293. $revs = $medialog->getRevisions(0, 1);
  294. $rev = $revs[0];
  295. $src_r = '';
  296. $src_l = '';
  297. if ($size = media_image_preview_size($id, '', new JpegMeta(mediaFN($id)), 300)) {
  298. $more = 'w=' . $size[0] . '&h=' . $size[1] . '&t=' . @filemtime(mediaFN($id));
  299. $src_r = ml($id, $more, true, '&amp;', true);
  300. }
  301. if ($rev && $size = media_image_preview_size($id, $rev, new JpegMeta(mediaFN($id, $rev)),
  302. 300)) {
  303. $more = 'rev=' . $rev . '&w=' . $size[0] . '&h=' . $size[1];
  304. $src_l = ml($id, $more, true, '&amp;', true);
  305. }
  306. $content = '';
  307. if ($src_r) {
  308. $content = '<table>';
  309. $content .= '<tr><th width="50%">' . $rev . '</th>';
  310. $content .= '<th width="50%">' . $lang['current'] . '</th></tr>';
  311. $content .= '<tr align="center"><td><img src="' . $src_l . '" alt="" /></td><td>';
  312. $content .= '<img src="' . $src_r . '" alt="' . $id . '" /></td></tr>';
  313. $content .= '</table>';
  314. }
  315. } else {
  316. require_once(DOKU_INC . 'inc/DifferenceEngine.php');
  317. $pagelog = new PageChangeLog($id);
  318. $revs = $pagelog->getRevisions(0, 1);
  319. $rev = $revs[0];
  320. if ($rev) {
  321. $df = new Diff(
  322. explode("\n", rawWiki($id, $rev)),
  323. explode("\n", rawWiki($id, ''))
  324. );
  325. } else {
  326. $df = new Diff(
  327. [''],
  328. explode("\n", rawWiki($id, ''))
  329. );
  330. }
  331. if ($opt['item_content'] == 'htmldiff') {
  332. // note: no need to escape diff output, TableDiffFormatter provides 'safe' html
  333. $tdf = new TableDiffFormatter();
  334. $content = '<table>';
  335. $content .= '<tr><th colspan="2" width="50%">' . $rev . '</th>';
  336. $content .= '<th colspan="2" width="50%">' . $lang['current'] . '</th></tr>';
  337. $content .= $tdf->format($df);
  338. $content .= '</table>';
  339. } else {
  340. // note: diff output must be escaped, UnifiedDiffFormatter provides plain text
  341. $udf = new UnifiedDiffFormatter();
  342. $content = "<pre>\n" . hsc($udf->format($df)) . "\n</pre>";
  343. }
  344. }
  345. break;
  346. case 'html':
  347. if ($ditem['media']) {
  348. if ($size = media_image_preview_size($id, '', new JpegMeta(mediaFN($id)))) {
  349. $more = 'w=' . $size[0] . '&h=' . $size[1] . '&t=' . @filemtime(mediaFN($id));
  350. $src = ml($id, $more, true, '&amp;', true);
  351. $content = '<img src="' . $src . '" alt="' . $id . '" />';
  352. } else {
  353. $content = '';
  354. }
  355. } else {
  356. if (@filemtime(wikiFN($id)) === $date) {
  357. $content = p_wiki_xhtml($id, '', false);
  358. } else {
  359. $content = p_wiki_xhtml($id, $date, false);
  360. }
  361. // no TOC in feeds
  362. $content = preg_replace('/(<!-- TOC START -->).*(<!-- TOC END -->)/s', '', $content);
  363. // add alignment for images
  364. $content = preg_replace('/(<img .*?class="medialeft")/s', '\\1 align="left"', $content);
  365. $content = preg_replace('/(<img .*?class="mediaright")/s', '\\1 align="right"', $content);
  366. // make URLs work when canonical is not set, regexp instead of rerendering!
  367. if (!$conf['canonical']) {
  368. $base = preg_quote(DOKU_REL, '/');
  369. $content = preg_replace(
  370. '/(<a href|<img src)="(' . $base . ')/s', '$1="' . DOKU_URL,
  371. $content
  372. );
  373. }
  374. }
  375. break;
  376. case 'abstract':
  377. default:
  378. if (isset($ditem['media'])) {
  379. if ($size = media_image_preview_size($id, '', new JpegMeta(mediaFN($id)))) {
  380. $more = 'w=' . $size[0] . '&h=' . $size[1] . '&t=' . @filemtime(mediaFN($id));
  381. $src = ml($id, $more, true, '&amp;', true);
  382. $content = '<img src="' . $src . '" alt="' . $id . '" />';
  383. } else {
  384. $content = '';
  385. }
  386. } else {
  387. $content = $meta['description']['abstract'];
  388. }
  389. }
  390. $item->description = $content; //FIXME a plugin hook here could be senseful
  391. // add user
  392. # FIXME should the user be pulled from metadata as well?
  393. $user = @$ditem['user']; // the @ spares time repeating lookup
  394. if (blank($user)) {
  395. $item->author = 'Anonymous';
  396. $item->authorEmail = 'anonymous@undisclosed.example.com';
  397. } else {
  398. $item->author = $user;
  399. $item->authorEmail = $user . '@undisclosed.example.com';
  400. // get real user name if configured
  401. if ($conf['useacl'] && $auth) {
  402. $userInfo = $auth->getUserData($user);
  403. if ($userInfo) {
  404. switch ($conf['showuseras']) {
  405. case 'username':
  406. case 'username_link':
  407. $item->author = $userInfo['name'];
  408. break;
  409. default:
  410. $item->author = $user;
  411. break;
  412. }
  413. } else {
  414. $item->author = $user;
  415. }
  416. }
  417. }
  418. // add category
  419. if (isset($meta['subject'])) {
  420. $item->category = $meta['subject'];
  421. } else {
  422. $cat = getNS($id);
  423. if ($cat) $item->category = $cat;
  424. }
  425. // finally add the item to the feed object, after handing it to registered plugins
  426. $evdata = [
  427. 'item' => &$item,
  428. 'opt' => &$opt,
  429. 'ditem' => &$ditem,
  430. 'rss' => &$rss
  431. ];
  432. $evt = new Event('FEED_ITEM_ADD', $evdata);
  433. if ($evt->advise_before()) {
  434. $rss->addItem($item);
  435. }
  436. $evt->advise_after(); // for completeness
  437. }
  438. }
  439. $event->advise_after();
  440. }
  441. /**
  442. * Add recent changed pages to the feed object
  443. *
  444. * @author Andreas Gohr <andi@splitbrain.org>
  445. */
  446. function rssRecentChanges($opt)
  447. {
  448. global $conf;
  449. $flags = 0;
  450. if (!$conf['rss_show_deleted']) $flags += RECENTS_SKIP_DELETED;
  451. if (!$opt['show_minor']) $flags += RECENTS_SKIP_MINORS;
  452. if ($opt['only_new']) $flags += RECENTS_ONLY_CREATION;
  453. if ($opt['content_type'] == 'media' && $conf['mediarevisions']) $flags += RECENTS_MEDIA_CHANGES;
  454. if ($opt['content_type'] == 'both' && $conf['mediarevisions']) $flags += RECENTS_MEDIA_PAGES_MIXED;
  455. $recents = getRecents(0, $opt['items'], $opt['namespace'], $flags);
  456. return $recents;
  457. }
  458. /**
  459. * Add all pages of a namespace to the feed object
  460. *
  461. * @author Andreas Gohr <andi@splitbrain.org>
  462. */
  463. function rssListNamespace($opt)
  464. {
  465. require_once(DOKU_INC . 'inc/search.php');
  466. global $conf;
  467. $ns = ':' . cleanID($opt['namespace']);
  468. $ns = utf8_encodeFN(str_replace(':', '/', $ns));
  469. $data = [];
  470. $search_opts = [
  471. 'depth' => 1,
  472. 'pagesonly' => true,
  473. 'listfiles' => true
  474. ];
  475. search($data, $conf['datadir'], 'search_universal', $search_opts, $ns, $lvl = 1, $opt['sort']);
  476. return $data;
  477. }
  478. /**
  479. * Add the result of a full text search to the feed object
  480. *
  481. * @author Andreas Gohr <andi@splitbrain.org>
  482. */
  483. function rssSearch($opt)
  484. {
  485. if (!$opt['search_query'] || !actionOK('search')) return [];
  486. require_once(DOKU_INC . 'inc/fulltext.php');
  487. $data = ft_pageSearch($opt['search_query'], $poswords);
  488. $data = array_keys($data);
  489. return $data;
  490. }
  491. //Setup VIM: ex: et ts=4 :