Passed
Push — master ( 570f3c...b306a8 )
by Roeland
10:53 queued 10s
created

CustomPropertiesBackend::formatPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2017, Georg Ehrke <[email protected]>
5
 *
6
 * @author Aaron Wood <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Roeland Jago Douma <[email protected]>
9
 * @author Thomas Müller <[email protected]>
10
 * @author Vincent Petry <[email protected]>
11
 *
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program. If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OCA\DAV\DAV;
29
30
use OCA\DAV\Connector\Sabre\Node;
31
use OCP\IDBConnection;
32
use OCP\IUser;
33
use Sabre\DAV\Exception\NotFound;
34
use Sabre\DAV\Exception\ServiceUnavailable;
35
use Sabre\DAV\INode;
36
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
37
use Sabre\DAV\PropFind;
38
use Sabre\DAV\PropPatch;
39
use Sabre\DAV\Tree;
40
41
class CustomPropertiesBackend implements BackendInterface {
42
43
	/**
44
	 * Ignored properties
45
	 *
46
	 * @var array
47
	 */
48
	private $ignoredProperties = [
49
		'{DAV:}getcontentlength',
50
		'{DAV:}getcontenttype',
51
		'{DAV:}getetag',
52
		'{DAV:}quota-used-bytes',
53
		'{DAV:}quota-available-bytes',
54
		'{http://owncloud.org/ns}permissions',
55
		'{http://owncloud.org/ns}downloadURL',
56
		'{http://owncloud.org/ns}dDC',
57
		'{http://owncloud.org/ns}size',
58
		'{http://nextcloud.org/ns}is-encrypted',
59
	];
60
61
	/**
62
	 * @var Tree
63
	 */
64
	private $tree;
65
66
	/**
67
	 * @var IDBConnection
68
	 */
69
	private $connection;
70
71
	/**
72
	 * @var IUser
73
	 */
74
	private $user;
75
76
	/**
77
	 * Properties cache
78
	 *
79
	 * @var array
80
	 */
81
	private $cache = [];
82
83
	/**
84
	 * @param Tree $tree node tree
85
	 * @param IDBConnection $connection database connection
86
	 * @param IUser $user owner of the tree and properties
87
	 */
88
	public function __construct(
89
		Tree $tree,
90
		IDBConnection $connection,
91
		IUser $user) {
92
		$this->tree = $tree;
93
		$this->connection = $connection;
94
		$this->user = $user;
95
	}
96
97
	/**
98
	 * Fetches properties for a path.
99
	 *
100
	 * @param string $path
101
	 * @param PropFind $propFind
102
	 * @return void
103
	 */
104
	public function propFind($path, PropFind $propFind) {
105
		$requestedProps = $propFind->get404Properties();
106
107
		// these might appear
108
		$requestedProps = array_diff(
109
			$requestedProps,
110
			$this->ignoredProperties
111
		);
112
113
		// substr of calendars/ => path is inside the CalDAV component
114
		// two '/' => this a calendar (no calendar-home nor calendar object)
115
		if (substr($path, 0, 10) === 'calendars/' && substr_count($path, '/') === 2) {
116
			$allRequestedProps = $propFind->getRequestedProperties();
117
			$customPropertiesForShares = [
118
				'{DAV:}displayname',
119
				'{urn:ietf:params:xml:ns:caldav}calendar-description',
120
				'{urn:ietf:params:xml:ns:caldav}calendar-timezone',
121
				'{http://apple.com/ns/ical/}calendar-order',
122
				'{http://apple.com/ns/ical/}calendar-color',
123
				'{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp',
124
			];
125
126
			foreach ($customPropertiesForShares as $customPropertyForShares) {
127
				if (in_array($customPropertyForShares, $allRequestedProps)) {
128
					$requestedProps[] = $customPropertyForShares;
129
				}
130
			}
131
		}
132
133
		if (empty($requestedProps)) {
134
			return;
135
		}
136
137
		$props = $this->getProperties($path, $requestedProps);
138
		foreach ($props as $propName => $propValue) {
139
			$propFind->set($propName, $propValue);
140
		}
141
	}
142
143
	/**
144
	 * Updates properties for a path
145
	 *
146
	 * @param string $path
147
	 * @param PropPatch $propPatch
148
	 *
149
	 * @return void
150
	 */
151
	public function propPatch($path, PropPatch $propPatch) {
152
		$propPatch->handleRemaining(function ($changedProps) use ($path) {
153
			return $this->updateProperties($path, $changedProps);
154
		});
155
	}
156
157
	/**
158
	 * This method is called after a node is deleted.
159
	 *
160
	 * @param string $path path of node for which to delete properties
161
	 */
162
	public function delete($path) {
163
		$statement = $this->connection->prepare(
164
			'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'
165
		);
166
		$statement->execute([$this->user->getUID(), $this->formatPath($path)]);
167
		$statement->closeCursor();
168
169
		unset($this->cache[$path]);
170
	}
171
172
	/**
173
	 * This method is called after a successful MOVE
174
	 *
175
	 * @param string $source
176
	 * @param string $destination
177
	 *
178
	 * @return void
179
	 */
180
	public function move($source, $destination) {
181
		$statement = $this->connection->prepare(
182
			'UPDATE `*PREFIX*properties` SET `propertypath` = ?' .
183
			' WHERE `userid` = ? AND `propertypath` = ?'
184
		);
185
		$statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]);
