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 adviceable 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) 2007-2015 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     * @return bool
138     */
139    function beforeCreateFile($uri, $data) {
140
141        if ($tempPath = $this->isTempFile($uri)) {
142
143            $hR = $this->server->httpResponse;
144            $hR->setHeader('X-Sabre-Temp', 'true');
145            file_put_contents($tempPath, $data);
146            return false;
147        }
148        return;
149
150    }
151
152    /**
153     * This method will check if the url matches the temporary file pattern
154     * if it does, it will return an path based on $this->dataDir for the
155     * temporary file storage.
156     *
157     * @param string $path
158     * @return bool|string
159     */
160    protected function isTempFile($path) {
161
162        // We're only interested in the basename.
163        list(, $tempPath) = URLUtil::splitPath($path);
164
165        foreach ($this->temporaryFilePatterns as $tempFile) {
166
167            if (preg_match($tempFile, $tempPath)) {
168                return $this->getDataDir() . '/sabredav_' . md5($path) . '.tempfile';
169            }
170
171        }
172
173        return false;
174
175    }
176
177
178    /**
179     * This method handles the GET method for temporary files.
180     * If the file doesn't exist, it will return false which will kick in
181     * the regular system for the GET method.
182     *
183     * @param RequestInterface $request
184     * @param ResponseInterface $hR
185     * @param string $tempLocation
186     * @return bool
187     */
188    function httpGet(RequestInterface $request, ResponseInterface $hR, $tempLocation) {
189
190        if (!file_exists($tempLocation)) return;
191
192        $hR->setHeader('Content-Type', 'application/octet-stream');
193        $hR->setHeader('Content-Length', filesize($tempLocation));
194        $hR->setHeader('X-Sabre-Temp', 'true');
195        $hR->setStatus(200);
196        $hR->setBody(fopen($tempLocation, 'r'));
197        return false;
198
199    }
200
201    /**
202     * This method handles the PUT method.
203     *
204     * @param RequestInterface $request
205     * @param ResponseInterface $hR
206     * @param string $tempLocation
207     * @return bool
208     */
209    function httpPut(RequestInterface $request, ResponseInterface $hR, $tempLocation) {
210
211        $hR->setHeader('X-Sabre-Temp', 'true');
212
213        $newFile = !file_exists($tempLocation);
214
215        if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) {
216             throw new Exception\PreconditionFailed('The resource already exists, and an If-None-Match header was supplied');
217        }
218
219        file_put_contents($tempLocation, $this->server->httpRequest->getBody());
220        $hR->setStatus($newFile ? 201 : 200);
221        return false;
222
223    }
224
225    /**
226     * This method handles the DELETE method.
227     *
228     * If the file didn't exist, it will return false, which will make the
229     * standard HTTP DELETE handler kick in.
230     *
231     * @param RequestInterface $request
232     * @param ResponseInterface $hR
233     * @param string $tempLocation
234     * @return bool
235     */
236    function httpDelete(RequestInterface $request, ResponseInterface $hR, $tempLocation) {
237
238        if (!file_exists($tempLocation)) return;
239
240        unlink($tempLocation);
241        $hR->setHeader('X-Sabre-Temp', 'true');
242        $hR->setStatus(204);
243        return false;
244
245    }
246
247    /**
248     * This method handles the PROPFIND method.
249     *
250     * It's a very lazy method, it won't bother checking the request body
251     * for which properties were requested, and just sends back a default
252     * set of properties.
253     *
254     * @param RequestInterface $request
255     * @param ResponseInterface $hR
256     * @param string $tempLocation
257     * @return bool
258     */
259    function httpPropfind(RequestInterface $request, ResponseInterface $hR, $tempLocation) {
260
261        if (!file_exists($tempLocation)) return;
262
263        $hR->setHeader('X-Sabre-Temp', 'true');
264        $hR->setStatus(207);
265        $hR->setHeader('Content-Type', 'application/xml; charset=utf-8');
266
267        $properties = [
268            'href' => $request->getPath(),
269            200    => [
270                '{DAV:}getlastmodified'                 => new Xml\Property\GetLastModified(filemtime($tempLocation)),
271                '{DAV:}getcontentlength'                => filesize($tempLocation),
272                '{DAV:}resourcetype'                    => new Xml\Property\ResourceType(null),
273                '{' . Server::NS_SABREDAV . '}tempFile' => true,
274
275            ],
276        ];
277
278        $data = $this->server->generateMultiStatus([$properties]);
279        $hR->setBody($data);
280        return false;
281
282    }
283
284
285    /**
286     * This method returns the directory where the temporary files should be stored.
287     *
288     * @return string
289     */
290    protected function getDataDir()
291    {
292        return $this->dataDir;
293    }
294}
295