Passed
Pull Request — master (#218)
by
unknown
03:31 queued 01:04
created

JibitFileCache::getCacheFileExtension()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
namespace Shetabit\Multipay\Drivers\Jibit;
3
4
/**
5
 * PhpFileCache - Light, simple and standalone PHP in-file caching class
6
 * This class was heavily inspired by Simple-PHP-Cache. Huge thanks to Christian Metz
7
 * @license MIT
8
 * @author Wruczek https://github.com/Wruczek
9
 */
10
class JibitFileCache
11
{
12
    /**
13
     * Path to the cache directory
14
     * @var string
15
     */
16
    private $cacheDir;
17
18
    /**
19
     * Cache file name
20
     * @var string
21
     */
22
    private $cacheFilename;
23
24
    /**
25
     * Cache file name, hashed with sha1. Used as an actual file name
26
     * @var string
27
     */
28
    private $cacheFilenameHashed;
29
30
    /**
31
     * Cache file extension
32
     * @var string
33
     */
34
    private $cacheFileExtension;
35
36
    /**
37
     * Holds current cache
38
     * @var array
39
     */
40
    private $cacheArray;
41
42
    /**
43
     * If true, cache expire after one second
44
     * @var bool
45
     */
46
    private $devMode;
47
48
    /**
49
     * Cache constructor.
50
     * @param string $cacheDirPath cache directory. Must end with "/"
51
     * @param string $cacheFileName cache file name
52
     * @param string $cacheFileExtension cache file extension. Must end with .php
53
     * @throws \Exception if there is a problem loading the cache
54
     */
55
    public function __construct($cacheDirPath = "cache/", $cacheFileName = "defaultcache", $cacheFileExtension = ".cache.php")
56
    {
57
        $this->setCacheFilename($cacheFileName);
58
        $this->setCacheDir($cacheDirPath);
59
        $this->setCacheFileExtension($cacheFileExtension);
60
        $this->setDevMode(false);
61
62
        $this->reloadFromDisc();
63
    }
64
65
    /**
66
     * Loads cache
67
     * @return array array filled with data
68
     * @throws \Exception if there is a problem loading the cache
69
     */
70
    private function loadCacheFile()
71
    {
72
        $filepath = $this->getCacheFilePath();
73
        $file = @file_get_contents($filepath);
74
75
        if (!$file) {
76
            unlink($filepath);
77
            throw new \Exception("Cannot load cache file! ({$this->getCacheFilename()})");
78
        }
79
80
        // Remove the first line which prevents direct access to the file
81
        $file = $this->stripFirstLine($file);
82
        $data = unserialize($file);
83
84
        if ($data === false) {
85
            unlink($filepath);
86
            throw new \Exception("Cannot unserialize cache file, cache file deleted. ({$this->getCacheFilename()})");
87
        }
88
89
        if (!isset($data["hash-sum"])) {
90
            unlink($filepath);
91
            throw new \Exception("No hash found in cache file, cache file deleted");
92
        }
93
94
        $hash = $data["hash-sum"];
95
        unset($data["hash-sum"]);
96
97
        if ($hash !== $this->getStringHash(serialize($data))) {
98
            unlink($filepath);
99
            throw new \Exception("Cache data miss-hashed, cache file deleted");
100
        }
101
102
        return $data;
103
    }
104
105
    /**
106
     * Saves current cacheArray into the cache file
107
     * @return $this
108
     * @throws \Exception if the file cannot be saved
109
     */
110
    private function saveCacheFile()
111
    {
112
        if (!file_exists($this->getCacheDir())) {
113
            @mkdir($this->getCacheDir());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

113
            /** @scrutinizer ignore-unhandled */ @mkdir($this->getCacheDir());

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...
114
        }
115
116
        $cache = $this->cacheArray;
117
        $cache["hash-sum"] = $this->getStringHash(serialize($cache));
118
        $data = serialize($cache);
119
        $firstLine = '<?php die("Access denied") ?>' . PHP_EOL;
120
        $success = file_put_contents($this->getCacheFilePath(), $firstLine . $data) !== false;
121
122
        if (!$success) {
123
            throw new \Exception("Cannot save cache");
124
        }
125
126
        return $this;
127
    }
128
129
    /**
130
     * Stores $data under $key for $expiration seconds
131
     * If $key is already used, then current data will be overwritten
132
     * @param $key string key associated with the current data
133
     * @param $data mixed data to store
134
     * @param $expiration int number of seconds before the $key expires
135
     * @param $permanent bool if true, this item will not be automatically cleared after expiring
136
     * @return $this
137
     * @throws \Exception if the file cannot be saved
138
     */
139
    public function store($key, $data, $expiration = 60, $permanent = false)
140
    {
141
        if (!is_string($key)) {
142
            throw new \InvalidArgumentException('$key must be a string, got type "' . get_class($key) . '" instead');
143
        }
144
145
        if ($this->isDevMode()) {
146
            $expiration = 1;
147
        }
148
149
        $storeData = [
150
            "time" => time(),
151
            "expire" => $expiration,
152
            "data" => $data,
153
            "permanent" => $permanent
154
        ];
155
156
        $this->cacheArray[$key] = $storeData;
157
        $this->saveCacheFile();
158
        return $this;
159
    }
160
161
    /**
162
     * Returns data associated with $key
163
     * @param $key string
164
     * @param bool $meta if true, array will be returned containing metadata alongside data itself
165
     * @return mixed|null returns data if $key is valid and not expired, NULL otherwise
166
     * @throws \Exception if the file cannot be saved
167
     */
168
    public function retrieve($key, $meta = false)
169
    {
170
        $this->eraseExpired();
171
172
        if (!isset($this->cacheArray[$key])) {
173
            return null;
174
        }
175
176
        $data = $this->cacheArray[$key];
177
        return $meta ? $data : @$data["data"];
178
    }
179
180
    /**
181
     * Calls $refreshCallback if $key does not exists or is expired.
182
     * Also returns latest data associated with $key.
183
     * This is basically a shortcut, turns this:
184
     * <code>
185
     * if($cache->isExpired(key)) {
186
     *     $cache->store(key, $newdata, 10);
187
     * }
188
     *
189
     * $data = $cache->retrieve(key);
190
     * </code>
191
     *
192
     * to this:
193
     *
194
     * <code>
195
     * $data = $cache->refreshIfExpired(key, function () {
196
     *    return $newdata;
197
     * }, 10);
198
     * </code>
199
     *
200
     * @param $key
201
     * @param $refreshCallback Callback called when data needs to be refreshed. Should return data to be cached.
202
     * @param int $cacheTime Cache time. Defaults to 60
203
     * @param bool $meta If true, returns data with meta. @see retrieve
204
     * @return mixed|null Data currently stored under key
205
     * @throws \Exception if the file cannot be saved
206
     */
207
    public function refreshIfExpired($key, $refreshCallback, $cacheTime = 60, $meta = false)
208
    {
209
        if ($this->isExpired($key)) {
210
            $this->store($key, $refreshCallback(), $cacheTime);
211
        }
212
213
        return $this->retrieve($key, $meta);
214
    }
215
216
    /**
217
     * Erases data associated with $key
218
     * @param $key string
219
     * @return bool true if $key was found and removed, false otherwise
220
     * @throws \Exception if the file cannot be saved
221
     */
222
    public function eraseKey($key)
223
    {
224
        if (!$this->isCached($key, false)) {
225
            return false;
226
        }
227
228
        unset($this->cacheArray[$key]);
229
        $this->saveCacheFile();
230
        return true;
231
    }
232
233
    /**
234
     * Erases expired keys from cache
235
     * @return int number of erased entries
236
     * @throws \Exception if the file cannot be saved
237
     */
238
    public function eraseExpired()
239
    {
240
        $counter = 0;
241
242
        foreach ($this->cacheArray as $key => $value) {
243
            if (!$value["permanent"] && $this->isExpired($key, false)) {
244
                $this->eraseKey($key);
245
                $counter++;
246
            }
247
        }
248
249
        if ($counter > 0) {
250
            $this->saveCacheFile();
251
        }
252
253
        return $counter;
254
    }
255
256
    /**
257
     * Clears the cache
258
     * @throws \Exception if the file cannot be saved
259
     */
260
    public function clearCache()
261
    {
262
        $this->cacheArray = [];
263
        $this->saveCacheFile();
264
    }
265
266
    /**
267
     * Checks if $key has expired
268
     * @param $key
269
     * @param bool $eraseExpired if true, expired data will
270
     * be cleared before running this function
271
     * @return bool
272
     * @throws \Exception if the file cannot be saved
273
     */
274
    public function isExpired($key, $eraseExpired = true)
275
    {
276
        if ($eraseExpired) {
277
            $this->eraseExpired();
278
        }
279
280
        if (!$this->isCached($key, false)) {
281
            return true;
282
        }
283
284
        $item = $this->cacheArray[$key];
285
286
        return $this->isTimestampExpired($item["time"], $item["expire"]);
287
    }
288
289
    /**
290
     * Checks if $key is cached
291
     * @param $key
292
     * @param bool $eraseExpired if true, expired data will
293
     * be cleared before running this function
294
     * @return bool
295
     * @throws \Exception if the file cannot be saved
296
     */
297
    public function isCached($key, $eraseExpired = true)
298
    {
299
        if ($eraseExpired) {
300
            $this->eraseExpired();
301
        }
302
303
        return isset($this->cacheArray[$key]);
304
    }
305
306
    /**
307
     * Checks if the timestamp expired
308
     * @param $timestamp int
309
     * @param $expiration int number of seconds after the timestamp expires
310
     * @return bool true if the timestamp expired, false otherwise
311
     */
312
    private function isTimestampExpired($timestamp, $expiration)
313
    {
314
        $timeDiff = time() - $timestamp;
315
        return $timeDiff >= $expiration;
316
    }
317
318
    /**
319
     * Prints cache file using var_dump, useful for debugging
320
     */
321
    public function debugCache()
322
    {
323
        if (file_exists($this->getCacheFilePath())) {
324
            var_dump(unserialize($this->stripFirstLine(file_get_contents($this->getCacheFilePath()))));
0 ignored issues
show
Security Debugging Code introduced by
var_dump(unserialize($th...>getCacheFilePath())))) looks like debug code. Are you sure you do not want to remove it?
Loading history...
325
        }
326
    }
