Completed
Pull Request — master (#1)
by
unknown
02:45
created

FileCache::clear()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 5
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
crap 6
1
<?php
2
3
namespace Kodus\Cache;
4
5
use DateInterval;
6
use FilesystemIterator;
7
use Generator;
8
use InvalidArgumentException;
9
use Psr\SimpleCache\CacheInterface;
10
use Psr\SimpleCache\CounterInterface;
11
use RecursiveDirectoryIterator;
12
use RecursiveIteratorIterator;
13
14
/**
15
 * This is a simple, file-based cache implementation, which is bootstrapped by
16
 * the Core Provider as a default.
17
 *
18
 * Bootstrapping a more powerful cache for production scenarios is highly recommended.
19
 *
20
 * @link https://github.com/matthiasmullie/scrapbook/
21
 */
22
class FileCache implements CacheInterface, CounterInterface
23
{
24
    // TODO garbage collection
25
26
    /**
27
     * @var string control characters for keys, reserved by PSR-16
28
     */
29
    const PSR16_RESERVED = '/\{|\}|\(|\)|\/|\\\\|\@|\:/u';
30
31
    /**
32
     * @var string
33
     */
34
    private $cache_path;
35
36
    /**
37
     * @var int
38
     */
39
    private $default_ttl;
40
41
    /**
42
     * @param string $cache_path  absolute root path of cache-file folder
43
     * @param int    $default_ttl default time-to-live (in seconds)
44
     *
45
     * @throws InvalidArgumentException if the specified cache-path does not exist (or is not writable)
46
     */
47 13
    public function __construct($cache_path, $default_ttl)
48
    {
49 13
        if (! file_exists($cache_path)) {
50 13
            @mkdir($cache_path, 0777, true); // ensure that the parent path exists
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
51
        }
52
53 13
        $path = realpath($cache_path);
54
55 13
        if ($path === false) {
56
            throw new InvalidArgumentException("cache path does not exist: {$cache_path}");
57
        }
58
59 13
        if (! is_writable($path . DIRECTORY_SEPARATOR)) {
60
            throw new InvalidArgumentException("cache path is not writable: {$cache_path}");
61
        }
62
63 13
        $this->cache_path = $path;
64 13
        $this->default_ttl = $default_ttl;
65 13
    }
66
67
    public function get($key, $default = null)
68
    {
69
        $path = $this->getPath($key);
70
71
        $expires_at = @filemtime($path);
72
73
        if ($expires_at === false) {
74
            return $default; // file not found
75
        }
76
77
        if ($this->getTime() >= $expires_at) {
78
            @unlink($path); // file expired
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
79
80
            return $default;
81
        }
82
83
        $data = @file_get_contents($path);
84
85
        if ($data === false) {
86
            return $default; // race condition: file not found
87
        }
88
89
        if ($data === 'b:0;') {
90
            return false; // because we can't otherwise distinguish a FALSE return-value from unserialize()
91
        }
92
93
        $value = @unserialize($data);
94
95
        if ($value === false) {
96
            return $default; // unserialize() failed
97
        }
98
99
        return $value;
100
    }
101
102
    public function set($key, $value, $ttl = null)
103
    {
104
        $path = $this->getPath($key);
105
106
        $dir = dirname($path);
107
108
        if (! file_exists($dir)) {
109
            @mkdir($dir, 0777, true); // ensure that the parent path exists
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
110
        }
111
112
        $temp_path = $this->cache_path . DIRECTORY_SEPARATOR . uniqid('', true);
113
114
        if (is_int($ttl)) {
115
            $expires_at = $this->getTime() + $ttl;
116
        } elseif ($ttl instanceof DateInterval) {
117
            $expires_at = date_create_from_format("U", $this->getTime())->add($ttl)->getTimestamp();
118
        } elseif ($ttl === null) {
119
            $expires_at = $this->getTime() + $this->default_ttl;
120
        } else {
121
            throw new InvalidArgumentException("invalid TTL: " . print_r($ttl, true));
122
        }
123
124
        if (false === @file_put_contents($temp_path, serialize($value))) {
125
            return false;
126
        }
127
128
        if (@touch($temp_path, $expires_at) && @rename($temp_path, $path)) {
129
            return true;
130
        }
131
132
        @unlink($temp_path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
133
134
        return false;
135
    }
136
137
    public function delete($key)
138
    {
139
        @unlink($this->getPath($key));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
140
    }
141
142
    public function clear()
143
    {
144
        $paths = $this->listPaths();
145
146
        foreach ($paths as $path) {
147
            @unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
148
        }
149
    }
150
151
    public function getMultiple($keys)
152
    {
153
        $values = [];
154
155
        foreach ($keys as $key) {
156
            $values[$key] = $this->get($key);
157
        }
158
159
        return $values;
160
    }
161
162
    public function setMultiple($items, $ttl = null)
163
    {
164
        $ok = true;
165
166
        foreach ($items as $key => $value) {
167
            $ok = $this->set($key, $value, $ttl) && $ok;
168
        }
169
170
        return $ok;
171
    }
172
173
    public function deleteMultiple($keys)
174
    {
175
        foreach ($keys as $key) {
176
            $this->delete($key);
177
        }
178
    }
179
180
    public function has($key)
181
    {
182
        return $this->get($key, $this) !== $this;
183
    }
184
185
    public function increment($key, $step = 1)
186
    {
187
        $path = $this->getPath($key);
188
189
        $dir = dirname($path);
190
191
        if (! file_exists($dir)) {
192
            @mkdir($dir, 0777, true); // ensure that the parent path exists
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
193
        }
194
195
        $lock_path = $dir . DIRECTORY_SEPARATOR . ".lock"; // allows max. 256 client locks at one time
196
197
        $lock_handle = fopen($lock_path, "w");
198
199
        flock($lock_handle, LOCK_EX);
200
201
        $value = $this->get($key, 0) + $step;
202
203
        $ok = $this->set($key, $value);
204
205
        flock($lock_handle, LOCK_UN);
206
207
        return $ok ? $value : false;
208
    }
209
210
    public function decrement($key, $step = 1)
211
    {
212
        return $this->increment($key, -$step);
213
    }
214
215
    /**
216
     * Clean up expired cache-files.
217
     *
218
     * This method is outside the scope of the PSR-16 cache concept, and is specific to
219
     * this implementation, being a file-cache.
220
     *
221
     * In scenarios with dynamic keys (such as Session IDs) you should call this method
222
     * periodically - for example from a scheduled daily cron-job.
223
     *
224
     * @return void
225
     */
226
    public function cleanExpired()
227
    {
228
        $now = $this->getTime();
229
230
        $paths = $this->listPaths();
231
232
        foreach ($paths as $path) {
233
            if ($now > filemtime($path)) {
234
                @unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
235
            }
236
        }
237
    }
238
239
    /**
240
     * For a given cache key, obtain the absolute file path
241
     *
242
     * @param string $key
243
     *
244
     * @return string absolute path to cache-file
245
     *
246
     * @throws InvalidArgumentException if the specified key contains a character reserved by PSR-16
247
     */
248
    protected function getPath($key)
249
    {
250
        if (preg_match(self::PSR16_RESERVED, $key, $match) === 1) {
251
            throw new InvalidArgumentException("invalid character in key: {$match[0]}");
252
        }
253
254
        $hash = hash("sha256", $key);
255
256
        return $this->cache_path
257
            . DIRECTORY_SEPARATOR
258
            . strtoupper($hash[0])
259
            . DIRECTORY_SEPARATOR
260
            . strtoupper($hash[1])
261
            . DIRECTORY_SEPARATOR
262
            . substr($hash, 2);
263
    }
264
265
    /**
266
     * @return int current timestamp
267
     */
268 13
    protected function getTime()
269
    {
270 13
        return time();
271
    }
272
273
    /**
274
     * @return Generator|string[]
275
     */
276
    protected function listPaths()
277
    {
278
        $iterator = new RecursiveDirectoryIterator(
279
            $this->cache_path,
280
            FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS
281
        );
282
283
        $iterator = new RecursiveIteratorIterator($iterator);
284
285
        foreach ($iterator as $path) {
286
            if (is_dir($path)) {
287
                continue; // ignore directories
288
            }
289
290
            yield $path;
291
        }
292
    }
293
}
294