1<?php
2
3namespace dokuwiki\Feed;
4
5use dokuwiki\Extension\Event;
6
7class FeedCreator
8{
9    /** @var \UniversalFeedCreator */
10    protected $feed;
11
12    /** @var FeedCreatorOptions */
13    protected $options;
14
15    /**
16     * @param FeedCreatorOptions $options
17     */
18    public function __construct(FeedCreatorOptions $options)
19    {
20        $this->options = $options;
21
22        $this->feed = new \UniversalFeedCreator();
23        $this->feed->title = $this->options->get('title');
24        $this->feed->description = $this->options->get('subtitle');
25        $this->feed->link = DOKU_URL;
26        $this->feed->syndicationURL = DOKU_URL . 'feed.php';
27        $this->feed->cssStyleSheet = DOKU_URL . 'lib/exe/css.php?s=feed';
28
29        $this->initLogo();
30    }
31
32    /**
33     * Build the feed
34     *
35     * @return string The raw XML for the feed
36     */
37    public function build()
38    {
39        switch ($this->options->get('feed_mode')) {
40            case 'list':
41                $items = $this->fetchItemsFromNamespace();
42                break;
43            case 'search':
44                $items = $this->fetchItemsFromSearch();
45                break;
46            case 'recent':
47                $items = $this->fetchItemsFromRecentChanges();
48                break;
49            default:
50                $items = $this->fetchItemsFromPlugin();
51        }
52
53        $eventData = [
54            'rss' => $this->feed,
55            'data' => &$items,
56            'opt' =>  &$this->options->options,
57        ];
58        $event = new Event('FEED_DATA_PROCESS', $eventData);
59        if ($event->advise_before(false)) {
60            foreach ($items as $item) {
61                $this->createAndAddItem($item);
62            }
63        }
64        $event->advise_after();
65
66        return $this->feed->createFeed($this->options->getType());
67    }
68
69    /**
70     * Process the raw data, create feed item and add it to the feed
71     *
72     * @param array|string $data raw item data
73     * @return \FeedItem
74     * @triggers FEED_ITEM_ADD
75     */
76    protected function createAndAddItem($data)
77    {
78        if (is_string($data)) {
79            $data = ['id' => $data];
80        }
81
82        if (($data['mode'] ?? '') == 'media' || isset($data['media'])) {
83            $data['id'] = $data['media'] ?? $data['id'];
84            $proc = new FeedMediaProcessor($data);
85        } else {
86            $proc = new FeedPageProcessor($data);
87        }
88
89        $item = new \FeedItem();
90        $item->title = $proc->getTitle();
91        if ($this->options->get('show_summary') && $proc->getSummary()) {
92            $item->title .= ' - ' . $proc->getSummary();
93        }
94        $item->date = $proc->getRev();
95        [$item->authorEmail, $item->author] = $proc->getAuthor();
96        $item->link = $proc->getURL($this->options->get('link_to'));
97        $item->description = $proc->getBody($this->options->get('item_content'));
98
99        $evdata = [
100            'item' => $item,
101            'opt' => &$this->options->options,
102            'ditem' => &$data,
103            'rss' => $this->feed,
104        ];
105
106        $evt = new Event('FEED_ITEM_ADD', $evdata);
107        if ($evt->advise_before()) {
108            $this->feed->addItem($item);
109        }
110        $evt->advise_after();
111
112        return $item;
113    }
114
115    /**
116     * Read all pages from a namespace
117     *
118     * @todo this currently does not honor the rss_media setting and only ever lists pages
119     * @return array
120     */
121    protected function fetchItemsFromNamespace()
122    {
123        global $conf;
124
125        $ns = ':' . cleanID($this->options->get('namespace'));
126        $ns = utf8_encodeFN(str_replace(':', '/', $ns));
127
128        $data = [];
129        $search_opts = [
130            'depth' => 1,
131            'pagesonly' => true,
132            'listfiles' => true
133        ];
134        search(
135            $data,
136            $conf['datadir'],
137            'search_universal',
138            $search_opts,
139            $ns,
140            $lvl = 1,
141            $this->options->get('sort')
142        );
143
144        return $data;
145    }
146
147    /**
148     * Add the result of a full text search to the feed object
149     *
150     * @return array
151     */
152    protected function fetchItemsFromSearch()
153    {
154        if (!actionOK('search')) throw new \RuntimeException('search is disabled');
155        if (!$this->options->get('search_query')) return [];
156
157        $data = ft_pageSearch($this->options->get('search_query'), $poswords);
158        return array_keys($data);
159    }
160
161    /**
162     * Add recent changed pages to the feed object
163     *
164     * @return array
165     */
166    protected function fetchItemsFromRecentChanges()
167    {
168        global $conf;
169        $flags = 0;
170        if (!$this->options->get('show_deleted')) $flags += RECENTS_SKIP_DELETED;
171        if (!$this->options->get('show_minor')) $flags += RECENTS_SKIP_MINORS;
172        if ($this->options->get('only_new')) $flags += RECENTS_ONLY_CREATION;
173        if ($this->options->get('content_type') == 'media' && $conf['mediarevisions']) {
174            $flags += RECENTS_MEDIA_CHANGES;
175        }
176        if ($this->options->get('content_type') == 'both' && $conf['mediarevisions']) {
177            $flags += RECENTS_MEDIA_PAGES_MIXED;
178        }
179
180        return getRecents(0, $this->options->get('items'), $this->options->get('namespace'), $flags);
181    }
182
183    /**
184     * Add items from a plugin to the feed object
185     *
186     * @triggers FEED_MODE_UNKNOWN
187     * @return array
188     */
189    protected function fetchItemsFromPlugin()
190    {
191        $eventData = [
192            'opt' => $this->options->options,
193            'data' => [],
194        ];
195        $event = new Event('FEED_MODE_UNKNOWN', $eventData);
196        if ($event->advise_before(true)) {
197            throw new \RuntimeException('unknown feed mode');
198        }
199        $event->advise_after();
200
201        return $eventData['data'];
202    }
203
204    /**
205     * Add a logo to the feed
206     *
207     * Looks at different possible candidates for a logo and adds the first one
208     *
209     * @return void
210     */
211    protected function initLogo()
212    {
213        global $conf;
214
215        $this->feed->image = new \FeedImage();
216        $this->feed->image->title = $conf['title'];
217        $this->feed->image->link = DOKU_URL;
218        $this->feed->image->url = tpl_getMediaFile([
219            ':wiki:logo.svg',
220            ':logo.svg',
221            ':wiki:logo.png',
222            ':logo.png',
223            ':wiki:logo.jpg',
224            ':logo.jpg',
225            ':wiki:favicon.ico',
226            ':favicon.ico',
227            ':wiki:dokuwiki.svg',
228            ':wiki:dokuwiki-128.png',
229            'images/favicon.ico'
230        ], true);
231    }
232}
233