Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
79.74% |
122 / 153 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
ResultFeed | |
79.74% |
122 / 153 |
|
16.67% |
1 / 6 |
33.06 | |
0.00% |
0 / 1 |
setOverrideTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
registerExtensions | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
__invoke | |
76.47% |
65 / 85 |
|
0.00% |
0 / 1 |
6.47 | |||
getDcDate | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
6.60 | |||
addEntry | |
85.71% |
24 / 28 |
|
0.00% |
0 / 1 |
11.35 | |||
getDateModified | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
3.07 |
1 | <?php |
2 | |
3 | /** |
4 | * "Results as feed" view helper |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package View_Helpers |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFind\View\Helper\Root; |
31 | |
32 | use DateTime; |
33 | use Laminas\Feed\Writer\Feed; |
34 | use Laminas\Feed\Writer\Writer as FeedWriter; |
35 | use Laminas\View\Helper\AbstractHelper; |
36 | use Psr\Container\ContainerInterface; |
37 | use VuFind\I18n\Translator\TranslatorAwareInterface; |
38 | |
39 | use function is_array; |
40 | use function is_string; |
41 | use function strlen; |
42 | |
43 | /** |
44 | * "Results as feed" view helper |
45 | * |
46 | * @category VuFind |
47 | * @package View_Helpers |
48 | * @author Demian Katz <demian.katz@villanova.edu> |
49 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
50 | * @link https://vufind.org/wiki/development Wiki |
51 | */ |
52 | class ResultFeed extends AbstractHelper implements TranslatorAwareInterface |
53 | { |
54 | use \VuFind\I18n\Translator\TranslatorAwareTrait; |
55 | |
56 | /** |
57 | * Override title |
58 | * |
59 | * @var string |
60 | */ |
61 | protected $overrideTitle = null; |
62 | |
63 | /** |
64 | * Set override title. |
65 | * |
66 | * @param string $title Title |
67 | * |
68 | * @return void |
69 | */ |
70 | public function setOverrideTitle($title) |
71 | { |
72 | $this->overrideTitle = $title; |
73 | } |
74 | |
75 | /** |
76 | * Set up custom extensions (should be called by factory). |
77 | * |
78 | * @param ContainerInterface $container Service container |
79 | * |
80 | * @return void |
81 | */ |
82 | public function registerExtensions(ContainerInterface $container) |
83 | { |
84 | $manager = new \Laminas\Feed\Writer\ExtensionPluginManager($container); |
85 | $manager->setInvokableClass( |
86 | 'DublinCore\Renderer\Entry', |
87 | \VuFind\Feed\Writer\Extension\DublinCore\Renderer\Entry::class |
88 | ); |
89 | $manager->setInvokableClass( |
90 | 'DublinCore\Entry', |
91 | \VuFind\Feed\Writer\Extension\DublinCore\Entry::class |
92 | ); |
93 | $manager->setInvokableClass( |
94 | 'OpenSearch\Renderer\Feed', |
95 | \VuFind\Feed\Writer\Extension\OpenSearch\Renderer\Feed::class |
96 | ); |
97 | $manager->setInvokableClass( |
98 | 'OpenSearch\Feed', |
99 | \VuFind\Feed\Writer\Extension\OpenSearch\Feed::class |
100 | ); |
101 | FeedWriter::setExtensionManager($manager); |
102 | FeedWriter::registerExtension('OpenSearch'); |
103 | } |
104 | |
105 | /** |
106 | * Represent the current search results as a feed. |
107 | * |
108 | * @param \VuFind\Search\Base\Results $results Search results to convert to |
109 | * feed |
110 | * @param string $currentPath Base path to display in feed |
111 | * (leave null to load dynamically using currentpath view helper) |
112 | * |
113 | * @return Feed |
114 | */ |
115 | public function __invoke($results, $currentPath = null) |
116 | { |
117 | // Determine base URL if not already provided: |
118 | if (null === $currentPath) { |
119 | $currentPath = ($this->getView()->plugin('currentPath'))(); |
120 | } |
121 | $serverUrl = $this->getView()->plugin('serverurl'); |
122 | $baseUrl = $serverUrl($currentPath); |
123 | $lang = $this->getTranslatorLocale(); |
124 | |
125 | // Create the parent feed |
126 | $feed = new Feed(); |
127 | $feed->setType('rss'); |
128 | if (null !== $this->overrideTitle) { |
129 | $feed->setTitle($this->translate($this->overrideTitle)); |
130 | } else { |
131 | $feed->setTitle( |
132 | $this->translate('Results for') . ' ' |
133 | . $results->getParams()->getDisplayQuery() |
134 | ); |
135 | } |
136 | $feed->setLink( |
137 | $baseUrl . $results->getUrlQuery() |
138 | ->setDefaultParameter('lng', $lang, true) |
139 | ->setViewParam(null) |
140 | ->getParams(false) |
141 | ); |
142 | $feed->setFeedLink( |
143 | $baseUrl . $results->getUrlQuery() |
144 | ->setDefaultParameter('lng', $lang, true) |
145 | ->getParams(false), |
146 | $results->getParams()->getView() |
147 | ); |
148 | $feed->setDescription( |
149 | strip_tags( |
150 | $this->translate( |
151 | 'showing_results_of_html', |
152 | [ |
153 | '%%start%%' => $results->getStartRecord(), |
154 | '%%end%%' => $results->getEndRecord(), |
155 | '%%total%%' => $results->getResultTotal(), |
156 | ] |
157 | ) |
158 | ) |
159 | ); |
160 | |
161 | $params = $results->getParams(); |
162 | |
163 | // add atom links for easier paging |
164 | $feed->addOpensearchLink( |
165 | $baseUrl . $results->getUrlQuery() |
166 | ->setPage(1) |
167 | ->setDefaultParameter('lng', $lang, true) |
168 | ->getParams(false), |
169 | 'first', |
170 | $params->getView(), |
171 | $this->translate('page_first') |
172 | ); |
173 | if ($params->getPage() > 1) { |
174 | $feed->addOpensearchLink( |
175 | $baseUrl . $results->getUrlQuery() |
176 | ->setPage($params->getPage() - 1) |
177 | ->setDefaultParameter('lng', $lang, true) |
178 | ->getParams(false), |
179 | 'previous', |
180 | $params->getView(), |
181 | $this->translate('page_prev') |
182 | ); |
183 | } |
184 | $lastPage = ceil($results->getResultTotal() / $params->getLimit()); |
185 | if ($params->getPage() < $lastPage) { |
186 | $feed->addOpensearchLink( |
187 | $baseUrl . $results->getUrlQuery() |
188 | ->setPage($params->getPage() + 1) |
189 | ->setDefaultParameter('lng', $lang, true) |
190 | ->getParams(false), |
191 | 'next', |
192 | $params->getView(), |
193 | $this->translate('page_next') |
194 | ); |
195 | } |
196 | $feed->addOpensearchLink( |
197 | $baseUrl . $results->getUrlQuery() |
198 | ->setPage($lastPage) |
199 | ->setDefaultParameter('lng', $lang, true) |
200 | ->getParams(false), |
201 | 'last', |
202 | $params->getView(), |
203 | $this->translate('page_last') |
204 | ); |
205 | |
206 | // add opensearch fields |
207 | $feed->setOpensearchTotalResults($results->getResultTotal()); |
208 | $feed->setOpensearchItemsPerPage($params->getLimit()); |
209 | $feed->setOpensearchStartIndex($results->getStartRecord() - 1); |
210 | $feed->setOpensearchSearchTerms($params->getQuery()->getAllTerms()); |
211 | |
212 | $records = $results->getResults(); |
213 | foreach ($records as $current) { |
214 | $this->addEntry($feed, $current); |
215 | } |
216 | |
217 | return $feed; |
218 | } |
219 | |
220 | /** |
221 | * Support method to extract a date from a record driver. Return empty string |
222 | * if no valid match is found. |
223 | * |
224 | * @param \VuFind\RecordDriver\AbstractBase $record Record to read from |
225 | * |
226 | * @return string |
227 | */ |
228 | protected function getDcDate($record) |
229 | { |
230 | // See if we can extract a date that's pre-formatted in a DC-friendly way: |
231 | $dates = (array)$record->tryMethod('getPublicationDates'); |
232 | $regex = '/[0-9]{4}(\-[01][0-9])?(\-[0-3][0-9])?/'; |
233 | foreach ($dates as $date) { |
234 | if (preg_match($regex, $date, $matches)) { |
235 | // If the full string is longer than the match, see if we can use |
236 | // DateTime to format it to something more useful: |
237 | if (strlen($date) > strlen($matches[0])) { |
238 | try { |
239 | $formatter = new DateTime($date); |
240 | return $formatter->format('Y-m-d'); |
241 | } catch (\Exception $ex) { |
242 | // DateTime failed; fall through to default behavior below. |
243 | } |
244 | } |
245 | return $matches[0]; |
246 | } |
247 | } |
248 | |
249 | // Still no good? Give up. |
250 | return ''; |
251 | } |
252 | |
253 | /** |
254 | * Support method to turn a record driver object into an RSS entry. |
255 | * |
256 | * @param Feed $feed Feed to update |
257 | * @param \VuFind\RecordDriver\AbstractBase $record Record to add to feed |
258 | * |
259 | * @return void |
260 | */ |
261 | protected function addEntry($feed, $record) |
262 | { |
263 | $entry = $feed->createEntry(); |
264 | $title = $record->tryMethod('getTitle'); |
265 | $title = empty($title) ? $record->getBreadcrumb() : $title; |
266 | $entry->setTitle( |
267 | empty($title) ? $this->translate('Title not available') : $title |
268 | ); |
269 | $serverUrl = $this->getView()->plugin('serverurl'); |
270 | $recordLinker = $this->getView()->plugin('recordLinker'); |
271 | try { |
272 | $url = $serverUrl($recordLinker->getUrl($record)); |
273 | } catch (\Laminas\Router\Exception\RuntimeException $e) { |
274 | // No route defined? See if we can get a URL out of the driver. |
275 | // Useful for web results, among other things. |
276 | $url = $record->tryMethod('getUrl'); |
277 | if (empty($url) || !is_string($url)) { |
278 | throw new \Exception('Cannot find URL for record.'); |
279 | } |
280 | } |
281 | $entry->setLink($url); |
282 | $date = $this->getDateModified($record); |
283 | if (!empty($date)) { |
284 | $entry->setDateModified($date); |
285 | } |
286 | $author = $record->tryMethod('getPrimaryAuthor'); |
287 | if (!empty($author)) { |
288 | $entry->addAuthor(['name' => $author]); |
289 | } |
290 | $formats = $record->tryMethod('getFormats'); |
291 | if (is_array($formats)) { |
292 | foreach ($formats as $format) { |
293 | $entry->addDCFormat($this->translate($format)); |
294 | } |
295 | } |
296 | $dcDate = $this->getDcDate($record); |
297 | if (!empty($dcDate)) { |
298 | $entry->setDCDate($dcDate); |
299 | } |
300 | |
301 | $feed->addEntry($entry); |
302 | } |
303 | |
304 | /** |
305 | * Support method to extract modified date from a record driver object. |
306 | * |
307 | * @param \VuFind\RecordDriver\AbstractBase $record Record to pull date from. |
308 | * |
309 | * @return int|DateTime|null |
310 | */ |
311 | protected function getDateModified($record) |
312 | { |
313 | // Best case -- "last indexed" date is available: |
314 | $date = $record->tryMethod('getLastIndexed'); |
315 | if (!empty($date)) { |
316 | return strtotime($date); |
317 | } |
318 | |
319 | // Next, try publication date: |
320 | $date = $record->tryMethod('getPublicationDates'); |
321 | if (isset($date[0])) { |
322 | // Extract first string of numbers -- this should be a year: |
323 | preg_match('/[^0-9]*([0-9]+).*/', $date[0], $matches); |
324 | $date = new DateTime(); |
325 | $date->setDate($matches[1], 1, 1); |
326 | return $date; |
327 | } |
328 | |
329 | // If we got this far, no date is available: |
330 | return null; |
331 | } |
332 | } |