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