• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 15880832508

25 Jun 2025 03:39PM UTC coverage: 84.209% (+0.007%) from 84.202%
15880832508

push

github

web-flow
refactor: fix various phpstan errors in Cache (#9610)

16 of 30 new or added lines in 6 files covered. (53.33%)

2 existing lines in 1 file now uncovered.

20793 of 24692 relevant lines covered (84.21%)

192.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.55
/system/Cache/Handlers/FileHandler.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Cache\Handlers;
15

16
use CodeIgniter\Cache\Exceptions\CacheException;
17
use CodeIgniter\I18n\Time;
18
use Config\Cache;
19
use Throwable;
20

21
/**
22
 * File system cache handler
23
 *
24
 * @see \CodeIgniter\Cache\Handlers\FileHandlerTest
25
 */
26
class FileHandler extends BaseHandler
27
{
28
    /**
29
     * Maximum key length.
30
     */
31
    public const MAX_KEY_LENGTH = 255;
32

33
    /**
34
     * Where to store cached files on the disk.
35
     *
36
     * @var string
37
     */
38
    protected $path;
39

40
    /**
41
     * Mode for the stored files.
42
     * Must be chmod-safe (octal).
43
     *
44
     * @var int
45
     *
46
     * @see https://www.php.net/manual/en/function.chmod.php
47
     */
48
    protected $mode;
49

50
    /**
51
     * Note: Use `CacheFactory::getHandler()` to instantiate.
52
     *
53
     * @throws CacheException
54
     */
55
    public function __construct(Cache $config)
56
    {
57
        $options = [
1,860✔
58
            ...['storePath' => WRITEPATH . 'cache', 'mode' => 0640],
1,860✔
59
            ...$config->file,
1,860✔
60
        ];
1,860✔
61

62
        $this->path = $options['storePath'] !== '' ? $options['storePath'] : WRITEPATH . 'cache';
1,860✔
63
        $this->path = rtrim($this->path, '\\/') . '/';
1,860✔
64

65
        if (! is_really_writable($this->path)) {
1,860✔
66
            throw CacheException::forUnableToWrite($this->path);
1✔
67
        }
68

69
        $this->mode   = $options['mode'];
1,860✔
70
        $this->prefix = $config->prefix;
1,860✔
71

72
        helper('filesystem');
1,860✔
73
    }
74

75
    /**
76
     * {@inheritDoc}
77
     */
78
    public function initialize()
79
    {
80
    }
1,860✔
81

82
    /**
83
     * {@inheritDoc}
84
     */
85
    public function get(string $key)
86
    {
87
        $key  = static::validateKey($key, $this->prefix);
99✔
88
        $data = $this->getItem($key);
99✔
89

90
        return is_array($data) ? $data['data'] : null;
99✔
91
    }
92

93
    /**
94
     * {@inheritDoc}
95
     */
96
    public function save(string $key, $value, int $ttl = 60)
97
    {
98
        $key = static::validateKey($key, $this->prefix);
28✔
99

100
        $contents = [
28✔
101
            'time' => Time::now()->getTimestamp(),
28✔
102
            'ttl'  => $ttl,
28✔
103
            'data' => $value,
28✔
104
        ];
28✔
105

106
        if (write_file($this->path . $key, serialize($contents))) {
28✔
107
            try {
108
                chmod($this->path . $key, $this->mode);
28✔
109

110
                // @codeCoverageIgnoreStart
111
            } catch (Throwable $e) {
×
112
                log_message('debug', 'Failed to set mode on cache file: ' . $e);
×
113
                // @codeCoverageIgnoreEnd
114
            }
115

116
            return true;
28✔
117
        }
118

119
        return false;
1✔
120
    }
121

122
    /**
123
     * {@inheritDoc}
124
     */
125
    public function delete(string $key)
126
    {
127
        $key = static::validateKey($key, $this->prefix);
4✔
128

129
        return is_file($this->path . $key) && unlink($this->path . $key);
4✔
130
    }
131

132
    /**
133
     * {@inheritDoc}
134
     *
135
     * @return int
136
     */
137
    public function deleteMatching(string $pattern)
138
    {
139
        $deleted = 0;
2✔
140

141
        foreach (glob($this->path . $pattern, GLOB_NOSORT) as $filename) {
2✔
142
            if (is_file($filename) && @unlink($filename)) {
2✔
143
                $deleted++;
2✔
144
            }
145
        }
146

147
        return $deleted;
2✔
148
    }
149

150
    /**
151
     * {@inheritDoc}
152
     */
153
    public function increment(string $key, int $offset = 1)
154
    {
155
        $prefixedKey = static::validateKey($key, $this->prefix);
4✔
156
        $tmp         = $this->getItem($prefixedKey);
4✔
157

158
        if ($tmp === false) {
4✔
159
            $tmp = ['data' => 0, 'ttl' => 60];
4✔
160
        }
161

162
        ['data' => $value, 'ttl' => $ttl] = $tmp;
4✔
163

164
        if (! is_int($value)) {
4✔
165
            return false;
4✔
166
        }
167

168
        $value += $offset;
4✔
169

170
        return $this->save($key, $value, $ttl) ? $value : false;
4✔
171
    }
172

173
    /**
174
     * {@inheritDoc}
175
     */
176
    public function decrement(string $key, int $offset = 1)
177
    {
178
        return $this->increment($key, -$offset);
2✔
179
    }
180

181
    /**
182
     * {@inheritDoc}
183
     */
184
    public function clean()
185
    {
186
        return delete_files($this->path, false, true);
8✔
187
    }
188

189
    /**
190
     * {@inheritDoc}
191
     */
192
    public function getCacheInfo()
193
    {
194
        return get_dir_file_info($this->path);
9✔
195
    }
196

197
    /**
198
     * {@inheritDoc}
199
     */
200
    public function getMetaData(string $key)
201
    {
202
        $key = static::validateKey($key, $this->prefix);
3✔
203

204
        if (false === $data = $this->getItem($key)) {
3✔
205
            return false; // @TODO This will return null in a future release
1✔
206
        }
207

208
        return [
2✔
209
            'expire' => $data['ttl'] > 0 ? $data['time'] + $data['ttl'] : null,
2✔
210
            'mtime'  => filemtime($this->path . $key),
2✔
211
            'data'   => $data['data'],
2✔
212
        ];
2✔
213
    }
214

215
    /**
216
     * {@inheritDoc}
217
     */
218
    public function isSupported(): bool
219
    {
220
        return is_writable($this->path);
1,860✔
221
    }
222

223
    /**
224
     * Does the heavy lifting of actually retrieving the file and
225
     * verifying its age.
226
     *
227
     * @return array{data: mixed, ttl: int, time: int}|false
228
     */
229
    protected function getItem(string $filename)
230
    {
231
        if (! is_file($this->path . $filename)) {
104✔
232
            return false;
98✔
233
        }
234

235
        $content = @file_get_contents($this->path . $filename);
15✔
236

237
        if ($content === false) {
15✔
238
            return false;
1✔
239
        }
240

241
        try {
242
            $data = unserialize($content);
14✔
243
        } catch (Throwable) {
1✔
244
            return false;
1✔
245
        }
246

247
        if (! is_array($data)) {
13✔
248
            return false;
×
249
        }
250

251
        if (! isset($data['ttl']) || ! is_int($data['ttl'])) {
13✔
252
            return false;
×
253
        }
254

255
        if (! isset($data['time']) || ! is_int($data['time'])) {
13✔
256
            return false;
×
257
        }
258

259
        if ($data['ttl'] > 0 && Time::now()->getTimestamp() > $data['time'] + $data['ttl']) {
13✔
260
            @unlink($this->path . $filename);
2✔
261

262
            return false;
2✔
263
        }
264

265
        return $data;
13✔
266
    }
267

268
    /**
269
     * Writes a file to disk, or returns false if not successful.
270
     *
271
     * @deprecated 4.6.1 Use `write_file()` instead.
272
     *
273
     * @param string $path
274
     * @param string $data
275
     * @param string $mode
276
     *
277
     * @return bool
278
     */
279
    protected function writeFile($path, $data, $mode = 'wb')
280
    {
281
        if (($fp = @fopen($path, $mode)) === false) {
×
282
            return false;
×
283
        }
284

285
        flock($fp, LOCK_EX);
×
286

287
        $result = 0;
×
288

289
        for ($written = 0, $length = strlen($data); $written < $length; $written += $result) {
×
290
            if (($result = fwrite($fp, substr($data, $written))) === false) {
×
291
                break;
×
292
            }
293
        }
294

295
        flock($fp, LOCK_UN);
×
296
        fclose($fp);
×
297

298
        return is_int($result);
×
299
    }
300

301
    /**
302
     * Deletes all files contained in the supplied directory path.
303
     * Files must be writable or owned by the system in order to be deleted.
304
     * If the second parameter is set to TRUE, any directories contained
305
     * within the supplied base directory will be nuked as well.
306
     *
307
     * @deprecated 4.6.1 Use `delete_files()` instead.
308
     *
309
     * @param string $path   File path
310
     * @param bool   $delDir Whether to delete any directories found in the path
311
     * @param bool   $htdocs Whether to skip deleting .htaccess and index page files
312
     * @param int    $_level Current directory depth level (default: 0; internal use only)
313
     */
314
    protected function deleteFiles(string $path, bool $delDir = false, bool $htdocs = false, int $_level = 0): bool
315
    {
316
        // Trim the trailing slash
317
        $path = rtrim($path, '/\\');
×
318

319
        if (! $currentDir = @opendir($path)) {
×
320
            return false;
×
321
        }
322

323
        while (false !== ($filename = @readdir($currentDir))) {
×
324
            if ($filename !== '.' && $filename !== '..') {
×
325
                if (is_dir($path . DIRECTORY_SEPARATOR . $filename) && $filename[0] !== '.') {
×
326
                    $this->deleteFiles($path . DIRECTORY_SEPARATOR . $filename, $delDir, $htdocs, $_level + 1);
×
327
                } elseif (! $htdocs || preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename) !== 1) {
×
328
                    @unlink($path . DIRECTORY_SEPARATOR . $filename);
×
329
                }
330
            }
331
        }
332

333
        closedir($currentDir);
×
334

335
        return ($delDir && $_level > 0) ? @rmdir($path) : true;
×
336
    }
337

338
    /**
339
     * Reads the specified directory and builds an array containing the filenames,
340
     * filesize, dates, and permissions
341
     *
342
     * Any sub-folders contained within the specified path are read as well.
343
     *
344
     * @deprecated 4.6.1 Use `get_dir_file_info()` instead.
345
     *
346
     * @param string $sourceDir    Path to source
347
     * @param bool   $topLevelOnly Look only at the top level directory specified?
348
     * @param bool   $_recursion   Internal variable to determine recursion status - do not use in calls
349
     *
350
     * @return array<string, array{
351
     *  name: string,
352
     *  server_path: string,
353
     *  size: int,
354
     *  date: int,
355
     *  relative_path: string,
356
     * }>|false
357
     */
358
    protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, bool $_recursion = false)
