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