Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 99 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
Manager | |
0.00% |
0 / 99 |
|
0.00% |
0 / 11 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getCache | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getCacheDir | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
getCacheList | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getNonPersistentCacheList | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
hasDirectoryCreationError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addDownloaderCache | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
addLanguageCacheForTheme | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
ensureFileCache | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
createNoCache | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
createFileCache | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Cache Manager |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2007 |
9 | * Copyright (C) Leipzig University Library <info@ub.uni-leipzig.de> 2018 |
10 | * Copyright (C) The National Library of Finland 2024 |
11 | * |
12 | * This program is free software; you can redistribute it and/or modify |
13 | * it under the terms of the GNU General Public License version 2, |
14 | * as published by the Free Software Foundation. |
15 | * |
16 | * This program is distributed in the hope that it will be useful, |
17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19 | * GNU General Public License for more details. |
20 | * |
21 | * You should have received a copy of the GNU General Public License |
22 | * along with this program; if not, write to the Free Software |
23 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
24 | * |
25 | * @category VuFind |
26 | * @package Cache |
27 | * @author Demian Katz <demian.katz@villanova.edu> |
28 | * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> |
29 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
30 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
31 | * @link https://vufind.org Main Page |
32 | */ |
33 | |
34 | namespace VuFind\Cache; |
35 | |
36 | use Laminas\Cache\Service\StorageAdapterFactory; |
37 | use Laminas\Cache\Storage\StorageInterface; |
38 | use Laminas\Config\Config; |
39 | |
40 | use function dirname; |
41 | use function is_array; |
42 | use function strlen; |
43 | |
44 | /** |
45 | * VuFind Cache Manager |
46 | * |
47 | * Creates caches based on configuration |
48 | * |
49 | * @category VuFind |
50 | * @package Cache |
51 | * @author Demian Katz <demian.katz@villanova.edu> |
52 | * @author Sebastian Kehr <kehr@ub.uni-leipzig.de> |
53 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
54 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
55 | * @link https://vufind.org Main Page |
56 | */ |
57 | class Manager |
58 | { |
59 | /** |
60 | * Default configuration settings. |
61 | * |
62 | * @var array |
63 | */ |
64 | protected $defaults; |
65 | |
66 | /** |
67 | * Was there a problem building cache directories? |
68 | * |
69 | * @var bool |
70 | */ |
71 | protected $directoryCreationError = false; |
72 | |
73 | /** |
74 | * Settings used to generate cache objects. |
75 | * |
76 | * @var array |
77 | */ |
78 | protected $cacheSettings = []; |
79 | |
80 | /** |
81 | * Actual cache objects generated from settings. |
82 | * |
83 | * @var StorageInterface[] |
84 | */ |
85 | protected $caches = []; |
86 | |
87 | /** |
88 | * Factory for creating storage adapters. |
89 | * |
90 | * @var StorageAdapterFactory |
91 | */ |
92 | protected $factory; |
93 | |
94 | /** |
95 | * Cache configuration. |
96 | * |
97 | * Following settings are supported: |
98 | * |
99 | * cliOverride Set to false to not allow cache directory override in CLI mode (optional, true by default) |
100 | * directory Cache directory (required) |
101 | * options Array of cache options (optional, e.g. disabled, ttl) |
102 | * persistent Set to true to disable clearing of the cache by default with the admin API clearCache command |
103 | * (optional, false by default) |
104 | * |
105 | * @var array |
106 | */ |
107 | protected $cacheSpecs = [ |
108 | 'browscap' => [ |
109 | 'cliOverride' => false, |
110 | 'directory' => 'browscap', |
111 | 'options' => [ |
112 | 'ttl' => 0, // no expiration - cache is updated with console util/browscap |
113 | 'keyPattern' => '/^[a-z0-9_\+\-\.]*$/Di', |
114 | ], |
115 | 'persistent' => true, |
116 | ], |
117 | 'config' => [ |
118 | 'directory' => 'configs', |
119 | ], |
120 | 'cover' => [ |
121 | 'directory' => 'covers', |
122 | 'persistent' => true, |
123 | ], |
124 | 'language' => [ |
125 | 'directory' => 'languages', |
126 | ], |
127 | 'object' => [ |
128 | 'directory' => 'objects', |
129 | ], |
130 | 'public' => [ |
131 | 'directory' => 'public', |
132 | ], |
133 | 'searchspecs' => [ |
134 | 'directory' => 'searchspecs', |
135 | ], |
136 | 'yaml' => [ |
137 | 'directory' => 'yamls', |
138 | ], |
139 | ]; |
140 | |
141 | /** |
142 | * Constructor |
143 | * |
144 | * @param Config $config Main VuFind configuration |
145 | * @param Config $searchConfig Search configuration |
146 | * @param StorageAdapterFactory $factory Cache storage adapter factory |
147 | */ |
148 | public function __construct( |
149 | Config $config, |
150 | Config $searchConfig, |
151 | StorageAdapterFactory $factory |
152 | ) { |
153 | $this->factory = $factory; |
154 | |
155 | // $config and $config->Cache are Laminas\Config\Config objects |
156 | // $cache is created immutable, so get the array, it will be modified |
157 | // downstream. |
158 | $this->defaults = $config->Cache?->toArray() ?? []; |
159 | |
160 | // Configure search specs cache based on config settings: |
161 | $searchCacheType = $searchConfig->Cache->type ?? false; |
162 | switch ($searchCacheType) { |
163 | case 'File': |
164 | // Default |
165 | break; |
166 | case false: |
167 | $this->cacheSpecs['searchspecs']['options']['disabled'] = true; |
168 | break; |
169 | default: |
170 | throw new \Exception("Unsupported cache setting: $searchCacheType"); |
171 | } |
172 | } |
173 | |
174 | /** |
175 | * Retrieve the specified cache object. |
176 | * |
177 | * @param string $name Name of the requested cache. |
178 | * @param string|null $namespace Optional namespace to use. Defaults to the |
179 | * value of $name. |
180 | * |
181 | * @return StorageInterface |
182 | * @throws \Exception |
183 | */ |
184 | public function getCache($name, $namespace = null) |
185 | { |
186 | $this->ensureFileCache($name); |
187 | $namespace ??= $name; |
188 | $key = "$name:$namespace"; |
189 | |
190 | if (!isset($this->caches[$key])) { |
191 | if (!isset($this->cacheSettings[$name])) { |
192 | throw new \Exception('Requested unknown cache: ' . $name); |
193 | } |
194 | $settings = $this->cacheSettings[$name]; |
195 | $settings['options']['namespace'] = $namespace; |
196 | $this->caches[$key] |
197 | = $this->factory->createFromArrayConfiguration($settings); |
198 | } |
199 | |
200 | return $this->caches[$key]; |
201 | } |
202 | |
203 | /** |
204 | * Get the path to the directory containing VuFind's cache data. |
205 | * |
206 | * @param bool $allowCliOverride If true, use a different cache subdirectory |
207 | * for CLI mode; otherwise, share the web directories. |
208 | * |
209 | * @return string |
210 | */ |
211 | public function getCacheDir($allowCliOverride = true) |
212 | { |
213 | if (isset($this->defaults['cache_dir'])) { |
214 | // cache_dir setting in config.ini is obsolete |
215 | throw new \Exception( |
216 | 'Obsolete cache_dir setting found in config.ini - please use ' |
217 | . 'Apache environment variable VUFIND_CACHE_DIR in ' |
218 | . 'httpd-vufind.conf instead.' |
219 | ); |
220 | } |
221 | |
222 | if (strlen(LOCAL_CACHE_DIR) > 0) { |
223 | $dir = LOCAL_CACHE_DIR . '/'; |
224 | } elseif (strlen(LOCAL_OVERRIDE_DIR) > 0) { |
225 | $dir = LOCAL_OVERRIDE_DIR . '/cache/'; |
226 | } else { |
227 | $dir = APPLICATION_PATH . '/data/cache/'; |
228 | } |
229 | |
230 | // Use separate cache dir in CLI mode to avoid permission issues: |
231 | if ($allowCliOverride && PHP_SAPI == 'cli') { |
232 | $dir .= 'cli/'; |
233 | } |
234 | |
235 | return $dir; |
236 | } |
237 | |
238 | /** |
239 | * Get the names of all available caches. |
240 | * |
241 | * @return array |
242 | */ |
243 | public function getCacheList() |
244 | { |
245 | return array_unique( |
246 | [ |
247 | ...array_keys($this->cacheSpecs), |
248 | ...array_keys($this->cacheSettings), |
249 | ] |
250 | ); |
251 | } |
252 | |
253 | /** |
254 | * Get the names of all non-persistent caches (ones that can be cleared). |
255 | * |
256 | * @return array |
257 | */ |
258 | public function getNonPersistentCacheList(): array |
259 | { |
260 | $result = []; |
261 | foreach ($this->getCacheList() as $cache) { |
262 | if (!($this->cacheSpecs[$cache]['persistent'] ?? false)) { |
263 | $result[] = $cache; |
264 | } |
265 | } |
266 | return $result; |
267 | } |
268 | |
269 | /** |
270 | * Check if there have been problems creating directories. |
271 | * |
272 | * @return bool |
273 | */ |
274 | public function hasDirectoryCreationError() |
275 | { |
276 | return $this->directoryCreationError; |
277 | } |
278 | |
279 | /** |
280 | * Create a downloader-specific file cache. |
281 | * |
282 | * @param string $downloaderName Name of the downloader. |
283 | * @param array $opts Cache options. |
284 | * |
285 | * @return string |
286 | */ |
287 | public function addDownloaderCache($downloaderName, $opts = []) |
288 | { |
289 | $cacheName = 'downloader-' . $downloaderName; |
290 | $this->createFileCache( |
291 | $cacheName, |
292 | $this->getCacheDir(), |
293 | $opts |
294 | ); |
295 | return $cacheName; |
296 | } |
297 | |
298 | /** |
299 | * Create a new file cache for the given theme name if necessary. Return |
300 | * the name of the cache. |
301 | * |
302 | * @param string $themeName Name of the theme |
303 | * |
304 | * @return string |
305 | */ |
306 | public function addLanguageCacheForTheme($themeName) |
307 | { |
308 | $cacheName = 'languages-' . $themeName; |
309 | $this->createFileCache( |
310 | $cacheName, |
311 | $this->getCacheDir() . 'languages/' . $themeName |
312 | ); |
313 | return $cacheName; |
314 | } |
315 | |
316 | /** |
317 | * Ensure that a file cache is properly set up |
318 | * |
319 | * @param string $name Cache name |
320 | * |
321 | * @return void |
322 | */ |
323 | protected function ensureFileCache(string $name): void |
324 | { |
325 | // Use $this->cacheSettings to determine if $this->createFileCache() has been called yet: |
326 | if (!isset($this->cacheSettings[$name]) && $config = $this->cacheSpecs[$name] ?? null) { |
327 | $base = $this->getCacheDir($config['cliOverride'] ?? true); |
328 | $this->createFileCache($name, $base . $config['directory'], $config['options'] ?? []); |
329 | } |
330 | } |
331 | |
332 | /** |
333 | * Create a "no-cache" setting. |
334 | * |
335 | * @param string $cacheName Name of "no cache" to create |
336 | * |
337 | * @return void |
338 | */ |
339 | protected function createNoCache($cacheName) |
340 | { |
341 | $this->cacheSettings[$cacheName] = [ |
342 | 'adapter' => \Laminas\Cache\Storage\Adapter\BlackHole::class, |
343 | 'options' => [], |
344 | ]; |
345 | } |
346 | |
347 | /** |
348 | * Add a file cache to the manager and ensure that necessary directory exists. |
349 | * |
350 | * @param string $cacheName Name of new cache to create |
351 | * @param string $dirName Directory to use for storage |
352 | * @param array $overrideOpts Options to override default values. |
353 | * |
354 | * @return void |
355 | */ |
356 | protected function createFileCache($cacheName, $dirName, $overrideOpts = []) |
357 | { |
358 | $opts = array_merge($this->defaults, $overrideOpts); |
359 | if ($opts['disabled'] ?? false) { |
360 | $this->createNoCache($cacheName); |
361 | return; |
362 | } else { |
363 | // Laminas does not support "disabled = false"; unset to avoid error. |
364 | unset($opts['disabled']); |
365 | } |
366 | |
367 | if (!is_dir($dirName)) { |
368 | if (isset($opts['umask'])) { |
369 | // convert umask from string |
370 | $umask = octdec($opts['umask']); |
371 | // validate |
372 | if ($umask & 0o700) { |
373 | throw new \Exception( |
374 | 'Invalid umask: ' . $opts['umask'] |
375 | . '; need permission to execute, read and write by owner' |
376 | ); |
377 | } |
378 | umask($umask); |
379 | } |
380 | if (isset($opts['dir_permission'])) { |
381 | $dir_perm = octdec($opts['dir_permission']); |
382 | } else { |
383 | // 0777 is chmod default, use if dir_permission is not explicitly set |
384 | $dir_perm = 0o777; |
385 | } |
386 | // Make sure cache parent directory and directory itself exist: |
387 | $parentDir = dirname($dirName); |
388 | if (!is_dir($parentDir) && !@mkdir($parentDir, $dir_perm)) { |
389 | $this->directoryCreationError = true; |
390 | } |
391 | if (!@mkdir($dirName, $dir_perm)) { |
392 | $this->directoryCreationError = true; |
393 | } |
394 | } |
395 | if (empty($opts)) { |
396 | $opts = ['cache_dir' => $dirName]; |
397 | } elseif (is_array($opts)) { |
398 | // If VUFIND_CACHE_DIR was set in the environment, the cache-specific |
399 | // name should have been appended to it to create the value $dirName. |
400 | $opts['cache_dir'] = $dirName; |
401 | } else { |
402 | // Dryrot |
403 | throw new \Exception('$opts is neither array nor false'); |
404 | } |
405 | $this->cacheSettings[$cacheName] = [ |
406 | 'adapter' => \Laminas\Cache\Storage\Adapter\Filesystem::class, |
407 | 'options' => $opts, |
408 | 'plugins' => [ |
409 | ['name' => 'serializer'], |
410 | ], |
411 | ]; |
412 | } |
413 | } |