359
    {
NEW
360
        static $filedata = [];
×
361

NEW
362
        $relativePath = $sourceDir;
×
NEW
363
        $filePointer  = @opendir($sourceDir);
×
364

NEW
365
        if (! is_bool($filePointer)) {
×
366
            // reset the array and make sure $sourceDir has a trailing slash on the initial call
367
            if ($_recursion === false) {
×
NEW
368
                $filedata = [];
×
369

NEW
370
                $resolvedSrc = realpath($sourceDir);
×
NEW
371
                $resolvedSrc = $resolvedSrc === false ? $sourceDir : $resolvedSrc;
×
372

NEW
373
                $sourceDir = rtrim($resolvedSrc, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
×
374
            }
375

376
            // Used to be foreach (scandir($sourceDir, 1) as $file), but scandir() is simply not as fast
NEW
377
            while (false !== $file = readdir($filePointer)) {
×
378
                if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) {
×
379
                    $this->getDirFileInfo($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true);
×
380
                } elseif (! is_dir($sourceDir . $file) && $file[0] !== '.') {
×
NEW
381
                    $filedata[$file] = $this->getFileInfo($sourceDir . $file);
×
382

NEW
383
                    $filedata[$file]['relative_path'] = $relativePath;
×
384
                }
385
            }
386

NEW
387
            closedir($filePointer);
×
388

NEW
389
            return $filedata;
×
390
        }
391

392
        return false;
×
393
    }
