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 20namespace Doctrine\Common\Cache; 21 22/** 23 * Base file cache driver. 24 * 25 * @since 2.3 26 * @author Fabio B. Silva <fabio.bat.silva@gmail.com> 27 * @author Tobias Schultze <http://tobion.de> 28 */ 29abstract 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 public function __construct($directory, $extension = '', $umask = 0002) 74 { 75 // YES, this needs to be *before* createPathIfNeeded() 76 if ( ! is_int($umask)) { 77 throw new \InvalidArgumentException(sprintf( 78 'The umask parameter is required to be integer, was: %s', 79 gettype($umask) 80 )); 81 } 82 $this->umask = $umask; 83 84 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 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 $this->directory = realpath($directory); 100 $this->extension = (string) $extension; 101 102 $this->directoryStringLength = strlen($this->directory); 103 $this->extensionStringLength = strlen($this->extension); 104 $this->isRunningOnWindows = defined('PHP_WINDOWS_VERSION_BUILD'); 105 } 106 107 /** 108 * Gets the cache directory. 109 * 110 * @return string 111 */ 112 public function getDirectory() 113 { 114 return $this->directory; 115 } 116 117 /** 118 * Gets the cache file extension. 119 * 120 * @return string 121 */ 122 public function getExtension() 123 { 124 return $this->extension; 125 } 126 127 /** 128 * @param string $id 129 * 130 * @return string 131 */ 132 protected function getFilename($id) 133 { 134 $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 '' === $id 139 || ((strlen($id) * 2 + $this->extensionStringLength) > 255) 140 || ($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 $filename = '_' . $hash; 148 } else { 149 $filename = bin2hex($id); 150 } 151 152 return $this->directory 153 . DIRECTORY_SEPARATOR 154 . substr($hash, 0, 2) 155 . DIRECTORY_SEPARATOR 156 . $filename 157 . $this->extension; 158 } 159 160 /** 161 * {@inheritdoc} 162 */ 163 protected function doDelete($id) 164 { 165 $filename = $this->getFilename($id); 166 167 return @unlink($filename) || ! file_exists($filename); 168 } 169 170 /** 171 * {@inheritdoc} 172 */ 173 protected function doFlush() 174 { 175 foreach ($this->getIterator() as $name => $file) { 176 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 @rmdir($name); 181 } 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 @unlink($name); 185 } 186 } 187 188 return true; 189 } 190 191 /** 192 * {@inheritdoc} 193 */ 194 protected function doGetStats() 195 { 196 $usage = 0; 197 foreach ($this->getIterator() as $name => $file) { 198 if (! $file->isDir() && $this->isFilenameEndingWithExtension($name)) { 199 $usage += $file->getSize(); 200 } 201 } 202 203 $free = disk_free_space($this->directory); 204 205 return array( 206 Cache::STATS_HITS => null, 207 Cache::STATS_MISSES => null, 208 Cache::STATS_UPTIME => null, 209 Cache::STATS_MEMORY_USAGE => $usage, 210 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 private function createPathIfNeeded($path) 221 { 222 if ( ! is_dir($path)) { 223 if (false === @mkdir($path, 0777 & (~$this->umask), true) && !is_dir($path)) { 224 return false; 225 } 226 } 227 228 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 protected function writeFile($filename, $content) 240 { 241 $filepath = pathinfo($filename, PATHINFO_DIRNAME); 242 243 if ( ! $this->createPathIfNeeded($filepath)) { 244 return false; 245 } 246 247 if ( ! is_writable($filepath)) { 248 return false; 249 } 250 251 $tmpFile = tempnam($filepath, 'swap'); 252 @chmod($tmpFile, 0666 & (~$this->umask)); 253 254 if (file_put_contents($tmpFile, $content) !== false) { 255 if (@rename($tmpFile, $filename)) { 256 return true; 257 } 258 259 @unlink($tmpFile); 260 } 261 262 return false; 263 } 264 265 /** 266 * @return \Iterator 267 */ 268 private function getIterator() 269 { 270 return new \RecursiveIteratorIterator( 271 new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), 272 \RecursiveIteratorIterator::CHILD_FIRST 273 ); 274 } 275 276 /** 277 * @param string $name The filename 278 * 279 * @return bool 280 */ 281 private function isFilenameEndingWithExtension($name) 282 { 283 return '' === $this->extension 284 || strrpos($name, $this->extension) === (strlen($name) - $this->extensionStringLength); 285 } 286} 287