CacheManager   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 31
eloc 70
dl 0
loc 233
rs 9.92
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A isCacheValid() 0 15 4
A createDomainThumbCacheFolder() 0 9 2
A rebuildCacheFolders() 0 17 6
A getDomainHash() 0 3 1
A createHtaccessFile() 0 17 6
A getCachePath() 0 12 4
A checkCacheType() 0 7 3
A getThumbFilename() 0 3 1
A getCacheFilePath() 0 28 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace WebThumbnailer\Application;
6
7
use WebThumbnailer\Exception\BadRulesException;
8
use WebThumbnailer\Exception\CacheException;
9
use WebThumbnailer\Exception\IOException;
10
use WebThumbnailer\Utils\FileUtils;
11
use WebThumbnailer\Utils\TemplatePolyfill;
12
13
/**
14
 * Handles file caching using static methods.
15
 * There are 2 types of cache:
16
 *  - thumb: thumbnail images after being resized.
17
 *  - finder: url->thumbnail url resolution is also cached.
18
 * Cache files are organized by domains name, and have a unique name
19
 * based on their URL, max-width and max-height.
20
 *
21
 * Cache duration is defined in JSON settings.
22
 */
23
class CacheManager
24
{
25
    /** Thumbnails image cache. */
26
    public const TYPE_THUMB  = 'thumb';
27
28
    /** Finder cache. */
29
    public const TYPE_FINDER = 'finder';
30
31
    /** @var string Clean filename, used to clean directories periodically. */
32
    protected static $CLEAN_FILE = '.clean';
33
34
    /**
35
     * Returns the cache path according to the given type.
36
     *
37
     * @param string $type    Type of cache.
38
     * @param bool   $rebuilt Flag to tell if a rebuild tentative has been done.
39
     *
40
     * @return string Cache path.
41
     *
42
     * @throws IOException Type not found.
43
     * @throws CacheException
44
     * @throws BadRulesException
45
     */
46
    public static function getCachePath(string $type, bool $rebuilt = false): string
47
    {
48
        static::checkCacheType($type);
49
        $cache = ConfigManager::get('settings.path.cache', 'cache/');
50
        $path = FileUtils::getPath($cache, $type);
51
        if (!$path && !$rebuilt) {
52
            static::rebuildCacheFolders();
53
            return static::getCachePath($type, true);
54
        } elseif (!$path) {
55
            throw new IOException('Cache folders are not writable: ' . $cache);
56
        }
57
        return $path;
58
    }
59
60
    /**
61
     * Get a thumb cache file absolute path.
62
     *
63
     * @param string     $url    URL of the thumbnail (unique file per URL).
64
     * @param string     $domain Domain concerned.
65
     * @param string     $type   Type of cache.
66
     * @param int|string $width  User setting for image width.
67
     * @param int|string $height User setting for image height.
68
     * @param bool|null  $crop   Crop enabled or not.
69
     *
70
     * @return string Absolute file path.
71
     *
72
     * @throws IOException
73
     * @throws CacheException
74
     * @throws BadRulesException
75
     */
76
    public static function getCacheFilePath(
77
        string $url,
78
        string $domain,
79
        string $type,
80
        $width = 0,
81
        $height = 0,
82
        ?bool $crop = false
83
    ): string {
84
        $domainHash = static::getDomainHash($domain);
85
86
        static::createDomainThumbCacheFolder($domainHash, $type);
87
88
        $domainFolder = FileUtils::getPath(static::getCachePath($type), $domainHash);
89
        if ($domainFolder === false) {
90
            throw new CacheException(sprintf(
91
                'Could not find cache path for type %s and domain hash %s',
92
                $type,
93
                $domainHash
94
            ));
95
        }
96
97
        if ($type === static::TYPE_THUMB) {
98
            $suffix = $width . $height . ($crop ? '1' : '0') . '.jpg';
99
        } else {
100
            $suffix = $width . $height;
101
        }
102
103
        return $domainFolder . static::getThumbFilename($url) . $suffix;
104
    }
105
106
    /**
107
     * Check whether a valid cache file exists or not.
108
     * Also check that that file is still valid.
109
     *
110
     * Support endless cache using a negative value.
111
     *
112
     * @param string $cacheFile Cache file path.
113
     * @param string $domain    Domain concerned.
114
     * @param string $type      Type of cache.
115
     *
116
     * @return bool true if valid cache exists, false otherwise.
117
     *
118
     * @throws CacheException
119
     * @throws IOException
120
     * @throws BadRulesException
121
     */
122
    public static function isCacheValid(string $cacheFile, string $domain, string $type): bool
123
    {
124
        $out = false;
125
        $cacheDuration = ConfigManager::get('settings.cache_duration', 3600 * 24 * 31);
126
127
        if (
128
            is_readable($cacheFile)
129
            && ($cacheDuration < 0 || (time() - filemtime($cacheFile)) < $cacheDuration)
130
        ) {
131
            $out = true;
132
        } else {
133
            static::createDomainThumbCacheFolder(static::getDomainHash($domain), $type);
134
        }
135
136
        return $out;
137
    }
138
139
    /**
140
     * Create the domains folder for thumb cache if it doesn't exists.
141
     *
142
     * @param string $domain Domain used.
143
     * @param string $type   Type of cache.
144
     *
145
     * @throws CacheException
146
     * @throws IOException
147
     * @throws BadRulesException
148
     */
149
    protected static function createDomainThumbCacheFolder(string $domain, string $type): void
150
    {
151
        $cachePath = static::getCachePath($type);
152
        $domainFolder = $cachePath . $domain;
153
        if (!file_exists($domainFolder)) {
154
            mkdir($domainFolder, 0775, false);
155
            touch($domainFolder . '/' . static::$CLEAN_FILE);
156
        }
157
        static::createHtaccessFile($cachePath, $type === static::TYPE_THUMB);
158
    }
159
160
    /**
161
     * Create a .htaccess file for Apache webserver if it doesn't exists.
162
     * The folder should be allowed for thumbs, and denied for finder's cache.
163
     *
164
     * @param string $path    Cache directory path
165
     * @param bool   $allowed Weather the access is allowed or not
166
     *
167
     * @throws BadRulesException
168
     * @throws IOException
169
     */
170
    protected static function createHtaccessFile(string $path, bool $allowed = false): void
171
    {
172
        $apacheVersion = ConfigManager::get('settings.apache_version', '');
173
        $htaccessFile = $path . '.htaccess';
174
        if (file_exists($htaccessFile)) {
175
            return;
176
        }
177
        $templateFile = file_exists(FileUtils::RESOURCES_PATH . 'htaccess' . $apacheVersion . '_template')
178
            ? FileUtils::RESOURCES_PATH . 'htaccess' . $apacheVersion . '_template'
179
            : FileUtils::RESOURCES_PATH . 'htaccess_template';
180
        $template = TemplatePolyfill::get($templateFile);
181
        $template->setVar([
182
            'new_all' => $allowed ? 'granted' : 'denied',
183
            'old_allow' => $allowed ? 'all' : 'none',
184
            'old_deny' => $allowed ? 'none' : 'all',
185
        ]);
186
        file_put_contents($htaccessFile, $template->render());
187
    }
188
189
    /**
190
     * Get the cache filename according to the given URL.
191
     * Using a sha1 hash to get unique valid filenames.
192
     *
193
     * @param string $url Thumbnail URL.
194
     *
195
     * @return string Thumb filename.
196
     */
197
    protected static function getThumbFilename(string $url): string
198
    {
199
        return hash('sha1', $url);
200
    }
201
202
    /**
203
     * Make sure that the cache type exists.
204
     *
205
     * @param string $type Cache type.
206
     *
207
     * @return bool True if the check was successful.
208
     *
209
     * @throws CacheException Cache type doesn't exists.
210
     */
211
    protected static function checkCacheType(string $type): bool
212
    {
213
        if ($type != static::TYPE_THUMB && $type != static::TYPE_FINDER) {
214
            throw new CacheException('Unknown cache type ' . $type);
215
        }
216
217
        return true;
218
    }
219
220
    /**
221
     * Recreates cache folders just in case the user delete them.
222
     *
223
     * @throws BadRulesException
224
     * @throws IOException
225
     */
226
    protected static function rebuildCacheFolders(): void
227
    {
228
        $mainFolder = ConfigManager::get('settings.path.cache', 'cache/');
229
        if (! is_dir($mainFolder)) {
230
            mkdir($mainFolder, 0755);
231
        }
232
        if (! is_dir($mainFolder . static::TYPE_THUMB)) {
233
            mkdir($mainFolder . static::TYPE_THUMB, 0755);
234
        }
235
        if (! is_readable($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep')) {
236
            touch($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep');
237
        }
238
        if (! is_dir($mainFolder . static::TYPE_FINDER)) {
239
            mkdir($mainFolder . static::TYPE_FINDER, 0755);
240
        }
241
        if (! is_readable($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep')) {
242
            touch($mainFolder . static::TYPE_FINDER . DIRECTORY_SEPARATOR . '.gitkeep');
243
        }
244
    }
245
246
    /**
247
     * Return the hashed folder name for a given domain.
248
     *
249
     * @param string $domain name
250
     *
251
     * @return string hash
252
     */
253
    protected static function getDomainHash(string $domain): string
254
    {
255
        return md5($domain);
256
    }
257
}
258