Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
CoverController
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 7
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getImageParams
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 proxyAllowedForUrl
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 isValidProxyImageContentType
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 showAction
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 unavailableAction
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 displayImage
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * Cover Controller
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2011.
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  Controller
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 Main Page
28 */
29
30namespace VuFind\Controller;
31
32use VuFind\Cover\CachingProxy;
33use VuFind\Cover\Loader;
34use VuFind\Session\Settings as SessionSettings;
35
36use function in_array;
37
38/**
39 * Generates covers for book entries
40 *
41 * @category VuFind
42 * @package  Controller
43 * @author   Demian Katz <demian.katz@villanova.edu>
44 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
45 * @link     https://vufind.org Main Page
46 */
47class CoverController extends \Laminas\Mvc\Controller\AbstractActionController
48{
49    /**
50     * Cover loader
51     *
52     * @var Loader
53     */
54    protected $loader;
55
56    /**
57     * Proxy loader
58     *
59     * @var CachingProxy
60     */
61    protected $proxy;
62
63    /**
64     * Session settings
65     *
66     * @var SessionSettings
67     */
68    protected $sessionSettings = null;
69
70    /**
71     * Configuration settings ([Content] section of config.ini)
72     *
73     * @var array
74     */
75    protected $config;
76
77    /**
78     * Constructor
79     *
80     * @param Loader          $loader Cover loader
81     * @param CachingProxy    $proxy  Proxy loader
82     * @param SessionSettings $ss     Session settings
83     * @param array           $config Configuration settings
84     */
85    public function __construct(
86        Loader $loader,
87        CachingProxy $proxy,
88        SessionSettings $ss,
89        array $config = []
90    ) {
91        $this->loader = $loader;
92        $this->proxy = $proxy;
93        $this->sessionSettings = $ss;
94        $this->config = $config;
95    }
96
97    /**
98     * Convert image parameters into an array for use by the image loader.
99     *
100     * @return array
101     */
102    protected function getImageParams()
103    {
104        $params = $this->params();  // shortcut for readability
105        $isbns = null;
106        // Legacy support for "isn", "isbn" param which has been superseded by isbns:
107        foreach (['isbns', 'isbn', 'isn'] as $identification) {
108            if ($isbns = $params()->fromQuery($identification)) {
109                $isbns = (array)$isbns;
110                break;
111            }
112        }
113        return [
114            'isbns' => $isbns,
115            'size' => $params()->fromQuery('size'),
116            'type' => $params()->fromQuery('contenttype'),
117            'title' => $params()->fromQuery('title'),
118            'author' => $params()->fromQuery('author'),
119            'callnumber' => $params()->fromQuery('callnumber'),
120            'issn' => $params()->fromQuery('issn'),
121            'oclc' => $params()->fromQuery('oclc'),
122            'upc' => $params()->fromQuery('upc'),
123            'recordid' => $params()->fromQuery('recordid'),
124            'source' => $params()->fromQuery('source'),
125            'nbn' => $params()->fromQuery('nbn'),
126            'ismn' => $params()->fromQuery('ismn'),
127        ];
128    }
129
130    /**
131     * Is the provided URL included on the configured allow list?
132     *
133     * @param string $url URL to check
134     *
135     * @return bool
136     */
137    protected function proxyAllowedForUrl(string $url): bool
138    {
139        $host = parse_url($url, PHP_URL_HOST);
140        if (!$host) {
141            return false;
142        }
143        foreach ((array)($this->config['coverproxyAllowedHosts'] ?? []) as $regEx) {
144            if (preg_match($regEx, $host)) {
145                return true;
146            }
147        }
148        return false;
149    }
150
151    /**
152     * Is the content type allowed by the cover proxy?
153     *
154     * @param string $contentType Type to check
155     *
156     * @return bool
157     */
158    protected function isValidProxyImageContentType(string $contentType): bool
159    {
160        $validTypes = $this->config['coverproxyAllowedTypes']
161            ?? ['image/gif', 'image/jpeg', 'image/png'];
162        return in_array(strtolower($contentType), array_map('strtolower', $validTypes));
163    }
164
165    /**
166     * Send image data for display in the view
167     *
168     * @return \Laminas\Http\Response
169     */
170    public function showAction()
171    {
172        $this->sessionSettings->disableWrite(); // avoid session write timing bug
173
174        // Special case: proxy a full URL:
175        $url = $this->params()->fromQuery('proxy');
176        if (!empty($url) && $this->proxyAllowedForUrl($url)) {
177            try {
178                $image = $this->proxy->fetch($url);
179                $contentType = $image?->getHeaders()?->get('content-type')?->getFieldValue() ?? '';
180                if ($this->isValidProxyImageContentType($contentType)) {
181                    return $this->displayImage(
182                        $contentType,
183                        $image->getContent()
184                    );
185                }
186            } catch (\Exception $e) {
187                // If an exception occurs, drop through to the standard case
188                // to display an image unavailable graphic.
189            }
190        }
191
192        // Default case -- use image loader:
193        $this->loader->loadImage($this->getImageParams());
194        return $this->displayImage();
195    }
196
197    /**
198     * Return the default 'image not found' information
199     *
200     * @return \Laminas\Http\Response
201     */
202    public function unavailableAction()
203    {
204        $this->sessionSettings->disableWrite(); // avoid session write timing bug
205        $this->loader->loadUnavailable();
206        return $this->displayImage();
207    }
208
209    /**
210     * Support method -- update the view to display the image currently found in the
211     * \VuFind\Cover\Loader.
212     *
213     * @param string $type  Content type of image (null to access loader)
214     * @param string $image Image data (null to access loader)
215     *
216     * @return \Laminas\Http\Response
217     */
218    protected function displayImage($type = null, $image = null)
219    {
220        $response = $this->getResponse();
221        $headers = $response->getHeaders();
222        $headers->addHeaderLine(
223            'Content-type',
224            $type ?: $this->loader->getContentType()
225        );
226
227        // Send proper caching headers so that the user's browser
228        // is able to cache the cover images and not have to re-request
229        // then on each page load. Default TTL set at 14 days
230
231        $coverImageTtl = (60 * 60 * 24 * 14); // 14 days
232        $headers->addHeaderLine(
233            'Cache-Control',
234            'maxage=' . $coverImageTtl
235        );
236        $headers->addHeaderLine(
237            'Pragma',
238            'public'
239        );
240        $headers->addHeaderLine(
241            'Expires',
242            gmdate('D, d M Y H:i:s', time() + $coverImageTtl) . ' GMT'
243        );
244
245        $response->setContent($image ?: $this->loader->getImage());
246        return $response;
247    }
248}