Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.68% covered (warning)
64.68%
141 / 218
70.27% covered (warning)
70.27%
26 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
Record
64.68% covered (warning)
64.68%
141 / 218
70.27% covered (warning)
70.27%
26 / 37
386.57
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
 setCoverRouter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderTemplate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getCoreMetadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCollectionBriefRecord
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCollectionMetadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getComments
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getExport
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFormatClass
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getFormatList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLabelList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getListEntry
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 getListNotes
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getPreviews
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPreviewData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getPreviewLink
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getPreviewIds
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
9
 getTags
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getTagsFromFavorites
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleHtml
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getLink
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getTab
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getToolbar
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSearchResult
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCheckbox
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getCover
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getPreviewCoverLinkSetting
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getCoverDetails
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getCoverSize
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getThumbnailAlignment
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 getQrCode
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 getThumbnail
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getUrlList
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getLinkDetails
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
10
 hasOpenUrlReplaceSetting
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 deduplicateLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Record driver 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 Laminas\Config\Config;
33use VuFind\Cover\Router as CoverRouter;
34use VuFind\Db\Entity\UserEntityInterface;
35use VuFind\Db\Entity\UserListEntityInterface;
36use VuFind\Db\Service\CommentsServiceInterface;
37use VuFind\Db\Service\DbServiceAwareInterface;
38use VuFind\Db\Service\DbServiceAwareTrait;
39use VuFind\Db\Service\UserListServiceInterface;
40use VuFind\Db\Service\UserResourceServiceInterface;
41use VuFind\Tags\TagsService;
42
43use function get_class;
44use function in_array;
45use function is_callable;
46
47/**
48 * Record driver view helper
49 *
50 * @category VuFind
51 * @package  View_Helpers
52 * @author   Demian Katz <demian.katz@villanova.edu>
53 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
54 * @link     https://vufind.org/wiki/development Wiki
55 */
56class Record extends \Laminas\View\Helper\AbstractHelper implements DbServiceAwareInterface
57{
58    use ClassBasedTemplateRendererTrait;
59    use DbServiceAwareTrait;
60
61    /**
62     * Context view helper
63     *
64     * @var \VuFind\View\Helper\Root\Context
65     */
66    protected $contextHelper;
67
68    /**
69     * Cover router
70     *
71     * @var CoverRouter
72     */
73    protected $coverRouter = null;
74
75    /**
76     * Record driver
77     *
78     * @var \VuFind\RecordDriver\AbstractBase
79     */
80    protected $driver;
81
82    /**
83     * Constructor
84     *
85     * @param TagsService $tagsService Tags service
86     * @param Config      $config      Configuration from config.ini
87     */
88    public function __construct(protected TagsService $tagsService, protected ?Config $config = null)
89    {
90        $this->config = $config;
91    }
92
93    /**
94     * Inject the cover router
95     *
96     * @param CoverRouter $router Cover router
97     *
98     * @return void
99     */
100    public function setCoverRouter($router)
101    {
102        $this->coverRouter = $router;
103    }
104
105    /**
106     * Render a template within a record driver folder.
107     *
108     * @param string $name    Template name to render
109     * @param array  $context Variables needed for rendering template; these will
110     * be temporarily added to the global view context, then reverted after the
111     * template is rendered (default = record driver only).
112     * @param bool   $throw   If true (default), an exception is thrown if the
113     * template is not found. Otherwise an empty string is returned.
114     *
115     * @return string
116     */
117    public function renderTemplate($name, $context = null, $throw = true)
118    {
119        $template = 'RecordDriver/%s/' . $name;
120        $className = get_class($this->driver);
121        return $this->renderClassTemplate(
122            $template,
123            $className,
124            $context ?? ['driver' => $this->driver],
125            $throw
126        );
127    }
128
129    /**
130     * Store a record driver object and return this object so that the appropriate
131     * template can be rendered.
132     *
133     * @param \VuFind\RecordDriver\AbstractBase $driver Record driver object.
134     *
135     * @return Record
136     */
137    public function __invoke($driver)
138    {
139        // Set up context helper:
140        $contextHelper = $this->getView()->plugin('context');
141        $this->contextHelper = $contextHelper($this->getView());
142
143        // Set up driver context:
144        $this->driver = $driver;
145        return $this;
146    }
147
148    /**
149     * Render the core metadata area of the record view.
150     *
151     * @return string
152     */
153    public function getCoreMetadata()
154    {
155        return $this->renderTemplate('core.phtml');
156    }
157
158    /**
159     * Render the a brief record for use in collection mode.
160     *
161     * @return string
162     */
163    public function getCollectionBriefRecord()
164    {
165        return $this->renderTemplate('collection-record.phtml');
166    }
167
168    /**
169     * Render the core metadata area of the collection view.
170     *
171     * @return string
172     */
173    public function getCollectionMetadata()
174    {
175        return $this->renderTemplate('collection-info.phtml');
176    }
177
178    /**
179     * Get comments associated with the current record.
180     *
181     * @return CommentsEntityInterface[]
182     */
183    public function getComments(): array
184    {
185        return $this->getDbService(CommentsServiceInterface::class)->getRecordComments(
186            $this->driver->getUniqueId(),
187            $this->driver->getSourceIdentifier()
188        );
189    }
190
191    /**
192     * Export the record in the requested format. For legal values, see
193     * the export helper's getFormatsForRecord() method.
194     *
195     * @param string $format Export format to display
196     *
197     * @return string        Exported data
198     */
199    public function getExport($format)
200    {
201        $format = strtolower($format);
202        return $this->renderTemplate('export-' . $format . '.phtml');
203    }
204
205    /**
206     * Get the CSS class used to properly render a format. (Note that this may
207     * not be used by every theme).
208     *
209     * @param string $format Format text to convert into CSS class
210     *
211     * @return string
212     */
213    public function getFormatClass($format)
214    {
215        return $this->renderTemplate(
216            'format-class.phtml',
217            ['format' => $format]
218        );
219    }
220
221    /**
222     * Render a list of record formats.
223     *
224     * @return string
225     */
226    public function getFormatList()
227    {
228        return $this->renderTemplate('format-list.phtml');
229    }
230
231    /**
232     * Render a list of record labels.
233     *
234     * @return string
235     */
236    public function getLabelList()
237    {
238        return $this->renderTemplate('label-list.phtml');
239    }
240
241    /**
242     * Render an entry in a favorite list.
243     *
244     * @param ?UserListEntityInterface $list Currently selected list (null for
245     * combined favorites)
246     * @param ?UserEntityInterface     $user Current logged in user (null if none)
247     *
248     * @return string
249     */
250    public function getListEntry($list = null, $user = null)
251    {
252        // Get list of lists containing this entry
253        $lists = null;
254        if ($user) {
255            $lists = $this->getDbService(UserListServiceInterface::class)->getListsContainingRecord(
256                $this->driver->getUniqueID(),
257                $this->driver->getSourceIdentifier(),
258                $user
259            );
260        }
261        return $this->renderTemplate(
262            'list-entry.phtml',
263            [
264                'driver' => $this->driver,
265                'list' => $list,
266                'user' => $user,
267                'lists' => $lists,
268            ]
269        );
270    }
271
272    /**
273     * Get notes associated with this record in user lists.
274     *
275     * @param int $list_id ID of list to load tags from (null for all lists)
276     * @param int $user_id ID of user to load tags from (null for all users)
277     *
278     * @return string[]
279     */
280    public function getListNotes($list_id = null, $user_id = null)
281    {
282        $data = $this->getDbService(UserResourceServiceInterface::class)->getFavoritesForRecord(
283            $this->driver->getUniqueId(),
284            $this->driver->getSourceIdentifier(),
285            $list_id,
286            $user_id
287        );
288        $notes = [];
289        foreach ($data as $current) {
290            if (!empty($note = $current->getNotes())) {
291                $notes[] = $note;
292            }
293        }
294        return $notes;
295    }
296
297    /**
298     * Render previews (data and link) of the item if configured.
299     *
300     * @return string
301     */
302    public function getPreviews()
303    {
304        return $this->getPreviewData() . $this->getPreviewLink();
305    }
306
307    /**
308     * Render data needed to get previews.
309     *
310     * @return string
311     */
312    public function getPreviewData()
313    {
314        return $this->renderTemplate(
315            'previewdata.phtml',
316            ['driver' => $this->driver, 'config' => $this->config]
317        );
318    }
319
320    /**
321     * Render links to previews of the item if configured.
322     *
323     * @return string
324     */
325    public function getPreviewLink()
326    {
327        return $this->renderTemplate(
328            'previewlink.phtml',
329            ['driver' => $this->driver, 'config' => $this->config]
330        );
331    }
332
333    /**
334     * Collects ISBN, LCCN, and OCLC numbers to use in calling preview APIs
335     *
336     * @return array
337     */
338    public function getPreviewIds()
339    {
340        // Extract identifiers from record driver if it supports appropriate methods:
341        $isbn = is_callable([$this->driver, 'getCleanISBN'])
342            ? $this->driver->getCleanISBN() : '';
343        $lccn = is_callable([$this->driver, 'getLCCN'])
344            ? $this->driver->getLCCN() : '';
345        $oclc = is_callable([$this->driver, 'getOCLC'])
346            ? $this->driver->getOCLC() : [];
347
348        // Turn identifiers into class names to communicate with jQuery logic:
349        $idClasses = [];
350        if (!empty($isbn)) {
351            $idClasses[] = 'ISBN' . $isbn;
352        }
353        if (!empty($lccn)) {
354            $idClasses[] = 'LCCN' . $lccn;
355        }
356        if (!empty($oclc)) {
357            foreach ($oclc as $oclcNum) {
358                if (!empty($oclcNum)) {
359                    $idClasses[] = 'OCLC' . $oclcNum;
360                }
361            }
362        }
363        return $idClasses;
364    }
365
366    /**
367     * Get tags associated with the currently-loaded record.
368     *
369     * @param UserListEntityInterface|int|null $listOrId  ID of list to load tags from (null for no restriction)
370     * @param UserEntityInterface|int|null     $userOrId  ID of user to load tags from (null for all users)
371     * @param string                           $sort      Sort type ('count' or 'tag')
372     * @param UserEntityInterface|int|null     $ownerOrId ID of user to check for ownership
373     *
374     * @return array
375     */
376    public function getTags(
377        UserListEntityInterface|int|null $listOrId = null,
378        UserEntityInterface|int|null $userOrId = null,
379        string $sort = 'count',
380        UserEntityInterface|int|null $ownerOrId = null
381    ): array {
382        return $this->tagsService->getRecordTags(
383            $this->driver->getUniqueId(),
384            $this->driver->getSourceIdentifier(),
385            0,
386            $listOrId,
387            $userOrId,
388            $sort,
389            $ownerOrId
390        );
391    }
392
393    /**
394     * Get tags associated with the currently-loaded record AND with a favorites list.
395     *
396     * @param UserListEntityInterface|int|null $listOrId  ID of list to load tags from (null for tags that
397     * are associated with ANY list, but excluding non-list tags)
398     * @param UserEntityInterface|int|null     $userOrId  ID of user to load tags from (null for all users)
399     * @param string                           $sort      Sort type ('count' or 'tag')
400     * @param UserEntityInterface|int|null     $ownerOrId ID of user to check for ownership
401     *
402     * @return array
403     */
404    public function getTagsFromFavorites(
405        UserListEntityInterface|int|null $listOrId = null,
406        UserEntityInterface|int|null $userOrId = null,
407        string $sort = 'count',
408        UserEntityInterface|int|null $ownerOrId = null
409    ): array {
410        return $this->tagsService->getRecordTagsFromFavorites(
411            $this->driver->getUniqueId(),
412            $this->driver->getSourceIdentifier(),
413            0,
414            $listOrId,
415            $userOrId,
416            $sort,
417            $ownerOrId
418        );
419    }
420
421    /**
422     * Get HTML to render a title.
423     *
424     * @param int $maxLength Maximum length of non-highlighted title.
425     *
426     * @return string
427     */
428    public function getTitleHtml($maxLength = 180)
429    {
430        $highlightedTitle = $this->driver->tryMethod('getHighlightedTitle');
431        $title = trim($this->driver->tryMethod('getTitle'));
432        if (!empty($highlightedTitle)) {
433            $highlight = $this->getView()->plugin('highlight');
434            $addEllipsis = $this->getView()->plugin('addEllipsis');
435            return $highlight($addEllipsis($highlightedTitle, $title));
436        }
437        if (!empty($title)) {
438            $escapeHtml = $this->getView()->plugin('escapeHtml');
439            $truncate = $this->getView()->plugin('truncate');
440            return $escapeHtml($truncate($title, $maxLength));
441        }
442        $transEsc = $this->getView()->plugin('transEsc');
443        return $transEsc('Title not available');
444    }
445
446    /**
447     * Render the link of the specified type.
448     *
449     * @param string $type    Link type
450     * @param string $lookfor String to search for at link
451     *
452     * @return string
453     */
454    public function getLink($type, $lookfor)
455    {
456        $link = $this->renderTemplate(
457            'link-' . $type . '.phtml',
458            ['driver' => $this->driver, 'lookfor' => $lookfor]
459        );
460
461        $prepend = (!str_contains($link, '?')) ? '?' : '&amp;';
462
463        $link .= $this->getView()->plugin('searchTabs')
464            ->getCurrentHiddenFilterParams(
465                $this->driver->getSearchBackendIdentifier(),
466                false,
467                $prepend
468            );
469        return $link;
470    }
471
472    /**
473     * Render the contents of the specified record tab.
474     *
475     * @param \VuFind\RecordTab\TabInterface $tab Tab to display
476     *
477     * @return string
478     */
479    public function getTab(\VuFind\RecordTab\TabInterface $tab)
480    {
481        $context = ['driver' => $this->driver, 'tab' => $tab];
482        $classParts = explode('\\', $tab::class);
483        $template = 'RecordTab/' . strtolower(array_pop($classParts)) . '.phtml';
484        $oldContext = $this->contextHelper->apply($context);
485        $html = $this->view->render($template);
486        $this->contextHelper->restore($oldContext);
487        return $html;
488    }
489
490    /**
491     * Render a toolbar for use on the record view.
492     *
493     * @return string
494     */
495    public function getToolbar()
496    {
497        return $this->renderTemplate('toolbar.phtml');
498    }
499
500    /**
501     * Render a search result for the specified view mode.
502     *
503     * @param string $view View mode to use.
504     *
505     * @return string
506     */
507    public function getSearchResult($view)
508    {
509        return $this->renderTemplate('result-' . $view . '.phtml');
510    }
511
512    /**
513     * Render an HTML checkbox control for the current record.
514     *
515     * @param string $idPrefix Prefix for checkbox HTML ids
516     * @param string $formAttr ID of form for [form] attribute
517     * @param int    $number   Result number (for label of checkbox)
518     *
519     * @return string
520     */
521    public function getCheckbox($idPrefix = '', $formAttr = false, $number = null)
522    {
523        $id = $this->driver->getSourceIdentifier() . '|'
524            . $this->driver->getUniqueId();
525        $context
526            = ['id' => $id, 'number' => $number, 'prefix' => $idPrefix];
527        if ($formAttr) {
528            $context['formAttr'] = $formAttr;
529        }
530        return $this->contextHelper->renderInContext(
531            'record/checkbox.phtml',
532            $context
533        );
534    }
535
536    /**
537     * Render a cover for the current record.
538     *
539     * @param string $context Context of code being generated
540     * @param string $default The default size of the cover
541     * @param string $link    The link for the anchor
542     *
543     * @return string
544     */
545    public function getCover($context, $default, $link = false)
546    {
547        $details = $this->getCoverDetails($context, $default, $link);
548        return $details['html'];
549    }
550
551    /**
552     * Should cover images be linked to previews (when applicable) in the provided
553     * template context?
554     *
555     * @param string $context Context of code being generated
556     *
557     * @return bool
558     */
559    protected function getPreviewCoverLinkSetting($context)
560    {
561        static $previewContexts = false;
562        if (false === $previewContexts) {
563            $previewContexts = isset($this->config->Content->linkPreviewsToCovers)
564                ? array_map(
565                    'trim',
566                    explode(',', $this->config->Content->linkPreviewsToCovers)
567                ) : ['*'];
568        }
569        return in_array('*', $previewContexts)
570            || in_array($context, $previewContexts);
571    }
572
573    /**
574     * Get the rendered cover plus some useful parameters.
575     *
576     * @param string $context Context of code being generated
577     * @param string $default The default size of the cover
578     * @param string $link    The link for the anchor
579     *
580     * @return array
581     */
582    public function getCoverDetails($context, $default, $link = false)
583    {
584        $details = compact('link', 'context') + [
585            'driver' => $this->driver, 'cover' => false, 'size' => false,
586            'linkPreview' => $this->getPreviewCoverLinkSetting($context),
587        ];
588        $preferredSize = $this->getCoverSize($context, $default);
589        if (empty($preferredSize)) {    // covers disabled entirely
590            $details['html'] = '';
591        } else {
592            // Find best option if more than one size is defined (e.g. small:medium)
593            foreach (explode(':', $preferredSize) as $size) {
594                if ($details['cover'] = $this->getThumbnail($size)) {
595                    $details['size'] = $size;
596                    break;
597                }
598            }
599            if ($details['size'] === false) {
600                [$details['size']] = explode(':', $preferredSize);
601            }
602            $details['html'] = $this->renderTemplate('cover.phtml', $details);
603        }
604        return $details;
605    }
606
607    /**
608     * Get the configured thumbnail size for record lists
609     *
610     * @param string $context Context of code being generated
611     * @param string $default The default size of the cover
612     *
613     * @return string
614     */
615    protected function getCoverSize($context, $default = 'medium')
616    {
617        if (
618            isset($this->config->Content->coversize)
619            && !$this->config->Content->coversize
620        ) {
621            // covers disabled entirely
622            return false;
623        }
624        // check for context-specific overrides
625        return $this->config->Content->coversize[$context] ?? $default;
626    }
627
628    /**
629     * Get the configured thumbnail alignment
630     *
631     * @param string $context telling the context asking, prepends the config key
632     *
633     * @return string
634     */
635    public function getThumbnailAlignment($context = 'result')
636    {
637        $view = $this->getView();
638        $configField = $context . 'ThumbnailsOnLeft';
639        $left = !isset($this->config->Site->$configField)
640            ? true : $this->config->Site->$configField;
641        $mirror = !isset($this->config->Site->mirrorThumbnailsRTL)
642            ? true : $this->config->Site->mirrorThumbnailsRTL;
643        if ($view->layout()->rtl && !$mirror) {
644            $left = !$left;
645        }
646        return $left ? 'left' : 'right';
647    }
648
649    /**
650     * Generate a qrcode URL (return false if unsupported).
651     *
652     * @param string $context Context of code being generated (core or results)
653     * @param array  $extra   Extra details to pass to the QR code template
654     * @param string $level   QR code level
655     * @param int    $size    QR code size
656     * @param int    $margin  QR code margin
657     *
658     * @return string|bool
659     */
660    public function getQrCode(
661        $context,
662        $extra = [],
663        $level = 'L',
664        $size = 3,
665        $margin = 4
666    ) {
667        if (!isset($this->config->QRCode)) {
668            return false;
669        }
670
671        switch ($context) {
672            case 'core':
673            case 'results':
674                $key = 'showIn' . ucwords(strtolower($context));
675                break;
676            default:
677                return false;
678        }
679
680        if (
681            !isset($this->config->QRCode->$key)
682            || !$this->config->QRCode->$key
683        ) {
684            return false;
685        }
686
687        $template = $context . '-qrcode.phtml';
688
689        // Try to build text:
690        $text = $this->renderTemplate(
691            $template,
692            $extra + ['driver' => $this->driver]
693        );
694        $qrcode = [
695            'text' => $text, 'level' => $level, 'size' => $size, 'margin' => $margin,
696        ];
697
698        $urlHelper = $this->getView()->plugin('url');
699        return $urlHelper('qrcode-show') . '?' . http_build_query($qrcode);
700    }
701
702    /**
703     * Generate a thumbnail URL (return false if unsupported).
704     *
705     * @param string $size Size of thumbnail (small, medium or large -- small is
706     * default).
707     *
708     * @return string|bool
709     */
710    public function getThumbnail($size = 'small')
711    {
712        // Find out whether or not AJAX covers are enabled; this will control
713        // whether dynamic URLs are resolved immediately or deferred until later
714        // (see third parameter of getUrl() below).
715        $ajaxcovers = $this->config->Content->ajaxcovers ?? false;
716        return $this->coverRouter
717            ? $this->coverRouter->getUrl($this->driver, $size, !$ajaxcovers)
718            : false;
719    }
720
721    /**
722     * Get all URLs associated with the record. Returns an array of strings.
723     *
724     * @return array
725     */
726    public function getUrlList()
727    {
728        // Use a filter to pick URLs from the output of getLinkDetails():
729        $filter = function ($i) {
730            return $i['url'];
731        };
732        return array_map($filter, $this->getLinkDetails());
733    }
734
735    /**
736     * Get all the links associated with this record. Returns an array of
737     * associative arrays each containing 'desc' and 'url' keys.
738     *
739     * @param bool $openUrlActive Is there an active OpenURL on the page?
740     *
741     * @return array
742     */
743    public function getLinkDetails($openUrlActive = false)
744    {
745        // See if there are any links available:
746        $urls = $this->driver->tryMethod('getURLs');
747        if (empty($urls) || ($openUrlActive && $this->hasOpenUrlReplaceSetting())) {
748            return [];
749        }
750
751        // If we found links, we may need to convert from the "route" format
752        // to the "full URL" format.
753        $urlHelper = $this->getView()->plugin('url');
754        $serverUrlHelper = $this->getView()->plugin('serverurl');
755        $formatLink = function ($link) use ($urlHelper, $serverUrlHelper) {
756            // Error if route AND URL are missing at this point!
757            if (!isset($link['route']) && !isset($link['url'])) {
758                throw new \Exception('Invalid URL array.');
759            }
760
761            // Build URL from route/query details if missing:
762            if (!isset($link['url'])) {
763                $routeParams = $link['routeParams'] ?? [];
764
765                $link['url'] = $serverUrlHelper(
766                    $urlHelper($link['route'], $routeParams)
767                );
768                if (isset($link['queryString'])) {
769                    $link['url'] .= $link['queryString'];
770                }
771            }
772
773            // Apply prefix if found
774            if (isset($link['prefix'])) {
775                $link['url'] = $link['prefix'] . $link['url'];
776            }
777            // Use URL as description if missing:
778            if (!isset($link['desc'])) {
779                $link['desc'] = $link['url'];
780            }
781
782            return $link;
783        };
784
785        return $this->deduplicateLinks(array_map($formatLink, $urls));
786    }
787
788    /**
789     * Get all the links associated with this record depending on the OpenURL setting
790     * replace_other_urls. Returns an array of associative arrays each containing
791     * 'desc' and 'url' keys.
792     *
793     * @return bool
794     */
795    protected function hasOpenUrlReplaceSetting()
796    {
797        return isset($this->config->OpenURL->replace_other_urls)
798            && $this->config->OpenURL->replace_other_urls;
799    }
800
801    /**
802     * Remove duplicates from the array. All keys and values are being used
803     * recursively to compare, so if there are 2 links with the same url
804     * but different desc, they will both be preserved.
805     *
806     * @param array $links array of associative arrays,
807     * each containing 'desc' and 'url' keys
808     *
809     * @return array
810     */
811    protected function deduplicateLinks($links)
812    {
813        return array_values(array_unique($links, SORT_REGULAR));
814    }
815}