Completed
Push — master ( 767356...53d951 )
by Marco
16:44 queued 11:45
created

FileCache   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 259
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1

Test Coverage

Coverage 86.59%

Importance

Changes 5
Bugs 1 Features 0
Metric Value
wmc 33
c 5
b 1
f 0
lcom 2
cbo 1
dl 0
loc 259
ccs 71
cts 82
cp 0.8659
rs 9.3999

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getDirectory() 0 4 1
A getExtension() 0 4 1
A doDelete() 0 6 2
B getFilename() 0 27 5
B __construct() 0 33 4
A doFlush() 0 17 4
A createPathIfNeeded() 0 10 4
A doGetStats() 0 19 4
B writeFile() 0 26 5
A getIterator() 0 7 1
A isFilenameEndingWithExtension() 0 5 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\Common\Cache;
21
22
/**
23
 * Base file cache driver.
24
 *
25
 * @since  2.3
26
 * @author Fabio B. Silva <[email protected]>
27
 * @author Tobias Schultze <http://tobion.de>
28
 */
29
abstract class FileCache extends CacheProvider
30
{
31
    /**
32
     * The cache directory.
33
     *
34
     * @var string
35
     */
36
    protected $directory;
37
38
    /**
39
     * The cache file extension.
40
     *
41
     * @var string
42
     */
43
    private $extension;
44
45
    /**
46
     * @var int
47
     */
48
    private $umask;
49
50
    /**
51
     * @var int
52
     */
53
    private $directoryStringLength;
54
55
    /**
56
     * @var int
57
     */
58
    private $extensionStringLength;
59
60
    /**
61
     * @var bool
62
     */
63
    private $isRunningOnWindows;
64
65
    /**
66
     * Constructor.
67
     *
68
     * @param string $directory The cache directory.
69
     * @param string $extension The cache file extension.
70
     *
71
     * @throws \InvalidArgumentException
72
     */
73 168
    public function __construct($directory, $extension = '', $umask = 0002)
74
    {
75
        // YES, this needs to be *before* createPathIfNeeded()
76 168
        if ( ! is_int($umask)) {
77 1
            throw new \InvalidArgumentException(sprintf(
78 1
                'The umask parameter is required to be integer, was: %s',
79 1
                gettype($umask)
80
            ));
81
        }
82 167
        $this->umask = $umask;
83
84 167
        if ( ! $this->createPathIfNeeded($directory)) {
85
            throw new \InvalidArgumentException(sprintf(
86
                'The directory "%s" does not exist and could not be created.',
87
                $directory
88
            ));
89
        }
90
91 167
        if ( ! is_writable($directory)) {
92
            throw new \InvalidArgumentException(sprintf(
93
                'The directory "%s" is not writable.',
94
                $directory
95
            ));
96
        }
97
98
        // YES, this needs to be *after* createPathIfNeeded()
99 167
        $this->directory = realpath($directory);
100 167
        $this->extension = (string) $extension;
101
102 167
        $this->directoryStringLength = strlen($this->directory);
103 167
        $this->extensionStringLength = strlen($this->extension);
104 167
        $this->isRunningOnWindows    = defined('PHP_WINDOWS_VERSION_BUILD');
105 167
    }
106
107
    /**
108
     * Gets the cache directory.
109
     *
110
     * @return string
111
     */
112 1
    public function getDirectory()
113
    {
114 1
        return $this->directory;
115
    }
116
117
    /**
118
     * Gets the cache file extension.
119
     *
120
     * @return string
121
     */
122 1
    public function getExtension()
123
    {
124 1
        return $this->extension;
125
    }
126
127
    /**
128
     * @param string $id
129
     *
130
     * @return string
131
     */
132 164
    protected function getFilename($id)
133
    {
134 164
        $hash = hash('sha256', $id);
135
136
        // This ensures that the filename is unique and that there are no invalid chars in it.
137
        if (
138 164
            '' === $id
139 164
            || ((strlen($id) * 2 + $this->extensionStringLength) > 255)
140 164
            || ($this->isRunningOnWindows && ($this->directoryStringLength + 4 + strlen($id) * 2 + $this->extensionStringLength) > 258)
141
        ) {
142
            // Most filesystems have a limit of 255 chars for each path component. On Windows the the whole path is limited
143
            // to 260 chars (including terminating null char). Using long UNC ("\\?\" prefix) does not work with the PHP API.
144
            // And there is a bug in PHP (https://bugs.php.net/bug.php?id=70943) with path lengths of 259.
145
            // So if the id in hex representation would surpass the limit, we use the hash instead. The prefix prevents
146
            // collisions between the hash and bin2hex.
147 12
            $filename = '_' . $hash;
148
        } else {
149 162
            $filename = bin2hex($id);
150
        }
151
152 164
        return $this->directory
153 164
            . DIRECTORY_SEPARATOR
154 164
            . substr($hash, 0, 2)
155 164
            . DIRECTORY_SEPARATOR
156 164
            . $filename
157 164
            . $this->extension;
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163 88
    protected function doDelete($id)
164
    {
165 88
        $filename = $this->getFilename($id);
166
167 88
        return @unlink($filename) || ! file_exists($filename);
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 6
    protected function doFlush()
174
    {
175 6
        foreach ($this->getIterator() as $name => $file) {
176 6
            if ($file->isDir()) {
177
                // Remove the intermediate directories which have been created to balance the tree. It only takes effect
178
                // if the directory is empty. If several caches share the same directory but with different file extensions,
179
                // the other ones are not removed.
180 6
                @rmdir($name);
181 6
            } elseif ($this->isFilenameEndingWithExtension($name)) {
182
                // If an extension is set, only remove files which end with the given extension.
183
                // If no extension is set, we have no other choice than removing everything.
184 6
                @unlink($name);
185
            }
186
        }
187
188 6
        return true;
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194 4
    protected function doGetStats()
195
    {
196 4
        $usage = 0;
197 4
        foreach ($this->getIterator() as $name => $file) {
198 2
            if (! $file->isDir() && $this->isFilenameEndingWithExtension($name)) {
199 2
                $usage += $file->getSize();
200
            }
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
     * @param string $path
218
     * @return bool TRUE on success or if path already exists, FALSE if path cannot be created.
219
     */
220 167
    private function createPathIfNeeded(string $path) : bool
221
    {
222 167
        if ( ! is_dir($path)) {
223 163
            if (false === @mkdir($path, 0777 & (~$this->umask), true) && !is_dir($path)) {
224
                return false;
225
            }
226
        }
227
228 167
        return true;
229
    }
230
231
    /**
232
     * Writes a string content to file in an atomic way.
233
     *
234
     * @param string $filename Path to the file where to write the data.
235
     * @param string $content  The content to write
236
     *
237
     * @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error.
238
     */
239 155
    protected function writeFile(string $filename, string $content) : bool
240
    {
241 155
        $filepath = pathinfo($filename, PATHINFO_DIRNAME);
242
243 155
        if ( ! $this->createPathIfNeeded($filepath)) {
244
            return false;
245
        }
246
247 155
        if ( ! is_writable($filepath)) {
248
            return false;
249
        }
250
251 155
        $tmpFile = tempnam($filepath, 'swap');
252 155
        @chmod($tmpFile, 0666 & (~$this->umask));
253
254 155
        if (file_put_contents($tmpFile, $content) !== false) {
255 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 here. This can introduce security issues, and is generally not recommended.

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