Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
23.95% covered (danger)
23.95%
40 / 167
37.04% covered (danger)
37.04%
10 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
SolrOverdrive
23.95% covered (danger)
23.95%
40 / 167
37.04% covered (danger)
37.04%
10 / 27
2616.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 supportsOpenUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 supportsCoinsOpenUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAvailableDigitalFormats
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getDigitalFormats
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getFormattedDigitalFormats
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
110
 getPreviewLinks
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 supportsAjaxStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOverdriveAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isLoggedIn
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getOverdriveID
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 getOverdriveAvailability
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 supportsPatronActions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCheckedOut
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
90
 isHeld
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 getBreadcrumb
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getMarcReader
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getTitleSection
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getGeneralNotes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getThumbnail
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
2.21
 getSummary
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getIsMarc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllSubjectHeadings
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getFormattedRawData
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getRealTimeTitleHold
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getURLs
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPermanentLink
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * VuFind Record Driver for SolrOverdrive Records
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2019.
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
22 * USA
23 *
24 * @category VuFind
25 * @package  RecordDrivers
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @author   Brent Palmer <brent-palmer@icpl.org>
28 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public
29 *           License
30 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
31 */
32
33namespace VuFind\RecordDriver;
34
35use Laminas\Config\Config;
36use Laminas\Log\LoggerAwareInterface;
37use VuFind\DigitalContent\OverdriveConnector;
38
39use function in_array;
40
41/**
42 * VuFind Record Driver for SolrOverdrive Records
43 *
44 * @category VuFind
45 * @package  RecordDrivers
46 * @author   Demian Katz <demian.katz@villanova.edu>
47 * @author   Brent Palmer <brent-palmer@icpl.org>
48 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public
49 *           License
50 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
51 */
52class SolrOverdrive extends SolrMarc implements LoggerAwareInterface
53{
54    use \VuFind\Log\LoggerAwareTrait {
55        logError as error;
56    }
57
58    /**
59     * Overdrive Connector
60     *
61     * @var OverdriveConnector $connector Overdrive Connector
62     */
63    protected $connector;
64
65    /**
66     * Overdrive Configuration Object
67     *
68     * @var object
69     */
70    protected $config;
71
72    /**
73     * Constructor
74     *
75     * @param Config             $mainConfig   VuFind main configuration
76     * @param Config             $recordConfig Record-specific configuration
77     * @param OverdriveConnector $connector    Overdrive Connector
78     */
79    public function __construct(
80        Config $mainConfig = null,
81        $recordConfig = null,
82        OverdriveConnector $connector = null
83    ) {
84        $this->connector = $connector;
85        $this->config = $connector->getConfig();
86        parent::__construct($mainConfig, $recordConfig, null);
87    }
88
89    /**
90     * Supports OpenURL
91     *
92     * @return bool
93     */
94    public function supportsOpenUrl()
95    {
96        return false;
97    }
98
99    /**
100     * Supports coins OpenURL
101     *
102     * @return bool
103     */
104    public function supportsCoinsOpenUrl()
105    {
106        return false;
107    }
108
109    /**
110     * Get Available Digital Formats
111     *
112     * Return the digital download formats that are available for linking to.
113     *
114     * @return array
115     * @throws \Exception
116     */
117    public function getAvailableDigitalFormats()
118    {
119        $formats = [];
120        $formatNames = $this->connector->getFormatNames();
121        $od_id = $this->getOverdriveID();
122
123        if ($checkout = $this->connector->getCheckout($od_id, false)) {
124            // If we're already locked in, then we need free ones and locked in ones.
125            if ($checkout->isFormatLockedIn) {
126                foreach ($checkout->formats as $format) {
127                    $formatType = $format->formatType;
128                    $formats[$formatType] = $formatNames[$formatType];
129                }
130            } else {
131                // Not locked in, we can show all formats
132                foreach ($this->getDigitalFormats() as $format) {
133                    $formats[$format->id] = $formatNames[$format->id];
134                }
135            }
136        }
137        return $formats;
138    }
139
140    /**
141     * Get Formats
142     *
143     * Returns an array of digital formats for this resource.
144     *
145     * @return array Array of formats.
146     * @throws \Exception
147     */
148    public function getDigitalFormats()
149    {
150        $formats = [];
151        $formatNames = $this->connector->getFormatNames();
152        if ($this->getIsMarc()) {
153            $od_id = $this->getOverdriveID();
154            $fulldata = $this->connector->getMetadata([$od_id]);
155            $data = $fulldata[strtolower($od_id)];
156        } else {
157            $jsonData = $this->fields['fullrecord'];
158            $data = json_decode($jsonData, false);
159        }
160
161        foreach ($data->formats as $format) {
162            $format->name = $formatNames[$format->id];
163            $formats[$format->id] = $format;
164        }
165
166        return $formats;
167    }
168
169    /**
170     * Get an array of all the formats associated with the record with metadata
171     * associated with it. This array is designed to be used in a template.
172     * The key for each entry is the translatable token for the format name
173     *
174     * @return array
175     * @throws \Exception
176     */
177    public function getFormattedDigitalFormats()
178    {
179        $results = [];
180        foreach ($this->getDigitalFormats() as $key => $format) {
181            $tmpresults = [];
182            if ($format->fileSize > 0) {
183                if ($format->fileSize > 1000000) {
184                    $size = round($format->fileSize / 1000000);
185                    $size .= ' GB';
186                } elseif ($format->fileSize > 1000) {
187                    $size = round($format->fileSize / 1000);
188                    $size .= ' MB';
189                } else {
190                    $size = $format->fileSize;
191                    $size .= ' KB';
192                }
193                $tmpresults['File Size'] = $size;
194            }
195            if ($format->partCount) {
196                $tmpresults['Parts'] = $format->partCount;
197            }
198            if ($format->identifiers) {
199                foreach ($format->identifiers as $id) {
200                    if (in_array($id->type, ['ISBN', 'ASIN'])) {
201                        $tmpresults[$id->type] = $id->value;
202                    }
203                }
204            }
205            if ($format->onSaleDate) {
206                $tmpresults['Release Date'] = $format->onSaleDate;
207            }
208            $results[$format->name] = $tmpresults;
209        }
210
211        return $results;
212    }
213
214    /**
215     * Returns links for showing previews
216     *
217     * @return array      an array of links
218     * @throws \Exception
219     */
220    public function getPreviewLinks()
221    {
222        $results = [];
223        if ($this->getIsMarc()) {
224            $od_id = $this->getOverdriveID();
225            $fulldata = $this->connector->getMetadata([$od_id]);
226            $data = $fulldata[strtolower($od_id)] ?? null;
227        } else {
228            $jsonData = $this->fields['fullrecord'];
229            $data = json_decode($jsonData, false);
230        }
231
232        if (isset($data->formats[0]->samples[0])) {
233            foreach ($data->formats[0]->samples as $format) {
234                if (
235                    $format->formatType == 'audiobook-overdrive'
236                    || $format->formatType == 'ebook-overdrive'
237                    || $format->formatType == 'magazine-overdrive'
238                ) {
239                    $results = $format;
240                }
241            }
242        }
243        return $results;
244    }
245
246    /**
247     * Returns true if the record supports real-time AJAX status lookups.
248     *
249     * @return bool
250     */
251    public function supportsAjaxStatus()
252    {
253        return $this->config->enableAjaxStatus ?? true;
254    }
255
256    /**
257     * Get Overdrive Access
258     *
259     * Pass-through to the connector to determine whether logged-in user
260     * has access to Overdrive actions
261     *
262     * @return bool Whether the logged-in user has access to Overdrive.
263     */
264    public function getOverdriveAccess()
265    {
266        return $this->connector->getAccess();
267    }
268
269    /**
270     * Is Logged in
271     *
272     * Returns whether the current user is logged in
273     *
274     * @return bool
275     */
276    public function isLoggedIn()
277    {
278        return $this->connector->getUser() ? true : false;
279    }
280
281    /**
282     * Get Overdrive ID
283     *
284     * Returns the Overdrive ID (or resource ID) for the current item. Note: for
285     * records in marc format, this may be different than the Solr Record ID
286     *
287     * @return string OverdriveID
288     * @throws \Exception
289     */
290    public function getOverdriveID()
291    {
292        $result = 0;
293
294        if ($this->config) {
295            if ($this->getIsMarc()) {
296                $field = $this->config->idField;
297                $subfield = $this->config->idSubfield;
298                $result = strtolower(
299                    $this->getFieldArray($field, $subfield)[0] ?? ''
300                );
301            } else {
302                $result = strtolower($this->getUniqueID());
303            }
304        }
305        return $result;
306    }
307
308    /**
309     * Returns the availability for the current record
310     *
311     * @return object|bool returns an object with the info in it (see URL above)
312     * or false if there was a problem.
313     * @throws \Exception
314     */
315    public function getOverdriveAvailability()
316    {
317        $overDriveId = $this->getOverdriveID();
318        return $this->connector->getAvailability($overDriveId);
319    }
320
321    /**
322     * Returns a boolean indicating if patron actions are supported
323     *
324     * @return bool
325     */
326    public function supportsPatronActions()
327    {
328        return $this->config->usePatronAPI;
329    }
330
331    /**
332     * Is Checked Out
333     *
334     * Is this resource already checked out to the user?
335     *
336     * @return object Returns the checkout information if currently checked out
337     *    by this user or false in the data property if not.
338     * @throws \Exception
339     */
340    public function isCheckedOut()
341    {
342        $result = $this->connector->getResultObject();
343        if ($this->isLoggedIn() && $this->supportsPatronActions()) {
344            $overdriveID = $this->getOverdriveID();
345            $result = $this->connector->getCheckouts(true);
346            if ($result->status) {
347                $checkedout = false;
348                $checkouts = $result->data;
349                // In case of a magazine issue, we have to get all the checkouts to see if the
350                // current title is the parentID of one of the user's checkouts. Return data as
351                // array in case there are multiple issues checked out to the user.
352                $result->data = [];
353                $result->isMagazine = false;
354                foreach ($checkouts as $checkout) {
355                    if ($checkout->metadata->mediaType == 'Magazine') {
356                        $idToCheck = strtolower($checkout->metadata->parentMagazineReferenceId);
357                    } else {
358                        $idToCheck = $checkout->reserveId;
359                    }
360                    if (strtolower($idToCheck) == $overdriveID) {
361                        $checkedout = true;
362                        $result->status = true;
363                        $result->data[] = $checkout;
364                        //this checkout is a magazine issue of the current title
365                        if ($checkout->metadata->mediaType == 'Magazine') {
366                            $result->isMagazine =  true;
367                        }
368                    }
369                }
370                if (!$checkedout) {
371                    $result->data = false;
372                }
373            }
374        }
375        return $result;
376    }
377
378    /**
379     * Is Held
380     * Checks to see if the current record is on hold through Overcdrive.
381     *
382     * @return object|bool Returns the hold info if on hold or false if not.
383     * @throws \Exception
384     */
385    public function isHeld()
386    {
387        if ($this->isLoggedIn() && $this->supportsPatronActions()) {
388            $overDriveId = $this->getOverdriveID();
389            $result = $this->connector->getHolds(true);
390            if ($result->status) {
391                $holds = $result->data;
392                foreach ($holds as $hold) {
393                    if (strtolower($hold->reserveId) == $overDriveId) {
394                        return $hold;
395                    }
396                }
397            }
398        }
399        // If it didn't work, an error should be logged from the connector
400        return false;
401    }
402
403    /**
404     * Get Bread Crumb
405     *
406     * @return string
407     */
408    public function getBreadcrumb()
409    {
410        $short = $this->getShortTitle();
411        return $short ? $short : $this->getTitle();
412    }
413
414    /**
415     * Get Marc Reader
416     *
417     * Override the base marc trait to return an empty marc reader object if no MARC
418     * is available.
419     *
420     * @return \VuFind\Marc\MarcReader
421     */
422    public function getMarcReader()
423    {
424        return $this->getIsMarc()
425            ? parent::getMarcReader()
426            : new $this->marcReaderClass('<record></record>');
427    }
428
429    /**
430     * Get Title Section
431     *
432     * @return string
433     */
434    public function getTitleSection()
435    {
436        return $this->getIsMarc()
437            ? parent::getTitleSection()
438            : ''; // I don't think Overdrive has this metadata
439    }
440
441    /**
442     * Get general notes on the record.
443     *
444     * @return array
445     */
446    public function getGeneralNotes()
447    {
448        return $this->getIsMarc() ? parent::getGeneralNotes() : [];
449    }
450
451    /**
452     * Returns one of three things: a full URL to a thumbnail preview of the
453     * record if an image is available in an external system; an array of
454     * parameters to send to VuFind's internal cover generator if no fixed URL
455     * exists; or false if no thumbnail can be generated.
456     *
457     * @param string $size Size of thumbnail (small, medium or large -- small
458     *                     is
459     *                     default).
460     *
461     * @return string|array|bool
462     * @throws \Exception
463     */
464    public function getThumbnail($size = 'small')
465    {
466        $coverMap = [
467            'large' => 'cover300Wide',
468            'medium' => 'cover150Wide',
469            'small' => 'thumbnail',
470        ];
471        $cover = $coverMap[$size] ?? 'cover';
472
473        // If the record is marc then the cover links probably aren't there.
474        if ($this->getIsMarc()) {
475            $od_id = $this->getOverdriveID();
476            $fulldata = $this->connector->getMetadata([$od_id]);
477            $data = $fulldata[strtolower($od_id)] ?? null;
478        } else {
479            $jsonData = $this->fields['fullrecord'];
480            $data = json_decode($jsonData, false);
481        }
482        return $data->images->{$cover}->href ?? false;
483    }
484
485    /**
486     * Get an array of summary strings for the record.
487     *
488     * @return array
489     */
490    public function getSummary()
491    {
492        if ($this->getIsMarc()) {
493            return parent::getSummary();
494        }
495        // Non-MARC case:
496        $desc = $this->fields['description'] ?? '';
497
498        $newDesc = preg_replace('/&#8217;/i', '', $desc);
499        $newDesc = strip_tags($newDesc);
500        return [$newDesc];
501    }
502
503    /**
504     * Is Marc Based Record
505     *
506     * Return whether this is a marc-based record.
507     *
508     * @return bool
509     */
510    public function getIsMarc()
511    {
512        return $this->config->isMarc;
513    }
514
515    /**
516     * Get all subject headings associated with this record. Each heading is
517     * returned as an array of chunks, increasing from least specific to most
518     * specific.
519     *
520     * @param bool $extended Whether to return a keyed array with the following
521     *                       keys:
522     *                       - heading: the actual subject heading chunks
523     *                       - type: heading type
524     *                       - source: source vocabulary
525     *
526     * @return array
527     */
528    public function getAllSubjectHeadings($extended = false)
529    {
530        if (!$this->config) {
531            return [];
532        }
533        return $this->getIsMarc()
534            ? parent::getAllSubjectHeadings($extended)
535            : DefaultRecord::getAllSubjectHeadings($extended);
536    }
537
538    /**
539     * Get Formatted Raw Data
540     *
541     * Returns the raw data formatted for staff display tab
542     *
543     * @return array Multidimensional array with data
544     */
545    public function getFormattedRawData()
546    {
547        $jsonData = $this->fields['fullrecord'];
548        $data = json_decode($jsonData, true);
549        $c_arr = [];
550        foreach ($data['creators'] as $creator) {
551            $c_arr[] = "<strong>{$creator['role']}<strong>: "
552                . $creator['name'];
553        }
554        $data['creators'] = implode('<br>', $c_arr);
555        return $data;
556    }
557
558    /**
559     * Get a link for placing a title level hold.
560     *
561     * @return mixed A url if a hold is possible, boolean false if not
562     */
563    public function getRealTimeTitleHold()
564    {
565        $od_id = $this->getOverdriveID();
566        $rec_id = $this->getUniqueID();
567        $urlDetails = [
568            'action' => 'Hold',
569            'record' => $rec_id,
570            'query' => "od_id=$od_id&rec_id=$rec_id",
571            'anchor' => '',
572        ];
573        return $urlDetails;
574    }
575
576    /**
577     * Return an array of associative URL arrays with one or more of the following
578     * keys:
579     *
580     * <li>
581     *   <ul>desc: URL description text to display (optional)</ul>
582     *   <ul>url: fully-formed URL (required if 'route' is absent)</ul>
583     *   <ul>route: VuFind route to build URL with (required if 'url' is absent)</ul>
584     *   <ul>routeParams: Parameters for route (optional)</ul>
585     *   <ul>queryString: Query params to append after building route (optional)</ul>
586     * </li>
587     *
588     * @return array
589     */
590    public function getURLs()
591    {
592        return $this->getIsMarc()
593            ? parent::getURLs()
594            : $this->getPermanentLink();
595    }
596
597    /**
598     * Get Permanent Link to the resource on your institution's OverDrive site
599     *
600     * @return array the permanent link to the resource
601     */
602    public function getPermanentLink()
603    {
604        if (!empty($libraryURL = $this->config->libraryURL)) {
605            $data = json_decode($this->fields['fullrecord'], false);
606            $desc = $this->translate('od_resource_page');
607            $permlink = "$libraryURL/media/" . $data->crossRefId;
608            return  [['url' => $permlink, 'desc' => $desc ?: $permlink]];
609        } else {
610            return [];
611        }
612    }
613}