327
328
    /**
329
     * Reloads cache from disc. Can be used after changing file name, extension or cache dir
330
     * using functions instead of constructor. (This class loads data once, when is created)
331
     * @throws \Exception if there is a problem loading the cache
332
     */
333
    public function reloadFromDisc()
334
    {
335
        // Try to load the cache, otherwise create a empty array
336
        $this->cacheArray = is_readable($this->getCacheFilePath()) ? $this->loadCacheFile() : [];
337
    }
338
339
    /**
340
     * Returns md5 hash of the given string.
341
     * @param $str string String to be hashed
342
     * @return string MD5 hash
343
     * @throws \InvalidArgumentException if $str is not a string
344
     */
345
    private function getStringHash($str)
346
    {
347
        if (!is_string($str)) {
348
            throw new \InvalidArgumentException('$key must be a string, got type "' . get_class($str) . '" instead');
349
        }
350
351
        return md5($str);
352
    }
353
354
    // Utils
355
356
    /**
357
     * Strips the first line from string
358
     * https://stackoverflow.com/a/7740485
359
     * @param $str
360
     * @return bool|string stripped text without the first line or false on failure
361
     */
362
    private function stripFirstLine($str)
363
    {
364
        $position = strpos($str, "\n");
365
366
        if ($position === false) {
367
            return $str;
368
        }
369
370
        return substr($str, $position + 1);
371
    }
