1<?php 2 3declare(strict_types=1); 4 5namespace Sabre\DAV; 6 7use Sabre\HTTP\RequestInterface; 8use Sabre\HTTP\ResponseInterface; 9use Sabre\Uri; 10 11/** 12 * Temporary File Filter Plugin. 13 * 14 * The purpose of this filter is to intercept some of the garbage files 15 * operation systems and applications tend to generate when mounting 16 * a WebDAV share as a disk. 17 * 18 * It will intercept these files and place them in a separate directory. 19 * these files are not deleted automatically, so it is advisable to 20 * delete these after they are not accessed for 24 hours. 21 * 22 * Currently it supports: 23 * * OS/X style resource forks and .DS_Store 24 * * desktop.ini and Thumbs.db (windows) 25 * * .*.swp (vim temporary files) 26 * * .dat.* (smultron temporary files) 27 * 28 * Additional patterns can be added, by adding on to the 29 * temporaryFilePatterns property. 30 * 31 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 32 * @author Evert Pot (http://evertpot.com/) 33 * @license http://sabre.io/license/ Modified BSD License 34 */ 35class TemporaryFileFilterPlugin extends ServerPlugin 36{ 37 /** 38 * This is the list of patterns we intercept. 39 * If new patterns are added, they must be valid patterns for preg_match. 40 * 41 * @var array 42 */ 43 public $temporaryFilePatterns = [ 44 '/^\._(.*)$/', // OS/X resource forks 45 '/^.DS_Store$/', // OS/X custom folder settings 46 '/^desktop.ini$/', // Windows custom folder settings 47 '/^Thumbs.db$/', // Windows thumbnail cache 48 '/^.(.*).swp$/', // ViM temporary files 49 '/^\.dat(.*)$/', // Smultron seems to create these 50 '/^~lock.(.*)#$/', // Windows 7 lockfiles 51 ]; 52 53 /** 54 * A reference to the main Server class. 55 * 56 * @var \Sabre\DAV\Server 57 */ 58 protected $server; 59 60 /** 61 * This is the directory where this plugin 62 * will store it's files. 63 * 64 * @var string 65 */ 66 private $dataDir; 67 68 /** 69 * Creates the plugin. 70 * 71 * Make sure you specify a directory for your files. If you don't, we 72 * will use PHP's directory for session-storage instead, and you might 73 * not want that. 74 * 75 * @param string|null $dataDir 76 */ 77 public function __construct($dataDir = null) 78 { 79 if (!$dataDir) { 80 $dataDir = ini_get('session.save_path').'/sabredav/'; 81 } 82 if (!is_dir($dataDir)) { 83 mkdir($dataDir); 84 } 85 $this->dataDir = $dataDir; 86 } 87 88 /** 89 * Initialize the plugin. 90 * 91 * This is called automatically be the Server class after this plugin is 92 * added with Sabre\DAV\Server::addPlugin() 93 */ 94 public function initialize(Server $server) 95 { 96 $this->server = $server; 97 $server->on('beforeMethod:*', [$this, 'beforeMethod']); 98 $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); 99 } 100 101 /** 102 * This method is called before any HTTP method handler. 103 * 104 * This method intercepts any GET, DELETE, PUT and PROPFIND calls to 105 * filenames that are known to match the 'temporary file' regex. 106 * 107 * @return bool 108 */ 109 public function beforeMethod(RequestInterface $request, ResponseInterface $response) 110 { 111 if (!$tempLocation = $this->isTempFile($request->getPath())) { 112 return; 113 } 114 115 switch ($request->getMethod()) { 116 case 'GET': 117 return $this->httpGet($request, $response, $tempLocation); 118 case 'PUT': 119 return $this->httpPut($request, $response, $tempLocation); 120 case 'PROPFIND': 121 return $this->httpPropfind($request, $response, $tempLocation); 122 case 'DELETE': 123 return $this->httpDelete($request, $response, $tempLocation); 124 } 125 126 return; 127 } 128 129 /** 130 * This method is invoked if some subsystem creates a new file. 131 * 132 * This is used to deal with HTTP LOCK requests which create a new 133 * file. 134 * 135 * @param string $uri 136 * @param resource $data 137 * @param bool $modified should be set to true, if this event handler 138 * changed &$data 139 * 140 * @return bool 141 */ 142 public function beforeCreateFile($uri, $data, ICollection $parent, $modified) 143 { 144 if ($tempPath = $this->isTempFile($uri)) { 145 $hR = $this->server->httpResponse; 146 $hR->setHeader('X-Sabre-Temp', 'true'); 147 file_put_contents($tempPath, $data); 148 149 return false; 150 } 151 152 return; 153 } 154 155 /** 156 * This method will check if the url matches the temporary file pattern 157 * if it does, it will return an path based on $this->dataDir for the 158 * temporary file storage. 159 * 160 * @param string $path 161 * 162 * @return bool|string 163 */ 164 protected function isTempFile($path) 165 { 166 // We're only interested in the basename. 167 list(, $tempPath) = Uri\split($path); 168 169 if (null === $tempPath) { 170 return false; 171 } 172 173 foreach ($this->temporaryFilePatterns as $tempFile) { 174 if (preg_match($tempFile, $tempPath)) { 175 return $this->getDataDir().'/sabredav_'.md5($path).'.tempfile'; 176 } 177 } 178 179 return false; 180 } 181 182 /** 183 * This method handles the GET method for temporary files. 184 * If the file doesn't exist, it will return false which will kick in 185 * the regular system for the GET method. 186 * 187 * @param string $tempLocation 188 * 189 * @return bool 190 */ 191 public function httpGet(RequestInterface $request, ResponseInterface $hR, $tempLocation) 192 { 193 if (!file_exists($tempLocation)) { 194 return; 195 } 196 197 $hR->setHeader('Content-Type', 'application/octet-stream'); 198 $hR->setHeader('Content-Length', filesize($tempLocation)); 199 $hR->setHeader('X-Sabre-Temp', 'true'); 200 $hR->setStatus(200); 201 $hR->setBody(fopen($tempLocation, 'r')); 202 203 return false; 204 } 205 206 /** 207 * This method handles the PUT method. 208 * 209 * @param string $tempLocation 210 * 211 * @return bool 212 */ 213 public function httpPut(RequestInterface $request, ResponseInterface $hR, $tempLocation) 214 { 215 $hR->setHeader('X-Sabre-Temp', 'true'); 216 217 $newFile = !file_exists($tempLocation); 218 219 if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) { 220 throw new Exception\PreconditionFailed('The resource already exists, and an If-None-Match header was supplied'); 221 } 222 223 file_put_contents($tempLocation, $this->server->httpRequest->getBody()); 224 $hR->setStatus($newFile ? 201 : 200); 225 226 return false; 227 } 228 229 /** 230 * This method handles the DELETE method. 231 * 232 * If the file didn't exist, it will return false, which will make the 233 * standard HTTP DELETE handler kick in. 234 * 235 * @param string $tempLocation 236 * 237 * @return bool 238 */ 239 public function httpDelete(RequestInterface $request, ResponseInterface $hR, $tempLocation) 240 { 241 if (!file_exists($tempLocation)) { 242 return; 243 } 244 245 unlink($tempLocation); 246 $hR->setHeader('X-Sabre-Temp', 'true'); 247 $hR->setStatus(204); 248 249 return false; 250 } 251 252 /** 253 * This method handles the PROPFIND method. 254 * 255 * It's a very lazy method, it won't bother checking the request body 256 * for which properties were requested, and just sends back a default 257 * set of properties. 258 * 259 * @param string $tempLocation 260 * 261 * @return bool 262 */ 263 public function httpPropfind(RequestInterface $request, ResponseInterface $hR, $tempLocation) 264 { 265 if (!file_exists($tempLocation)) { 266 return; 267 } 268 269 $hR->setHeader('X-Sabre-Temp', 'true'); 270 $hR->setStatus(207); 271 $hR->setHeader('Content-Type', 'application/xml; charset=utf-8'); 272 273 $properties = [ 274 'href' => $request->getPath(), 275 200 => [ 276 '{DAV:}getlastmodified' => new Xml\Property\GetLastModified(filemtime($tempLocation)), 277 '{DAV:}getcontentlength' => filesize($tempLocation), 278 '{DAV:}resourcetype' => new Xml\Property\ResourceType(null), 279 '{'.Server::NS_SABREDAV.'}tempFile' => true, 280 ], 281 ]; 282 283 $data = $this->server->generateMultiStatus([$properties]); 284 $hR->setBody($data); 285 286 return false; 287 } 288 289 /** 290 * This method returns the directory where the temporary files should be stored. 291 * 292 * @return string 293 */ 294 protected function getDataDir() 295 { 296 return $this->dataDir; 297 } 298} 299