Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
35.92% covered (danger)
35.92%
37 / 103
30.77% covered (danger)
30.77%
4 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
RecordLinker
35.92% covered (danger)
35.92%
37 / 103
30.77% covered (danger)
30.77%
4 / 13
250.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 related
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 getActionUrl
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getRequestUrl
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getTabUrl
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
3.00
 getUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBreadcrumbHtml
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getChildRecordSearchUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getVersionsSearchUrl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getSearchActionForSource
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getVersionsActionForSource
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRecordUrlParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
1<?php
2
3/**
4 * Record linker view helper
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 * Copyright (C) The National Library of Finland 2023.
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2,
13 * as published by the Free Software Foundation.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
23 *
24 * @category VuFind
25 * @package  View_Helpers
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @author   Ere Maijala <ere.maijala@helsinki.fi>
28 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
29 * @link     https://vufind.org/wiki/development Wiki
30 */
31
32namespace VuFind\View\Helper\Root;
33
34use VuFind\RecordDriver\AbstractBase as AbstractRecord;
35
36use function is_array;
37use function is_string;
38
39/**
40 * Record linker view helper
41 *
42 * @category VuFind
43 * @package  View_Helpers
44 * @author   Demian Katz <demian.katz@villanova.edu>
45 * @author   Ere Maijala <ere.maijala@helsinki.fi>
46 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
47 * @link     https://vufind.org/wiki/development Wiki
48 */
49class RecordLinker extends \Laminas\View\Helper\AbstractHelper
50{
51    /**
52     * Record router
53     *
54     * @var \VuFind\Record\Router
55     */
56    protected $router;
57
58    /**
59     * Search results (optional)
60     *
61     * @var \VuFind\Search\Base\Results
62     */
63    protected $results = null;
64
65    /**
66     * Cached record URLs
67     *
68     * @var array
69     */
70    protected $cachedDriverUrls = [];
71
72    /**
73     * Constructor
74     *
75     * @param \VuFind\Record\Router $router Record router
76     */
77    public function __construct(\VuFind\Record\Router $router)
78    {
79        $this->router = $router;
80    }
81
82    /**
83     * Store an optional results object and return this object so that the
84     * appropriate link can be rendered.
85     *
86     * @param ?\VuFind\Search\Base\Results $results Results object.
87     *
88     * @return RecordLinker
89     */
90    public function __invoke($results = null)
91    {
92        $this->results = $results;
93        return $this;
94    }
95
96    /**
97     * Given an array representing a related record (which may be a bib ID or OCLC
98     * number), this helper renders a URL linking to that record.
99     *
100     * @param array  $link   Link information from record model
101     * @param string $source Source ID for backend being used to retrieve records
102     *
103     * @return string       URL derived from link information
104     */
105    public function related($link, $source = DEFAULT_SEARCH_BACKEND)
106    {
107        $urlHelper = $this->getView()->plugin('url');
108        $baseUrl = $urlHelper($this->getSearchActionForSource($source));
109        switch ($link['type']) {
110            case 'bib':
111                return $baseUrl
112                    . '?lookfor=' . urlencode($link['value'])
113                    . '&type=id&jumpto=1';
114            case 'dlc':
115                return $baseUrl
116                    . '?lookfor=' . urlencode('"' . $link['value'] . '"')
117                    . '&type=lccn&jumpto=1';
118            case 'isn':
119                return $baseUrl
120                    . '?join=AND&bool0[]=AND&lookfor0[]=%22'
121                    . urlencode($link['value'])
122                    . '%22&type0[]=isn&bool1[]=NOT&lookfor1[]=%22'
123                    . urlencode($link['exclude'])
124                    . '%22&type1[]=id&sort=title&view=list';
125            case 'oclc':
126                return $baseUrl
127                    . '?lookfor=' . urlencode($link['value'])
128                    . '&type=oclc_num&jumpto=1';
129            case 'title':
130                return $baseUrl
131                    . '?lookfor=' . urlencode($link['value'])
132                    . '&type=title';
133        }
134        throw new \Exception('Unexpected link type: ' . $link['type']);
135    }
136
137    /**
138     * Given a record driver, get a URL for that record.
139     *
140     * @param AbstractRecord|string $driver  Record driver representing record
141     * to link to, or source|id pipe-delimited string
142     * @param string                $action  Record action to access
143     * @param array                 $query   Optional query parameters
144     * @param string                $anchor  Optional anchor
145     * @param array                 $options Record URL parameter options (optional)
146     *
147     * @return string
148     */
149    public function getActionUrl(
150        $driver,
151        $action,
152        $query = [],
153        $anchor = '',
154        $options = []
155    ) {
156        // Build the URL:
157        $urlHelper = $this->getView()->plugin('url');
158        $details = $this->router->getActionRouteDetails($driver, $action);
159        return $urlHelper(
160            $details['route'],
161            $details['params'] ?: [],
162            [
163                'query' => $this->getRecordUrlParams($options) + $query,
164                'fragment' => $anchor ? ltrim($anchor, '#') : '',
165                'normalize_path' => false, // required to keep slashes encoded
166            ]
167        );
168    }
169
170    /**
171     * Given a string or array of parts, build a request (e.g. hold) URL.
172     *
173     * @param string|array $url           URL to process
174     * @param bool         $includeAnchor Should we include an anchor?
175     *
176     * @return string
177     */
178    public function getRequestUrl($url, $includeAnchor = true)
179    {
180        if (is_array($url)) {
181            // Assemble URL string from array parts:
182            $source = $url['source'] ?? DEFAULT_SEARCH_BACKEND;
183            parse_str($url['query'] ?? '', $query);
184            $finalUrl = $this->getActionUrl(
185                "{$source}|" . $url['record'],
186                $url['action'],
187                $query,
188                $includeAnchor ? ($url['anchor'] ?? '') : ''
189            );
190        } else {
191            // If URL is already a string but we don't want anchors, strip
192            // the anchor now:
193            if (!$includeAnchor) {
194                [$finalUrl] = explode('#', $url);
195            } else {
196                $finalUrl = $url;
197            }
198        }
199        return $finalUrl;
200    }
201
202    /**
203     * Given a record driver, get a URL for that record.
204     *
205     * @param AbstractRecord|string $driver  Record driver representing record to
206     * link to, or source|id pipe-delimited string
207     * @param ?string               $tab     Optional record tab to access
208     * @param array                 $query   Optional query params
209     * @param array                 $options Any additional options:
210     * - excludeSearchId (default: false)
211     *
212     * @return string
213     */
214    public function getTabUrl($driver, $tab = null, $query = [], $options = [])
215    {
216        $driverId = is_string($driver)
217            ? $driver
218            : ($driver->getSourceIdentifier() . '|' . $driver->getUniqueID());
219        $cacheKey = md5(
220            $driverId . '|' . ($tab ?? '-') . '|' . var_export($query, true)
221            . var_export($options, true)
222        );
223        if (!isset($this->cachedDriverUrls[$cacheKey])) {
224            // Build the URL:
225            $urlHelper = $this->getView()->plugin('url');
226            $details = $this->router->getTabRouteDetails($driver, $tab, $query);
227            $this->cachedDriverUrls[$cacheKey] = $urlHelper(
228                $details['route'],
229                $details['params'],
230                array_merge_recursive(
231                    $details['options'] ?? [],
232                    ['query' => $this->getRecordUrlParams($options)]
233                )
234            );
235        }
236        return $this->cachedDriverUrls[$cacheKey];
237    }
238
239    /**
240     * Get the default URL for a record.
241     *
242     * @param AbstractRecord|string $driver  Record driver representing record to
243     * link to, or source|id pipe-delimited string
244     * @param array                 $options Any additional options:
245     * - excludeSearchId (default: false)
246     *
247     * @return string
248     */
249    public function getUrl($driver, $options = [])
250    {
251        return $this->getTabUrl($driver, null, [], $options);
252    }
253
254    /**
255     * Given a record driver, generate HTML to link to the record from breadcrumbs.
256     *
257     * @param AbstractRecord $driver Record to link to.
258     *
259     * @return string
260     */
261    public function getBreadcrumbHtml($driver)
262    {
263        $truncateHelper = $this->getView()->plugin('truncate');
264        $escapeHelper = $this->getView()->plugin('escapeHtml');
265        return '<a href="' . $this->getUrl($driver) . '">' .
266            $escapeHelper($truncateHelper($driver->getBreadcrumb(), 30))
267            . '</a>';
268    }
269
270    /**
271     * Given a record driver, generate a URL to fetch all child records for it.
272     *
273     * @param AbstractRecord $driver Host Record.
274     *
275     * @return string
276     */
277    public function getChildRecordSearchUrl($driver)
278    {
279        $urlHelper = $this->getView()->plugin('url');
280        $route = $this->getSearchActionForSource($driver->getSourceIdentifier());
281        return $urlHelper($route)
282            . '?lookfor='
283            . urlencode(addcslashes($driver->getUniqueID(), '"'))
284            . '&type=ParentID';
285    }
286
287    /**
288     * Return search URL for all versions
289     *
290     * @param AbstractRecord $driver Record driver
291     *
292     * @return string
293     */
294    public function getVersionsSearchUrl($driver)
295    {
296        $route = $this->getVersionsActionForSource($driver->getSourceIdentifier());
297        if (false === $route) {
298            return '';
299        }
300
301        $urlParams = [
302            'id' => $driver->getUniqueID(),
303            'search' => 'versions',
304        ];
305
306        $urlHelper = $this->getView()->plugin('url');
307        return $urlHelper($route, [], ['query' => $urlParams]);
308    }
309
310    /**
311     * Given a record source ID, return the route name for searching its backend.
312     *
313     * @param string $source Record source identifier.
314     *
315     * @return string
316     */
317    protected function getSearchActionForSource($source)
318    {
319        $optionsHelper = $this->getView()->plugin('searchOptions');
320        return $optionsHelper($source)->getSearchAction();
321    }
322
323    /**
324     * Given a record source ID, return the route name for version search with its
325     * backend.
326     *
327     * @param string $source Record source identifier.
328     *
329     * @return string|bool
330     */
331    protected function getVersionsActionForSource($source)
332    {
333        $optionsHelper = $this->getView()->plugin('searchOptions');
334        return $optionsHelper($source)->getVersionsAction();
335    }
336
337    /**
338     * Get query parameters for a record URL
339     *
340     * @param array $options Any additional options:
341     * - excludeSearchId (default: false)
342     *
343     * @return array
344     */
345    protected function getRecordUrlParams(array $options = []): array
346    {
347        if (!empty($options['excludeSearchId'])) {
348            return [];
349        }
350        $sid = ($this->results ? $this->results->getSearchId() : null)
351            ?? $this->getView()->plugin('searchMemory')->getLastSearchId();
352        return $sid ? compact('sid') : [];
353    }
354}