Completed
Pull Request — master (#1)
by
unknown
04:54
created

FileCache::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
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) && file_exists(dirname($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 12
    public function get($key, $default = null)
68
    {
69 12
        $path = $this->getPath($key);
70
71 12
        $expires_at = @filemtime($path);
72
73 12
        if ($expires_at === false) {
74 11
            return $default; // file not found
75
        }
76
77 10
        if ($this->getTime() >= $expires_at) {
78 4
            @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 4
            return $default;
81
        }
82
83 9
        $data = @file_get_contents($path);
84
85 9
        if ($data === false) {
86
            return $default; // race condition: file not found
87
        }
88
89 9
        if ($data === 'b:0;') {
90
            return false; // because we can't otherwise distinguish a FALSE return-value from unserialize()
91
        }
92
93 9
        $value = @unserialize($data);
94
95 9
        if ($value === false) {
96
            return $default; // unserialize() failed
97
        }
98
99 9
        return $value;
100
    }
101
102 12
    public function set($key, $value, $ttl = null)
103
    {
104 12
        $path = $this->getPath($key);
105
106 12
        $dir = dirname($path);
107
108 12
        if (! file_exists($dir)) {
109 10
            @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 12
        $temp_path = $this->cache_path . DIRECTORY_SEPARATOR . uniqid('', true);
113
114 12
        if (is_int($ttl)) {
115 3
            $expires_at = $this->getTime() + $ttl;
116
        } elseif ($ttl instanceof DateInterval) {
117 1
            $expires_at = date_create_from_format("U", $this->getTime())->add($ttl)->getTimestamp();
118 8
        } elseif ($ttl === null) {
119 8
            $expires_at = $this->getTime() + $this->default_ttl;
120
        } else {
121
            throw new InvalidArgumentException("invalid TTL: " . print_r($ttl, true));
122
        }
123
124 12
        if (false === @file_put_contents($temp_path, serialize($value))) {
125
            return false;
126
        }
127
128 12
        if (@touch($temp_path, $expires_at) && @rename($temp_path, $path)) {
129 12
            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 2
    public function delete($key)
138
    {
139 2
        @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 2
    }
141
142 13
    public function clear()
143
    {
144 13
        $paths = $this->listPaths();
145
146 13
        foreach ($paths as $path) {
147 8
            @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 13
    }
150
151 2
    public function getMultiple($keys)
152
    {
153 2
        $values = [];
154
155 2
        foreach ($keys as $key) {
156 2
            $values[$key] = $this->get($key);
157
        }
158
159 2
        return $values;
160
    }
161
162 2
    public function setMultiple($items, $ttl = null)
163
    {
164 2
        $ok = true;
165
166 2
        foreach ($items as $key => $value) {
167 2
            $ok = $this->set($key, $value, $ttl) && $ok;
168
        }
169
170 2
        return $ok;
171
    }
172
173 1
    public function deleteMultiple($keys)
174
    {
175 1
        foreach ($keys as $key) {
176 1
            $this->delete($key);
177
        }
178 1
    }
179
180 1
    public function has($key)
181
    {
182 1
        return $this->get($key, $this) !== $this;
183
    }
184
185 2
    public function increment($key, $step = 1)
186
    {
187 2
        $path = $this->getPath($key);
188
189 2
        $dir = dirname($path);
190
191 2
        if (! file_exists($dir)) {
192 2
            @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 2
        $lock_path = $dir . DIRECTORY_SEPARATOR . ".lock"; // allows max. 256 client locks at one time
196
197 2
        $lock_handle = fopen($lock_path, "w");
198
199 2
        flock($lock_handle, LOCK_EX);
200
201 2
        $value = $this->get($key, 0) + $step;
202
203 2
        $ok = $this->set($key, $value);
204
205 2
        flock($lock_handle, LOCK_UN);
206
207 2
        return $ok ? $value : false;
208
    }
209
210 1
    public function decrement($key, $step = 1)
211
    {
212 1
        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 1
    public function cleanExpired()
227
    {
228 1
        $now = $this->getTime();
229
230 1
        $paths = $this->listPaths();
231
232 1
        foreach ($paths as $path) {
233 1
            if ($now > filemtime($path)) {
234 1
                @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 1
    }
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 13
    protected function getPath($key)
249
    {
250 13
        if (preg_match(self::PSR16_RESERVED, $key, $match) === 1) {
251
            throw new InvalidArgumentException("invalid character in key: {$match[0]}");
252
        }
253
254 13
        $hash = hash("sha256", $key);
255
256 13
        return $this->cache_path
257 13
            . DIRECTORY_SEPARATOR
258 13
            . strtoupper($hash[0])
259 13
            . DIRECTORY_SEPARATOR
260 13
            . strtoupper($hash[1])
261 13
            . DIRECTORY_SEPARATOR
262 13
            . 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 13
    protected function listPaths()
277
    {
278 13
        $iterator = new RecursiveDirectoryIterator(
279 13
            $this->cache_path,
280 13
            FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS
281
        );
282
283 13
        $iterator = new RecursiveIteratorIterator($iterator);
284
285 13
        foreach ($iterator as $path) {
286 8
            if (is_dir($path)) {
287
                continue; // ignore directories
288
            }
289
290 8
            yield $path;
291
        }
292 13
    }
293
}
294