Issues (36)

lib/Doctrine/Common/Cache/FileCache.php (1 issue)

1
<?php
2
3
namespace Doctrine\Common\Cache;
4
5
use FilesystemIterator;
6
use InvalidArgumentException;
7
use Iterator;
8
use RecursiveDirectoryIterator;
9
use RecursiveIteratorIterator;
10
use const DIRECTORY_SEPARATOR;
11
use const PATHINFO_DIRNAME;
12
use function bin2hex;
13
use function chmod;
14
use function defined;
15
use function disk_free_space;
16
use function file_exists;
17
use function file_put_contents;
18
use function gettype;
19
use function hash;
20
use function is_dir;
21
use function is_int;
22
use function is_writable;
23
use function mkdir;
24
use function pathinfo;
25
use function realpath;
26
use function rename;
27
use function rmdir;
28
use function sprintf;
29
use function strlen;
30
use function strrpos;
31
use function substr;
32
use function tempnam;
33
use function unlink;
34
35
/**
36
 * Base file cache driver.
37
 */
38
abstract class FileCache extends CacheProvider
39
{
40
    /**
41
     * The cache directory.
42
     *
43
     * @var string
44
     */
45
    protected $directory;
46
47
    /**
48
     * The cache file extension.
49
     *
50
     * @var string
51
     */
52
    private $extension;
53
54
    /** @var int */
55
    private $umask;
56
57
    /** @var int */
58
    private $directoryStringLength;
59
60
    /** @var int */
61
    private $extensionStringLength;
62
63
    /** @var bool */
64
    private $isRunningOnWindows;
65
66
    /**
67
     * @param string $directory The cache directory.
68
     * @param string $extension The cache file extension.
69
     *
70
     * @throws InvalidArgumentException
71
     */
72 168
    public function __construct($directory, $extension = '', $umask = 0002)
73
    {
74
        // YES, this needs to be *before* createPathIfNeeded()
75 168
        if (! is_int($umask)) {
76 1
            throw new InvalidArgumentException(sprintf(
77 1
                'The umask parameter is required to be integer, was: %s',
78 1
                gettype($umask)
79
            ));
80
        }
81 167
        $this->umask = $umask;
82
83 167
        if (! $this->createPathIfNeeded($directory)) {
84
            throw new InvalidArgumentException(sprintf(
85
                'The directory "%s" does not exist and could not be created.',
86
                $directory
87
            ));
88
        }
89
90 167
        if (! is_writable($directory)) {
91
            throw new InvalidArgumentException(sprintf(
92
                'The directory "%s" is not writable.',
93
                $directory
94
            ));
95
        }
96
97
        // YES, this needs to be *after* createPathIfNeeded()
98 167
        $this->directory = realpath($directory);
99 167
        $this->extension = (string) $extension;
100
101 167
        $this->directoryStringLength = strlen($this->directory);
102 167
        $this->extensionStringLength = strlen($this->extension);
103 167
        $this->isRunningOnWindows    = defined('PHP_WINDOWS_VERSION_BUILD');
104 167
    }
105
106
    /**
107
     * Gets the cache directory.
108
     *
109
     * @return string
110
     */
111 1
    public function getDirectory()
112
    {
113 1
        return $this->directory;
114
    }
115
116
    /**
117
     * Gets the cache file extension.
118
     *
119
     * @return string
120
     */
121 1
    public function getExtension()
122
    {
123 1
        return $this->extension;
124
    }
125
126
    /**
127
     * @param string $id
128
     *
129
     * @return string
130
     */
131 164
    protected function getFilename($id)
132
    {
133 164
        $hash = hash('sha256', $id);
134
135
        // This ensures that the filename is unique and that there are no invalid chars in it.
136 164
        if ($id === ''
137 164
            || ((strlen($id) * 2 + $this->extensionStringLength) > 255)
138 164
            || ($this->isRunningOnWindows && ($this->directoryStringLength + 4 + strlen($id) * 2 + $this->extensionStringLength) > 258)
139
        ) {
140
            // Most filesystems have a limit of 255 chars for each path component. On Windows the the whole path is limited
141
            // to 260 chars (including terminating null char). Using long UNC ("\\?\" prefix) does not work with the PHP API.
142
            // And there is a bug in PHP (https://bugs.php.net/bug.php?id=70943) with path lengths of 259.
143
            // So if the id in hex representation would surpass the limit, we use the hash instead. The prefix prevents
144
            // collisions between the hash and bin2hex.
145 12
            $filename = '_' . $hash;
146
        } else {
147 162
            $filename = bin2hex($id);
148
        }
149
150 164
        return $this->directory
151 164
            . DIRECTORY_SEPARATOR
152 164
            . substr($hash, 0, 2)
153 164
            . DIRECTORY_SEPARATOR
154 164
            . $filename
155 164
            . $this->extension;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 88
    protected function doDelete($id)
162
    {
163 88
        $filename = $this->getFilename($id);
164
165 88
        return @unlink($filename) || ! file_exists($filename);
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171 6
    protected function doFlush()
172
    {
173 6
        foreach ($this->getIterator() as $name => $file) {
174 6
            if ($file->isDir()) {
175
                // Remove the intermediate directories which have been created to balance the tree. It only takes effect
176
                // if the directory is empty. If several caches share the same directory but with different file extensions,
177
                // the other ones are not removed.
178 6
                @rmdir($name);
179 6
            } elseif ($this->isFilenameEndingWithExtension($name)) {
180
                // If an extension is set, only remove files which end with the given extension.
181
                // If no extension is set, we have no other choice than removing everything.
182 6
                @unlink($name);
183
            }
184
        }
185
186 6
        return true;
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 4
    protected function doGetStats()
193
    {
194 4
        $usage = 0;
195 4
        foreach ($this->getIterator() as $name => $file) {
196 2
            if ($file->isDir() || ! $this->isFilenameEndingWithExtension($name)) {
197 2
                continue;
198
            }
199
200 2
            $usage += $file->getSize();
201
        }
202
203 4
        $free = disk_free_space($this->directory);
204
205
        return [
206 4
            Cache::STATS_HITS               => null,
207
            Cache::STATS_MISSES             => null,
208
            Cache::STATS_UPTIME             => null,
209 4
            Cache::STATS_MEMORY_USAGE       => $usage,
210 4
            Cache::STATS_MEMORY_AVAILABLE   => $free,
211
        ];
212
    }
213
214
    /**
215
     * Create path if needed.
216
     *
217
     * @return bool TRUE on success or if path already exists, FALSE if path cannot be created.
218
     */
219 167
    private function createPathIfNeeded(string $path) : bool
220
    {
221 167
        if (! is_dir($path)) {
222 163
            if (@mkdir($path, 0777 & (~$this->umask), true) === false && ! is_dir($path)) {
223
                return false;
224
            }
225
        }
226
227 167
        return true;
228
    }
229
230
    /**
231
     * Writes a string content to file in an atomic way.
232
     *
233
     * @param string $filename Path to the file where to write the data.
234
     * @param string $content  The content to write
235
     *
236
     * @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error.
237
     */
238 155
    protected function writeFile(string $filename, string $content) : bool
239
    {
240 155
        $filepath = pathinfo($filename, PATHINFO_DIRNAME);
241
242 155
        if (! $this->createPathIfNeeded($filepath)) {
243
            return false;
244
        }
245
246 155
        if (! is_writable($filepath)) {
247
            return false;
248
        }
249
250 155
        $tmpFile = tempnam($filepath, 'swap');
251 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

251
        /** @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...
252
253 155
        if (file_put_contents($tmpFile, $content) !== false) {
254 155
            @chmod($tmpFile, 0666 & (~$this->umask));
255 155
            if (@rename($tmpFile, $filename)) {
256 155
                return true;
257
            }
258
259
            @unlink($tmpFile);
260
        }
261
262
        return false;
263
    }
264
265 10
    private function getIterator() : Iterator
266
    {
267 10
        return new RecursiveIteratorIterator(
268 10
            new RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS),
269 10
            RecursiveIteratorIterator::CHILD_FIRST
270
        );
271
    }
272
273
    /**
274
     * @param string $name The filename
275
     */
276 8
    private function isFilenameEndingWithExtension(string $name) : bool
277
    {
278 8
        return $this->extension === ''
279 8
            || strrpos($name, $this->extension) === (strlen($name) - $this->extensionStringLength);
280
    }
281
}
282