Passed
Push — master ( 830a0c...6b86a6 )
by Asmir
01:30 queued 48s
created

FileCache::sanitizeCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Metadata\Cache;
6
7
use Metadata\ClassMetadata;
8
9
class FileCache implements CacheInterface
10
{
11
    /**
12
     * @var string
13
     */
14
    private $dir;
15
16
    public function __construct(string $dir)
17
    {
18
        if (!is_dir($dir)) {
19
            throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
20
        }
21
22
        $this->dir = rtrim($dir, '\\/');
23
    }
24
25
    /**
26
     * {@inheritDoc}
27
     */
28
    public function load(string $class): ?ClassMetadata
29
    {
30
        $path = $this->dir . '/' . $this->sanitizeCacheKey($class) . '.cache.php';
31
        if (!file_exists($path)) {
32
            return null;
33
        }
34
35
        try {
36
            $metadata = include $path;
37
            if ($metadata instanceof ClassMetadata) {
38
                return $metadata;
39
            }
40
            // if the file does not return anything, the return value is integer `1`.
41
        } catch (\ParseError $e) {
42
            // ignore corrupted cache
43
        }
44
45
        return null;
46
    }
47
48
    /**
49
     * {@inheritDoc}
50
     */
51
    public function put(ClassMetadata $metadata): void
52
    {
53
        if (!is_writable($this->dir)) {
54
            throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $this->dir));
55
        }
56
57
        $path = $this->dir . '/' . $this->sanitizeCacheKey($metadata->name) . '.cache.php';
58
59
        $tmpFile = tempnam($this->dir, 'metadata-cache');
60
        if (false === $tmpFile) {
61
            $this->evict($metadata->name);
62
63
            return;
64
        }
65
        $data = '<?php return unserialize(' . var_export(serialize($metadata), true) . ');';
66
        $bytesWritten = file_put_contents($tmpFile, $data);
67
        // use strlen and not mb_strlen. if there is utf8 in the code, it also writes more bytes.
68
        if ($bytesWritten !== strlen($data)) {
69
            @unlink($tmpFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

69
            /** @scrutinizer ignore-unhandled */ @unlink($tmpFile);

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...
70
            $this->evict($metadata->name); // also evict the cache to not use an outdated version.
71
72
            return;
73
        }
74
75
        // Let's not break filesystems which do not support chmod.
76
        @chmod($tmpFile, 0666 & ~umask());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). 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

76
        /** @scrutinizer ignore-unhandled */ @chmod($tmpFile, 0666 & ~umask());

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...
77
78
        $this->renameFile($tmpFile, $path);
79
    }
80
81
    /**
82
     * Renames a file with fallback for windows
83
     *
84
     */
85
    private function renameFile(string $source, string $target): void
86
    {
87
        if (false === @rename($source, $target)) {
88
            if (defined('PHP_WINDOWS_VERSION_BUILD')) {
89
                if (false === copy($source, $target)) {
90
                    throw new \RuntimeException(sprintf('(WIN) Could not write new cache file to %s.', $target));
91
                }
92
                if (false === unlink($source)) {
93
                    throw new \RuntimeException(sprintf('(WIN) Could not delete temp cache file to %s.', $source));
94
                }
95
            } else {
96
                throw new \RuntimeException(sprintf('Could not write new cache file to %s.', $target));
97
            }
98
        }
99
    }
100
101
    /**
102
     * {@inheritDoc}
103
     */
104
    public function evict(string $class): void
105
    {
106
        $path = $this->dir . '/' . $this->sanitizeCacheKey($class) . '.cache.php';
107
        if (file_exists($path)) {
108
            @unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

108
            /** @scrutinizer ignore-unhandled */ @unlink($path);

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...
109
        }
110
    }
111
112
    /**
113
     * If anonymous class is to be cached, it contains invalid path characters that need to be removed/replaced
114
     * Example of anonymous class name: class@anonymous\x00/app/src/Controller/DefaultController.php0x7f82a7e026ec
115
     *
116
     */
117
    private function sanitizeCacheKey(string $key): string
118
    {
119
        return str_replace(['\\', "\0", '@', '/', '$', '{', '}', ':'], '-', $key);
120
    }
121
}
122