372
373
    // Generic setters and getters below
374
375
    /**
376
     * Returns cache directory
377
     * @return string
378
     */
379
    public function getCacheDir()
380
    {
381
        return $this->cacheDir;
382
    }
383
384
    /**
385
     * Sets new cache directory. If you want to read data from new file, consider calling reloadFromDisc.
386
     * @param string $cacheDir new cache directory. Must end with "/"
387
     * @return $this
388
     */
389
    public function setCacheDir($cacheDir)
390
    {
391
        // Add "/" to the end if its not here
392
        if (substr($cacheDir, -1) !== "/") {
393
            $cacheDir .= "/";
394
        }
395
396
        $this->cacheDir = $cacheDir;
397
        return $this;
398
    }
399
400
    /**
401
     * Returns cache file name, hashed with sha1. Used as an actual file name
402
     * The new value is computed when using setCacheFilename method.
403
     * @return string
404
     */
405
    public function getCacheFilenameHashed()
406
    {
407
        return $this->cacheFilenameHashed;
408
    }
409
410
    /**
411
     * Returns cache file name
412
     * @return string
413
     */
414
    public function getCacheFilename()
415
    {
416
        return $this->cacheFilename;
417
    }
418
419
    /**
420
     * Sets new cache file name. If you want to read data from new file, consider calling reloadFromDisc.
421
     * @param string $cacheFilename
422
     * @return $this
423
     * @throws \InvalidArgumentException if $cacheFilename is not a string
424
     */
425
    public function setCacheFilename($cacheFilename)
426
    {
427
        if (!is_string($cacheFilename)) {
0 ignored issues
show
introduced by
The condition is_string($cacheFilename) is always true.
Loading history...
428
            throw new \InvalidArgumentException('$key must be a string, got type "' . get_class($cacheFilename) . '" instead');
429
        }
430
431
        $this->cacheFilename = $cacheFilename;
432
        $this->cacheFilenameHashed = $this->getStringHash($cacheFilename);
433
        return $this;
434
    }
435
436
    /**
437
     * Returns cache file extension
438
     * @return string
439
     */
440
    public function getCacheFileExtension()
441
    {
442
        return $this->cacheFileExtension;
443
    }
444
445
    /**
446
     * Sets new cache file extension. If you want to read data from new file, consider calling reloadFromDisc.
447
     * @param string $cacheFileExtension new cache file extension. Must end with ".php"
448
     * @return $this
449
     */
450
    public function setCacheFileExtension($cacheFileExtension)
451
    {
452
        // Add ".php" to the end if its not here
453
        if (substr($cacheFileExtension, -4) !== ".php") {
454
            $cacheFileExtension .= ".php";
455
        }
456
457
        $this->cacheFileExtension = $cacheFileExtension;
458
        return $this;
459
    }
460
461
    /**
462
     * Combines directory, filename and extension into a path
463
     * @return string
464
     */
465
    public function getCacheFilePath()
466
    {
467
        return $this->getCacheDir() . $this->getCacheFilenameHashed() . $this->getCacheFileExtension();
468
    }
469
470
    /**
471
     * Returns raw cache array
472
     * @return array
473
     */
474
    public function getCacheArray()
475
    {
476
        return $this->cacheArray;
477
    }
478
479
    /**
480
     * Returns true if dev mode is on
481
     * If dev mode is on, cache expire after one second
482
     * @return bool
483
     */
484
    public function isDevMode()
485
    {
486
        return $this->devMode;
487
    }
488
489
    /**
490
     * Sets dev mode on or off
491
     * If dev mode is on, cache expire after one second
492
     * @param $devMode
493
     * @return $this
494
     */
495
    public function setDevMode($devMode)
496
    {
497
        $this->devMode = $devMode;
498
        return $this;
499
    }
500
}
501