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