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