Passed
Push — master ( 6cf174...2d1d93 )
by Christoph
22:01 queued 11s
created
apps/dav/lib/DAV/CustomPropertiesBackend.php 1 patch
Indentation   +309 added lines, -309 removed lines patch added patch discarded remove patch
@@ -35,313 +35,313 @@
 block discarded – undo
35 35
 
36 36
 class CustomPropertiesBackend implements BackendInterface {
37 37
 
38
-	/** @var string */
39
-	private const TABLE_NAME = 'properties';
40
-
41
-	/**
42
-	 * Ignored properties
43
-	 *
44
-	 * @var string[]
45
-	 */
46
-	private const IGNORED_PROPERTIES = [
47
-		'{DAV:}getcontentlength',
48
-		'{DAV:}getcontenttype',
49
-		'{DAV:}getetag',
50
-		'{DAV:}quota-used-bytes',
51
-		'{DAV:}quota-available-bytes',
52
-		'{http://owncloud.org/ns}permissions',
53
-		'{http://owncloud.org/ns}downloadURL',
54
-		'{http://owncloud.org/ns}dDC',
55
-		'{http://owncloud.org/ns}size',
56
-		'{http://nextcloud.org/ns}is-encrypted',
57
-	];
58
-
59
-	/**
60
-	 * Properties set by one user, readable by all others
61
-	 *
62
-	 * @var array[]
63
-	 */
64
-	private const PUBLISHED_READ_ONLY_PROPERTIES = [
65
-		'{urn:ietf:params:xml:ns:caldav}calendar-availability',
66
-	];
67
-
68
-	/**
69
-	 * @var Tree
70
-	 */
71
-	private $tree;
72
-
73
-	/**
74
-	 * @var IDBConnection
75
-	 */
76
-	private $connection;
77
-
78
-	/**
79
-	 * @var IUser
80
-	 */
81
-	private $user;
82
-
83
-	/**
84
-	 * Properties cache
85
-	 *
86
-	 * @var array
87
-	 */
88
-	private $userCache = [];
89
-
90
-	/**
91
-	 * @param Tree $tree node tree
92
-	 * @param IDBConnection $connection database connection
93
-	 * @param IUser $user owner of the tree and properties
94
-	 */
95
-	public function __construct(
96
-		Tree $tree,
97
-		IDBConnection $connection,
98
-		IUser $user) {
99
-		$this->tree = $tree;
100
-		$this->connection = $connection;
101
-		$this->user = $user;
102
-	}
103
-
104
-	/**
105
-	 * Fetches properties for a path.
106
-	 *
107
-	 * @param string $path
108
-	 * @param PropFind $propFind
109
-	 * @return void
110
-	 */
111
-	public function propFind($path, PropFind $propFind) {
112
-		$requestedProps = $propFind->get404Properties();
113
-
114
-		// these might appear
115
-		$requestedProps = array_diff(
116
-			$requestedProps,
117
-			self::IGNORED_PROPERTIES
118
-		);
119
-
120
-		// substr of calendars/ => path is inside the CalDAV component
121
-		// two '/' => this a calendar (no calendar-home nor calendar object)
122
-		if (substr($path, 0, 10) === 'calendars/' && substr_count($path, '/') === 2) {
123
-			$allRequestedProps = $propFind->getRequestedProperties();
124
-			$customPropertiesForShares = [
125
-				'{DAV:}displayname',
126
-				'{urn:ietf:params:xml:ns:caldav}calendar-description',
127
-				'{urn:ietf:params:xml:ns:caldav}calendar-timezone',
128
-				'{http://apple.com/ns/ical/}calendar-order',
129
-				'{http://apple.com/ns/ical/}calendar-color',
130
-				'{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp',
131
-			];
132
-
133
-			foreach ($customPropertiesForShares as $customPropertyForShares) {
134
-				if (in_array($customPropertyForShares, $allRequestedProps)) {
135
-					$requestedProps[] = $customPropertyForShares;
136
-				}
137
-			}
138
-		}
139
-
140
-		if (empty($requestedProps)) {
141
-			return;
142
-		}
143
-
144
-		// First fetch the published properties (set by another user), then get the ones set by
145
-		// the current user. If both are set then the latter as priority.
146
-		foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
147
-			$propFind->set($propName, $propValue);
148
-		}
149
-		foreach ($this->getUserProperties($path, $requestedProps) as $propName => $propValue) {
150
-			$propFind->set($propName, $propValue);
151
-		}
152
-	}
153
-
154
-	/**
155
-	 * Updates properties for a path
156
-	 *
157
-	 * @param string $path
158
-	 * @param PropPatch $propPatch
159
-	 *
160
-	 * @return void
161
-	 */
162
-	public function propPatch($path, PropPatch $propPatch) {
163
-		$propPatch->handleRemaining(function ($changedProps) use ($path) {
164
-			return $this->updateProperties($path, $changedProps);
165
-		});
166
-	}
167
-
168
-	/**
169
-	 * This method is called after a node is deleted.
170
-	 *
171
-	 * @param string $path path of node for which to delete properties
172
-	 */
173
-	public function delete($path) {
174
-		$statement = $this->connection->prepare(
175
-			'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'
176
-		);
177
-		$statement->execute([$this->user->getUID(), $this->formatPath($path)]);
178
-		$statement->closeCursor();
179
-
180
-		unset($this->userCache[$path]);
181
-	}
182
-
183
-	/**
184
-	 * This method is called after a successful MOVE
185
-	 *
186
-	 * @param string $source
187
-	 * @param string $destination
188
-	 *
189
-	 * @return void
190
-	 */
191
-	public function move($source, $destination) {
192
-		$statement = $this->connection->prepare(
193
-			'UPDATE `*PREFIX*properties` SET `propertypath` = ?' .
194
-			' WHERE `userid` = ? AND `propertypath` = ?'
195
-		);
196
-		$statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]);
197
-		$statement->closeCursor();
198
-	}
199
-
200
-	/**
201
-	 * @param string $path
202
-	 * @param string[] $requestedProperties
203
-	 *
204
-	 * @return array
205
-	 */
206
-	private function getPublishedProperties(string $path, array $requestedProperties): array {
207
-		$allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties);
208
-
209
-		if (empty($allowedProps)) {
210
-			return [];
211
-		}
212
-
213
-		$qb = $this->connection->getQueryBuilder();
214
-		$qb->select('*')
215
-			->from(self::TABLE_NAME)
216
-			->where($qb->expr()->eq('propertypath', $qb->createNamedParameter($path)));
217
-		$result = $qb->executeQuery();
218
-		$props = [];
219
-		while ($row = $result->fetch()) {
220
-			$props[$row['propertyname']] = $row['propertyvalue'];
221
-		}
222
-		$result->closeCursor();
223
-		return $props;
224
-	}
225
-
226
-	/**
227
-	 * Returns a list of properties for the given path and current user
228
-	 *
229
-	 * @param string $path
230
-	 * @param array $requestedProperties requested properties or empty array for "all"
231
-	 * @return array
232
-	 * @note The properties list is a list of propertynames the client
233
-	 * requested, encoded as xmlnamespace#tagName, for example:
234
-	 * http://www.example.org/namespace#author If the array is empty, all
235
-	 * properties should be returned
236
-	 */
237
-	private function getUserProperties(string $path, array $requestedProperties) {
238
-		if (isset($this->userCache[$path])) {
239
-			return $this->userCache[$path];
240
-		}
241
-
242
-		// TODO: chunking if more than 1000 properties
243
-		$sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?';
244
-
245
-		$whereValues = [$this->user->getUID(), $this->formatPath($path)];
246
-		$whereTypes = [null, null];
247
-
248
-		if (!empty($requestedProperties)) {
249
-			// request only a subset
250
-			$sql .= ' AND `propertyname` in (?)';
251
-			$whereValues[] = $requestedProperties;
252
-			$whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
253
-		}
254
-
255
-		$result = $this->connection->executeQuery(
256
-			$sql,
257
-			$whereValues,
258
-			$whereTypes
259
-		);
260
-
261
-		$props = [];
262
-		while ($row = $result->fetch()) {
263
-			$props[$row['propertyname']] = $row['propertyvalue'];
264
-		}
265
-
266
-		$result->closeCursor();
267
-
268
-		$this->userCache[$path] = $props;
269
-		return $props;
270
-	}
271
-
272
-	/**
273
-	 * Update properties
274
-	 *
275
-	 * @param string $path path for which to update properties
276
-	 * @param array $properties array of properties to update
277
-	 *
278
-	 * @return bool
279
-	 */
280
-	private function updateProperties(string $path, array $properties) {
281
-		$deleteStatement = 'DELETE FROM `*PREFIX*properties`' .
282
-			' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
283
-
284
-		$insertStatement = 'INSERT INTO `*PREFIX*properties`' .
285
-			' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)';
286
-
287
-		$updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' .
288
-			' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
289
-
290
-		// TODO: use "insert or update" strategy ?
291
-		$existing = $this->getUserProperties($path, []);
292
-		$this->connection->beginTransaction();
293
-		foreach ($properties as $propertyName => $propertyValue) {
294
-			// If it was null, we need to delete the property
295
-			if (is_null($propertyValue)) {
296
-				if (array_key_exists($propertyName, $existing)) {
297
-					$this->connection->executeUpdate($deleteStatement,
298
-						[
299
-							$this->user->getUID(),
300
-							$this->formatPath($path),
301
-							$propertyName,
302
-						]
303
-					);
304
-				}
305
-			} else {
306
-				if (!array_key_exists($propertyName, $existing)) {
307
-					$this->connection->executeUpdate($insertStatement,
308
-						[
309
-							$this->user->getUID(),
310
-							$this->formatPath($path),
311
-							$propertyName,
312
-							$propertyValue,
313
-						]
314
-					);
315
-				} else {
316
-					$this->connection->executeUpdate($updateStatement,
317
-						[
318
-							$propertyValue,
319
-							$this->user->getUID(),
320
-							$this->formatPath($path),
321
-							$propertyName,
322
-						]
323
-					);
324
-				}
325
-			}
326
-		}
327
-
328
-		$this->connection->commit();
329
-		unset($this->userCache[$path]);
330
-
331
-		return true;
332
-	}
333
-
334
-	/**
335
-	 * long paths are hashed to ensure they fit in the database
336
-	 *
337
-	 * @param string $path
338
-	 * @return string
339
-	 */
340
-	private function formatPath(string $path): string {
341
-		if (strlen($path) > 250) {
342
-			return sha1($path);
343
-		} else {
344
-			return $path;
345
-		}
346
-	}
38
+    /** @var string */
39
+    private const TABLE_NAME = 'properties';
40
+
41
+    /**
42
+     * Ignored properties
43
+     *
44
+     * @var string[]
45
+     */
46
+    private const IGNORED_PROPERTIES = [
47
+        '{DAV:}getcontentlength',
48
+        '{DAV:}getcontenttype',
49
+        '{DAV:}getetag',
50
+        '{DAV:}quota-used-bytes',
51
+        '{DAV:}quota-available-bytes',
52
+        '{http://owncloud.org/ns}permissions',
53
+        '{http://owncloud.org/ns}downloadURL',
54
+        '{http://owncloud.org/ns}dDC',
55
+        '{http://owncloud.org/ns}size',
56
+        '{http://nextcloud.org/ns}is-encrypted',
57
+    ];
58
+
59
+    /**
60
+     * Properties set by one user, readable by all others
61
+     *
62
+     * @var array[]
63
+     */
64
+    private const PUBLISHED_READ_ONLY_PROPERTIES = [
65
+        '{urn:ietf:params:xml:ns:caldav}calendar-availability',
66
+    ];
67
+
68
+    /**
69
+     * @var Tree
70
+     */
71
+    private $tree;
72
+
73
+    /**
74
+     * @var IDBConnection
75
+     */
76
+    private $connection;
77
+
78
+    /**
79
+     * @var IUser
80
+     */
81
+    private $user;
82
+
83
+    /**
84
+     * Properties cache
85
+     *
86
+     * @var array
87
+     */
88
+    private $userCache = [];
89
+
90
+    /**
91
+     * @param Tree $tree node tree
92
+     * @param IDBConnection $connection database connection
93
+     * @param IUser $user owner of the tree and properties
94
+     */
95
+    public function __construct(
96
+        Tree $tree,
97
+        IDBConnection $connection,
98
+        IUser $user) {
99
+        $this->tree = $tree;
100
+        $this->connection = $connection;
101
+        $this->user = $user;
102
+    }
103
+
104
+    /**
105
+     * Fetches properties for a path.
106
+     *
107
+     * @param string $path
108
+     * @param PropFind $propFind
109
+     * @return void
110
+     */
111
+    public function propFind($path, PropFind $propFind) {
112
+        $requestedProps = $propFind->get404Properties();
113
+
114
+        // these might appear
115
+        $requestedProps = array_diff(
116
+            $requestedProps,
117
+            self::IGNORED_PROPERTIES
118
+        );
119
+
120
+        // substr of calendars/ => path is inside the CalDAV component
121
+        // two '/' => this a calendar (no calendar-home nor calendar object)
122
+        if (substr($path, 0, 10) === 'calendars/' && substr_count($path, '/') === 2) {
123
+            $allRequestedProps = $propFind->getRequestedProperties();
124
+            $customPropertiesForShares = [
125
+                '{DAV:}displayname',
126
+                '{urn:ietf:params:xml:ns:caldav}calendar-description',
127
+                '{urn:ietf:params:xml:ns:caldav}calendar-timezone',
128
+                '{http://apple.com/ns/ical/}calendar-order',
129
+                '{http://apple.com/ns/ical/}calendar-color',
130
+                '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp',
131
+            ];
132
+
133
+            foreach ($customPropertiesForShares as $customPropertyForShares) {
134
+                if (in_array($customPropertyForShares, $allRequestedProps)) {
135
+                    $requestedProps[] = $customPropertyForShares;
136
+                }
137
+            }
138
+        }
139
+
140
+        if (empty($requestedProps)) {
141
+            return;
142
+        }
143
+
144
+        // First fetch the published properties (set by another user), then get the ones set by
145
+        // the current user. If both are set then the latter as priority.
146
+        foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
147
+            $propFind->set($propName, $propValue);
148
+        }
149
+        foreach ($this->getUserProperties($path, $requestedProps) as $propName => $propValue) {
150
+            $propFind->set($propName, $propValue);
151
+        }
152
+    }
153
+
154
+    /**
155
+     * Updates properties for a path
156
+     *
157
+     * @param string $path
158
+     * @param PropPatch $propPatch
159
+     *
160
+     * @return void
161
+     */
162
+    public function propPatch($path, PropPatch $propPatch) {
163
+        $propPatch->handleRemaining(function ($changedProps) use ($path) {
164
+            return $this->updateProperties($path, $changedProps);
165
+        });
166
+    }
167
+
168
+    /**
169
+     * This method is called after a node is deleted.
170
+     *
171
+     * @param string $path path of node for which to delete properties
172
+     */
173
+    public function delete($path) {
174
+        $statement = $this->connection->prepare(
175
+            'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'
176
+        );
177
+        $statement->execute([$this->user->getUID(), $this->formatPath($path)]);
178
+        $statement->closeCursor();
179
+
180
+        unset($this->userCache[$path]);
181
+    }
182
+
183
+    /**
184
+     * This method is called after a successful MOVE
185
+     *
186
+     * @param string $source
187
+     * @param string $destination
188
+     *
189
+     * @return void
190
+     */
191
+    public function move($source, $destination) {
192
+        $statement = $this->connection->prepare(
193
+            'UPDATE `*PREFIX*properties` SET `propertypath` = ?' .
194
+            ' WHERE `userid` = ? AND `propertypath` = ?'
195
+        );
196
+        $statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]);
197
+        $statement->closeCursor();
198
+    }
199
+
200
+    /**
201
+     * @param string $path
202
+     * @param string[] $requestedProperties
203
+     *
204
+     * @return array
205
+     */
206
+    private function getPublishedProperties(string $path, array $requestedProperties): array {
207
+        $allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties);
208
+
209
+        if (empty($allowedProps)) {
210
+            return [];
211
+        }
212
+
213
+        $qb = $this->connection->getQueryBuilder();
214
+        $qb->select('*')
215
+            ->from(self::TABLE_NAME)
216
+            ->where($qb->expr()->eq('propertypath', $qb->createNamedParameter($path)));
217
+        $result = $qb->executeQuery();
218
+        $props = [];
219
+        while ($row = $result->fetch()) {
220
+            $props[$row['propertyname']] = $row['propertyvalue'];
221
+        }
222
+        $result->closeCursor();
223
+        return $props;
224
+    }
225
+
226
+    /**
227
+     * Returns a list of properties for the given path and current user
228
+     *
229
+     * @param string $path
230
+     * @param array $requestedProperties requested properties or empty array for "all"
231
+     * @return array
232
+     * @note The properties list is a list of propertynames the client
233
+     * requested, encoded as xmlnamespace#tagName, for example:
234
+     * http://www.example.org/namespace#author If the array is empty, all
235
+     * properties should be returned
236
+     */
237
+    private function getUserProperties(string $path, array $requestedProperties) {
238
+        if (isset($this->userCache[$path])) {
239
+            return $this->userCache[$path];
240
+        }
241
+
242
+        // TODO: chunking if more than 1000 properties
243
+        $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?';
244
+
245
+        $whereValues = [$this->user->getUID(), $this->formatPath($path)];
246
+        $whereTypes = [null, null];
247
+
248
+        if (!empty($requestedProperties)) {
249
+            // request only a subset
250
+            $sql .= ' AND `propertyname` in (?)';
251
+            $whereValues[] = $requestedProperties;
252
+            $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
253
+        }
254
+
255
+        $result = $this->connection->executeQuery(
256
+            $sql,
257
+            $whereValues,
258
+            $whereTypes
259
+        );
260
+
261
+        $props = [];
262
+        while ($row = $result->fetch()) {
263
+            $props[$row['propertyname']] = $row['propertyvalue'];
264
+        }
265
+
266
+        $result->closeCursor();
267
+
268
+        $this->userCache[$path] = $props;
269
+        return $props;
270
+    }
271
+
272
+    /**
273
+     * Update properties
274
+     *
275
+     * @param string $path path for which to update properties
276
+     * @param array $properties array of properties to update
277
+     *
278
+     * @return bool
279
+     */
280
+    private function updateProperties(string $path, array $properties) {
281
+        $deleteStatement = 'DELETE FROM `*PREFIX*properties`' .
282
+            ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
283
+
284
+        $insertStatement = 'INSERT INTO `*PREFIX*properties`' .
285
+            ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)';
286
+
287
+        $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' .
288
+            ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
289
+
290
+        // TODO: use "insert or update" strategy ?
291
+        $existing = $this->getUserProperties($path, []);
292
+        $this->connection->beginTransaction();
293
+        foreach ($properties as $propertyName => $propertyValue) {
294
+            // If it was null, we need to delete the property
295
+            if (is_null($propertyValue)) {
296
+                if (array_key_exists($propertyName, $existing)) {
297
+                    $this->connection->executeUpdate($deleteStatement,
298
+                        [
299
+                            $this->user->getUID(),
300
+                            $this->formatPath($path),
301
+                            $propertyName,
302
+                        ]
303
+                    );
304
+                }
305
+            } else {
306
+                if (!array_key_exists($propertyName, $existing)) {
307
+                    $this->connection->executeUpdate($insertStatement,
308
+                        [
309
+                            $this->user->getUID(),
310
+                            $this->formatPath($path),
311
+                            $propertyName,
312
+                            $propertyValue,
313
+                        ]
314
+                    );
315
+                } else {
316
+                    $this->connection->executeUpdate($updateStatement,
317
+                        [
318
+                            $propertyValue,
319
+                            $this->user->getUID(),
320
+                            $this->formatPath($path),
321
+                            $propertyName,
322
+                        ]
323
+                    );
324
+                }
325
+            }
326
+        }
327
+
328
+        $this->connection->commit();
329
+        unset($this->userCache[$path]);
330
+
331
+        return true;
332
+    }
333
+
334
+    /**
335
+     * long paths are hashed to ensure they fit in the database
336
+     *
337
+     * @param string $path
338
+     * @return string
339
+     */
340
+    private function formatPath(string $path): string {
341
+        if (strlen($path) > 250) {
342
+            return sha1($path);
343
+        } else {
344
+            return $path;
345
+        }
346
+    }
347 347
 }
Please login to merge, or discard this patch.