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