Passed
Push — master ( b2988e...9f380c )
by Jonathan
33:05
created

FileCache::writeFile()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.1158

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 5
nop 2
dl 0
loc 25
ccs 10
cts 12
cp 0.8333
crap 5.1158
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace Doctrine\Common\Cache;
4
5
use const DIRECTORY_SEPARATOR;
6
use const PATHINFO_DIRNAME;
7
use function bin2hex;
8
use function chmod;
9
use function defined;
10
use function disk_free_space;
11
use function file_exists;
12
use function file_put_contents;
13
use function gettype;
14
use function hash;
15
use function is_dir;
16
use function is_int;
17
use function is_writable;
18
use function mkdir;
19
use function pathinfo;
20
use function realpath;
21
use function rename;
22
use function rmdir;
23
use function sprintf;
24
use function strlen;
25
use function strrpos;
26
use function substr;
27
use function tempnam;
28
use function unlink;
29
30
/**
31
 * Base file cache driver.
32
 */
33
abstract class FileCache extends CacheProvider
34
{
35
    /**
36
     * The cache directory.
37
     *
38
     * @var string
39
     */
40
    protected $directory;
41
42
    /**
43
     * The cache file extension.
44
     *
45
     * @var string
46
     */
47
    private $extension;
48
49
    /** @var int */
50
    private $umask;
51
52
    /** @var int */
53
    private $directoryStringLength;
54
55
    /** @var int */
56
    private $extensionStringLength;
57
58
    /** @var bool */
59
    private $isRunningOnWindows;
60
61
    /**
62
     * @param string $directory The cache directory.
63
     * @param string $extension The cache file extension.
64
     *
65
     * @throws \InvalidArgumentException
66
     */
67
    public function __construct($directory, $extension = '', $umask = 0002)
68
    {
69
        // YES, this needs to be *before* createPathIfNeeded()
70
        if (! is_int($umask)) {
71
            throw new \InvalidArgumentException(sprintf(
72
                'The umask parameter is required to be integer, was: %s',
73 168
                gettype($umask)
74
            ));
75
        }
76 168
        $this->umask = $umask;
77 1
78 1
        if (! $this->createPathIfNeeded($directory)) {
79 1
            throw new \InvalidArgumentException(sprintf(
80
                'The directory "%s" does not exist and could not be created.',
81
                $directory
82 167
            ));
83
        }
84 167
85
        if (! is_writable($directory)) {
86
            throw new \InvalidArgumentException(sprintf(
87
                'The directory "%s" is not writable.',
88
                $directory
89
            ));
90
        }
91 167
92
        // YES, this needs to be *after* createPathIfNeeded()
93
        $this->directory = realpath($directory);
94
        $this->extension = (string) $extension;
95
96
        $this->directoryStringLength = strlen($this->directory);
97
        $this->extensionStringLength = strlen($this->extension);
98
        $this->isRunningOnWindows    = defined('PHP_WINDOWS_VERSION_BUILD');
99 167
    }
100 167
101
    /**
102 167
     * Gets the cache directory.
103 167
     *
104 167
     * @return string
105 167
     */
106
    public function getDirectory()
107
    {
108
        return $this->directory;
109
    }
110
111
    /**
112 1
     * Gets the cache file extension.
113
     *
114 1
     * @return string
115
     */
116
    public function getExtension()
117
    {
118
        return $this->extension;
119
    }
120
121
    /**
122 1
     * @param string $id
123
     *
124 1
     * @return string
125
     */
126
    protected function getFilename($id)
127
    {
128
        $hash = hash('sha256', $id);
129
130
        // This ensures that the filename is unique and that there are no invalid chars in it.
131
        if ($id === ''
132 164
            || ((strlen($id) * 2 + $this->extensionStringLength) > 255)
133
            || ($this->isRunningOnWindows && ($this->directoryStringLength + 4 + strlen($id) * 2 + $this->extensionStringLength) > 258)
134 164
        ) {
135
            // Most filesystems have a limit of 255 chars for each path component. On Windows the the whole path is limited
136
            // to 260 chars (including terminating null char). Using long UNC ("\\?\" prefix) does not work with the PHP API.
137 164
            // And there is a bug in PHP (https://bugs.php.net/bug.php?id=70943) with path lengths of 259.
138 164
            // So if the id in hex representation would surpass the limit, we use the hash instead. The prefix prevents
139 164
            // collisions between the hash and bin2hex.
140
            $filename = '_' . $hash;
141
        } else {
142
            $filename = bin2hex($id);
143
        }
144
145
        return $this->directory
146 12
            . DIRECTORY_SEPARATOR
147
            . substr($hash, 0, 2)
148 162
            . DIRECTORY_SEPARATOR
149
            . $filename
150
            . $this->extension;
151 164
    }
152 164
153 164
    /**
154 164
     * {@inheritdoc}
155 164
     */
156 164
    protected function doDelete($id)
157
    {
158
        $filename = $this->getFilename($id);
159
160
        return @unlink($filename) || ! file_exists($filename);
161
    }
162 88
163
    /**
164 88
     * {@inheritdoc}
165
     */
166 88
    protected function doFlush()
167
    {
168
        foreach ($this->getIterator() as $name => $file) {
169
            if ($file->isDir()) {
170
                // Remove the intermediate directories which have been created to balance the tree. It only takes effect
171
                // if the directory is empty. If several caches share the same directory but with different file extensions,
172 6
                // the other ones are not removed.
173
                @rmdir($name);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rmdir(). 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

173
                /** @scrutinizer ignore-unhandled */ @rmdir($name);

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...
174 6
            } elseif ($this->isFilenameEndingWithExtension($name)) {
175 6
                // If an extension is set, only remove files which end with the given extension.
176
                // If no extension is set, we have no other choice than removing everything.
177
                @unlink($name);
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

177
                /** @scrutinizer ignore-unhandled */ @unlink($name);

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...
178
            }
179 6
        }
180 6
181
        return true;
182
    }
183 6
184
    /**
185
     * {@inheritdoc}
186
     */
187 6
    protected function doGetStats()
188
    {
189
        $usage = 0;
190
        foreach ($this->getIterator() as $name => $file) {
191
            if ($file->isDir() || ! $this->isFilenameEndingWithExtension($name)) {
192
                continue;
193 4
            }
194
195 4
            $usage += $file->getSize();
196 4
        }
197 2
198 2
        $free = disk_free_space($this->directory);
199
200
        return [
201
            Cache::STATS_HITS               => null,
202 4
            Cache::STATS_MISSES             => null,
203
            Cache::STATS_UPTIME             => null,
204
            Cache::STATS_MEMORY_USAGE       => $usage,
205 4
            Cache::STATS_MEMORY_AVAILABLE   => $free,
206
        ];
207
    }
208 4
209 4
    /**
210
     * Create path if needed.
211
     *
212
     * @return bool TRUE on success or if path already exists, FALSE if path cannot be created.
213
     */
214
    private function createPathIfNeeded(string $path) : bool
215
    {
216
        if (! is_dir($path)) {
217
            if (@mkdir($path, 0777 & (~$this->umask), true) === false && ! is_dir($path)) {
218
                return false;
219 167
            }
220
        }
221 167
222 163
        return true;
223
    }
224
225
    /**
226
     * Writes a string content to file in an atomic way.
227 167
     *
228
     * @param string $filename Path to the file where to write the data.
229
     * @param string $content  The content to write
230
     *
231
     * @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error.
232
     */
233
    protected function writeFile(string $filename, string $content) : bool
234
    {
235
        $filepath = pathinfo($filename, PATHINFO_DIRNAME);
236
237
        if (! $this->createPathIfNeeded($filepath)) {
238 155
            return false;
239
        }
240 155
241
        if (! is_writable($filepath)) {
242 155
            return false;
243
        }
244
245
        $tmpFile = tempnam($filepath, 'swap');
246 155
        @chmod($tmpFile, 0666 & (~$this->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

246
        /** @scrutinizer ignore-unhandled */ @chmod($tmpFile, 0666 & (~$this->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...
247
248
        if (file_put_contents($tmpFile, $content) !== false) {
249
            @chmod($tmpFile, 0666 & (~$this->umask));
250 155
            if (@rename($tmpFile, $filename)) {
251 155
                return true;
252
            }
253 155
254 155
            @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

254
            /** @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...
255 155
        }
256 155
257
        return false;
258
    }
259
260
    private function getIterator() : \Iterator
261
    {
262
        return new \RecursiveIteratorIterator(
263
            new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS),
264
            \RecursiveIteratorIterator::CHILD_FIRST
265
        );
266
    }
267
268 10
    /**
269
     * @param string $name The filename
270 10
     */
271 10
    private function isFilenameEndingWithExtension(string $name) : bool
272 10
    {
273
        return $this->extension === ''
274
            || strrpos($name, $this->extension) === (strlen($name) - $this->extensionStringLength);
275
    }
276
}
277