394

395
    /**
396
     * Given a file and path, returns the name, path, size, date modified
397
     * Second parameter allows you to explicitly declare what information you want returned
398
     * Options are: name, server_path, size, date, readable, writable, executable, fileperms
399
     * Returns FALSE if the file cannot be found.
400
     *
401
     * @deprecated 4.6.1 Use `get_file_info()` instead.
402
     *
403
     * @param string              $file           Path to file
404
     * @param list<string>|string $returnedValues Array or comma separated string of information returned
405
     *
406
     * @return array{
407
     *  name?: string,
408
     *  server_path?: string,
409
     *  size?: int,
410
     *  date?: int,
411
     *  readable?: bool,
412
     *  writable?: bool,
413
     *  executable?: bool,
414
     *  fileperms?: int
415
     * }|false
416
     */
417
    protected function getFileInfo(string $file, $returnedValues = ['name', 'server_path', 'size', 'date'])
418
    {
419
        if (! is_file($file)) {
×
420
            return false;
×
421
        }
422

423
        if (is_string($returnedValues)) {
×
424
            $returnedValues = explode(',', $returnedValues);
×
425
        }
426

427
        $fileInfo = [];
×
428

429
        foreach ($returnedValues as $key) {
×
430
            switch ($key) {
431
                case 'name':
×
432
                    $fileInfo['name'] = basename($file);
×
433
                    break;
×
434

435
                case 'server_path':
×
436
                    $fileInfo['server_path'] = $file;
×
437
                    break;
×
438

439
                case 'size':
×
440
                    $fileInfo['size'] = filesize($file);
×
441
                    break;
×
442

443
                case 'date':
×
444
                    $fileInfo['date'] = filemtime($file);
×
445
                    break;
×
446

447
                case 'readable':
×
448
                    $fileInfo['readable'] = is_readable($file);
×
449
                    break;
×
450

451
                case 'writable':
×
452
                    $fileInfo['writable'] = is_writable($file);
×
453
                    break;
×
454

455
                case 'executable':
×
456
                    $fileInfo['executable'] = is_executable($file);
×
457
                    break;
×
458

459
                case 'fileperms':
×
460
                    $fileInfo['fileperms'] = fileperms($file);
×
461
                    break;
×
462
            }
463
        }
464

465
        return $fileInfo;
×
466
    }
467
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc