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