Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
23.20% covered (danger)
23.20%
58 / 250
14.29% covered (danger)
14.29%
3 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Loader
23.20% covered (danger)
23.20%
58 / 250
14.29% covered (danger)
14.29%
3 / 21
4721.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 getCoverGeneratorSettings
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 setCoverGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultSettings
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 getImageSettingsFromLegacyArgs
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 storeSanitizedSettings
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
3.01
 loadImage
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 loadUnavailable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasLoadedUnavailable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 determineLocalFile
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
156
 getIdentifiers
52.38% covered (warning)
52.38%
11 / 21
0.00% covered (danger)
0.00%
0 / 1
52.99
 fetchFromAPI
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 getCachePath
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 fetchFromContentType
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 convertNonJpeg
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 validateAndMoveTempFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 processImageURLForSource
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
132
 processImageURL
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 getCoverUrls
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 getHandlers
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
13.26
 getIdentifiersForSettings
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Book Cover Generator
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2007.
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  Cover_Generator
25 * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org/wiki/configuration:external_content Wiki
29 */
30
31namespace VuFind\Cover;
32
33use VuFind\Content\Covers\PluginManager as ApiManager;
34use VuFindCode\ISBN;
35use VuFindCode\ISMN;
36
37use function func_get_args;
38use function in_array;
39use function is_array;
40use function is_callable;
41use function strlen;
42
43/**
44 * Book Cover Generator
45 *
46 * @category VuFind
47 * @package  Cover_Generator
48 * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
49 * @author   Demian Katz <demian.katz@villanova.edu>
50 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
51 * @link     https://vufind.org/wiki/configuration:external_content Wiki
52 */
53class Loader extends \VuFind\ImageLoader
54{
55    /**
56     * Class for rendering cover images dynamically if no API match found. Omit
57     * to disable functionality.
58     *
59     * @var Generator
60     */
61    protected $generator = null;
62
63    /**
64     * Filename constructed from ISBN
65     *
66     * @var string
67     */
68    protected $localFile = '';
69
70    /**
71     * Valid image sizes to request
72     *
73     * @var array
74     */
75    protected $validSizes = ['small', 'medium', 'large'];
76
77    /**
78     * VuFind configuration settings
79     *
80     * @var \Laminas\Config\Config
81     */
82    protected $config;
83
84    /**
85     * Plugin manager for API handlers
86     *
87     * @var ApiManager
88     */
89    protected $apiManager;
90
91    /**
92     * HTTP client factory
93     *
94     * @var \VuFindHttp\HttpService
95     */
96    protected $httpService;
97
98    /**
99     * Directory to store downloaded images
100     *
101     * @var string
102     */
103    protected $baseDir;
104
105    /**
106     * User ISBNs parameter
107     *
108     * @var ISBN[]
109     */
110    protected $isbns = null;
111
112    /**
113     * User ISSN parameter
114     *
115     * @var string
116     */
117    protected $issn = null;
118
119    /**
120     * User OCLC number parameter
121     *
122     * @var string
123     */
124    protected $oclc = null;
125
126    /**
127     * User UPC number parameter
128     *
129     * @var string
130     */
131    protected $upc = null;
132
133    /**
134     * User National bibliography number parameter
135     *
136     * @var array
137     */
138    protected $nbn = null;
139
140    /**
141     * User ISMN parameter
142     *
143     * @var ISMN
144     */
145    protected $ismn = null;
146
147    /**
148     * User UUID parameter
149     *
150     * @var string
151     */
152    protected $uuid = null;
153
154    /**
155     * User record id number parameter
156     *
157     * @var string
158     */
159    protected $recordid = null;
160
161    /**
162     * User record source parameter
163     *
164     * @var string
165     */
166    protected $source = null;
167
168    /**
169     * User size parameter
170     *
171     * @var string
172     */
173    protected $size;
174
175    /**
176     * User type parameter
177     *
178     * @var string
179     */
180    protected $type;
181
182    /**
183     * Flag denoting the last loaded image was a FailImage
184     *
185     * @var bool
186     */
187    protected $hasLoadedUnavailable = false;
188
189    /**
190     * Constructor
191     *
192     * @param \Laminas\Config\Config  $config      VuFind configuration
193     * @param ApiManager              $manager     Plugin manager for API handlers
194     * @param \VuFindTheme\ThemeInfo  $theme       VuFind theme tools
195     * @param \VuFindHttp\HttpService $httpService HTTP client factory
196     * @param string                  $baseDir     Directory to store downloaded
197     * images (set to system temp dir if not otherwise specified)
198     */
199    public function __construct(
200        $config,
201        ApiManager $manager,
202        \VuFindTheme\ThemeInfo $theme,
203        \VuFindHttp\HttpService $httpService,
204        $baseDir = null
205    ) {
206        $this->setThemeInfo($theme);
207        $this->config = $config;
208        $this->configuredFailImage = $config->Content->noCoverAvailableImage ?? null;
209        $this->apiManager = $manager;
210        $this->httpService = $httpService;
211        $this->baseDir = (null === $baseDir)
212            ? rtrim(sys_get_temp_dir(), '\\/') . '/covers'
213            : rtrim($baseDir, '\\/');
214    }
215
216    /**
217     * Get settings for the cover generator.
218     *
219     * @return array
220     */
221    protected function getCoverGeneratorSettings()
222    {
223        $settings = isset($this->config->DynamicCovers)
224            ? $this->config->DynamicCovers->toArray() : [];
225        if (
226            !isset($settings['backgroundMode'])
227            && isset($this->config->Content->makeDynamicCovers)
228        ) {
229            $settings['backgroundMode'] = $this->config->Content->makeDynamicCovers;
230        }
231        $size = $this->size;
232        $pickSize = function ($setting) use ($size) {
233            if (isset($setting[$size])) {
234                return $setting[$size];
235            }
236            if (isset($setting['*'])) {
237                return $setting['*'];
238            }
239            return $setting;
240        };
241        return array_map($pickSize, $settings);
242    }
243
244    /**
245     * Set Cover Generator Object
246     *
247     * @param Generator $generator Cover generator
248     *
249     * @return void
250     */
251    public function setCoverGenerator(Generator $generator)
252    {
253        $this->generator = $generator;
254    }
255
256    /**
257     * Get default settings for loadImage().
258     *
259     * @return array
260     */
261    protected function getDefaultSettings()
262    {
263        return [
264            'isbns' => null,
265            'size' => 'small',
266            'type' => null,
267            'title' => null,
268            'author' => null,
269            'callnumber' => null,
270            'issn' => null,
271            'oclc' => null,
272            'upc' => null,
273            'recordid' => null,
274            'source' => null,
275            'nbn' => null,
276            'ismn' => null,
277            'uuid' => null,
278        ];
279    }
280
281    /**
282     * Translate legacy function arguments into new-style array.
283     *
284     * @param array $args Function arguments
285     *
286     * @return array
287     */
288    protected function getImageSettingsFromLegacyArgs($args)
289    {
290        return [
291            'isbn' => $args[0],
292            'size' => $args[1],
293            'type' => $args[2],
294            'title' => $args[3],
295            'author' => $args[4],
296            'callnumber' => $args[5],
297            'issn' => $args[6],
298            'oclc' => $args[7],
299            'upc' => $args[8],
300        ];
301    }
302
303    /**
304     * Support method for loadImage() -- sanitize and store some key values.
305     *
306     * @param array $settings Settings from loadImage
307     *
308     * @return void
309     */
310    protected function storeSanitizedSettings($settings)
311    {
312        $settings = array_merge($this->getDefaultSettings(), $settings);
313        $this->isbns = array_map(
314            function ($isbn) {
315                return new ISBN($isbn);
316            },
317            $settings['isbns']
318                ?? (empty($settings['isbn']) ? [] : [$settings['isbn']])
319        );
320        $this->ismn = new ISMN($settings['ismn'] ?? '');
321        if (!empty($settings['issn'])) {
322            $rawissn = preg_replace('/[^0-9X]/', '', strtoupper($settings['issn']));
323            $this->issn = substr($rawissn, 0, 8);
324        } else {
325            $this->issn = null;
326        }
327        $this->oclc = $settings['oclc'];
328        $this->upc = $settings['upc'];
329        $this->recordid = $settings['recordid'];
330        $this->source = $settings['source'];
331        $this->nbn = $settings['nbn'];
332        $this->uuid = $settings['uuid'];
333        $this->type = preg_replace('/[^a-zA-Z]/', '', $settings['type'] ?? '');
334        $this->size = $settings['size'];
335    }
336
337    /**
338     * Load an image given an ISBN and/or content type.
339     *
340     * @param array $settings Array of settings used to calculate a cover; may
341     * contain any or all of these keys: 'isbns' (array of ISBNs), 'size' (requested
342     * size), 'type' (content type), 'title' (title of book, for dynamic covers),
343     * 'author' (author of book, for dynamic covers), 'callnumber' (unique ID, for
344     * dynamic covers), 'issn' (ISSN), 'oclc' (OCLC number), 'upc' (UPC number),
345     * 'nbn' (national bibliography number), 'ismn' (ISMN), 'uuid' (Universally
346     * unique identifier).
347     *
348     * @return void
349     */
350    public function loadImage($settings = [])
351    {
352        // reset to normal
353        $this->hasLoadedUnavailable = false;
354        // Load settings from legacy function parameters if they are not passed
355        // in as an array:
356        $settings = is_array($settings)
357            ? $settings
358            : $this->getImageSettingsFromLegacyArgs(func_get_args());
359
360        // Store sanitized versions of some parameters for future reference:
361        $this->storeSanitizedSettings($settings);
362
363        // Display a fail image unless our parameters pass inspection and we
364        // are able to display an ISBN or content-type-based image.
365        if (!in_array($this->size, $this->validSizes)) {
366            $this->loadUnavailable();
367        } elseif (
368            !$this->fetchFromAPI()
369            && !$this->fetchFromContentType()
370        ) {
371            if ($this->generator) {
372                $this->generator->setOptions($this->getCoverGeneratorSettings());
373                $this->image = $this->generator->generate(
374                    $settings['title'],
375                    $settings['author'],
376                    $settings['callnumber']
377                );
378                $this->contentType = 'image/png';
379            } else {
380                $this->loadUnavailable();
381            }
382        }
383    }
384
385    /**
386     * {@inheritdoc}
387     * Adds @see self::$hasLoadedUnavailable flag
388     *
389     * @return void
390     */
391    public function loadUnavailable()
392    {
393        $this->hasLoadedUnavailable = true;
394        parent::loadUnavailable();
395    }
396
397    /**
398     * Returns true if the last loaded image was the FailImage
399     *
400     * @return bool
401     */
402    public function hasLoadedUnavailable()
403    {
404        return $this->hasLoadedUnavailable;
405    }
406
407    /**
408     * Support method for fetchFromAPI() -- set the localFile property.
409     *
410     * @param array $ids IDs returned by getIdentifiers() method
411     *
412     * @return string
413     */
414    protected function determineLocalFile($ids)
415    {
416        // We should check whether we have cached images for the 13- or 10-digit
417        // ISBNs. If no file exists, we'll favor the 10-digit number if
418        // available for the sake of brevity.
419        if (isset($ids['isbn'])) {
420            $file = $this->getCachePath($this->size, $ids['isbn']->get13());
421            if (!is_readable($file) && $ids['isbn']->get10()) {
422                return $this->getCachePath($this->size, $ids['isbn']->get10());
423            }
424            return $file;
425        } elseif (isset($ids['issn'])) {
426            return $this->getCachePath($this->size, $ids['issn']);
427        } elseif (isset($ids['oclc'])) {
428            return $this->getCachePath($this->size, 'OCLC' . $ids['oclc']);
429        } elseif (isset($ids['upc'])) {
430            return $this->getCachePath($this->size, 'UPC' . $ids['upc']);
431        } elseif (isset($ids['nbn'])) {
432            return $this->getCachePath($this->size, 'NBN' . $ids['nbn']);
433        } elseif (isset($ids['ismn'])) {
434            return $this->getCachePath($this->size, 'ISMN' . $ids['ismn']->get13());
435        } elseif (isset($ids['uuid'])) {
436            return $this->getCachePath($this->size, 'UUID' . $ids['uuid']);
437        } elseif (isset($ids['recordid']) && isset($ids['source'])) {
438            return $this->getCachePath(
439                $this->size,
440                'ID' . md5($ids['source'] . '|' . $ids['recordid'])
441            );
442        }
443        throw new \Exception('Cannot determine local file path.');
444    }
445
446    /**
447     * Get all valid identifiers as an associative array.
448     *
449     * @return array
450     */
451    protected function getIdentifiers()
452    {
453        $ids = [];
454        if (!empty($this->isbns)) {
455            $ids['isbn'] = $this->isbns[0];
456            $ids['isbns'] = $this->isbns;
457        }
458        if ($this->issn && strlen($this->issn) == 8) {
459            $ids['issn'] = $this->issn;
460        }
461        if ($this->oclc && strlen($this->oclc) > 0) {
462            $ids['oclc'] = $this->oclc;
463        }
464        if ($this->upc && strlen($this->upc) > 0) {
465            $ids['upc'] = $this->upc;
466        }
467        if ($this->nbn && strlen($this->nbn) > 0) {
468            $ids['nbn'] = $this->nbn;
469        }
470        if ($this->ismn && $this->ismn->isValid()) {
471            $ids['ismn'] = $this->ismn;
472        }
473        if ($this->uuid && strlen($this->uuid) > 0) {
474            $ids['uuid'] = $this->uuid;
475        }
476        if ($this->recordid && strlen($this->recordid) > 0) {
477            $ids['recordid'] = $this->recordid;
478        }
479        if ($this->source && strlen($this->source) > 0) {
480            $ids['source'] = $this->source;
481        }
482        return $ids;
483    }
484
485    /**
486     * Load bookcover from cache or remote provider and display if possible.
487     *
488     * @return bool        True if image loaded, false on failure.
489     */
490    protected function fetchFromAPI()
491    {
492        // Check that we have at least one valid identifier:
493        $ids = $this->getIdentifiers();
494        if (empty($ids)) {
495            return false;
496        }
497
498        // Set up local file path:
499        $this->localFile = $this->determineLocalFile($ids);
500        if (is_readable($this->localFile)) {
501            // Load local cache if available
502            $this->contentType = 'image/jpeg';
503            $this->image = file_get_contents($this->localFile);
504            return true;
505        } else {
506            $urls = $this->getCoverUrls();
507            foreach ($urls as $url) {
508                $success = $this->processImageURLForSource(
509                    $url['url'],
510                    $url['handler']->isCacheAllowed(),
511                    $url['apiName']
512                );
513                if ($success) {
514                    return true;
515                }
516            }
517        }
518        return false;
519    }
520
521    /**
522     * Return a path to the image cache for the given size and ID; ensure that
523     * directories are created as needed.
524     *
525     * @param string $size      Size category
526     * @param string $id        Unique identifier (ISBN / ISSN)
527     * @param string $extension File extension to use (default = jpg)
528     *
529     * @return string      Cache path
530     */
531    protected function getCachePath($size, $id, $extension = 'jpg')
532    {
533        $base = $this->baseDir;
534        if (!is_dir($base)) {
535            mkdir($base);
536        }
537        $base .= '/' . $size;
538        if (!is_dir($base)) {
539            mkdir($base);
540        }
541        return $base . '/' . $id . '.' . $extension;
542    }
543
544    /**
545     * Load content type icon image from URL from theme images and display if
546     * possible.
547     *
548     * @return bool        True if image loaded, false on failure.
549     */
550    protected function fetchFromContentType()
551    {
552        // Give up if no content type was passed in:
553        if (empty($this->type)) {
554            return false;
555        }
556
557        // Try to find an icon:
558        $iconFile = $this->searchTheme(
559            'images/' . $this->size . '/' . $this->type,
560            ['.png', '.gif', '.jpg']
561        );
562        if ($iconFile !== false) {
563            // Most content-type headers match file extensions... but
564            // include a special case for jpg vs. jpeg:
565            $format = substr($iconFile, -3);
566            $this->contentType
567                = 'image/' . ($format == 'jpg' ? 'jpeg' : $format);
568            $this->image = file_get_contents($iconFile);
569            return true;
570        }
571
572        // If we got this far, no icon was found:
573        return false;
574    }
575
576    /**
577     * Support method for validateAndMoveTempFile -- convert non-JPEG image data to a
578     * JPEG file.
579     *
580     * @param string $imageData Raw image data
581     * @param string $jpeg      JPEG file (output)
582     *
583     * @return bool             Did we succeed?
584     */
585    protected function convertNonJpeg($imageData, $jpeg)
586    {
587        // We can't proceed if we don't have image conversion functions:
588        if (!is_callable('imagecreatefromstring')) {
589            return false;
590        }
591
592        // Try to create a GD image and rewrite as JPEG, fail if we can't:
593        if (!($imageGD = @imagecreatefromstring($imageData))) {
594            return false;
595        }
596        if (!@imagejpeg($imageGD, $jpeg)) {
597            return false;
598        }
599
600        return true;
601    }
602
603    /**
604     * This method either moves the temporary file to its final location (true)
605     * or detects an error and deletes it (false).
606     *
607     * @param string $image     Raw image data
608     * @param string $tempFile  Temporary file
609     * @param string $finalFile Final file location
610     *
611     * @return bool
612     */
613    protected function validateAndMoveTempFile($image, $tempFile, $finalFile)
614    {
615        [$width, $height, $type] = @getimagesize($tempFile);
616
617        // File too small -- delete it and report failure.
618        if ($width < 2 && $height < 2) {
619            @unlink($tempFile);
620            return false;
621        }
622
623        // Conversion needed -- do some normalization for non-JPEG images:
624        if ($type != IMAGETYPE_JPEG) {
625            // We no longer need the temp file:
626            @unlink($tempFile);
627            return $this->convertNonJpeg($image, $finalFile);
628        }
629
630        // If $tempFile is already a JPEG, let's store it in the cache.
631        return @rename($tempFile, $finalFile);
632    }
633
634    /**
635     * Wrapper around processImageURL to determine cache setting based on
636     * image source.
637     *
638     * @param string $url        URL to load image from
639     * @param bool   $allowCache Is caching allowed by the service?
640     * @param string $source     Service being used for image loading
641     *
642     * @return bool         True if image loaded, false on failure.
643     */
644    protected function processImageURLForSource($url, $allowCache, $source)
645    {
646        // If caching is allowed at the source level, let's see if it's locally
647        // configured....
648        if ($allowCache) {
649            // All other services cache based on configuration:
650            $conf = isset($this->config->Content->coverimagesCache)
651                ? trim(strtolower($this->config->Content->coverimagesCache)) : true;
652            if ($conf === true || $conf === 1 || $conf === '1' || $conf === 'true') {
653                $cache = true;
654            } elseif (
655                $conf === false || $conf === 0 || $conf === '0'
656                || $conf === 'false'
657            ) {
658                $cache = false;
659            } else {
660                $conf = array_map('trim', explode(',', $conf));
661                $source = strtolower($source);
662                $cache = in_array($source, $conf);
663            }
664        } else {
665            $cache = false;
666        }
667        return $this->processImageURL($url, $cache);
668    }
669
670    /**
671     * Load image from URL, store in cache if requested, display if possible.
672     *
673     * @param string $url   URL to load image from
674     * @param string $cache Boolean -- should we store in local cache?
675     *
676     * @return bool         True if image loaded, false on failure.
677     */
678    protected function processImageURL($url, $cache = true)
679    {
680        // Check to see if url is a file path
681        if (str_starts_with($url, 'file://')) {
682            $imagePath = substr($url, 7);
683
684            // Display the image:
685            $this->contentType = mime_content_type($imagePath);
686            $this->image = file_get_contents($imagePath);
687            return true;
688        } else {
689            // Attempt to pull down the image:
690            $result = $this->httpService->createClient($url)->send();
691            if (!$result->isSuccess()) {
692                $this->debug('Failed to retrieve image from ' . $url);
693                return false;
694            }
695            $image = $result->getBody();
696
697            if ('' == $image) {
698                return false;
699            }
700
701            // Figure out file paths -- $tempFile will be used to store the
702            // image for analysis. $finalFile will be used for long-term storage if
703            // $cache is true or for temporary display purposes if $cache is false.
704            $tempFile = str_replace('.jpg', uniqid(), $this->localFile);
705            $finalFile = $cache ? $this->localFile : $tempFile . '.jpg';
706
707            // Write image data to disk:
708            if (!@file_put_contents($tempFile, $image)) {
709                throw new \Exception('Unable to write to image directory.');
710            }
711
712            // Move temporary file to final location:
713            if (!$this->validateAndMoveTempFile($image, $tempFile, $finalFile)) {
714                return false;
715            }
716
717            // Display the image:
718            $this->contentType = 'image/jpeg';
719            $this->image = file_get_contents($finalFile);
720
721            // If we don't want to cache the image, delete it now that we're done.
722            if (!$cache) {
723                @unlink($finalFile);
724            }
725            return true;
726        }
727    }
728
729    /**
730     * Get urls for defined provider, works as generator
731     *
732     * @return array
733     */
734    protected function getCoverUrls()
735    {
736        $ids = $this->getIdentifiers();
737        $handlers = $this->getHandlers();
738        foreach ($handlers as $handler) {
739            try {
740                // Is the current provider appropriate for the available data?
741                if ($handler['handler']->supports($ids)) {
742                    $url = $handler['handler']
743                        ->getUrl($handler['key'], $this->size, $ids);
744                    if ($url) {
745                        yield [
746                            'url' => $url,
747                            'apiName' => $handler['apiName'],
748                            'handler' => $handler['handler'],
749                        ];
750                    }
751                }
752            } catch (\Exception $e) {
753                $this->debug(
754                    $e::class . ' during processing of ' . $handler['apiName']
755                    . ': ' . $e->getMessage()
756                );
757            }
758        }
759    }
760
761    /**
762     * Return API handlers
763     *
764     * @return \Generator Array with keys: key - API key, apiName - api name from
765     * configuration, handler - handler object
766     */
767    public function getHandlers()
768    {
769        if (!isset($this->config->Content->coverimages)) {
770            return [];
771        }
772        $providers = explode(',', $this->config->Content->coverimages);
773        foreach ($providers as $provider) {
774            $provider = explode(':', trim($provider));
775            $apiName = strtolower(trim($provider[0]));
776            $key = isset($provider[1]) ? trim($provider[1]) : null;
777            yield [
778                'key' => $key,
779                'apiName' => $apiName,
780                'handler' => $this->apiManager->get($apiName),
781            ];
782        }
783    }
784
785    /**
786     * Get identifiers for given settings
787     *
788     * @param array $settings Settings from loadImage
789     *
790     * @return array
791     */
792    public function getIdentifiersForSettings($settings)
793    {
794        $this->storeSanitizedSettings($settings);
795        return $this->getIdentifiers();
796    }
797}