Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.74% covered (warning)
79.74%
122 / 153
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResultFeed
79.74% covered (warning)
79.74%
122 / 153
16.67% covered (danger)
16.67%
1 / 6
33.06
0.00% covered (danger)
0.00%
0 / 1
 setOverrideTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 registerExtensions
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
76.47% covered (warning)
76.47%
65 / 85
0.00% covered (danger)
0.00%
0 / 1
6.47
 getDcDate
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
6.60
 addEntry
85.71% covered (warning)
85.71%
24 / 28
0.00% covered (danger)
0.00%
0 / 1
11.35
 getDateModified
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
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
30namespace VuFind\View\Helper\Root;
31
32use DateTime;
33use Laminas\Feed\Writer\Feed;
34use Laminas\Feed\Writer\Writer as FeedWriter;
35use Laminas\View\Helper\AbstractHelper;
36use Psr\Container\ContainerInterface;
37use VuFind\I18n\Translator\TranslatorAwareInterface;
38
39use function is_array;
40use function is_string;
41use 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 */
52class 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}