186
		$statement->closeCursor();
187
	}
188
189
	/**
190
	 * Returns a list of properties for this nodes.;
191
	 *
192
	 * @param string $path
193
	 * @param array $requestedProperties requested properties or empty array for "all"
194
	 * @return array
195
	 * @note The properties list is a list of propertynames the client
196
	 * requested, encoded as xmlnamespace#tagName, for example:
197
	 * http://www.example.org/namespace#author If the array is empty, all
198
	 * properties should be returned
199
	 */
200
	private function getProperties(string $path, array $requestedProperties) {
201
		if (isset($this->cache[$path])) {
202
			return $this->cache[$path];
203
		}
204
205
		// TODO: chunking if more than 1000 properties
206
		$sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?';
207
208
		$whereValues = [$this->user->getUID(), $this->formatPath($path)];
209
		$whereTypes = [null, null];
210
211
		if (!empty($requestedProperties)) {
212
			// request only a subset
213
			$sql .= ' AND `propertyname` in (?)';
214
			$whereValues[] = $requestedProperties;
215
			$whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
216
		}
217
218
		$result = $this->connection->executeQuery(
219
			$sql,
220
			$whereValues,
221
			$whereTypes
222
		);
223
224
		$props = [];
225
		while ($row = $result->fetch()) {
226
			$props[$row['propertyname']] = $row['propertyvalue'];
227
		}
228
229
		$result->closeCursor();
230
231
		$this->cache[$path] = $props;
232
		return $props;
233
	}
234
235
	/**
236
	 * Update properties
237
	 *
238
	 * @param string $path path for which to update properties
239
	 * @param array $properties array of properties to update
240
	 *
241
	 * @return bool
242
	 */
243
	private function updateProperties(string $path, array $properties) {
244
245
		$deleteStatement = 'DELETE FROM `*PREFIX*properties`' .
246
			' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
247
248
		$insertStatement = 'INSERT INTO `*PREFIX*properties`' .
249
			' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)';
250
251
		$updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' .
252
			' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
253
254
		// TODO: use "insert or update" strategy ?
255
		$existing = $this->getProperties($path, []);
256
		$this->connection->beginTransaction();
257
		foreach ($properties as $propertyName => $propertyValue) {
258
			// If it was null, we need to delete the property
259
			if (is_null($propertyValue)) {
260
				if (array_key_exists($propertyName, $existing)) {
261
					$this->connection->executeUpdate($deleteStatement,
262
						[
263
							$this->user->getUID(),
264
							$this->formatPath($path),
265
							$propertyName,
266
						]
267
					);
268
				}
269
			} else {
270
				if (!array_key_exists($propertyName, $existing)) {
271
					$this->connection->executeUpdate($insertStatement,
272
						[
273
							$this->user->getUID(),
274
							$this->formatPath($path),
275
							$propertyName,
276
							$propertyValue,
277
						]
278
					);
279
				} else {
280
					$this->connection->executeUpdate($updateStatement,
281
						[
282
							$propertyValue,
283
							$this->user->getUID(),
284
							$this->formatPath($path),
285
							$propertyName,
286
						]
287
					);
288
				}
289
			}
290
		}
291
292
		$this->connection->commit();
293
		unset($this->cache[$path]);
294
295
		return true;
296
	}
297
298
	/**
299
	 * long paths are hashed to ensure they fit in the database
300
	 *
301
	 * @param string $path
302
	 * @return string
303
	 */
304
	private function formatPath(string $path): string {
305
		if (strlen($path) > 250) {
306
			return sha1($path);
307
		} else {
308
			return $path;
309
		}
310
	}
311
}
312