Passed
Push — master ( f7c59f...a72edb )
by Morris
14:26 queued 11s
created
lib/private/Setup/PostgreSQL.php 1 patch
Indentation   +131 added lines, -131 removed lines patch added patch discarded remove patch
@@ -34,135 +34,135 @@
 block discarded – undo
34 34
 use OCP\IDBConnection;
35 35
 
36 36
 class PostgreSQL extends AbstractDatabase {
37
-	public $dbprettyname = 'PostgreSQL';
38
-
39
-	/**
40
-	 * @param string $username
41
-	 * @throws \OC\DatabaseSetupException
42
-	 */
43
-	public function setupDatabase($username) {
44
-		try {
45
-			$connection = $this->connect([
46
-				'dbname' => 'postgres'
47
-			]);
48
-			//check for roles creation rights in postgresql
49
-			$builder = $connection->getQueryBuilder();
50
-			$builder->automaticTablePrefix(false);
51
-			$query = $builder
52
-				->select('rolname')
53
-				->from('pg_roles')
54
-				->where($builder->expr()->eq('rolcreaterole', new Literal('TRUE')))
55
-				->andWhere($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser)));
56
-
57
-			try {
58
-				$result = $query->execute();
59
-				$canCreateRoles = $result->rowCount() > 0;
60
-			} catch (DatabaseException $e) {
61
-				$canCreateRoles = false;
62
-			}
63
-
64
-			if ($canCreateRoles) {
65
-				//use the admin login data for the new database user
66
-
67
-				//add prefix to the postgresql user name to prevent collisions
68
-				$this->dbUser = 'oc_' . strtolower($username);
69
-				//create a new password so we don't need to store the admin config in the config file
70
-				$this->dbPassword = \OC::$server->getSecureRandom()->generate(30, \OCP\Security\ISecureRandom::CHAR_LOWER . \OCP\Security\ISecureRandom::CHAR_DIGITS);
71
-
72
-				$this->createDBUser($connection);
73
-			}
74
-
75
-			$this->config->setValues([
76
-				'dbuser' => $this->dbUser,
77
-				'dbpassword' => $this->dbPassword,
78
-			]);
79
-
80
-			//create the database
81
-			$this->createDatabase($connection);
82
-			// the connection to dbname=postgres is not needed anymore
83
-			$connection->close();
84
-		} catch (\Exception $e) {
85
-			$this->logger->logException($e);
86
-			$this->logger->warning('Error trying to connect as "postgres", assuming database is setup and tables need to be created');
87
-			$this->config->setValues([
88
-				'dbuser' => $this->dbUser,
89
-				'dbpassword' => $this->dbPassword,
90
-			]);
91
-		}
92
-
93
-		// connect to the database (dbname=$this->dbname) and check if it needs to be filled
94
-		$this->dbUser = $this->config->getValue('dbuser');
95
-		$this->dbPassword = $this->config->getValue('dbpassword');
96
-		$connection = $this->connect();
97
-		try {
98
-			$connection->connect();
99
-		} catch (\Exception $e) {
100
-			$this->logger->logException($e);
101
-			throw new \OC\DatabaseSetupException($this->trans->t('PostgreSQL username and/or password not valid'),
102
-				$this->trans->t('You need to enter details of an existing account.'));
103
-		}
104
-	}
105
-
106
-	private function createDatabase(IDBConnection $connection) {
107
-		if (!$this->databaseExists($connection)) {
108
-			//The database does not exists... let's create it
109
-			$query = $connection->prepare("CREATE DATABASE " . addslashes($this->dbName) . " OWNER " . addslashes($this->dbUser));
110
-			try {
111
-				$query->execute();
112
-			} catch (DatabaseException $e) {
113
-				$this->logger->error('Error while trying to create database');
114
-				$this->logger->logException($e);
115
-			}
116
-		} else {
117
-			$query = $connection->prepare("REVOKE ALL PRIVILEGES ON DATABASE " . addslashes($this->dbName) . " FROM PUBLIC");
118
-			try {
119
-				$query->execute();
120
-			} catch (DatabaseException $e) {
121
-				$this->logger->error('Error while trying to restrict database permissions');
122
-				$this->logger->logException($e);
123
-			}
124
-		}
125
-	}
126
-
127
-	private function userExists(IDBConnection $connection) {
128
-		$builder = $connection->getQueryBuilder();
129
-		$builder->automaticTablePrefix(false);
130
-		$query = $builder->select('*')
131
-			->from('pg_roles')
132
-			->where($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser)));
133
-		$result = $query->execute();
134
-		return $result->rowCount() > 0;
135
-	}
136
-
137
-	private function databaseExists(IDBConnection $connection) {
138
-		$builder = $connection->getQueryBuilder();
139
-		$builder->automaticTablePrefix(false);
140
-		$query = $builder->select('datname')
141
-			->from('pg_database')
142
-			->where($builder->expr()->eq('datname', $builder->createNamedParameter($this->dbName)));
143
-		$result = $query->execute();
144
-		return $result->rowCount() > 0;
145
-	}
146
-
147
-	private function createDBUser(IDBConnection $connection) {
148
-		$dbUser = $this->dbUser;
149
-		try {
150
-			$i = 1;
151
-			while ($this->userExists($connection)) {
152
-				$i++;
153
-				$this->dbUser = $dbUser . $i;
154
-			}
155
-
156
-			// create the user
157
-			$query = $connection->prepare("CREATE USER " . addslashes($this->dbUser) . " CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'");
158
-			$query->execute();
159
-			if ($this->databaseExists($connection)) {
160
-				$query = $connection->prepare('GRANT CONNECT ON DATABASE ' . addslashes($this->dbName) . ' TO '.addslashes($this->dbUser));
161
-				$query->execute();
162
-			}
163
-		} catch (DatabaseException $e) {
164
-			$this->logger->error('Error while trying to create database user');
165
-			$this->logger->logException($e);
166
-		}
167
-	}
37
+    public $dbprettyname = 'PostgreSQL';
38
+
39
+    /**
40
+     * @param string $username
41
+     * @throws \OC\DatabaseSetupException
42
+     */
43
+    public function setupDatabase($username) {
44
+        try {
45
+            $connection = $this->connect([
46
+                'dbname' => 'postgres'
47
+            ]);
48
+            //check for roles creation rights in postgresql
49
+            $builder = $connection->getQueryBuilder();
50
+            $builder->automaticTablePrefix(false);
51
+            $query = $builder
52
+                ->select('rolname')
53
+                ->from('pg_roles')
54
+                ->where($builder->expr()->eq('rolcreaterole', new Literal('TRUE')))
55
+                ->andWhere($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser)));
56
+
57
+            try {
58
+                $result = $query->execute();
59
+                $canCreateRoles = $result->rowCount() > 0;
60
+            } catch (DatabaseException $e) {
61
+                $canCreateRoles = false;
62
+            }
63
+
64
+            if ($canCreateRoles) {
65
+                //use the admin login data for the new database user
66
+
67
+                //add prefix to the postgresql user name to prevent collisions
68
+                $this->dbUser = 'oc_' . strtolower($username);
69
+                //create a new password so we don't need to store the admin config in the config file
70
+                $this->dbPassword = \OC::$server->getSecureRandom()->generate(30, \OCP\Security\ISecureRandom::CHAR_LOWER . \OCP\Security\ISecureRandom::CHAR_DIGITS);
71
+
72
+                $this->createDBUser($connection);
73
+            }
74
+
75
+            $this->config->setValues([
76
+                'dbuser' => $this->dbUser,
77
+                'dbpassword' => $this->dbPassword,
78
+            ]);
79
+
80
+            //create the database
81
+            $this->createDatabase($connection);
82
+            // the connection to dbname=postgres is not needed anymore
83
+            $connection->close();
84
+        } catch (\Exception $e) {
85
+            $this->logger->logException($e);
86
+            $this->logger->warning('Error trying to connect as "postgres", assuming database is setup and tables need to be created');
87
+            $this->config->setValues([
88
+                'dbuser' => $this->dbUser,
89
+                'dbpassword' => $this->dbPassword,
90
+            ]);
91
+        }
92
+
93
+        // connect to the database (dbname=$this->dbname) and check if it needs to be filled
94
+        $this->dbUser = $this->config->getValue('dbuser');
95
+        $this->dbPassword = $this->config->getValue('dbpassword');
96
+        $connection = $this->connect();
97
+        try {
98
+            $connection->connect();
99
+        } catch (\Exception $e) {
100
+            $this->logger->logException($e);
101
+            throw new \OC\DatabaseSetupException($this->trans->t('PostgreSQL username and/or password not valid'),
102
+                $this->trans->t('You need to enter details of an existing account.'));
103
+        }
104
+    }
105
+
106
+    private function createDatabase(IDBConnection $connection) {
107
+        if (!$this->databaseExists($connection)) {
108
+            //The database does not exists... let's create it
109
+            $query = $connection->prepare("CREATE DATABASE " . addslashes($this->dbName) . " OWNER " . addslashes($this->dbUser));
110
+            try {
111
+                $query->execute();
112
+            } catch (DatabaseException $e) {
113
+                $this->logger->error('Error while trying to create database');
114
+                $this->logger->logException($e);
115
+            }
116
+        } else {
117
+            $query = $connection->prepare("REVOKE ALL PRIVILEGES ON DATABASE " . addslashes($this->dbName) . " FROM PUBLIC");
118
+            try {
119
+                $query->execute();
120
+            } catch (DatabaseException $e) {
121
+                $this->logger->error('Error while trying to restrict database permissions');
122
+                $this->logger->logException($e);
123
+            }
124
+        }
125
+    }
126
+
127
+    private function userExists(IDBConnection $connection) {
128
+        $builder = $connection->getQueryBuilder();
129
+        $builder->automaticTablePrefix(false);
130
+        $query = $builder->select('*')
131
+            ->from('pg_roles')
132
+            ->where($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser)));
133
+        $result = $query->execute();
134
+        return $result->rowCount() > 0;
135
+    }
136
+
137
+    private function databaseExists(IDBConnection $connection) {
138
+        $builder = $connection->getQueryBuilder();
139
+        $builder->automaticTablePrefix(false);
140
+        $query = $builder->select('datname')
141
+            ->from('pg_database')
142
+            ->where($builder->expr()->eq('datname', $builder->createNamedParameter($this->dbName)));
143
+        $result = $query->execute();
144
+        return $result->rowCount() > 0;
145
+    }
146
+
147
+    private function createDBUser(IDBConnection $connection) {
148
+        $dbUser = $this->dbUser;
149
+        try {
150
+            $i = 1;
151
+            while ($this->userExists($connection)) {
152
+                $i++;
153
+                $this->dbUser = $dbUser . $i;
154
+            }
155
+
156
+            // create the user
157
+            $query = $connection->prepare("CREATE USER " . addslashes($this->dbUser) . " CREATEDB PASSWORD '" . addslashes($this->dbPassword) . "'");
158
+            $query->execute();
159
+            if ($this->databaseExists($connection)) {
160
+                $query = $connection->prepare('GRANT CONNECT ON DATABASE ' . addslashes($this->dbName) . ' TO '.addslashes($this->dbUser));
161
+                $query->execute();
162
+            }
163
+        } catch (DatabaseException $e) {
164
+            $this->logger->error('Error while trying to create database user');
165
+            $this->logger->logException($e);
166
+        }
167
+    }
168 168
 }
Please login to merge, or discard this patch.
lib/private/Comments/Manager.php 1 patch
Indentation   +1075 added lines, -1075 removed lines patch added patch discarded remove patch
@@ -43,1079 +43,1079 @@
 block discarded – undo
43 43
 
44 44
 class Manager implements ICommentsManager {
45 45
 
46
-	/** @var  IDBConnection */
47
-	protected $dbConn;
48
-
49
-	/** @var  ILogger */
50
-	protected $logger;
51
-
52
-	/** @var IConfig */
53
-	protected $config;
54
-
55
-	/** @var IComment[] */
56
-	protected $commentsCache = [];
57
-
58
-	/** @var  \Closure[] */
59
-	protected $eventHandlerClosures = [];
60
-
61
-	/** @var  ICommentsEventHandler[] */
62
-	protected $eventHandlers = [];
63
-
64
-	/** @var \Closure[] */
65
-	protected $displayNameResolvers = [];
66
-
67
-	/**
68
-	 * Manager constructor.
69
-	 *
70
-	 * @param IDBConnection $dbConn
71
-	 * @param ILogger $logger
72
-	 * @param IConfig $config
73
-	 */
74
-	public function __construct(
75
-		IDBConnection $dbConn,
76
-		ILogger $logger,
77
-		IConfig $config
78
-	) {
79
-		$this->dbConn = $dbConn;
80
-		$this->logger = $logger;
81
-		$this->config = $config;
82
-	}
83
-
84
-	/**
85
-	 * converts data base data into PHP native, proper types as defined by
86
-	 * IComment interface.
87
-	 *
88
-	 * @param array $data
89
-	 * @return array
90
-	 */
91
-	protected function normalizeDatabaseData(array $data) {
92
-		$data['id'] = (string)$data['id'];
93
-		$data['parent_id'] = (string)$data['parent_id'];
94
-		$data['topmost_parent_id'] = (string)$data['topmost_parent_id'];
95
-		$data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
96
-		if (!is_null($data['latest_child_timestamp'])) {
97
-			$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
98
-		}
99
-		$data['children_count'] = (int)$data['children_count'];
100
-		$data['reference_id'] = $data['reference_id'] ?? null;
101
-		return $data;
102
-	}
103
-
104
-
105
-	/**
106
-	 * @param array $data
107
-	 * @return IComment
108
-	 */
109
-	public function getCommentFromData(array $data): IComment {
110
-		return new Comment($this->normalizeDatabaseData($data));
111
-	}
112
-
113
-	/**
114
-	 * prepares a comment for an insert or update operation after making sure
115
-	 * all necessary fields have a value assigned.
116
-	 *
117
-	 * @param IComment $comment
118
-	 * @return IComment returns the same updated IComment instance as provided
119
-	 *                  by parameter for convenience
120
-	 * @throws \UnexpectedValueException
121
-	 */
122
-	protected function prepareCommentForDatabaseWrite(IComment $comment) {
123
-		if (!$comment->getActorType()
124
-			|| $comment->getActorId() === ''
125
-			|| !$comment->getObjectType()
126
-			|| $comment->getObjectId() === ''
127
-			|| !$comment->getVerb()
128
-		) {
129
-			throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
130
-		}
131
-
132
-		if ($comment->getId() === '') {
133
-			$comment->setChildrenCount(0);
134
-			$comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
135
-			$comment->setLatestChildDateTime(null);
136
-		}
137
-
138
-		if (is_null($comment->getCreationDateTime())) {
139
-			$comment->setCreationDateTime(new \DateTime());
140
-		}
141
-
142
-		if ($comment->getParentId() !== '0') {
143
-			$comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
144
-		} else {
145
-			$comment->setTopmostParentId('0');
146
-		}
147
-
148
-		$this->cache($comment);
149
-
150
-		return $comment;
151
-	}
152
-
153
-	/**
154
-	 * returns the topmost parent id of a given comment identified by ID
155
-	 *
156
-	 * @param string $id
157
-	 * @return string
158
-	 * @throws NotFoundException
159
-	 */
160
-	protected function determineTopmostParentId($id) {
161
-		$comment = $this->get($id);
162
-		if ($comment->getParentId() === '0') {
163
-			return $comment->getId();
164
-		}
165
-
166
-		return $this->determineTopmostParentId($comment->getParentId());
167
-	}
168
-
169
-	/**
170
-	 * updates child information of a comment
171
-	 *
172
-	 * @param string $id
173
-	 * @param \DateTime $cDateTime the date time of the most recent child
174
-	 * @throws NotFoundException
175
-	 */
176
-	protected function updateChildrenInformation($id, \DateTime $cDateTime) {
177
-		$qb = $this->dbConn->getQueryBuilder();
178
-		$query = $qb->select($qb->func()->count('id'))
179
-			->from('comments')
180
-			->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
181
-			->setParameter('id', $id);
182
-
183
-		$resultStatement = $query->execute();
184
-		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
185
-		$resultStatement->closeCursor();
186
-		$children = (int)$data[0];
187
-
188
-		$comment = $this->get($id);
189
-		$comment->setChildrenCount($children);
190
-		$comment->setLatestChildDateTime($cDateTime);
191
-		$this->save($comment);
192
-	}
193
-
194
-	/**
195
-	 * Tests whether actor or object type and id parameters are acceptable.
196
-	 * Throws exception if not.
197
-	 *
198
-	 * @param string $role
199
-	 * @param string $type
200
-	 * @param string $id
201
-	 * @throws \InvalidArgumentException
202
-	 */
203
-	protected function checkRoleParameters($role, $type, $id) {
204
-		if (
205
-			!is_string($type) || empty($type)
206
-			|| !is_string($id) || empty($id)
207
-		) {
208
-			throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
209
-		}
210
-	}
211
-
212
-	/**
213
-	 * run-time caches a comment
214
-	 *
215
-	 * @param IComment $comment
216
-	 */
217
-	protected function cache(IComment $comment) {
218
-		$id = $comment->getId();
219
-		if (empty($id)) {
220
-			return;
221
-		}
222
-		$this->commentsCache[(string)$id] = $comment;
223
-	}
224
-
225
-	/**
226
-	 * removes an entry from the comments run time cache
227
-	 *
228
-	 * @param mixed $id the comment's id
229
-	 */
230
-	protected function uncache($id) {
231
-		$id = (string)$id;
232
-		if (isset($this->commentsCache[$id])) {
233
-			unset($this->commentsCache[$id]);
234
-		}
235
-	}
236
-
237
-	/**
238
-	 * returns a comment instance
239
-	 *
240
-	 * @param string $id the ID of the comment
241
-	 * @return IComment
242
-	 * @throws NotFoundException
243
-	 * @throws \InvalidArgumentException
244
-	 * @since 9.0.0
245
-	 */
246
-	public function get($id) {
247
-		if ((int)$id === 0) {
248
-			throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
249
-		}
250
-
251
-		if (isset($this->commentsCache[$id])) {
252
-			return $this->commentsCache[$id];
253
-		}
254
-
255
-		$qb = $this->dbConn->getQueryBuilder();
256
-		$resultStatement = $qb->select('*')
257
-			->from('comments')
258
-			->where($qb->expr()->eq('id', $qb->createParameter('id')))
259
-			->setParameter('id', $id, IQueryBuilder::PARAM_INT)
260
-			->execute();
261
-
262
-		$data = $resultStatement->fetch();
263
-		$resultStatement->closeCursor();
264
-		if (!$data) {
265
-			throw new NotFoundException();
266
-		}
267
-
268
-
269
-		$comment = $this->getCommentFromData($data);
270
-		$this->cache($comment);
271
-		return $comment;
272
-	}
273
-
274
-	/**
275
-	 * returns the comment specified by the id and all it's child comments.
276
-	 * At this point of time, we do only support one level depth.
277
-	 *
278
-	 * @param string $id
279
-	 * @param int $limit max number of entries to return, 0 returns all
280
-	 * @param int $offset the start entry
281
-	 * @return array
282
-	 * @since 9.0.0
283
-	 *
284
-	 * The return array looks like this
285
-	 * [
286
-	 *   'comment' => IComment, // root comment
287
-	 *   'replies' =>
288
-	 *   [
289
-	 *     0 =>
290
-	 *     [
291
-	 *       'comment' => IComment,
292
-	 *       'replies' => []
293
-	 *     ]
294
-	 *     1 =>
295
-	 *     [
296
-	 *       'comment' => IComment,
297
-	 *       'replies'=> []
298
-	 *     ],
299
-	 *     …
300
-	 *   ]
301
-	 * ]
302
-	 */
303
-	public function getTree($id, $limit = 0, $offset = 0) {
304
-		$tree = [];
305
-		$tree['comment'] = $this->get($id);
306
-		$tree['replies'] = [];
307
-
308
-		$qb = $this->dbConn->getQueryBuilder();
309
-		$query = $qb->select('*')
310
-			->from('comments')
311
-			->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
312
-			->orderBy('creation_timestamp', 'DESC')
313
-			->setParameter('id', $id);
314
-
315
-		if ($limit > 0) {
316
-			$query->setMaxResults($limit);
317
-		}
318
-		if ($offset > 0) {
319
-			$query->setFirstResult($offset);
320
-		}
321
-
322
-		$resultStatement = $query->execute();
323
-		while ($data = $resultStatement->fetch()) {
324
-			$comment = $this->getCommentFromData($data);
325
-			$this->cache($comment);
326
-			$tree['replies'][] = [
327
-				'comment' => $comment,
328
-				'replies' => []
329
-			];
330
-		}
331
-		$resultStatement->closeCursor();
332
-
333
-		return $tree;
334
-	}
335
-
336
-	/**
337
-	 * returns comments for a specific object (e.g. a file).
338
-	 *
339
-	 * The sort order is always newest to oldest.
340
-	 *
341
-	 * @param string $objectType the object type, e.g. 'files'
342
-	 * @param string $objectId the id of the object
343
-	 * @param int $limit optional, number of maximum comments to be returned. if
344
-	 * not specified, all comments are returned.
345
-	 * @param int $offset optional, starting point
346
-	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
347
-	 * that may be returned
348
-	 * @return IComment[]
349
-	 * @since 9.0.0
350
-	 */
351
-	public function getForObject(
352
-		$objectType,
353
-		$objectId,
354
-		$limit = 0,
355
-		$offset = 0,
356
-		\DateTime $notOlderThan = null
357
-	) {
358
-		$comments = [];
359
-
360
-		$qb = $this->dbConn->getQueryBuilder();
361
-		$query = $qb->select('*')
362
-			->from('comments')
363
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
364
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
365
-			->orderBy('creation_timestamp', 'DESC')
366
-			->setParameter('type', $objectType)
367
-			->setParameter('id', $objectId);
368
-
369
-		if ($limit > 0) {
370
-			$query->setMaxResults($limit);
371
-		}
372
-		if ($offset > 0) {
373
-			$query->setFirstResult($offset);
374
-		}
375
-		if (!is_null($notOlderThan)) {
376
-			$query
377
-				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
378
-				->setParameter('notOlderThan', $notOlderThan, 'datetime');
379
-		}
380
-
381
-		$resultStatement = $query->execute();
382
-		while ($data = $resultStatement->fetch()) {
383
-			$comment = $this->getCommentFromData($data);
384
-			$this->cache($comment);
385
-			$comments[] = $comment;
386
-		}
387
-		$resultStatement->closeCursor();
388
-
389
-		return $comments;
390
-	}
391
-
392
-	/**
393
-	 * @param string $objectType the object type, e.g. 'files'
394
-	 * @param string $objectId the id of the object
395
-	 * @param int $lastKnownCommentId the last known comment (will be used as offset)
396
-	 * @param string $sortDirection direction of the comments (`asc` or `desc`)
397
-	 * @param int $limit optional, number of maximum comments to be returned. if
398
-	 * set to 0, all comments are returned.
399
-	 * @return IComment[]
400
-	 * @return array
401
-	 */
402
-	public function getForObjectSince(
403
-		string $objectType,
404
-		string $objectId,
405
-		int $lastKnownCommentId,
406
-		string $sortDirection = 'asc',
407
-		int $limit = 30
408
-	): array {
409
-		$comments = [];
410
-
411
-		$query = $this->dbConn->getQueryBuilder();
412
-		$query->select('*')
413
-			->from('comments')
414
-			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
415
-			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
416
-			->orderBy('creation_timestamp', $sortDirection === 'desc' ? 'DESC' : 'ASC')
417
-			->addOrderBy('id', $sortDirection === 'desc' ? 'DESC' : 'ASC');
418
-
419
-		if ($limit > 0) {
420
-			$query->setMaxResults($limit);
421
-		}
422
-
423
-		$lastKnownComment = $lastKnownCommentId > 0 ? $this->getLastKnownComment(
424
-			$objectType,
425
-			$objectId,
426
-			$lastKnownCommentId
427
-		) : null;
428
-		if ($lastKnownComment instanceof IComment) {
429
-			$lastKnownCommentDateTime = $lastKnownComment->getCreationDateTime();
430
-			if ($sortDirection === 'desc') {
431
-				$query->andWhere(
432
-					$query->expr()->orX(
433
-						$query->expr()->lt(
434
-							'creation_timestamp',
435
-							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
436
-							IQueryBuilder::PARAM_DATE
437
-						),
438
-						$query->expr()->andX(
439
-							$query->expr()->eq(
440
-								'creation_timestamp',
441
-								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
442
-								IQueryBuilder::PARAM_DATE
443
-							),
444
-							$query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId))
445
-						)
446
-					)
447
-				);
448
-			} else {
449
-				$query->andWhere(
450
-					$query->expr()->orX(
451
-						$query->expr()->gt(
452
-							'creation_timestamp',
453
-							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
454
-							IQueryBuilder::PARAM_DATE
455
-						),
456
-						$query->expr()->andX(
457
-							$query->expr()->eq(
458
-								'creation_timestamp',
459
-								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
460
-								IQueryBuilder::PARAM_DATE
461
-							),
462
-							$query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId))
463
-						)
464
-					)
465
-				);
466
-			}
467
-		}
468
-
469
-		$resultStatement = $query->execute();
470
-		while ($data = $resultStatement->fetch()) {
471
-			$comment = $this->getCommentFromData($data);
472
-			$this->cache($comment);
473
-			$comments[] = $comment;
474
-		}
475
-		$resultStatement->closeCursor();
476
-
477
-		return $comments;
478
-	}
479
-
480
-	/**
481
-	 * @param string $objectType the object type, e.g. 'files'
482
-	 * @param string $objectId the id of the object
483
-	 * @param int $id the comment to look for
484
-	 * @return Comment|null
485
-	 */
486
-	protected function getLastKnownComment(string $objectType,
487
-										   string $objectId,
488
-										   int $id) {
489
-		$query = $this->dbConn->getQueryBuilder();
490
-		$query->select('*')
491
-			->from('comments')
492
-			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
493
-			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
494
-			->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
495
-
496
-		$result = $query->execute();
497
-		$row = $result->fetch();
498
-		$result->closeCursor();
499
-
500
-		if ($row) {
501
-			$comment = $this->getCommentFromData($row);
502
-			$this->cache($comment);
503
-			return $comment;
504
-		}
505
-
506
-		return null;
507
-	}
508
-
509
-	/**
510
-	 * Search for comments with a given content
511
-	 *
512
-	 * @param string $search content to search for
513
-	 * @param string $objectType Limit the search by object type
514
-	 * @param string $objectId Limit the search by object id
515
-	 * @param string $verb Limit the verb of the comment
516
-	 * @param int $offset
517
-	 * @param int $limit
518
-	 * @return IComment[]
519
-	 */
520
-	public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array {
521
-		$query = $this->dbConn->getQueryBuilder();
522
-
523
-		$query->select('*')
524
-			->from('comments')
525
-			->where($query->expr()->iLike('message', $query->createNamedParameter(
526
-				'%' . $this->dbConn->escapeLikeParameter($search). '%'
527
-			)))
528
-			->orderBy('creation_timestamp', 'DESC')
529
-			->addOrderBy('id', 'DESC')
530
-			->setMaxResults($limit);
531
-
532
-		if ($objectType !== '') {
533
-			$query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
534
-		}
535
-		if ($objectId !== '') {
536
-			$query->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
537
-		}
538
-		if ($verb !== '') {
539
-			$query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
540
-		}
541
-		if ($offset !== 0) {
542
-			$query->setFirstResult($offset);
543
-		}
544
-
545
-		$comments = [];
546
-		$result = $query->execute();
547
-		while ($data = $result->fetch()) {
548
-			$comment = $this->getCommentFromData($data);
549
-			$this->cache($comment);
550
-			$comments[] = $comment;
551
-		}
552
-		$result->closeCursor();
553
-
554
-		return $comments;
555
-	}
556
-
557
-	/**
558
-	 * @param $objectType string the object type, e.g. 'files'
559
-	 * @param $objectId string the id of the object
560
-	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
561
-	 * that may be returned
562
-	 * @param string $verb Limit the verb of the comment - Added in 14.0.0
563
-	 * @return Int
564
-	 * @since 9.0.0
565
-	 */
566
-	public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null, $verb = '') {
567
-		$qb = $this->dbConn->getQueryBuilder();
568
-		$query = $qb->select($qb->func()->count('id'))
569
-			->from('comments')
570
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
571
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
572
-			->setParameter('type', $objectType)
573
-			->setParameter('id', $objectId);
574
-
575
-		if (!is_null($notOlderThan)) {
576
-			$query
577
-				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
578
-				->setParameter('notOlderThan', $notOlderThan, 'datetime');
579
-		}
580
-
581
-		if ($verb !== '') {
582
-			$query->andWhere($qb->expr()->eq('verb', $qb->createNamedParameter($verb)));
583
-		}
584
-
585
-		$resultStatement = $query->execute();
586
-		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
587
-		$resultStatement->closeCursor();
588
-		return (int)$data[0];
589
-	}
590
-
591
-	/**
592
-	 * Get the number of unread comments for all files in a folder
593
-	 *
594
-	 * @param int $folderId
595
-	 * @param IUser $user
596
-	 * @return array [$fileId => $unreadCount]
597
-	 */
598
-	public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
599
-		$qb = $this->dbConn->getQueryBuilder();
600
-
601
-		$query = $qb->select('f.fileid')
602
-			->addSelect($qb->func()->count('c.id', 'num_ids'))
603
-			->from('filecache', 'f')
604
-			->leftJoin('f', 'comments', 'c', $qb->expr()->andX(
605
-				$qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT)),
606
-				$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files'))
607
-			))
608
-			->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
609
-				$qb->expr()->eq('c.object_id', 'm.object_id'),
610
-				$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files'))
611
-			))
612
-			->where(
613
-				$qb->expr()->andX(
614
-					$qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)),
615
-					$qb->expr()->orX(
616
-						$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
617
-						$qb->expr()->isNull('c.object_type')
618
-					),
619
-					$qb->expr()->orX(
620
-						$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
621
-						$qb->expr()->isNull('m.object_type')
622
-					),
623
-					$qb->expr()->orX(
624
-						$qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID())),
625
-						$qb->expr()->isNull('m.user_id')
626
-					),
627
-					$qb->expr()->orX(
628
-						$qb->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
629
-						$qb->expr()->isNull('m.marker_datetime')
630
-					)
631
-				)
632
-			)->groupBy('f.fileid');
633
-
634
-		$resultStatement = $query->execute();
635
-
636
-		$results = [];
637
-		while ($row = $resultStatement->fetch()) {
638
-			$results[$row['fileid']] = (int) $row['num_ids'];
639
-		}
640
-		$resultStatement->closeCursor();
641
-		return $results;
642
-	}
643
-
644
-	/**
645
-	 * creates a new comment and returns it. At this point of time, it is not
646
-	 * saved in the used data storage. Use save() after setting other fields
647
-	 * of the comment (e.g. message or verb).
648
-	 *
649
-	 * @param string $actorType the actor type (e.g. 'users')
650
-	 * @param string $actorId a user id
651
-	 * @param string $objectType the object type the comment is attached to
652
-	 * @param string $objectId the object id the comment is attached to
653
-	 * @return IComment
654
-	 * @since 9.0.0
655
-	 */
656
-	public function create($actorType, $actorId, $objectType, $objectId) {
657
-		$comment = new Comment();
658
-		$comment
659
-			->setActor($actorType, $actorId)
660
-			->setObject($objectType, $objectId);
661
-		return $comment;
662
-	}
663
-
664
-	/**
665
-	 * permanently deletes the comment specified by the ID
666
-	 *
667
-	 * When the comment has child comments, their parent ID will be changed to
668
-	 * the parent ID of the item that is to be deleted.
669
-	 *
670
-	 * @param string $id
671
-	 * @return bool
672
-	 * @throws \InvalidArgumentException
673
-	 * @since 9.0.0
674
-	 */
675
-	public function delete($id) {
676
-		if (!is_string($id)) {
677
-			throw new \InvalidArgumentException('Parameter must be string');
678
-		}
679
-
680
-		try {
681
-			$comment = $this->get($id);
682
-		} catch (\Exception $e) {
683
-			// Ignore exceptions, we just don't fire a hook then
684
-			$comment = null;
685
-		}
686
-
687
-		$qb = $this->dbConn->getQueryBuilder();
688
-		$query = $qb->delete('comments')
689
-			->where($qb->expr()->eq('id', $qb->createParameter('id')))
690
-			->setParameter('id', $id);
691
-
692
-		try {
693
-			$affectedRows = $query->execute();
694
-			$this->uncache($id);
695
-		} catch (DriverException $e) {
696
-			$this->logger->logException($e, ['app' => 'core_comments']);
697
-			return false;
698
-		}
699
-
700
-		if ($affectedRows > 0 && $comment instanceof IComment) {
701
-			$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
702
-		}
703
-
704
-		return ($affectedRows > 0);
705
-	}
706
-
707
-	/**
708
-	 * saves the comment permanently
709
-	 *
710
-	 * if the supplied comment has an empty ID, a new entry comment will be
711
-	 * saved and the instance updated with the new ID.
712
-	 *
713
-	 * Otherwise, an existing comment will be updated.
714
-	 *
715
-	 * Throws NotFoundException when a comment that is to be updated does not
716
-	 * exist anymore at this point of time.
717
-	 *
718
-	 * @param IComment $comment
719
-	 * @return bool
720
-	 * @throws NotFoundException
721
-	 * @since 9.0.0
722
-	 */
723
-	public function save(IComment $comment) {
724
-		if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
725
-			$result = $this->insert($comment);
726
-		} else {
727
-			$result = $this->update($comment);
728
-		}
729
-
730
-		if ($result && !!$comment->getParentId()) {
731
-			$this->updateChildrenInformation(
732
-				$comment->getParentId(),
733
-				$comment->getCreationDateTime()
734
-			);
735
-			$this->cache($comment);
736
-		}
737
-
738
-		return $result;
739
-	}
740
-
741
-	/**
742
-	 * inserts the provided comment in the database
743
-	 *
744
-	 * @param IComment $comment
745
-	 * @return bool
746
-	 */
747
-	protected function insert(IComment $comment): bool {
748
-		try {
749
-			$result = $this->insertQuery($comment, true);
750
-		} catch (InvalidFieldNameException $e) {
751
-			// The reference id field was only added in Nextcloud 19.
752
-			// In order to not cause too long waiting times on the update,
753
-			// it was decided to only add it lazy, as it is also not a critical
754
-			// feature, but only helps to have a better experience while commenting.
755
-			// So in case the reference_id field is missing,
756
-			// we simply save the comment without that field.
757
-			$result = $this->insertQuery($comment, false);
758
-		}
759
-
760
-		return $result;
761
-	}
762
-
763
-	protected function insertQuery(IComment $comment, bool $tryWritingReferenceId): bool {
764
-		$qb = $this->dbConn->getQueryBuilder();
765
-
766
-		$values = [
767
-			'parent_id' => $qb->createNamedParameter($comment->getParentId()),
768
-			'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
769
-			'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
770
-			'actor_type' => $qb->createNamedParameter($comment->getActorType()),
771
-			'actor_id' => $qb->createNamedParameter($comment->getActorId()),
772
-			'message' => $qb->createNamedParameter($comment->getMessage()),
773
-			'verb' => $qb->createNamedParameter($comment->getVerb()),
774
-			'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
775
-			'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
776
-			'object_type' => $qb->createNamedParameter($comment->getObjectType()),
777
-			'object_id' => $qb->createNamedParameter($comment->getObjectId()),
778
-		];
779
-
780
-		if ($tryWritingReferenceId) {
781
-			$values['reference_id'] = $qb->createNamedParameter($comment->getReferenceId());
782
-		}
783
-
784
-		$affectedRows = $qb->insert('comments')
785
-			->values($values)
786
-			->execute();
787
-
788
-		if ($affectedRows > 0) {
789
-			$comment->setId((string)$qb->getLastInsertId());
790
-			$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
791
-		}
792
-
793
-		return $affectedRows > 0;
794
-	}
795
-
796
-	/**
797
-	 * updates a Comment data row
798
-	 *
799
-	 * @param IComment $comment
800
-	 * @return bool
801
-	 * @throws NotFoundException
802
-	 */
803
-	protected function update(IComment $comment) {
804
-		// for properly working preUpdate Events we need the old comments as is
805
-		// in the DB and overcome caching. Also avoid that outdated information stays.
806
-		$this->uncache($comment->getId());
807
-		$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
808
-		$this->uncache($comment->getId());
809
-
810
-		try {
811
-			$result = $this->updateQuery($comment, true);
812
-		} catch (InvalidFieldNameException $e) {
813
-			// See function insert() for explanation
814
-			$result = $this->updateQuery($comment, false);
815
-		}
816
-
817
-		$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
818
-
819
-		return $result;
820
-	}
821
-
822
-	protected function updateQuery(IComment $comment, bool $tryWritingReferenceId): bool {
823
-		$qb = $this->dbConn->getQueryBuilder();
824
-		$qb
825
-			->update('comments')
826
-			->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
827
-			->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
828
-			->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
829
-			->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
830
-			->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
831
-			->set('message', $qb->createNamedParameter($comment->getMessage()))
832
-			->set('verb', $qb->createNamedParameter($comment->getVerb()))
833
-			->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
834
-			->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
835
-			->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
836
-			->set('object_id', $qb->createNamedParameter($comment->getObjectId()));
837
-
838
-		if ($tryWritingReferenceId) {
839
-			$qb->set('reference_id', $qb->createNamedParameter($comment->getReferenceId()));
840
-		}
841
-
842
-		$affectedRows = $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($comment->getId())))
843
-			->execute();
844
-
845
-		if ($affectedRows === 0) {
846
-			throw new NotFoundException('Comment to update does ceased to exist');
847
-		}
848
-
849
-		return $affectedRows > 0;
850
-	}
851
-
852
-	/**
853
-	 * removes references to specific actor (e.g. on user delete) of a comment.
854
-	 * The comment itself must not get lost/deleted.
855
-	 *
856
-	 * @param string $actorType the actor type (e.g. 'users')
857
-	 * @param string $actorId a user id
858
-	 * @return boolean
859
-	 * @since 9.0.0
860
-	 */
861
-	public function deleteReferencesOfActor($actorType, $actorId) {
862
-		$this->checkRoleParameters('Actor', $actorType, $actorId);
863
-
864
-		$qb = $this->dbConn->getQueryBuilder();
865
-		$affectedRows = $qb
866
-			->update('comments')
867
-			->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
868
-			->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
869
-			->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
870
-			->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
871
-			->setParameter('type', $actorType)
872
-			->setParameter('id', $actorId)
873
-			->execute();
874
-
875
-		$this->commentsCache = [];
876
-
877
-		return is_int($affectedRows);
878
-	}
879
-
880
-	/**
881
-	 * deletes all comments made of a specific object (e.g. on file delete)
882
-	 *
883
-	 * @param string $objectType the object type (e.g. 'files')
884
-	 * @param string $objectId e.g. the file id
885
-	 * @return boolean
886
-	 * @since 9.0.0
887
-	 */
888
-	public function deleteCommentsAtObject($objectType, $objectId) {
889
-		$this->checkRoleParameters('Object', $objectType, $objectId);
890
-
891
-		$qb = $this->dbConn->getQueryBuilder();
892
-		$affectedRows = $qb
893
-			->delete('comments')
894
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
895
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
896
-			->setParameter('type', $objectType)
897
-			->setParameter('id', $objectId)
898
-			->execute();
899
-
900
-		$this->commentsCache = [];
901
-
902
-		return is_int($affectedRows);
903
-	}
904
-
905
-	/**
906
-	 * deletes the read markers for the specified user
907
-	 *
908
-	 * @param \OCP\IUser $user
909
-	 * @return bool
910
-	 * @since 9.0.0
911
-	 */
912
-	public function deleteReadMarksFromUser(IUser $user) {
913
-		$qb = $this->dbConn->getQueryBuilder();
914
-		$query = $qb->delete('comments_read_markers')
915
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
916
-			->setParameter('user_id', $user->getUID());
917
-
918
-		try {
919
-			$affectedRows = $query->execute();
920
-		} catch (DriverException $e) {
921
-			$this->logger->logException($e, ['app' => 'core_comments']);
922
-			return false;
923
-		}
924
-		return ($affectedRows > 0);
925
-	}
926
-
927
-	/**
928
-	 * sets the read marker for a given file to the specified date for the
929
-	 * provided user
930
-	 *
931
-	 * @param string $objectType
932
-	 * @param string $objectId
933
-	 * @param \DateTime $dateTime
934
-	 * @param IUser $user
935
-	 * @since 9.0.0
936
-	 */
937
-	public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
938
-		$this->checkRoleParameters('Object', $objectType, $objectId);
939
-
940
-		$qb = $this->dbConn->getQueryBuilder();
941
-		$values = [
942
-			'user_id' => $qb->createNamedParameter($user->getUID()),
943
-			'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
944
-			'object_type' => $qb->createNamedParameter($objectType),
945
-			'object_id' => $qb->createNamedParameter($objectId),
946
-		];
947
-
948
-		// Strategy: try to update, if this does not return affected rows, do an insert.
949
-		$affectedRows = $qb
950
-			->update('comments_read_markers')
951
-			->set('user_id', $values['user_id'])
952
-			->set('marker_datetime', $values['marker_datetime'])
953
-			->set('object_type', $values['object_type'])
954
-			->set('object_id', $values['object_id'])
955
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
956
-			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
957
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
958
-			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
959
-			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
960
-			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
961
-			->execute();
962
-
963
-		if ($affectedRows > 0) {
964
-			return;
965
-		}
966
-
967
-		$qb->insert('comments_read_markers')
968
-			->values($values)
969
-			->execute();
970
-	}
971
-
972
-	/**
973
-	 * returns the read marker for a given file to the specified date for the
974
-	 * provided user. It returns null, when the marker is not present, i.e.
975
-	 * no comments were marked as read.
976
-	 *
977
-	 * @param string $objectType
978
-	 * @param string $objectId
979
-	 * @param IUser $user
980
-	 * @return \DateTime|null
981
-	 * @since 9.0.0
982
-	 */
983
-	public function getReadMark($objectType, $objectId, IUser $user) {
984
-		$qb = $this->dbConn->getQueryBuilder();
985
-		$resultStatement = $qb->select('marker_datetime')
986
-			->from('comments_read_markers')
987
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
988
-			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
989
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
990
-			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
991
-			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
992
-			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
993
-			->execute();
994
-
995
-		$data = $resultStatement->fetch();
996
-		$resultStatement->closeCursor();
997
-		if (!$data || is_null($data['marker_datetime'])) {
998
-			return null;
999
-		}
1000
-
1001
-		return new \DateTime($data['marker_datetime']);
1002
-	}
1003
-
1004
-	/**
1005
-	 * deletes the read markers on the specified object
1006
-	 *
1007
-	 * @param string $objectType
1008
-	 * @param string $objectId
1009
-	 * @return bool
1010
-	 * @since 9.0.0
1011
-	 */
1012
-	public function deleteReadMarksOnObject($objectType, $objectId) {
1013
-		$this->checkRoleParameters('Object', $objectType, $objectId);
1014
-
1015
-		$qb = $this->dbConn->getQueryBuilder();
1016
-		$query = $qb->delete('comments_read_markers')
1017
-			->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
1018
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
1019
-			->setParameter('object_type', $objectType)
1020
-			->setParameter('object_id', $objectId);
1021
-
1022
-		try {
1023
-			$affectedRows = $query->execute();
1024
-		} catch (DriverException $e) {
1025
-			$this->logger->logException($e, ['app' => 'core_comments']);
1026
-			return false;
1027
-		}
1028
-		return ($affectedRows > 0);
1029
-	}
1030
-
1031
-	/**
1032
-	 * registers an Entity to the manager, so event notifications can be send
1033
-	 * to consumers of the comments infrastructure
1034
-	 *
1035
-	 * @param \Closure $closure
1036
-	 */
1037
-	public function registerEventHandler(\Closure $closure) {
1038
-		$this->eventHandlerClosures[] = $closure;
1039
-		$this->eventHandlers = [];
1040
-	}
1041
-
1042
-	/**
1043
-	 * registers a method that resolves an ID to a display name for a given type
1044
-	 *
1045
-	 * @param string $type
1046
-	 * @param \Closure $closure
1047
-	 * @throws \OutOfBoundsException
1048
-	 * @since 11.0.0
1049
-	 *
1050
-	 * Only one resolver shall be registered per type. Otherwise a
1051
-	 * \OutOfBoundsException has to thrown.
1052
-	 */
1053
-	public function registerDisplayNameResolver($type, \Closure $closure) {
1054
-		if (!is_string($type)) {
1055
-			throw new \InvalidArgumentException('String expected.');
1056
-		}
1057
-		if (isset($this->displayNameResolvers[$type])) {
1058
-			throw new \OutOfBoundsException('Displayname resolver for this type already registered');
1059
-		}
1060
-		$this->displayNameResolvers[$type] = $closure;
1061
-	}
1062
-
1063
-	/**
1064
-	 * resolves a given ID of a given Type to a display name.
1065
-	 *
1066
-	 * @param string $type
1067
-	 * @param string $id
1068
-	 * @return string
1069
-	 * @throws \OutOfBoundsException
1070
-	 * @since 11.0.0
1071
-	 *
1072
-	 * If a provided type was not registered, an \OutOfBoundsException shall
1073
-	 * be thrown. It is upon the resolver discretion what to return of the
1074
-	 * provided ID is unknown. It must be ensured that a string is returned.
1075
-	 */
1076
-	public function resolveDisplayName($type, $id) {
1077
-		if (!is_string($type)) {
1078
-			throw new \InvalidArgumentException('String expected.');
1079
-		}
1080
-		if (!isset($this->displayNameResolvers[$type])) {
1081
-			throw new \OutOfBoundsException('No Displayname resolver for this type registered');
1082
-		}
1083
-		return (string)$this->displayNameResolvers[$type]($id);
1084
-	}
1085
-
1086
-	/**
1087
-	 * returns valid, registered entities
1088
-	 *
1089
-	 * @return \OCP\Comments\ICommentsEventHandler[]
1090
-	 */
1091
-	private function getEventHandlers() {
1092
-		if (!empty($this->eventHandlers)) {
1093
-			return $this->eventHandlers;
1094
-		}
1095
-
1096
-		$this->eventHandlers = [];
1097
-		foreach ($this->eventHandlerClosures as $name => $closure) {
1098
-			$entity = $closure();
1099
-			if (!($entity instanceof ICommentsEventHandler)) {
1100
-				throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
1101
-			}
1102
-			$this->eventHandlers[$name] = $entity;
1103
-		}
1104
-
1105
-		return $this->eventHandlers;
1106
-	}
1107
-
1108
-	/**
1109
-	 * sends notifications to the registered entities
1110
-	 *
1111
-	 * @param $eventType
1112
-	 * @param IComment $comment
1113
-	 */
1114
-	private function sendEvent($eventType, IComment $comment) {
1115
-		$entities = $this->getEventHandlers();
1116
-		$event = new CommentsEvent($eventType, $comment);
1117
-		foreach ($entities as $entity) {
1118
-			$entity->handle($event);
1119
-		}
1120
-	}
46
+    /** @var  IDBConnection */
47
+    protected $dbConn;
48
+
49
+    /** @var  ILogger */
50
+    protected $logger;
51
+
52
+    /** @var IConfig */
53
+    protected $config;
54
+
55
+    /** @var IComment[] */
56
+    protected $commentsCache = [];
57
+
58
+    /** @var  \Closure[] */
59
+    protected $eventHandlerClosures = [];
60
+
61
+    /** @var  ICommentsEventHandler[] */
62
+    protected $eventHandlers = [];
63
+
64
+    /** @var \Closure[] */
65
+    protected $displayNameResolvers = [];
66
+
67
+    /**
68
+     * Manager constructor.
69
+     *
70
+     * @param IDBConnection $dbConn
71
+     * @param ILogger $logger
72
+     * @param IConfig $config
73
+     */
74
+    public function __construct(
75
+        IDBConnection $dbConn,
76
+        ILogger $logger,
77
+        IConfig $config
78
+    ) {
79
+        $this->dbConn = $dbConn;
80
+        $this->logger = $logger;
81
+        $this->config = $config;
82
+    }
83
+
84
+    /**
85
+     * converts data base data into PHP native, proper types as defined by
86
+     * IComment interface.
87
+     *
88
+     * @param array $data
89
+     * @return array
90
+     */
91
+    protected function normalizeDatabaseData(array $data) {
92
+        $data['id'] = (string)$data['id'];
93
+        $data['parent_id'] = (string)$data['parent_id'];
94
+        $data['topmost_parent_id'] = (string)$data['topmost_parent_id'];
95
+        $data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
96
+        if (!is_null($data['latest_child_timestamp'])) {
97
+            $data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
98
+        }
99
+        $data['children_count'] = (int)$data['children_count'];
100
+        $data['reference_id'] = $data['reference_id'] ?? null;
101
+        return $data;
102
+    }
103
+
104
+
105
+    /**
106
+     * @param array $data
107
+     * @return IComment
108
+     */
109
+    public function getCommentFromData(array $data): IComment {
110
+        return new Comment($this->normalizeDatabaseData($data));
111
+    }
112
+
113
+    /**
114
+     * prepares a comment for an insert or update operation after making sure
115
+     * all necessary fields have a value assigned.
116
+     *
117
+     * @param IComment $comment
118
+     * @return IComment returns the same updated IComment instance as provided
119
+     *                  by parameter for convenience
120
+     * @throws \UnexpectedValueException
121
+     */
122
+    protected function prepareCommentForDatabaseWrite(IComment $comment) {
123
+        if (!$comment->getActorType()
124
+            || $comment->getActorId() === ''
125
+            || !$comment->getObjectType()
126
+            || $comment->getObjectId() === ''
127
+            || !$comment->getVerb()
128
+        ) {
129
+            throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
130
+        }
131
+
132
+        if ($comment->getId() === '') {
133
+            $comment->setChildrenCount(0);
134
+            $comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
135
+            $comment->setLatestChildDateTime(null);
136
+        }
137
+
138
+        if (is_null($comment->getCreationDateTime())) {
139
+            $comment->setCreationDateTime(new \DateTime());
140
+        }
141
+
142
+        if ($comment->getParentId() !== '0') {
143
+            $comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
144
+        } else {
145
+            $comment->setTopmostParentId('0');
146
+        }
147
+
148
+        $this->cache($comment);
149
+
150
+        return $comment;
151
+    }
152
+
153
+    /**
154
+     * returns the topmost parent id of a given comment identified by ID
155
+     *
156
+     * @param string $id
157
+     * @return string
158
+     * @throws NotFoundException
159
+     */
160
+    protected function determineTopmostParentId($id) {
161
+        $comment = $this->get($id);
162
+        if ($comment->getParentId() === '0') {
163
+            return $comment->getId();
164
+        }
165
+
166
+        return $this->determineTopmostParentId($comment->getParentId());
167
+    }
168
+
169
+    /**
170
+     * updates child information of a comment
171
+     *
172
+     * @param string $id
173
+     * @param \DateTime $cDateTime the date time of the most recent child
174
+     * @throws NotFoundException
175
+     */
176
+    protected function updateChildrenInformation($id, \DateTime $cDateTime) {
177
+        $qb = $this->dbConn->getQueryBuilder();
178
+        $query = $qb->select($qb->func()->count('id'))
179
+            ->from('comments')
180
+            ->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
181
+            ->setParameter('id', $id);
182
+
183
+        $resultStatement = $query->execute();
184
+        $data = $resultStatement->fetch(\PDO::FETCH_NUM);
185
+        $resultStatement->closeCursor();
186
+        $children = (int)$data[0];
187
+
188
+        $comment = $this->get($id);
189
+        $comment->setChildrenCount($children);
190
+        $comment->setLatestChildDateTime($cDateTime);
191
+        $this->save($comment);
192
+    }
193
+
194
+    /**
195
+     * Tests whether actor or object type and id parameters are acceptable.
196
+     * Throws exception if not.
197
+     *
198
+     * @param string $role
199
+     * @param string $type
200
+     * @param string $id
201
+     * @throws \InvalidArgumentException
202
+     */
203
+    protected function checkRoleParameters($role, $type, $id) {
204
+        if (
205
+            !is_string($type) || empty($type)
206
+            || !is_string($id) || empty($id)
207
+        ) {
208
+            throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
209
+        }
210
+    }
211
+
212
+    /**
213
+     * run-time caches a comment
214
+     *
215
+     * @param IComment $comment
216
+     */
217
+    protected function cache(IComment $comment) {
218
+        $id = $comment->getId();
219
+        if (empty($id)) {
220
+            return;
221
+        }
222
+        $this->commentsCache[(string)$id] = $comment;
223
+    }
224
+
225
+    /**
226
+     * removes an entry from the comments run time cache
227
+     *
228
+     * @param mixed $id the comment's id
229
+     */
230
+    protected function uncache($id) {
231
+        $id = (string)$id;
232
+        if (isset($this->commentsCache[$id])) {
233
+            unset($this->commentsCache[$id]);
234
+        }
235
+    }
236
+
237
+    /**
238
+     * returns a comment instance
239
+     *
240
+     * @param string $id the ID of the comment
241
+     * @return IComment
242
+     * @throws NotFoundException
243
+     * @throws \InvalidArgumentException
244
+     * @since 9.0.0
245
+     */
246
+    public function get($id) {
247
+        if ((int)$id === 0) {
248
+            throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
249
+        }
250
+
251
+        if (isset($this->commentsCache[$id])) {
252
+            return $this->commentsCache[$id];
253
+        }
254
+
255
+        $qb = $this->dbConn->getQueryBuilder();
256
+        $resultStatement = $qb->select('*')
257
+            ->from('comments')
258
+            ->where($qb->expr()->eq('id', $qb->createParameter('id')))
259
+            ->setParameter('id', $id, IQueryBuilder::PARAM_INT)
260
+            ->execute();
261
+
262
+        $data = $resultStatement->fetch();
263
+        $resultStatement->closeCursor();
264
+        if (!$data) {
265
+            throw new NotFoundException();
266
+        }
267
+
268
+
269
+        $comment = $this->getCommentFromData($data);
270
+        $this->cache($comment);
271
+        return $comment;
272
+    }
273
+
274
+    /**
275
+     * returns the comment specified by the id and all it's child comments.
276
+     * At this point of time, we do only support one level depth.
277
+     *
278
+     * @param string $id
279
+     * @param int $limit max number of entries to return, 0 returns all
280
+     * @param int $offset the start entry
281
+     * @return array
282
+     * @since 9.0.0
283
+     *
284
+     * The return array looks like this
285
+     * [
286
+     *   'comment' => IComment, // root comment
287
+     *   'replies' =>
288
+     *   [
289
+     *     0 =>
290
+     *     [
291
+     *       'comment' => IComment,
292
+     *       'replies' => []
293
+     *     ]
294
+     *     1 =>
295
+     *     [
296
+     *       'comment' => IComment,
297
+     *       'replies'=> []
298
+     *     ],
299
+     *     …
300
+     *   ]
301
+     * ]
302
+     */
303
+    public function getTree($id, $limit = 0, $offset = 0) {
304
+        $tree = [];
305
+        $tree['comment'] = $this->get($id);
306
+        $tree['replies'] = [];
307
+
308
+        $qb = $this->dbConn->getQueryBuilder();
309
+        $query = $qb->select('*')
310
+            ->from('comments')
311
+            ->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
312
+            ->orderBy('creation_timestamp', 'DESC')
313
+            ->setParameter('id', $id);
314
+
315
+        if ($limit > 0) {
316
+            $query->setMaxResults($limit);
317
+        }
318
+        if ($offset > 0) {
319
+            $query->setFirstResult($offset);
320
+        }
321
+
322
+        $resultStatement = $query->execute();
323
+        while ($data = $resultStatement->fetch()) {
324
+            $comment = $this->getCommentFromData($data);
325
+            $this->cache($comment);
326
+            $tree['replies'][] = [
327
+                'comment' => $comment,
328
+                'replies' => []
329
+            ];
330
+        }
331
+        $resultStatement->closeCursor();
332
+
333
+        return $tree;
334
+    }
335
+
336
+    /**
337
+     * returns comments for a specific object (e.g. a file).
338
+     *
339
+     * The sort order is always newest to oldest.
340
+     *
341
+     * @param string $objectType the object type, e.g. 'files'
342
+     * @param string $objectId the id of the object
343
+     * @param int $limit optional, number of maximum comments to be returned. if
344
+     * not specified, all comments are returned.
345
+     * @param int $offset optional, starting point
346
+     * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
347
+     * that may be returned
348
+     * @return IComment[]
349
+     * @since 9.0.0
350
+     */
351
+    public function getForObject(
352
+        $objectType,
353
+        $objectId,
354
+        $limit = 0,
355
+        $offset = 0,
356
+        \DateTime $notOlderThan = null
357
+    ) {
358
+        $comments = [];
359
+
360
+        $qb = $this->dbConn->getQueryBuilder();
361
+        $query = $qb->select('*')
362
+            ->from('comments')
363
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
364
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
365
+            ->orderBy('creation_timestamp', 'DESC')
366
+            ->setParameter('type', $objectType)
367
+            ->setParameter('id', $objectId);
368
+
369
+        if ($limit > 0) {
370
+            $query->setMaxResults($limit);
371
+        }
372
+        if ($offset > 0) {
373
+            $query->setFirstResult($offset);
374
+        }
375
+        if (!is_null($notOlderThan)) {
376
+            $query
377
+                ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
378
+                ->setParameter('notOlderThan', $notOlderThan, 'datetime');
379
+        }
380
+
381
+        $resultStatement = $query->execute();
382
+        while ($data = $resultStatement->fetch()) {
383
+            $comment = $this->getCommentFromData($data);
384
+            $this->cache($comment);
385
+            $comments[] = $comment;
386
+        }
387
+        $resultStatement->closeCursor();
388
+
389
+        return $comments;
390
+    }
391
+
392
+    /**
393
+     * @param string $objectType the object type, e.g. 'files'
394
+     * @param string $objectId the id of the object
395
+     * @param int $lastKnownCommentId the last known comment (will be used as offset)
396
+     * @param string $sortDirection direction of the comments (`asc` or `desc`)
397
+     * @param int $limit optional, number of maximum comments to be returned. if
398
+     * set to 0, all comments are returned.
399
+     * @return IComment[]
400
+     * @return array
401
+     */
402
+    public function getForObjectSince(
403
+        string $objectType,
404
+        string $objectId,
405
+        int $lastKnownCommentId,
406
+        string $sortDirection = 'asc',
407
+        int $limit = 30
408
+    ): array {
409
+        $comments = [];
410
+
411
+        $query = $this->dbConn->getQueryBuilder();
412
+        $query->select('*')
413
+            ->from('comments')
414
+            ->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
415
+            ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
416
+            ->orderBy('creation_timestamp', $sortDirection === 'desc' ? 'DESC' : 'ASC')
417
+            ->addOrderBy('id', $sortDirection === 'desc' ? 'DESC' : 'ASC');
418
+
419
+        if ($limit > 0) {
420
+            $query->setMaxResults($limit);
421
+        }
422
+
423
+        $lastKnownComment = $lastKnownCommentId > 0 ? $this->getLastKnownComment(
424
+            $objectType,
425
+            $objectId,
426
+            $lastKnownCommentId
427
+        ) : null;
428
+        if ($lastKnownComment instanceof IComment) {
429
+            $lastKnownCommentDateTime = $lastKnownComment->getCreationDateTime();
430
+            if ($sortDirection === 'desc') {
431
+                $query->andWhere(
432
+                    $query->expr()->orX(
433
+                        $query->expr()->lt(
434
+                            'creation_timestamp',
435
+                            $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
436
+                            IQueryBuilder::PARAM_DATE
437
+                        ),
438
+                        $query->expr()->andX(
439
+                            $query->expr()->eq(
440
+                                'creation_timestamp',
441
+                                $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
442
+                                IQueryBuilder::PARAM_DATE
443
+                            ),
444
+                            $query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId))
445
+                        )
446
+                    )
447
+                );
448
+            } else {
449
+                $query->andWhere(
450
+                    $query->expr()->orX(
451
+                        $query->expr()->gt(
452
+                            'creation_timestamp',
453
+                            $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
454
+                            IQueryBuilder::PARAM_DATE
455
+                        ),
456
+                        $query->expr()->andX(
457
+                            $query->expr()->eq(
458
+                                'creation_timestamp',
459
+                                $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
460
+                                IQueryBuilder::PARAM_DATE
461
+                            ),
462
+                            $query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId))
463
+                        )
464
+                    )
465
+                );
466
+            }
467
+        }
468
+
469
+        $resultStatement = $query->execute();
470
+        while ($data = $resultStatement->fetch()) {
471
+            $comment = $this->getCommentFromData($data);
472
+            $this->cache($comment);
473
+            $comments[] = $comment;
474
+        }
475
+        $resultStatement->closeCursor();
476
+
477
+        return $comments;
478
+    }
479
+
480
+    /**
481
+     * @param string $objectType the object type, e.g. 'files'
482
+     * @param string $objectId the id of the object
483
+     * @param int $id the comment to look for
484
+     * @return Comment|null
485
+     */
486
+    protected function getLastKnownComment(string $objectType,
487
+                                            string $objectId,
488
+                                            int $id) {
489
+        $query = $this->dbConn->getQueryBuilder();
490
+        $query->select('*')
491
+            ->from('comments')
492
+            ->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
493
+            ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
494
+            ->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
495
+
496
+        $result = $query->execute();
497
+        $row = $result->fetch();
498
+        $result->closeCursor();
499
+
500
+        if ($row) {
501
+            $comment = $this->getCommentFromData($row);
502
+            $this->cache($comment);
503
+            return $comment;
504
+        }
505
+
506
+        return null;
507
+    }
508
+
509
+    /**
510
+     * Search for comments with a given content
511
+     *
512
+     * @param string $search content to search for
513
+     * @param string $objectType Limit the search by object type
514
+     * @param string $objectId Limit the search by object id
515
+     * @param string $verb Limit the verb of the comment
516
+     * @param int $offset
517
+     * @param int $limit
518
+     * @return IComment[]
519
+     */
520
+    public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array {
521
+        $query = $this->dbConn->getQueryBuilder();
522
+
523
+        $query->select('*')
524
+            ->from('comments')
525
+            ->where($query->expr()->iLike('message', $query->createNamedParameter(
526
+                '%' . $this->dbConn->escapeLikeParameter($search). '%'
527
+            )))
528
+            ->orderBy('creation_timestamp', 'DESC')
529
+            ->addOrderBy('id', 'DESC')
530
+            ->setMaxResults($limit);
531
+
532
+        if ($objectType !== '') {
533
+            $query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
534
+        }
535
+        if ($objectId !== '') {
536
+            $query->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
537
+        }
538
+        if ($verb !== '') {
539
+            $query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
540
+        }
541
+        if ($offset !== 0) {
542
+            $query->setFirstResult($offset);
543
+        }
544
+
545
+        $comments = [];
546
+        $result = $query->execute();
547
+        while ($data = $result->fetch()) {
548
+            $comment = $this->getCommentFromData($data);
549
+            $this->cache($comment);
550
+            $comments[] = $comment;
551
+        }
552
+        $result->closeCursor();
553
+
554
+        return $comments;
555
+    }
556
+
557
+    /**
558
+     * @param $objectType string the object type, e.g. 'files'
559
+     * @param $objectId string the id of the object
560
+     * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
561
+     * that may be returned
562
+     * @param string $verb Limit the verb of the comment - Added in 14.0.0
563
+     * @return Int
564
+     * @since 9.0.0
565
+     */
566
+    public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null, $verb = '') {
567
+        $qb = $this->dbConn->getQueryBuilder();
568
+        $query = $qb->select($qb->func()->count('id'))
569
+            ->from('comments')
570
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
571
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
572
+            ->setParameter('type', $objectType)
573
+            ->setParameter('id', $objectId);
574
+
575
+        if (!is_null($notOlderThan)) {
576
+            $query
577
+                ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
578
+                ->setParameter('notOlderThan', $notOlderThan, 'datetime');
579
+        }
580
+
581
+        if ($verb !== '') {
582
+            $query->andWhere($qb->expr()->eq('verb', $qb->createNamedParameter($verb)));
583
+        }
584
+
585
+        $resultStatement = $query->execute();
586
+        $data = $resultStatement->fetch(\PDO::FETCH_NUM);
587
+        $resultStatement->closeCursor();
588
+        return (int)$data[0];
589
+    }
590
+
591
+    /**
592
+     * Get the number of unread comments for all files in a folder
593
+     *
594
+     * @param int $folderId
595
+     * @param IUser $user
596
+     * @return array [$fileId => $unreadCount]
597
+     */
598
+    public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
599
+        $qb = $this->dbConn->getQueryBuilder();
600
+
601
+        $query = $qb->select('f.fileid')
602
+            ->addSelect($qb->func()->count('c.id', 'num_ids'))
603
+            ->from('filecache', 'f')
604
+            ->leftJoin('f', 'comments', 'c', $qb->expr()->andX(
605
+                $qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT)),
606
+                $qb->expr()->eq('c.object_type', $qb->createNamedParameter('files'))
607
+            ))
608
+            ->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
609
+                $qb->expr()->eq('c.object_id', 'm.object_id'),
610
+                $qb->expr()->eq('m.object_type', $qb->createNamedParameter('files'))
611
+            ))
612
+            ->where(
613
+                $qb->expr()->andX(
614
+                    $qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)),
615
+                    $qb->expr()->orX(
616
+                        $qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
617
+                        $qb->expr()->isNull('c.object_type')
618
+                    ),
619
+                    $qb->expr()->orX(
620
+                        $qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
621
+                        $qb->expr()->isNull('m.object_type')
622
+                    ),
623
+                    $qb->expr()->orX(
624
+                        $qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID())),
625
+                        $qb->expr()->isNull('m.user_id')
626
+                    ),
627
+                    $qb->expr()->orX(
628
+                        $qb->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
629
+                        $qb->expr()->isNull('m.marker_datetime')
630
+                    )
631
+                )
632
+            )->groupBy('f.fileid');
633
+
634
+        $resultStatement = $query->execute();
635
+
636
+        $results = [];
637
+        while ($row = $resultStatement->fetch()) {
638
+            $results[$row['fileid']] = (int) $row['num_ids'];
639
+        }
640
+        $resultStatement->closeCursor();
641
+        return $results;
642
+    }
643
+
644
+    /**
645
+     * creates a new comment and returns it. At this point of time, it is not
646
+     * saved in the used data storage. Use save() after setting other fields
647
+     * of the comment (e.g. message or verb).
648
+     *
649
+     * @param string $actorType the actor type (e.g. 'users')
650
+     * @param string $actorId a user id
651
+     * @param string $objectType the object type the comment is attached to
652
+     * @param string $objectId the object id the comment is attached to
653
+     * @return IComment
654
+     * @since 9.0.0
655
+     */
656
+    public function create($actorType, $actorId, $objectType, $objectId) {
657
+        $comment = new Comment();
658
+        $comment
659
+            ->setActor($actorType, $actorId)
660
+            ->setObject($objectType, $objectId);
661
+        return $comment;
662
+    }
663
+
664
+    /**
665
+     * permanently deletes the comment specified by the ID
666
+     *
667
+     * When the comment has child comments, their parent ID will be changed to
668
+     * the parent ID of the item that is to be deleted.
669
+     *
670
+     * @param string $id
671
+     * @return bool
672
+     * @throws \InvalidArgumentException
673
+     * @since 9.0.0
674
+     */
675
+    public function delete($id) {
676
+        if (!is_string($id)) {
677
+            throw new \InvalidArgumentException('Parameter must be string');
678
+        }
679
+
680
+        try {
681
+            $comment = $this->get($id);
682
+        } catch (\Exception $e) {
683
+            // Ignore exceptions, we just don't fire a hook then
684
+            $comment = null;
685
+        }
686
+
687
+        $qb = $this->dbConn->getQueryBuilder();
688
+        $query = $qb->delete('comments')
689
+            ->where($qb->expr()->eq('id', $qb->createParameter('id')))
690
+            ->setParameter('id', $id);
691
+
692
+        try {
693
+            $affectedRows = $query->execute();
694
+            $this->uncache($id);
695
+        } catch (DriverException $e) {
696
+            $this->logger->logException($e, ['app' => 'core_comments']);
697
+            return false;
698
+        }
699
+
700
+        if ($affectedRows > 0 && $comment instanceof IComment) {
701
+            $this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
702
+        }
703
+
704
+        return ($affectedRows > 0);
705
+    }
706
+
707
+    /**
708
+     * saves the comment permanently
709
+     *
710
+     * if the supplied comment has an empty ID, a new entry comment will be
711
+     * saved and the instance updated with the new ID.
712
+     *
713
+     * Otherwise, an existing comment will be updated.
714
+     *
715
+     * Throws NotFoundException when a comment that is to be updated does not
716
+     * exist anymore at this point of time.
717
+     *
718
+     * @param IComment $comment
719
+     * @return bool
720
+     * @throws NotFoundException
721
+     * @since 9.0.0
722
+     */
723
+    public function save(IComment $comment) {
724
+        if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
725
+            $result = $this->insert($comment);
726
+        } else {
727
+            $result = $this->update($comment);
728
+        }
729
+
730
+        if ($result && !!$comment->getParentId()) {
731
+            $this->updateChildrenInformation(
732
+                $comment->getParentId(),
733
+                $comment->getCreationDateTime()
734
+            );
735
+            $this->cache($comment);
736
+        }
737
+
738
+        return $result;
739
+    }
740
+
741
+    /**
742
+     * inserts the provided comment in the database
743
+     *
744
+     * @param IComment $comment
745
+     * @return bool
746
+     */
747
+    protected function insert(IComment $comment): bool {
748
+        try {
749
+            $result = $this->insertQuery($comment, true);
750
+        } catch (InvalidFieldNameException $e) {
751
+            // The reference id field was only added in Nextcloud 19.
752
+            // In order to not cause too long waiting times on the update,
753
+            // it was decided to only add it lazy, as it is also not a critical
754
+            // feature, but only helps to have a better experience while commenting.
755
+            // So in case the reference_id field is missing,
756
+            // we simply save the comment without that field.
757
+            $result = $this->insertQuery($comment, false);
758
+        }
759
+
760
+        return $result;
761
+    }
762
+
763
+    protected function insertQuery(IComment $comment, bool $tryWritingReferenceId): bool {
764
+        $qb = $this->dbConn->getQueryBuilder();
765
+
766
+        $values = [
767
+            'parent_id' => $qb->createNamedParameter($comment->getParentId()),
768
+            'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
769
+            'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
770
+            'actor_type' => $qb->createNamedParameter($comment->getActorType()),
771
+            'actor_id' => $qb->createNamedParameter($comment->getActorId()),
772
+            'message' => $qb->createNamedParameter($comment->getMessage()),
773
+            'verb' => $qb->createNamedParameter($comment->getVerb()),
774
+            'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
775
+            'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
776
+            'object_type' => $qb->createNamedParameter($comment->getObjectType()),
777
+            'object_id' => $qb->createNamedParameter($comment->getObjectId()),
778
+        ];
779
+
780
+        if ($tryWritingReferenceId) {
781
+            $values['reference_id'] = $qb->createNamedParameter($comment->getReferenceId());
782
+        }
783
+
784
+        $affectedRows = $qb->insert('comments')
785
+            ->values($values)
786
+            ->execute();
787
+
788
+        if ($affectedRows > 0) {
789
+            $comment->setId((string)$qb->getLastInsertId());
790
+            $this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
791
+        }
792
+
793
+        return $affectedRows > 0;
794
+    }
795
+
796
+    /**
797
+     * updates a Comment data row
798
+     *
799
+     * @param IComment $comment
800
+     * @return bool
801
+     * @throws NotFoundException
802
+     */
803
+    protected function update(IComment $comment) {
804
+        // for properly working preUpdate Events we need the old comments as is
805
+        // in the DB and overcome caching. Also avoid that outdated information stays.
806
+        $this->uncache($comment->getId());
807
+        $this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
808
+        $this->uncache($comment->getId());
809
+
810
+        try {
811
+            $result = $this->updateQuery($comment, true);
812
+        } catch (InvalidFieldNameException $e) {
813
+            // See function insert() for explanation
814
+            $result = $this->updateQuery($comment, false);
815
+        }
816
+
817
+        $this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
818
+
819
+        return $result;
820
+    }
821
+
822
+    protected function updateQuery(IComment $comment, bool $tryWritingReferenceId): bool {
823
+        $qb = $this->dbConn->getQueryBuilder();
824
+        $qb
825
+            ->update('comments')
826
+            ->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
827
+            ->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
828
+            ->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
829
+            ->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
830
+            ->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
831
+            ->set('message', $qb->createNamedParameter($comment->getMessage()))
832
+            ->set('verb', $qb->createNamedParameter($comment->getVerb()))
833
+            ->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
834
+            ->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
835
+            ->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
836
+            ->set('object_id', $qb->createNamedParameter($comment->getObjectId()));
837
+
838
+        if ($tryWritingReferenceId) {
839
+            $qb->set('reference_id', $qb->createNamedParameter($comment->getReferenceId()));
840
+        }
841
+
842
+        $affectedRows = $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($comment->getId())))
843
+            ->execute();
844
+
845
+        if ($affectedRows === 0) {
846
+            throw new NotFoundException('Comment to update does ceased to exist');
847
+        }
848
+
849
+        return $affectedRows > 0;
850
+    }
851
+
852
+    /**
853
+     * removes references to specific actor (e.g. on user delete) of a comment.
854
+     * The comment itself must not get lost/deleted.
855
+     *
856
+     * @param string $actorType the actor type (e.g. 'users')
857
+     * @param string $actorId a user id
858
+     * @return boolean
859
+     * @since 9.0.0
860
+     */
861
+    public function deleteReferencesOfActor($actorType, $actorId) {
862
+        $this->checkRoleParameters('Actor', $actorType, $actorId);
863
+
864
+        $qb = $this->dbConn->getQueryBuilder();
865
+        $affectedRows = $qb
866
+            ->update('comments')
867
+            ->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
868
+            ->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
869
+            ->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
870
+            ->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
871
+            ->setParameter('type', $actorType)
872
+            ->setParameter('id', $actorId)
873
+            ->execute();
874
+
875
+        $this->commentsCache = [];
876
+
877
+        return is_int($affectedRows);
878
+    }
879
+
880
+    /**
881
+     * deletes all comments made of a specific object (e.g. on file delete)
882
+     *
883
+     * @param string $objectType the object type (e.g. 'files')
884
+     * @param string $objectId e.g. the file id
885
+     * @return boolean
886
+     * @since 9.0.0
887
+     */
888
+    public function deleteCommentsAtObject($objectType, $objectId) {
889
+        $this->checkRoleParameters('Object', $objectType, $objectId);
890
+
891
+        $qb = $this->dbConn->getQueryBuilder();
892
+        $affectedRows = $qb
893
+            ->delete('comments')
894
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
895
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
896
+            ->setParameter('type', $objectType)
897
+            ->setParameter('id', $objectId)
898
+            ->execute();
899
+
900
+        $this->commentsCache = [];
901
+
902
+        return is_int($affectedRows);
903
+    }
904
+
905
+    /**
906
+     * deletes the read markers for the specified user
907
+     *
908
+     * @param \OCP\IUser $user
909
+     * @return bool
910
+     * @since 9.0.0
911
+     */
912
+    public function deleteReadMarksFromUser(IUser $user) {
913
+        $qb = $this->dbConn->getQueryBuilder();
914
+        $query = $qb->delete('comments_read_markers')
915
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
916
+            ->setParameter('user_id', $user->getUID());
917
+
918
+        try {
919
+            $affectedRows = $query->execute();
920
+        } catch (DriverException $e) {
921
+            $this->logger->logException($e, ['app' => 'core_comments']);
922
+            return false;
923
+        }
924
+        return ($affectedRows > 0);
925
+    }
926
+
927
+    /**
928
+     * sets the read marker for a given file to the specified date for the
929
+     * provided user
930
+     *
931
+     * @param string $objectType
932
+     * @param string $objectId
933
+     * @param \DateTime $dateTime
934
+     * @param IUser $user
935
+     * @since 9.0.0
936
+     */
937
+    public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
938
+        $this->checkRoleParameters('Object', $objectType, $objectId);
939
+
940
+        $qb = $this->dbConn->getQueryBuilder();
941
+        $values = [
942
+            'user_id' => $qb->createNamedParameter($user->getUID()),
943
+            'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
944
+            'object_type' => $qb->createNamedParameter($objectType),
945
+            'object_id' => $qb->createNamedParameter($objectId),
946
+        ];
947
+
948
+        // Strategy: try to update, if this does not return affected rows, do an insert.
949
+        $affectedRows = $qb
950
+            ->update('comments_read_markers')
951
+            ->set('user_id', $values['user_id'])
952
+            ->set('marker_datetime', $values['marker_datetime'])
953
+            ->set('object_type', $values['object_type'])
954
+            ->set('object_id', $values['object_id'])
955
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
956
+            ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
957
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
958
+            ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
959
+            ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
960
+            ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
961
+            ->execute();
962
+
963
+        if ($affectedRows > 0) {
964
+            return;
965
+        }
966
+
967
+        $qb->insert('comments_read_markers')
968
+            ->values($values)
969
+            ->execute();
970
+    }
971
+
972
+    /**
973
+     * returns the read marker for a given file to the specified date for the
974
+     * provided user. It returns null, when the marker is not present, i.e.
975
+     * no comments were marked as read.
976
+     *
977
+     * @param string $objectType
978
+     * @param string $objectId
979
+     * @param IUser $user
980
+     * @return \DateTime|null
981
+     * @since 9.0.0
982
+     */
983
+    public function getReadMark($objectType, $objectId, IUser $user) {
984
+        $qb = $this->dbConn->getQueryBuilder();
985
+        $resultStatement = $qb->select('marker_datetime')
986
+            ->from('comments_read_markers')
987
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
988
+            ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
989
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
990
+            ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
991
+            ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
992
+            ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
993
+            ->execute();
994
+
995
+        $data = $resultStatement->fetch();
996
+        $resultStatement->closeCursor();
997
+        if (!$data || is_null($data['marker_datetime'])) {
998
+            return null;
999
+        }
1000
+
1001
+        return new \DateTime($data['marker_datetime']);
1002
+    }
1003
+
1004
+    /**
1005
+     * deletes the read markers on the specified object
1006
+     *
1007
+     * @param string $objectType
1008
+     * @param string $objectId
1009
+     * @return bool
1010
+     * @since 9.0.0
1011
+     */
1012
+    public function deleteReadMarksOnObject($objectType, $objectId) {
1013
+        $this->checkRoleParameters('Object', $objectType, $objectId);
1014
+
1015
+        $qb = $this->dbConn->getQueryBuilder();
1016
+        $query = $qb->delete('comments_read_markers')
1017
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
1018
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
1019
+            ->setParameter('object_type', $objectType)
1020
+            ->setParameter('object_id', $objectId);
1021
+
1022
+        try {
1023
+            $affectedRows = $query->execute();
1024
+        } catch (DriverException $e) {
1025
+            $this->logger->logException($e, ['app' => 'core_comments']);
1026
+            return false;
1027
+        }
1028
+        return ($affectedRows > 0);
1029
+    }
1030
+
1031
+    /**
1032
+     * registers an Entity to the manager, so event notifications can be send
1033
+     * to consumers of the comments infrastructure
1034
+     *
1035
+     * @param \Closure $closure
1036
+     */
1037
+    public function registerEventHandler(\Closure $closure) {
1038
+        $this->eventHandlerClosures[] = $closure;
1039
+        $this->eventHandlers = [];
1040
+    }
1041
+
1042
+    /**
1043
+     * registers a method that resolves an ID to a display name for a given type
1044
+     *
1045
+     * @param string $type
1046
+     * @param \Closure $closure
1047
+     * @throws \OutOfBoundsException
1048
+     * @since 11.0.0
1049
+     *
1050
+     * Only one resolver shall be registered per type. Otherwise a
1051
+     * \OutOfBoundsException has to thrown.
1052
+     */
1053
+    public function registerDisplayNameResolver($type, \Closure $closure) {
1054
+        if (!is_string($type)) {
1055
+            throw new \InvalidArgumentException('String expected.');
1056
+        }
1057
+        if (isset($this->displayNameResolvers[$type])) {
1058
+            throw new \OutOfBoundsException('Displayname resolver for this type already registered');
1059
+        }
1060
+        $this->displayNameResolvers[$type] = $closure;
1061
+    }
1062
+
1063
+    /**
1064
+     * resolves a given ID of a given Type to a display name.
1065
+     *
1066
+     * @param string $type
1067
+     * @param string $id
1068
+     * @return string
1069
+     * @throws \OutOfBoundsException
1070
+     * @since 11.0.0
1071
+     *
1072
+     * If a provided type was not registered, an \OutOfBoundsException shall
1073
+     * be thrown. It is upon the resolver discretion what to return of the
1074
+     * provided ID is unknown. It must be ensured that a string is returned.
1075
+     */
1076
+    public function resolveDisplayName($type, $id) {
1077
+        if (!is_string($type)) {
1078
+            throw new \InvalidArgumentException('String expected.');
1079
+        }
1080
+        if (!isset($this->displayNameResolvers[$type])) {
1081
+            throw new \OutOfBoundsException('No Displayname resolver for this type registered');
1082
+        }
1083
+        return (string)$this->displayNameResolvers[$type]($id);
1084
+    }
1085
+
1086
+    /**
1087
+     * returns valid, registered entities
1088
+     *
1089
+     * @return \OCP\Comments\ICommentsEventHandler[]
1090
+     */
1091
+    private function getEventHandlers() {
1092
+        if (!empty($this->eventHandlers)) {
1093
+            return $this->eventHandlers;
1094
+        }
1095
+
1096
+        $this->eventHandlers = [];
1097
+        foreach ($this->eventHandlerClosures as $name => $closure) {
1098
+            $entity = $closure();
1099
+            if (!($entity instanceof ICommentsEventHandler)) {
1100
+                throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
1101
+            }
1102
+            $this->eventHandlers[$name] = $entity;
1103
+        }
1104
+
1105
+        return $this->eventHandlers;
1106
+    }
1107
+
1108
+    /**
1109
+     * sends notifications to the registered entities
1110
+     *
1111
+     * @param $eventType
1112
+     * @param IComment $comment
1113
+     */
1114
+    private function sendEvent($eventType, IComment $comment) {
1115
+        $entities = $this->getEventHandlers();
1116
+        $event = new CommentsEvent($eventType, $comment);
1117
+        foreach ($entities as $entity) {
1118
+            $entity->handle($event);
1119
+        }
1120
+    }
1121 1121
 }
Please login to merge, or discard this patch.
lib/private/Files/Config/UserMountCache.php 1 patch
Indentation   +367 added lines, -367 removed lines patch added patch discarded remove patch
@@ -47,371 +47,371 @@
 block discarded – undo
47 47
  * Cache mounts points per user in the cache so we can easilly look them up
48 48
  */
49 49
 class UserMountCache implements IUserMountCache {
50
-	/**
51
-	 * @var IDBConnection
52
-	 */
53
-	private $connection;
54
-
55
-	/**
56
-	 * @var IUserManager
57
-	 */
58
-	private $userManager;
59
-
60
-	/**
61
-	 * Cached mount info.
62
-	 * Map of $userId to ICachedMountInfo.
63
-	 *
64
-	 * @var ICache
65
-	 **/
66
-	private $mountsForUsers;
67
-
68
-	/**
69
-	 * @var ILogger
70
-	 */
71
-	private $logger;
72
-
73
-	/**
74
-	 * @var ICache
75
-	 */
76
-	private $cacheInfoCache;
77
-
78
-	/**
79
-	 * UserMountCache constructor.
80
-	 *
81
-	 * @param IDBConnection $connection
82
-	 * @param IUserManager $userManager
83
-	 * @param ILogger $logger
84
-	 */
85
-	public function __construct(IDBConnection $connection, IUserManager $userManager, ILogger $logger) {
86
-		$this->connection = $connection;
87
-		$this->userManager = $userManager;
88
-		$this->logger = $logger;
89
-		$this->cacheInfoCache = new CappedMemoryCache();
90
-		$this->mountsForUsers = new CappedMemoryCache();
91
-	}
92
-
93
-	public function registerMounts(IUser $user, array $mounts) {
94
-		// filter out non-proper storages coming from unit tests
95
-		$mounts = array_filter($mounts, function (IMountPoint $mount) {
96
-			return $mount instanceof SharedMount || $mount->getStorage() && $mount->getStorage()->getCache();
97
-		});
98
-		/** @var ICachedMountInfo[] $newMounts */
99
-		$newMounts = array_map(function (IMountPoint $mount) use ($user) {
100
-			// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
101
-			if ($mount->getStorageRootId() === -1) {
102
-				return null;
103
-			} else {
104
-				return new LazyStorageMountInfo($user, $mount);
105
-			}
106
-		}, $mounts);
107
-		$newMounts = array_values(array_filter($newMounts));
108
-		$newMountRootIds = array_map(function (ICachedMountInfo $mount) {
109
-			return $mount->getRootId();
110
-		}, $newMounts);
111
-		$newMounts = array_combine($newMountRootIds, $newMounts);
112
-
113
-		$cachedMounts = $this->getMountsForUser($user);
114
-		$cachedMountRootIds = array_map(function (ICachedMountInfo $mount) {
115
-			return $mount->getRootId();
116
-		}, $cachedMounts);
117
-		$cachedMounts = array_combine($cachedMountRootIds, $cachedMounts);
118
-
119
-		$addedMounts = [];
120
-		$removedMounts = [];
121
-
122
-		foreach ($newMounts as $rootId => $newMount) {
123
-			if (!isset($cachedMounts[$rootId])) {
124
-				$addedMounts[] = $newMount;
125
-			}
126
-		}
127
-
128
-		foreach ($cachedMounts as $rootId => $cachedMount) {
129
-			if (!isset($newMounts[$rootId])) {
130
-				$removedMounts[] = $cachedMount;
131
-			}
132
-		}
133
-
134
-		$changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
135
-
136
-		foreach ($addedMounts as $mount) {
137
-			$this->addToCache($mount);
138
-			$this->mountsForUsers[$user->getUID()][] = $mount;
139
-		}
140
-		foreach ($removedMounts as $mount) {
141
-			$this->removeFromCache($mount);
142
-			$index = array_search($mount, $this->mountsForUsers[$user->getUID()]);
143
-			unset($this->mountsForUsers[$user->getUID()][$index]);
144
-		}
145
-		foreach ($changedMounts as $mount) {
146
-			$this->updateCachedMount($mount);
147
-		}
148
-	}
149
-
150
-	/**
151
-	 * @param ICachedMountInfo[] $newMounts
152
-	 * @param ICachedMountInfo[] $cachedMounts
153
-	 * @return ICachedMountInfo[]
154
-	 */
155
-	private function findChangedMounts(array $newMounts, array $cachedMounts) {
156
-		$new = [];
157
-		foreach ($newMounts as $mount) {
158
-			$new[$mount->getRootId()] = $mount;
159
-		}
160
-		$changed = [];
161
-		foreach ($cachedMounts as $cachedMount) {
162
-			$rootId = $cachedMount->getRootId();
163
-			if (isset($new[$rootId])) {
164
-				$newMount = $new[$rootId];
165
-				if (
166
-					$newMount->getMountPoint() !== $cachedMount->getMountPoint() ||
167
-					$newMount->getStorageId() !== $cachedMount->getStorageId() ||
168
-					$newMount->getMountId() !== $cachedMount->getMountId()
169
-				) {
170
-					$changed[] = $newMount;
171
-				}
172
-			}
173
-		}
174
-		return $changed;
175
-	}
176
-
177
-	private function addToCache(ICachedMountInfo $mount) {
178
-		if ($mount->getStorageId() !== -1) {
179
-			$this->connection->insertIfNotExist('*PREFIX*mounts', [
180
-				'storage_id' => $mount->getStorageId(),
181
-				'root_id' => $mount->getRootId(),
182
-				'user_id' => $mount->getUser()->getUID(),
183
-				'mount_point' => $mount->getMountPoint(),
184
-				'mount_id' => $mount->getMountId()
185
-			], ['root_id', 'user_id']);
186
-		} else {
187
-			// in some cases this is legitimate, like orphaned shares
188
-			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
189
-		}
190
-	}
191
-
192
-	private function updateCachedMount(ICachedMountInfo $mount) {
193
-		$builder = $this->connection->getQueryBuilder();
194
-
195
-		$query = $builder->update('mounts')
196
-			->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
197
-			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
198
-			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
199
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
200
-			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
201
-
202
-		$query->execute();
203
-	}
204
-
205
-	private function removeFromCache(ICachedMountInfo $mount) {
206
-		$builder = $this->connection->getQueryBuilder();
207
-
208
-		$query = $builder->delete('mounts')
209
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
210
-			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
211
-		$query->execute();
212
-	}
213
-
214
-	private function dbRowToMountInfo(array $row) {
215
-		$user = $this->userManager->get($row['user_id']);
216
-		if (is_null($user)) {
217
-			return null;
218
-		}
219
-		$mount_id = $row['mount_id'];
220
-		if (!is_null($mount_id)) {
221
-			$mount_id = (int)$mount_id;
222
-		}
223
-		return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $mount_id, isset($row['path']) ? $row['path'] : '');
224
-	}
225
-
226
-	/**
227
-	 * @param IUser $user
228
-	 * @return ICachedMountInfo[]
229
-	 */
230
-	public function getMountsForUser(IUser $user) {
231
-		if (!isset($this->mountsForUsers[$user->getUID()])) {
232
-			$builder = $this->connection->getQueryBuilder();
233
-			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
234
-				->from('mounts', 'm')
235
-				->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
236
-				->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
237
-
238
-			$rows = $query->execute()->fetchAll();
239
-
240
-			$this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
241
-		}
242
-		return $this->mountsForUsers[$user->getUID()];
243
-	}
244
-
245
-	/**
246
-	 * @param int $numericStorageId
247
-	 * @param string|null $user limit the results to a single user
248
-	 * @return CachedMountInfo[]
249
-	 */
250
-	public function getMountsForStorageId($numericStorageId, $user = null) {
251
-		$builder = $this->connection->getQueryBuilder();
252
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
253
-			->from('mounts', 'm')
254
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
255
-			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
256
-
257
-		if ($user) {
258
-			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
259
-		}
260
-
261
-		$rows = $query->execute()->fetchAll();
262
-
263
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
264
-	}
265
-
266
-	/**
267
-	 * @param int $rootFileId
268
-	 * @return CachedMountInfo[]
269
-	 */
270
-	public function getMountsForRootId($rootFileId) {
271
-		$builder = $this->connection->getQueryBuilder();
272
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
273
-			->from('mounts', 'm')
274
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
275
-			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
276
-
277
-		$rows = $query->execute()->fetchAll();
278
-
279
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
280
-	}
281
-
282
-	/**
283
-	 * @param $fileId
284
-	 * @return array
285
-	 * @throws \OCP\Files\NotFoundException
286
-	 */
287
-	private function getCacheInfoFromFileId($fileId) {
288
-		if (!isset($this->cacheInfoCache[$fileId])) {
289
-			$builder = $this->connection->getQueryBuilder();
290
-			$query = $builder->select('storage', 'path', 'mimetype')
291
-				->from('filecache')
292
-				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
293
-
294
-			$row = $query->execute()->fetch();
295
-			if (is_array($row)) {
296
-				$this->cacheInfoCache[$fileId] = [
297
-					(int)$row['storage'],
298
-					$row['path'],
299
-					(int)$row['mimetype']
300
-				];
301
-			} else {
302
-				throw new NotFoundException('File with id "' . $fileId . '" not found');
303
-			}
304
-		}
305
-		return $this->cacheInfoCache[$fileId];
306
-	}
307
-
308
-	/**
309
-	 * @param int $fileId
310
-	 * @param string|null $user optionally restrict the results to a single user
311
-	 * @return ICachedMountFileInfo[]
312
-	 * @since 9.0.0
313
-	 */
314
-	public function getMountsForFileId($fileId, $user = null) {
315
-		try {
316
-			list($storageId, $internalPath) = $this->getCacheInfoFromFileId($fileId);
317
-		} catch (NotFoundException $e) {
318
-			return [];
319
-		}
320
-		$mountsForStorage = $this->getMountsForStorageId($storageId, $user);
321
-
322
-		// filter mounts that are from the same storage but a different directory
323
-		$filteredMounts = array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
324
-			if ($fileId === $mount->getRootId()) {
325
-				return true;
326
-			}
327
-			$internalMountPath = $mount->getRootInternalPath();
328
-
329
-			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
330
-		});
331
-
332
-		return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
333
-			return new CachedMountFileInfo(
334
-				$mount->getUser(),
335
-				$mount->getStorageId(),
336
-				$mount->getRootId(),
337
-				$mount->getMountPoint(),
338
-				$mount->getMountId(),
339
-				$mount->getRootInternalPath(),
340
-				$internalPath
341
-			);
342
-		}, $filteredMounts);
343
-	}
344
-
345
-	/**
346
-	 * Remove all cached mounts for a user
347
-	 *
348
-	 * @param IUser $user
349
-	 */
350
-	public function removeUserMounts(IUser $user) {
351
-		$builder = $this->connection->getQueryBuilder();
352
-
353
-		$query = $builder->delete('mounts')
354
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
355
-		$query->execute();
356
-	}
357
-
358
-	public function removeUserStorageMount($storageId, $userId) {
359
-		$builder = $this->connection->getQueryBuilder();
360
-
361
-		$query = $builder->delete('mounts')
362
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
363
-			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
364
-		$query->execute();
365
-	}
366
-
367
-	public function remoteStorageMounts($storageId) {
368
-		$builder = $this->connection->getQueryBuilder();
369
-
370
-		$query = $builder->delete('mounts')
371
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
372
-		$query->execute();
373
-	}
374
-
375
-	/**
376
-	 * @param array $users
377
-	 * @return array
378
-	 */
379
-	public function getUsedSpaceForUsers(array $users) {
380
-		$builder = $this->connection->getQueryBuilder();
381
-
382
-		$slash = $builder->createNamedParameter('/');
383
-
384
-		$mountPoint = $builder->func()->concat(
385
-			$builder->func()->concat($slash, 'user_id'),
386
-			$slash
387
-		);
388
-
389
-		$userIds = array_map(function (IUser $user) {
390
-			return $user->getUID();
391
-		}, $users);
392
-
393
-		$query = $builder->select('m.user_id', 'f.size')
394
-			->from('mounts', 'm')
395
-			->innerJoin('m', 'filecache', 'f',
396
-				$builder->expr()->andX(
397
-					$builder->expr()->eq('m.storage_id', 'f.storage'),
398
-					$builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
399
-				))
400
-			->where($builder->expr()->eq('m.mount_point', $mountPoint))
401
-			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
402
-
403
-		$result = $query->execute();
404
-
405
-		$results = [];
406
-		while ($row = $result->fetch()) {
407
-			$results[$row['user_id']] = $row['size'];
408
-		}
409
-		$result->closeCursor();
410
-		return $results;
411
-	}
412
-
413
-	public function clear(): void {
414
-		$this->cacheInfoCache = new CappedMemoryCache();
415
-		$this->mountsForUsers = new CappedMemoryCache();
416
-	}
50
+    /**
51
+     * @var IDBConnection
52
+     */
53
+    private $connection;
54
+
55
+    /**
56
+     * @var IUserManager
57
+     */
58
+    private $userManager;
59
+
60
+    /**
61
+     * Cached mount info.
62
+     * Map of $userId to ICachedMountInfo.
63
+     *
64
+     * @var ICache
65
+     **/
66
+    private $mountsForUsers;
67
+
68
+    /**
69
+     * @var ILogger
70
+     */
71
+    private $logger;
72
+
73
+    /**
74
+     * @var ICache
75
+     */
76
+    private $cacheInfoCache;
77
+
78
+    /**
79
+     * UserMountCache constructor.
80
+     *
81
+     * @param IDBConnection $connection
82
+     * @param IUserManager $userManager
83
+     * @param ILogger $logger
84
+     */
85
+    public function __construct(IDBConnection $connection, IUserManager $userManager, ILogger $logger) {
86
+        $this->connection = $connection;
87
+        $this->userManager = $userManager;
88
+        $this->logger = $logger;
89
+        $this->cacheInfoCache = new CappedMemoryCache();
90
+        $this->mountsForUsers = new CappedMemoryCache();
91
+    }
92
+
93
+    public function registerMounts(IUser $user, array $mounts) {
94
+        // filter out non-proper storages coming from unit tests
95
+        $mounts = array_filter($mounts, function (IMountPoint $mount) {
96
+            return $mount instanceof SharedMount || $mount->getStorage() && $mount->getStorage()->getCache();
97
+        });
98
+        /** @var ICachedMountInfo[] $newMounts */
99
+        $newMounts = array_map(function (IMountPoint $mount) use ($user) {
100
+            // filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
101
+            if ($mount->getStorageRootId() === -1) {
102
+                return null;
103
+            } else {
104
+                return new LazyStorageMountInfo($user, $mount);
105
+            }
106
+        }, $mounts);
107
+        $newMounts = array_values(array_filter($newMounts));
108
+        $newMountRootIds = array_map(function (ICachedMountInfo $mount) {
109
+            return $mount->getRootId();
110
+        }, $newMounts);
111
+        $newMounts = array_combine($newMountRootIds, $newMounts);
112
+
113
+        $cachedMounts = $this->getMountsForUser($user);
114
+        $cachedMountRootIds = array_map(function (ICachedMountInfo $mount) {
115
+            return $mount->getRootId();
116
+        }, $cachedMounts);
117
+        $cachedMounts = array_combine($cachedMountRootIds, $cachedMounts);
118
+
119
+        $addedMounts = [];
120
+        $removedMounts = [];
121
+
122
+        foreach ($newMounts as $rootId => $newMount) {
123
+            if (!isset($cachedMounts[$rootId])) {
124
+                $addedMounts[] = $newMount;
125
+            }
126
+        }
127
+
128
+        foreach ($cachedMounts as $rootId => $cachedMount) {
129
+            if (!isset($newMounts[$rootId])) {
130
+                $removedMounts[] = $cachedMount;
131
+            }
132
+        }
133
+
134
+        $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
135
+
136
+        foreach ($addedMounts as $mount) {
137
+            $this->addToCache($mount);
138
+            $this->mountsForUsers[$user->getUID()][] = $mount;
139
+        }
140
+        foreach ($removedMounts as $mount) {
141
+            $this->removeFromCache($mount);
142
+            $index = array_search($mount, $this->mountsForUsers[$user->getUID()]);
143
+            unset($this->mountsForUsers[$user->getUID()][$index]);
144
+        }
145
+        foreach ($changedMounts as $mount) {
146
+            $this->updateCachedMount($mount);
147
+        }
148
+    }
149
+
150
+    /**
151
+     * @param ICachedMountInfo[] $newMounts
152
+     * @param ICachedMountInfo[] $cachedMounts
153
+     * @return ICachedMountInfo[]
154
+     */
155
+    private function findChangedMounts(array $newMounts, array $cachedMounts) {
156
+        $new = [];
157
+        foreach ($newMounts as $mount) {
158
+            $new[$mount->getRootId()] = $mount;
159
+        }
160
+        $changed = [];
161
+        foreach ($cachedMounts as $cachedMount) {
162
+            $rootId = $cachedMount->getRootId();
163
+            if (isset($new[$rootId])) {
164
+                $newMount = $new[$rootId];
165
+                if (
166
+                    $newMount->getMountPoint() !== $cachedMount->getMountPoint() ||
167
+                    $newMount->getStorageId() !== $cachedMount->getStorageId() ||
168
+                    $newMount->getMountId() !== $cachedMount->getMountId()
169
+                ) {
170
+                    $changed[] = $newMount;
171
+                }
172
+            }
173
+        }
174
+        return $changed;
175
+    }
176
+
177
+    private function addToCache(ICachedMountInfo $mount) {
178
+        if ($mount->getStorageId() !== -1) {
179
+            $this->connection->insertIfNotExist('*PREFIX*mounts', [
180
+                'storage_id' => $mount->getStorageId(),
181
+                'root_id' => $mount->getRootId(),
182
+                'user_id' => $mount->getUser()->getUID(),
183
+                'mount_point' => $mount->getMountPoint(),
184
+                'mount_id' => $mount->getMountId()
185
+            ], ['root_id', 'user_id']);
186
+        } else {
187
+            // in some cases this is legitimate, like orphaned shares
188
+            $this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
189
+        }
190
+    }
191
+
192
+    private function updateCachedMount(ICachedMountInfo $mount) {
193
+        $builder = $this->connection->getQueryBuilder();
194
+
195
+        $query = $builder->update('mounts')
196
+            ->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
197
+            ->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
198
+            ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
199
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
200
+            ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
201
+
202
+        $query->execute();
203
+    }
204
+
205
+    private function removeFromCache(ICachedMountInfo $mount) {
206
+        $builder = $this->connection->getQueryBuilder();
207
+
208
+        $query = $builder->delete('mounts')
209
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
210
+            ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
211
+        $query->execute();
212
+    }
213
+
214
+    private function dbRowToMountInfo(array $row) {
215
+        $user = $this->userManager->get($row['user_id']);
216
+        if (is_null($user)) {
217
+            return null;
218
+        }
219
+        $mount_id = $row['mount_id'];
220
+        if (!is_null($mount_id)) {
221
+            $mount_id = (int)$mount_id;
222
+        }
223
+        return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $mount_id, isset($row['path']) ? $row['path'] : '');
224
+    }
225
+
226
+    /**
227
+     * @param IUser $user
228
+     * @return ICachedMountInfo[]
229
+     */
230
+    public function getMountsForUser(IUser $user) {
231
+        if (!isset($this->mountsForUsers[$user->getUID()])) {
232
+            $builder = $this->connection->getQueryBuilder();
233
+            $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
234
+                ->from('mounts', 'm')
235
+                ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
236
+                ->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
237
+
238
+            $rows = $query->execute()->fetchAll();
239
+
240
+            $this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
241
+        }
242
+        return $this->mountsForUsers[$user->getUID()];
243
+    }
244
+
245
+    /**
246
+     * @param int $numericStorageId
247
+     * @param string|null $user limit the results to a single user
248
+     * @return CachedMountInfo[]
249
+     */
250
+    public function getMountsForStorageId($numericStorageId, $user = null) {
251
+        $builder = $this->connection->getQueryBuilder();
252
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
253
+            ->from('mounts', 'm')
254
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
255
+            ->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
256
+
257
+        if ($user) {
258
+            $query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
259
+        }
260
+
261
+        $rows = $query->execute()->fetchAll();
262
+
263
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
264
+    }
265
+
266
+    /**
267
+     * @param int $rootFileId
268
+     * @return CachedMountInfo[]
269
+     */
270
+    public function getMountsForRootId($rootFileId) {
271
+        $builder = $this->connection->getQueryBuilder();
272
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
273
+            ->from('mounts', 'm')
274
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
275
+            ->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
276
+
277
+        $rows = $query->execute()->fetchAll();
278
+
279
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
280
+    }
281
+
282
+    /**
283
+     * @param $fileId
284
+     * @return array
285
+     * @throws \OCP\Files\NotFoundException
286
+     */
287
+    private function getCacheInfoFromFileId($fileId) {
288
+        if (!isset($this->cacheInfoCache[$fileId])) {
289
+            $builder = $this->connection->getQueryBuilder();
290
+            $query = $builder->select('storage', 'path', 'mimetype')
291
+                ->from('filecache')
292
+                ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
293
+
294
+            $row = $query->execute()->fetch();
295
+            if (is_array($row)) {
296
+                $this->cacheInfoCache[$fileId] = [
297
+                    (int)$row['storage'],
298
+                    $row['path'],
299
+                    (int)$row['mimetype']
300
+                ];
301
+            } else {
302
+                throw new NotFoundException('File with id "' . $fileId . '" not found');
303
+            }
304
+        }
305
+        return $this->cacheInfoCache[$fileId];
306
+    }
307
+
308
+    /**
309
+     * @param int $fileId
310
+     * @param string|null $user optionally restrict the results to a single user
311
+     * @return ICachedMountFileInfo[]
312
+     * @since 9.0.0
313
+     */
314
+    public function getMountsForFileId($fileId, $user = null) {
315
+        try {
316
+            list($storageId, $internalPath) = $this->getCacheInfoFromFileId($fileId);
317
+        } catch (NotFoundException $e) {
318
+            return [];
319
+        }
320
+        $mountsForStorage = $this->getMountsForStorageId($storageId, $user);
321
+
322
+        // filter mounts that are from the same storage but a different directory
323
+        $filteredMounts = array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
324
+            if ($fileId === $mount->getRootId()) {
325
+                return true;
326
+            }
327
+            $internalMountPath = $mount->getRootInternalPath();
328
+
329
+            return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
330
+        });
331
+
332
+        return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
333
+            return new CachedMountFileInfo(
334
+                $mount->getUser(),
335
+                $mount->getStorageId(),
336
+                $mount->getRootId(),
337
+                $mount->getMountPoint(),
338
+                $mount->getMountId(),
339
+                $mount->getRootInternalPath(),
340
+                $internalPath
341
+            );
342
+        }, $filteredMounts);
343
+    }
344
+
345
+    /**
346
+     * Remove all cached mounts for a user
347
+     *
348
+     * @param IUser $user
349
+     */
350
+    public function removeUserMounts(IUser $user) {
351
+        $builder = $this->connection->getQueryBuilder();
352
+
353
+        $query = $builder->delete('mounts')
354
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
355
+        $query->execute();
356
+    }
357
+
358
+    public function removeUserStorageMount($storageId, $userId) {
359
+        $builder = $this->connection->getQueryBuilder();
360
+
361
+        $query = $builder->delete('mounts')
362
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
363
+            ->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
364
+        $query->execute();
365
+    }
366
+
367
+    public function remoteStorageMounts($storageId) {
368
+        $builder = $this->connection->getQueryBuilder();
369
+
370
+        $query = $builder->delete('mounts')
371
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
372
+        $query->execute();
373
+    }
374
+
375
+    /**
376
+     * @param array $users
377
+     * @return array
378
+     */
379
+    public function getUsedSpaceForUsers(array $users) {
380
+        $builder = $this->connection->getQueryBuilder();
381
+
382
+        $slash = $builder->createNamedParameter('/');
383
+
384
+        $mountPoint = $builder->func()->concat(
385
+            $builder->func()->concat($slash, 'user_id'),
386
+            $slash
387
+        );
388
+
389
+        $userIds = array_map(function (IUser $user) {
390
+            return $user->getUID();
391
+        }, $users);
392
+
393
+        $query = $builder->select('m.user_id', 'f.size')
394
+            ->from('mounts', 'm')
395
+            ->innerJoin('m', 'filecache', 'f',
396
+                $builder->expr()->andX(
397
+                    $builder->expr()->eq('m.storage_id', 'f.storage'),
398
+                    $builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
399
+                ))
400
+            ->where($builder->expr()->eq('m.mount_point', $mountPoint))
401
+            ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
402
+
403
+        $result = $query->execute();
404
+
405
+        $results = [];
406
+        while ($row = $result->fetch()) {
407
+            $results[$row['user_id']] = $row['size'];
408
+        }
409
+        $result->closeCursor();
410
+        return $results;
411
+    }
412
+
413
+    public function clear(): void {
414
+        $this->cacheInfoCache = new CappedMemoryCache();
415
+        $this->mountsForUsers = new CappedMemoryCache();
416
+    }
417 417
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Propagator.php 1 patch
Indentation   +165 added lines, -165 removed lines patch added patch discarded remove patch
@@ -33,169 +33,169 @@
 block discarded – undo
33 33
  * Propagate etags and mtimes within the storage
34 34
  */
35 35
 class Propagator implements IPropagator {
36
-	private $inBatch = false;
37
-
38
-	private $batch = [];
39
-
40
-	/**
41
-	 * @var \OC\Files\Storage\Storage
42
-	 */
43
-	protected $storage;
44
-
45
-	/**
46
-	 * @var IDBConnection
47
-	 */
48
-	private $connection;
49
-
50
-	/**
51
-	 * @var array
52
-	 */
53
-	private $ignore = [];
54
-
55
-	public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
56
-		$this->storage = $storage;
57
-		$this->connection = $connection;
58
-		$this->ignore = $ignore;
59
-	}
60
-
61
-
62
-	/**
63
-	 * @param string $internalPath
64
-	 * @param int $time
65
-	 * @param int $sizeDifference number of bytes the file has grown
66
-	 */
67
-	public function propagateChange($internalPath, $time, $sizeDifference = 0) {
68
-		// Do not propogate changes in ignored paths
69
-		foreach ($this->ignore as $ignore) {
70
-			if (strpos($internalPath, $ignore) === 0) {
71
-				return;
72
-			}
73
-		}
74
-
75
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
76
-
77
-		$parents = $this->getParents($internalPath);
78
-
79
-		if ($this->inBatch) {
80
-			foreach ($parents as $parent) {
81
-				$this->addToBatch($parent, $time, $sizeDifference);
82
-			}
83
-			return;
84
-		}
85
-
86
-		$parentHashes = array_map('md5', $parents);
87
-		$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
88
-
89
-		$builder = $this->connection->getQueryBuilder();
90
-		$hashParams = array_map(function ($hash) use ($builder) {
91
-			return $builder->expr()->literal($hash);
92
-		}, $parentHashes);
93
-
94
-		$builder->update('filecache')
95
-			->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
96
-			->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR))
97
-			->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
98
-			->andWhere($builder->expr()->in('path_hash', $hashParams));
99
-
100
-		$builder->execute();
101
-
102
-		if ($sizeDifference !== 0) {
103
-			// we need to do size separably so we can ignore entries with uncalculated size
104
-			$builder = $this->connection->getQueryBuilder();
105
-			$builder->update('filecache')
106
-				->set('size', $builder->func()->greatest(
107
-					$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT),
108
-					$builder->func()->add('size', $builder->createNamedParameter($sizeDifference)))
109
-				)
110
-				->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
111
-				->andWhere($builder->expr()->in('path_hash', $hashParams))
112
-				->andWhere($builder->expr()->gt('size', $builder->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
113
-
114
-			$builder->execute();
115
-		}
116
-	}
117
-
118
-	protected function getParents($path) {
119
-		$parts = explode('/', $path);
120
-		$parent = '';
121
-		$parents = [];
122
-		foreach ($parts as $part) {
123
-			$parents[] = $parent;
124
-			$parent = trim($parent . '/' . $part, '/');
125
-		}
126
-		return $parents;
127
-	}
128
-
129
-	/**
130
-	 * Mark the beginning of a propagation batch
131
-	 *
132
-	 * Note that not all cache setups support propagation in which case this will be a noop
133
-	 *
134
-	 * Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
135
-	 * before the batch is committed.
136
-	 */
137
-	public function beginBatch() {
138
-		$this->inBatch = true;
139
-	}
140
-
141
-	private function addToBatch($internalPath, $time, $sizeDifference) {
142
-		if (!isset($this->batch[$internalPath])) {
143
-			$this->batch[$internalPath] = [
144
-				'hash' => md5($internalPath),
145
-				'time' => $time,
146
-				'size' => $sizeDifference
147
-			];
148
-		} else {
149
-			$this->batch[$internalPath]['size'] += $sizeDifference;
150
-			if ($time > $this->batch[$internalPath]['time']) {
151
-				$this->batch[$internalPath]['time'] = $time;
152
-			}
153
-		}
154
-	}
155
-
156
-	/**
157
-	 * Commit the active propagation batch
158
-	 */
159
-	public function commitBatch() {
160
-		if (!$this->inBatch) {
161
-			throw new \BadMethodCallException('Not in batch');
162
-		}
163
-		$this->inBatch = false;
164
-
165
-		$this->connection->beginTransaction();
166
-
167
-		$query = $this->connection->getQueryBuilder();
168
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
169
-
170
-		$query->update('filecache')
171
-			->set('mtime', $query->createFunction('GREATEST(' . $query->getColumnName('mtime') . ', ' . $query->createParameter('time') . ')'))
172
-			->set('etag', $query->expr()->literal(uniqid()))
173
-			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
174
-			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
175
-
176
-		$sizeQuery = $this->connection->getQueryBuilder();
177
-		$sizeQuery->update('filecache')
178
-			->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
179
-			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
180
-			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
181
-			->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
182
-
183
-		foreach ($this->batch as $item) {
184
-			$query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
185
-			$query->setParameter('hash', $item['hash']);
186
-
187
-			$query->execute();
188
-
189
-			if ($item['size']) {
190
-				$sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
191
-				$sizeQuery->setParameter('hash', $item['hash']);
192
-
193
-				$sizeQuery->execute();
194
-			}
195
-		}
196
-
197
-		$this->batch = [];
198
-
199
-		$this->connection->commit();
200
-	}
36
+    private $inBatch = false;
37
+
38
+    private $batch = [];
39
+
40
+    /**
41
+     * @var \OC\Files\Storage\Storage
42
+     */
43
+    protected $storage;
44
+
45
+    /**
46
+     * @var IDBConnection
47
+     */
48
+    private $connection;
49
+
50
+    /**
51
+     * @var array
52
+     */
53
+    private $ignore = [];
54
+
55
+    public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
56
+        $this->storage = $storage;
57
+        $this->connection = $connection;
58
+        $this->ignore = $ignore;
59
+    }
60
+
61
+
62
+    /**
63
+     * @param string $internalPath
64
+     * @param int $time
65
+     * @param int $sizeDifference number of bytes the file has grown
66
+     */
67
+    public function propagateChange($internalPath, $time, $sizeDifference = 0) {
68
+        // Do not propogate changes in ignored paths
69
+        foreach ($this->ignore as $ignore) {
70
+            if (strpos($internalPath, $ignore) === 0) {
71
+                return;
72
+            }
73
+        }
74
+
75
+        $storageId = (int)$this->storage->getStorageCache()->getNumericId();
76
+
77
+        $parents = $this->getParents($internalPath);
78
+
79
+        if ($this->inBatch) {
80
+            foreach ($parents as $parent) {
81
+                $this->addToBatch($parent, $time, $sizeDifference);
82
+            }
83
+            return;
84
+        }
85
+
86
+        $parentHashes = array_map('md5', $parents);
87
+        $etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
88
+
89
+        $builder = $this->connection->getQueryBuilder();
90
+        $hashParams = array_map(function ($hash) use ($builder) {
91
+            return $builder->expr()->literal($hash);
92
+        }, $parentHashes);
93
+
94
+        $builder->update('filecache')
95
+            ->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
96
+            ->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR))
97
+            ->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
98
+            ->andWhere($builder->expr()->in('path_hash', $hashParams));
99
+
100
+        $builder->execute();
101
+
102
+        if ($sizeDifference !== 0) {
103
+            // we need to do size separably so we can ignore entries with uncalculated size
104
+            $builder = $this->connection->getQueryBuilder();
105
+            $builder->update('filecache')
106
+                ->set('size', $builder->func()->greatest(
107
+                    $builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT),
108
+                    $builder->func()->add('size', $builder->createNamedParameter($sizeDifference)))
109
+                )
110
+                ->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
111
+                ->andWhere($builder->expr()->in('path_hash', $hashParams))
112
+                ->andWhere($builder->expr()->gt('size', $builder->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
113
+
114
+            $builder->execute();
115
+        }
116
+    }
117
+
118
+    protected function getParents($path) {
119
+        $parts = explode('/', $path);
120
+        $parent = '';
121
+        $parents = [];
122
+        foreach ($parts as $part) {
123
+            $parents[] = $parent;
124
+            $parent = trim($parent . '/' . $part, '/');
125
+        }
126
+        return $parents;
127
+    }
128
+
129
+    /**
130
+     * Mark the beginning of a propagation batch
131
+     *
132
+     * Note that not all cache setups support propagation in which case this will be a noop
133
+     *
134
+     * Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
135
+     * before the batch is committed.
136
+     */
137
+    public function beginBatch() {
138
+        $this->inBatch = true;
139
+    }
140
+
141
+    private function addToBatch($internalPath, $time, $sizeDifference) {
142
+        if (!isset($this->batch[$internalPath])) {
143
+            $this->batch[$internalPath] = [
144
+                'hash' => md5($internalPath),
145
+                'time' => $time,
146
+                'size' => $sizeDifference
147
+            ];
148
+        } else {
149
+            $this->batch[$internalPath]['size'] += $sizeDifference;
150
+            if ($time > $this->batch[$internalPath]['time']) {
151
+                $this->batch[$internalPath]['time'] = $time;
152
+            }
153
+        }
154
+    }
155
+
156
+    /**
157
+     * Commit the active propagation batch
158
+     */
159
+    public function commitBatch() {
160
+        if (!$this->inBatch) {
161
+            throw new \BadMethodCallException('Not in batch');
162
+        }
163
+        $this->inBatch = false;
164
+
165
+        $this->connection->beginTransaction();
166
+
167
+        $query = $this->connection->getQueryBuilder();
168
+        $storageId = (int)$this->storage->getStorageCache()->getNumericId();
169
+
170
+        $query->update('filecache')
171
+            ->set('mtime', $query->createFunction('GREATEST(' . $query->getColumnName('mtime') . ', ' . $query->createParameter('time') . ')'))
172
+            ->set('etag', $query->expr()->literal(uniqid()))
173
+            ->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
174
+            ->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
175
+
176
+        $sizeQuery = $this->connection->getQueryBuilder();
177
+        $sizeQuery->update('filecache')
178
+            ->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
179
+            ->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
180
+            ->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
181
+            ->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
182
+
183
+        foreach ($this->batch as $item) {
184
+            $query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
185
+            $query->setParameter('hash', $item['hash']);
186
+
187
+            $query->execute();
188
+
189
+            if ($item['size']) {
190
+                $sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
191
+                $sizeQuery->setParameter('hash', $item['hash']);
192
+
193
+                $sizeQuery->execute();
194
+            }
195
+        }
196
+
197
+        $this->batch = [];
198
+
199
+        $this->connection->commit();
200
+    }
201 201
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Cache.php 1 patch
Indentation   +937 added lines, -937 removed lines patch added patch discarded remove patch
@@ -62,941 +62,941 @@
 block discarded – undo
62 62
  * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
63 63
  */
64 64
 class Cache implements ICache {
65
-	use MoveFromCacheTrait {
66
-		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
67
-	}
68
-
69
-	/**
70
-	 * @var array partial data for the cache
71
-	 */
72
-	protected $partial = [];
73
-
74
-	/**
75
-	 * @var string
76
-	 */
77
-	protected $storageId;
78
-
79
-	private $storage;
80
-
81
-	/**
82
-	 * @var Storage $storageCache
83
-	 */
84
-	protected $storageCache;
85
-
86
-	/** @var IMimeTypeLoader */
87
-	protected $mimetypeLoader;
88
-
89
-	/**
90
-	 * @var IDBConnection
91
-	 */
92
-	protected $connection;
93
-
94
-	protected $eventDispatcher;
95
-
96
-	/** @var QuerySearchHelper */
97
-	protected $querySearchHelper;
98
-
99
-	/**
100
-	 * @param IStorage $storage
101
-	 */
102
-	public function __construct(IStorage $storage) {
103
-		$this->storageId = $storage->getId();
104
-		$this->storage = $storage;
105
-		if (strlen($this->storageId) > 64) {
106
-			$this->storageId = md5($this->storageId);
107
-		}
108
-
109
-		$this->storageCache = new Storage($storage);
110
-		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
111
-		$this->connection = \OC::$server->getDatabaseConnection();
112
-		$this->eventDispatcher = \OC::$server->getEventDispatcher();
113
-		$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
114
-	}
115
-
116
-	private function getQueryBuilder() {
117
-		return new CacheQueryBuilder(
118
-			$this->connection,
119
-			\OC::$server->getSystemConfig(),
120
-			\OC::$server->getLogger(),
121
-			$this
122
-		);
123
-	}
124
-
125
-	/**
126
-	 * Get the numeric storage id for this cache's storage
127
-	 *
128
-	 * @return int
129
-	 */
130
-	public function getNumericStorageId() {
131
-		return $this->storageCache->getNumericId();
132
-	}
133
-
134
-	/**
135
-	 * get the stored metadata of a file or folder
136
-	 *
137
-	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
138
-	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
139
-	 */
140
-	public function get($file) {
141
-		$query = $this->getQueryBuilder();
142
-		$query->selectFileCache();
143
-
144
-		if (is_string($file) or $file == '') {
145
-			// normalize file
146
-			$file = $this->normalize($file);
147
-
148
-			$query->whereStorageId()
149
-				->wherePath($file);
150
-		} else { //file id
151
-			$query->whereFileId($file);
152
-		}
153
-
154
-		$data = $query->execute()->fetch();
155
-
156
-		//merge partial data
157
-		if (!$data and is_string($file) and isset($this->partial[$file])) {
158
-			return $this->partial[$file];
159
-		} elseif (!$data) {
160
-			return $data;
161
-		} else {
162
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
163
-		}
164
-	}
165
-
166
-	/**
167
-	 * Create a CacheEntry from database row
168
-	 *
169
-	 * @param array $data
170
-	 * @param IMimeTypeLoader $mimetypeLoader
171
-	 * @return CacheEntry
172
-	 */
173
-	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
174
-		//fix types
175
-		$data['fileid'] = (int)$data['fileid'];
176
-		$data['parent'] = (int)$data['parent'];
177
-		$data['size'] = 0 + $data['size'];
178
-		$data['mtime'] = (int)$data['mtime'];
179
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
180
-		$data['encryptedVersion'] = (int)$data['encrypted'];
181
-		$data['encrypted'] = (bool)$data['encrypted'];
182
-		$data['storage_id'] = $data['storage'];
183
-		$data['storage'] = (int)$data['storage'];
184
-		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
185
-		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
186
-		if ($data['storage_mtime'] == 0) {
187
-			$data['storage_mtime'] = $data['mtime'];
188
-		}
189
-		$data['permissions'] = (int)$data['permissions'];
190
-		if (isset($data['creation_time'])) {
191
-			$data['creation_time'] = (int) $data['creation_time'];
192
-		}
193
-		if (isset($data['upload_time'])) {
194
-			$data['upload_time'] = (int) $data['upload_time'];
195
-		}
196
-		return new CacheEntry($data);
197
-	}
198
-
199
-	/**
200
-	 * get the metadata of all files stored in $folder
201
-	 *
202
-	 * @param string $folder
203
-	 * @return ICacheEntry[]
204
-	 */
205
-	public function getFolderContents($folder) {
206
-		$fileId = $this->getId($folder);
207
-		return $this->getFolderContentsById($fileId);
208
-	}
209
-
210
-	/**
211
-	 * get the metadata of all files stored in $folder
212
-	 *
213
-	 * @param int $fileId the file id of the folder
214
-	 * @return ICacheEntry[]
215
-	 */
216
-	public function getFolderContentsById($fileId) {
217
-		if ($fileId > -1) {
218
-			$query = $this->getQueryBuilder();
219
-			$query->selectFileCache()
220
-				->whereParent($fileId)
221
-				->orderBy('name', 'ASC');
222
-
223
-			$files = $query->execute()->fetchAll();
224
-			return array_map(function (array $data) {
225
-				return self::cacheEntryFromData($data, $this->mimetypeLoader);
226
-			}, $files);
227
-		}
228
-		return [];
229
-	}
230
-
231
-	/**
232
-	 * insert or update meta data for a file or folder
233
-	 *
234
-	 * @param string $file
235
-	 * @param array $data
236
-	 *
237
-	 * @return int file id
238
-	 * @throws \RuntimeException
239
-	 */
240
-	public function put($file, array $data) {
241
-		if (($id = $this->getId($file)) > -1) {
242
-			$this->update($id, $data);
243
-			return $id;
244
-		} else {
245
-			return $this->insert($file, $data);
246
-		}
247
-	}
248
-
249
-	/**
250
-	 * insert meta data for a new file or folder
251
-	 *
252
-	 * @param string $file
253
-	 * @param array $data
254
-	 *
255
-	 * @return int file id
256
-	 * @throws \RuntimeException
257
-	 */
258
-	public function insert($file, array $data) {
259
-		// normalize file
260
-		$file = $this->normalize($file);
261
-
262
-		if (isset($this->partial[$file])) { //add any saved partial data
263
-			$data = array_merge($this->partial[$file], $data);
264
-			unset($this->partial[$file]);
265
-		}
266
-
267
-		$requiredFields = ['size', 'mtime', 'mimetype'];
268
-		foreach ($requiredFields as $field) {
269
-			if (!isset($data[$field])) { //data not complete save as partial and return
270
-				$this->partial[$file] = $data;
271
-				return -1;
272
-			}
273
-		}
274
-
275
-		$data['path'] = $file;
276
-		if (!isset($data['parent'])) {
277
-			$data['parent'] = $this->getParentId($file);
278
-		}
279
-		$data['name'] = basename($file);
280
-
281
-		[$values, $extensionValues] = $this->normalizeData($data);
282
-		$values['storage'] = $this->getNumericStorageId();
283
-
284
-		try {
285
-			$builder = $this->connection->getQueryBuilder();
286
-			$builder->insert('filecache');
287
-
288
-			foreach ($values as $column => $value) {
289
-				$builder->setValue($column, $builder->createNamedParameter($value));
290
-			}
291
-
292
-			if ($builder->execute()) {
293
-				$fileId = $builder->getLastInsertId();
294
-
295
-				if (count($extensionValues)) {
296
-					$query = $this->getQueryBuilder();
297
-					$query->insert('filecache_extended');
298
-
299
-					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
300
-					foreach ($extensionValues as $column => $value) {
301
-						$query->setValue($column, $query->createNamedParameter($value));
302
-					}
303
-					$query->execute();
304
-				}
305
-
306
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
307
-				return $fileId;
308
-			}
309
-		} catch (UniqueConstraintViolationException $e) {
310
-			// entry exists already
311
-			if ($this->connection->inTransaction()) {
312
-				$this->connection->commit();
313
-				$this->connection->beginTransaction();
314
-			}
315
-		}
316
-
317
-		// The file was created in the mean time
318
-		if (($id = $this->getId($file)) > -1) {
319
-			$this->update($id, $data);
320
-			return $id;
321
-		} else {
322
-			throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
323
-		}
324
-	}
325
-
326
-	/**
327
-	 * update the metadata of an existing file or folder in the cache
328
-	 *
329
-	 * @param int $id the fileid of the existing file or folder
330
-	 * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
331
-	 */
332
-	public function update($id, array $data) {
333
-		if (isset($data['path'])) {
334
-			// normalize path
335
-			$data['path'] = $this->normalize($data['path']);
336
-		}
337
-
338
-		if (isset($data['name'])) {
339
-			// normalize path
340
-			$data['name'] = $this->normalize($data['name']);
341
-		}
342
-
343
-		[$values, $extensionValues] = $this->normalizeData($data);
344
-
345
-		if (count($values)) {
346
-			$query = $this->getQueryBuilder();
347
-
348
-			$query->update('filecache')
349
-				->whereFileId($id)
350
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
351
-					return $query->expr()->orX(
352
-						$query->expr()->neq($key, $query->createNamedParameter($value)),
353
-						$query->expr()->isNull($key)
354
-					);
355
-				}, array_keys($values), array_values($values))));
356
-
357
-			foreach ($values as $key => $value) {
358
-				$query->set($key, $query->createNamedParameter($value));
359
-			}
360
-
361
-			$query->execute();
362
-		}
363
-
364
-		if (count($extensionValues)) {
365
-			try {
366
-				$query = $this->getQueryBuilder();
367
-				$query->insert('filecache_extended');
368
-
369
-				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
370
-				foreach ($extensionValues as $column => $value) {
371
-					$query->setValue($column, $query->createNamedParameter($value));
372
-				}
373
-
374
-				$query->execute();
375
-			} catch (UniqueConstraintViolationException $e) {
376
-				$query = $this->getQueryBuilder();
377
-				$query->update('filecache_extended')
378
-					->whereFileId($id)
379
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
380
-						return $query->expr()->orX(
381
-							$query->expr()->neq($key, $query->createNamedParameter($value)),
382
-							$query->expr()->isNull($key)
383
-						);
384
-					}, array_keys($extensionValues), array_values($extensionValues))));
385
-
386
-				foreach ($extensionValues as $key => $value) {
387
-					$query->set($key, $query->createNamedParameter($value));
388
-				}
389
-
390
-				$query->execute();
391
-			}
392
-		}
393
-
394
-		$path = $this->getPathById($id);
395
-		// path can still be null if the file doesn't exist
396
-		if ($path !== null) {
397
-			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
398
-		}
399
-	}
400
-
401
-	/**
402
-	 * extract query parts and params array from data array
403
-	 *
404
-	 * @param array $data
405
-	 * @return array
406
-	 */
407
-	protected function normalizeData(array $data): array {
408
-		$fields = [
409
-			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
410
-			'etag', 'permissions', 'checksum', 'storage'];
411
-		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
412
-
413
-		$doNotCopyStorageMTime = false;
414
-		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
415
-			// this horrific magic tells it to not copy storage_mtime to mtime
416
-			unset($data['mtime']);
417
-			$doNotCopyStorageMTime = true;
418
-		}
419
-
420
-		$params = [];
421
-		$extensionParams = [];
422
-		foreach ($data as $name => $value) {
423
-			if (array_search($name, $fields) !== false) {
424
-				if ($name === 'path') {
425
-					$params['path_hash'] = md5($value);
426
-				} elseif ($name === 'mimetype') {
427
-					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
428
-					$value = $this->mimetypeLoader->getId($value);
429
-				} elseif ($name === 'storage_mtime') {
430
-					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
431
-						$params['mtime'] = $value;
432
-					}
433
-				} elseif ($name === 'encrypted') {
434
-					if (isset($data['encryptedVersion'])) {
435
-						$value = $data['encryptedVersion'];
436
-					} else {
437
-						// Boolean to integer conversion
438
-						$value = $value ? 1 : 0;
439
-					}
440
-				}
441
-				$params[$name] = $value;
442
-			}
443
-			if (array_search($name, $extensionFields) !== false) {
444
-				$extensionParams[$name] = $value;
445
-			}
446
-		}
447
-		return [$params, array_filter($extensionParams)];
448
-	}
449
-
450
-	/**
451
-	 * get the file id for a file
452
-	 *
453
-	 * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
454
-	 *
455
-	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
456
-	 *
457
-	 * @param string $file
458
-	 * @return int
459
-	 */
460
-	public function getId($file) {
461
-		// normalize file
462
-		$file = $this->normalize($file);
463
-
464
-		$query = $this->getQueryBuilder();
465
-		$query->select('fileid')
466
-			->from('filecache')
467
-			->whereStorageId()
468
-			->wherePath($file);
469
-
470
-		$id = $query->execute()->fetchColumn();
471
-		return $id === false ? -1 : (int)$id;
472
-	}
473
-
474
-	/**
475
-	 * get the id of the parent folder of a file
476
-	 *
477
-	 * @param string $file
478
-	 * @return int
479
-	 */
480
-	public function getParentId($file) {
481
-		if ($file === '') {
482
-			return -1;
483
-		} else {
484
-			$parent = $this->getParentPath($file);
485
-			return (int)$this->getId($parent);
486
-		}
487
-	}
488
-
489
-	private function getParentPath($path) {
490
-		$parent = dirname($path);
491
-		if ($parent === '.') {
492
-			$parent = '';
493
-		}
494
-		return $parent;
495
-	}
496
-
497
-	/**
498
-	 * check if a file is available in the cache
499
-	 *
500
-	 * @param string $file
501
-	 * @return bool
502
-	 */
503
-	public function inCache($file) {
504
-		return $this->getId($file) != -1;
505
-	}
506
-
507
-	/**
508
-	 * remove a file or folder from the cache
509
-	 *
510
-	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
511
-	 *
512
-	 * @param string $file
513
-	 */
514
-	public function remove($file) {
515
-		$entry = $this->get($file);
516
-
517
-		if ($entry) {
518
-			$query = $this->getQueryBuilder();
519
-			$query->delete('filecache')
520
-				->whereFileId($entry->getId());
521
-			$query->execute();
522
-
523
-			$query = $this->getQueryBuilder();
524
-			$query->delete('filecache_extended')
525
-				->whereFileId($entry->getId());
526
-			$query->execute();
527
-
528
-			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
529
-				$this->removeChildren($entry);
530
-			}
531
-		}
532
-	}
533
-
534
-	/**
535
-	 * Get all sub folders of a folder
536
-	 *
537
-	 * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
538
-	 * @return ICacheEntry[] the cache entries for the subfolders
539
-	 */
540
-	private function getSubFolders(ICacheEntry $entry) {
541
-		$children = $this->getFolderContentsById($entry->getId());
542
-		return array_filter($children, function ($child) {
543
-			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
544
-		});
545
-	}
546
-
547
-	/**
548
-	 * Recursively remove all children of a folder
549
-	 *
550
-	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
551
-	 * @throws \OC\DatabaseException
552
-	 */
553
-	private function removeChildren(ICacheEntry $entry) {
554
-		$parentIds = [$entry->getId()];
555
-		$queue = [$entry->getId()];
556
-
557
-		// we walk depth first trough the file tree, removing all filecache_extended attributes while we walk
558
-		// and collecting all folder ids to later use to delete the filecache entries
559
-		while ($entryId = array_pop($queue)) {
560
-			$children = $this->getFolderContentsById($entryId);
561
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
562
-				return $cacheEntry->getId();
563
-			}, $children);
564
-
565
-			$query = $this->getQueryBuilder();
566
-			$query->delete('filecache_extended')
567
-				->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
568
-			$query->execute();
569
-
570
-			/** @var ICacheEntry[] $childFolders */
571
-			$childFolders = array_filter($children, function ($child) {
572
-				return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
573
-			});
574
-			foreach ($childFolders as $folder) {
575
-				$parentIds[] = $folder->getId();
576
-				$queue[] = $folder->getId();
577
-			}
578
-		}
579
-
580
-		$query = $this->getQueryBuilder();
581
-		$query->delete('filecache')
582
-			->whereParentIn($parentIds);
583
-		$query->execute();
584
-	}
585
-
586
-	/**
587
-	 * Move a file or folder in the cache
588
-	 *
589
-	 * @param string $source
590
-	 * @param string $target
591
-	 */
592
-	public function move($source, $target) {
593
-		$this->moveFromCache($this, $source, $target);
594
-	}
595
-
596
-	/**
597
-	 * Get the storage id and path needed for a move
598
-	 *
599
-	 * @param string $path
600
-	 * @return array [$storageId, $internalPath]
601
-	 */
602
-	protected function getMoveInfo($path) {
603
-		return [$this->getNumericStorageId(), $path];
604
-	}
605
-
606
-	/**
607
-	 * Move a file or folder in the cache
608
-	 *
609
-	 * @param \OCP\Files\Cache\ICache $sourceCache
610
-	 * @param string $sourcePath
611
-	 * @param string $targetPath
612
-	 * @throws \OC\DatabaseException
613
-	 * @throws \Exception if the given storages have an invalid id
614
-	 */
615
-	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
616
-		if ($sourceCache instanceof Cache) {
617
-			// normalize source and target
618
-			$sourcePath = $this->normalize($sourcePath);
619
-			$targetPath = $this->normalize($targetPath);
620
-
621
-			$sourceData = $sourceCache->get($sourcePath);
622
-			$sourceId = $sourceData['fileid'];
623
-			$newParentId = $this->getParentId($targetPath);
624
-
625
-			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
626
-			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
627
-
628
-			if (is_null($sourceStorageId) || $sourceStorageId === false) {
629
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
630
-			}
631
-			if (is_null($targetStorageId) || $targetStorageId === false) {
632
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
633
-			}
634
-
635
-			$this->connection->beginTransaction();
636
-			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
637
-				//update all child entries
638
-				$sourceLength = mb_strlen($sourcePath);
639
-				$query = $this->connection->getQueryBuilder();
640
-
641
-				$fun = $query->func();
642
-				$newPathFunction = $fun->concat(
643
-					$query->createNamedParameter($targetPath),
644
-					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
645
-				);
646
-				$query->update('filecache')
647
-					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
648
-					->set('path_hash', $fun->md5($newPathFunction))
649
-					->set('path', $newPathFunction)
650
-					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
651
-					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
652
-
653
-				try {
654
-					$query->execute();
655
-				} catch (\OC\DatabaseException $e) {
656
-					$this->connection->rollBack();
657
-					throw $e;
658
-				}
659
-			}
660
-
661
-			$query = $this->getQueryBuilder();
662
-			$query->update('filecache')
663
-				->set('storage', $query->createNamedParameter($targetStorageId))
664
-				->set('path', $query->createNamedParameter($targetPath))
665
-				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
666
-				->set('name', $query->createNamedParameter(basename($targetPath)))
667
-				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
668
-				->whereFileId($sourceId);
669
-			$query->execute();
670
-
671
-			$this->connection->commit();
672
-		} else {
673
-			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
674
-		}
675
-	}
676
-
677
-	/**
678
-	 * remove all entries for files that are stored on the storage from the cache
679
-	 */
680
-	public function clear() {
681
-		$query = $this->getQueryBuilder();
682
-		$query->delete('filecache')
683
-			->whereStorageId();
684
-		$query->execute();
685
-
686
-		$query = $this->connection->getQueryBuilder();
687
-		$query->delete('storages')
688
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
689
-		$query->execute();
690
-	}
691
-
692
-	/**
693
-	 * Get the scan status of a file
694
-	 *
695
-	 * - Cache::NOT_FOUND: File is not in the cache
696
-	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
697
-	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
698
-	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
699
-	 *
700
-	 * @param string $file
701
-	 *
702
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
703
-	 */
704
-	public function getStatus($file) {
705
-		// normalize file
706
-		$file = $this->normalize($file);
707
-
708
-		$query = $this->getQueryBuilder();
709
-		$query->select('size')
710
-			->from('filecache')
711
-			->whereStorageId()
712
-			->wherePath($file);
713
-		$size = $query->execute()->fetchColumn();
714
-		if ($size !== false) {
715
-			if ((int)$size === -1) {
716
-				return self::SHALLOW;
717
-			} else {
718
-				return self::COMPLETE;
719
-			}
720
-		} else {
721
-			if (isset($this->partial[$file])) {
722
-				return self::PARTIAL;
723
-			} else {
724
-				return self::NOT_FOUND;
725
-			}
726
-		}
727
-	}
728
-
729
-	/**
730
-	 * search for files matching $pattern
731
-	 *
732
-	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
733
-	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
734
-	 */
735
-	public function search($pattern) {
736
-		// normalize pattern
737
-		$pattern = $this->normalize($pattern);
738
-
739
-		if ($pattern === '%%') {
740
-			return [];
741
-		}
742
-
743
-		$query = $this->getQueryBuilder();
744
-		$query->selectFileCache()
745
-			->whereStorageId()
746
-			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
747
-
748
-		return array_map(function (array $data) {
749
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
750
-		}, $query->execute()->fetchAll());
751
-	}
752
-
753
-	/**
754
-	 * @param Statement $result
755
-	 * @return CacheEntry[]
756
-	 */
757
-	private function searchResultToCacheEntries(Statement $result) {
758
-		$files = $result->fetchAll();
759
-
760
-		return array_map(function (array $data) {
761
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
762
-		}, $files);
763
-	}
764
-
765
-	/**
766
-	 * search for files by mimetype
767
-	 *
768
-	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
769
-	 *        where it will search for all mimetypes in the group ('image/*')
770
-	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
771
-	 */
772
-	public function searchByMime($mimetype) {
773
-		$mimeId = $this->mimetypeLoader->getId($mimetype);
774
-
775
-		$query = $this->getQueryBuilder();
776
-		$query->selectFileCache()
777
-			->whereStorageId();
778
-
779
-		if (strpos($mimetype, '/')) {
780
-			$query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
781
-		} else {
782
-			$query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
783
-		}
784
-
785
-		return array_map(function (array $data) {
786
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
787
-		}, $query->execute()->fetchAll());
788
-	}
789
-
790
-	public function searchQuery(ISearchQuery $searchQuery) {
791
-		$builder = $this->getQueryBuilder();
792
-
793
-		$query = $builder->selectFileCache('file');
794
-
795
-		$query->whereStorageId();
796
-
797
-		if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
798
-			$query
799
-				->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
800
-				->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
801
-					$builder->expr()->eq('tagmap.type', 'tag.type'),
802
-					$builder->expr()->eq('tagmap.categoryid', 'tag.id')
803
-				))
804
-				->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
805
-				->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
806
-		}
807
-
808
-		$searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
809
-		if ($searchExpr) {
810
-			$query->andWhere($searchExpr);
811
-		}
812
-
813
-		if ($searchQuery->limitToHome() && ($this instanceof HomeCache)) {
814
-			$query->andWhere($builder->expr()->like('path', $query->expr()->literal('files/%')));
815
-		}
816
-
817
-		$this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
818
-
819
-		if ($searchQuery->getLimit()) {
820
-			$query->setMaxResults($searchQuery->getLimit());
821
-		}
822
-		if ($searchQuery->getOffset()) {
823
-			$query->setFirstResult($searchQuery->getOffset());
824
-		}
825
-
826
-		$result = $query->execute();
827
-		return $this->searchResultToCacheEntries($result);
828
-	}
829
-
830
-	/**
831
-	 * Re-calculate the folder size and the size of all parent folders
832
-	 *
833
-	 * @param string|boolean $path
834
-	 * @param array $data (optional) meta data of the folder
835
-	 */
836
-	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
837
-		$this->calculateFolderSize($path, $data);
838
-		if ($path !== '') {
839
-			$parent = dirname($path);
840
-			if ($parent === '.' or $parent === '/') {
841
-				$parent = '';
842
-			}
843
-			if ($isBackgroundScan) {
844
-				$parentData = $this->get($parent);
845
-				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
846
-					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
847
-				}
848
-			} else {
849
-				$this->correctFolderSize($parent);
850
-			}
851
-		}
852
-	}
853
-
854
-	/**
855
-	 * get the incomplete count that shares parent $folder
856
-	 *
857
-	 * @param int $fileId the file id of the folder
858
-	 * @return int
859
-	 */
860
-	public function getIncompleteChildrenCount($fileId) {
861
-		if ($fileId > -1) {
862
-			$query = $this->getQueryBuilder();
863
-			$query->select($query->func()->count())
864
-				->from('filecache')
865
-				->whereParent($fileId)
866
-				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
867
-
868
-			return (int)$query->execute()->fetchColumn();
869
-		}
870
-		return -1;
871
-	}
872
-
873
-	/**
874
-	 * calculate the size of a folder and set it in the cache
875
-	 *
876
-	 * @param string $path
877
-	 * @param array $entry (optional) meta data of the folder
878
-	 * @return int
879
-	 */
880
-	public function calculateFolderSize($path, $entry = null) {
881
-		$totalSize = 0;
882
-		if (is_null($entry) or !isset($entry['fileid'])) {
883
-			$entry = $this->get($path);
884
-		}
885
-		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
886
-			$id = $entry['fileid'];
887
-
888
-			$query = $this->getQueryBuilder();
889
-			$query->selectAlias($query->func()->sum('size'), 'f1')
890
-				->selectAlias($query->func()->min('size'), 'f2')
891
-				->from('filecache')
892
-				->whereStorageId()
893
-				->whereParent($id);
894
-
895
-			if ($row = $query->execute()->fetch()) {
896
-				[$sum, $min] = array_values($row);
897
-				$sum = 0 + $sum;
898
-				$min = 0 + $min;
899
-				if ($min === -1) {
900
-					$totalSize = $min;
901
-				} else {
902
-					$totalSize = $sum;
903
-				}
904
-				if ($entry['size'] !== $totalSize) {
905
-					$this->update($id, ['size' => $totalSize]);
906
-				}
907
-			}
908
-		}
909
-		return $totalSize;
910
-	}
911
-
912
-	/**
913
-	 * get all file ids on the files on the storage
914
-	 *
915
-	 * @return int[]
916
-	 */
917
-	public function getAll() {
918
-		$query = $this->getQueryBuilder();
919
-		$query->select('fileid')
920
-			->from('filecache')
921
-			->whereStorageId();
922
-
923
-		return array_map(function ($id) {
924
-			return (int)$id;
925
-		}, $query->execute()->fetchAll(\PDO::FETCH_COLUMN));
926
-	}
927
-
928
-	/**
929
-	 * find a folder in the cache which has not been fully scanned
930
-	 *
931
-	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
932
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
933
-	 * likely the folder where we stopped scanning previously
934
-	 *
935
-	 * @return string|bool the path of the folder or false when no folder matched
936
-	 */
937
-	public function getIncomplete() {
938
-		$query = $this->getQueryBuilder();
939
-		$query->select('path')
940
-			->from('filecache')
941
-			->whereStorageId()
942
-			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
943
-			->orderBy('fileid', 'DESC');
944
-
945
-		return $query->execute()->fetchColumn();
946
-	}
947
-
948
-	/**
949
-	 * get the path of a file on this storage by it's file id
950
-	 *
951
-	 * @param int $id the file id of the file or folder to search
952
-	 * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
953
-	 */
954
-	public function getPathById($id) {
955
-		$query = $this->getQueryBuilder();
956
-		$query->select('path')
957
-			->from('filecache')
958
-			->whereStorageId()
959
-			->whereFileId($id);
960
-
961
-		$path = $query->execute()->fetchColumn();
962
-		return $path === false ? null : $path;
963
-	}
964
-
965
-	/**
966
-	 * get the storage id of the storage for a file and the internal path of the file
967
-	 * unlike getPathById this does not limit the search to files on this storage and
968
-	 * instead does a global search in the cache table
969
-	 *
970
-	 * @param int $id
971
-	 * @return array first element holding the storage id, second the path
972
-	 * @deprecated use getPathById() instead
973
-	 */
974
-	public static function getById($id) {
975
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
976
-		$query->select('path', 'storage')
977
-			->from('filecache')
978
-			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
979
-		if ($row = $query->execute()->fetch()) {
980
-			$numericId = $row['storage'];
981
-			$path = $row['path'];
982
-		} else {
983
-			return null;
984
-		}
985
-
986
-		if ($id = Storage::getStorageId($numericId)) {
987
-			return [$id, $path];
988
-		} else {
989
-			return null;
990
-		}
991
-	}
992
-
993
-	/**
994
-	 * normalize the given path
995
-	 *
996
-	 * @param string $path
997
-	 * @return string
998
-	 */
999
-	public function normalize($path) {
1000
-		return trim(\OC_Util::normalizeUnicode($path), '/');
1001
-	}
65
+    use MoveFromCacheTrait {
66
+        MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
67
+    }
68
+
69
+    /**
70
+     * @var array partial data for the cache
71
+     */
72
+    protected $partial = [];
73
+
74
+    /**
75
+     * @var string
76
+     */
77
+    protected $storageId;
78
+
79
+    private $storage;
80
+
81
+    /**
82
+     * @var Storage $storageCache
83
+     */
84
+    protected $storageCache;
85
+
86
+    /** @var IMimeTypeLoader */
87
+    protected $mimetypeLoader;
88
+
89
+    /**
90
+     * @var IDBConnection
91
+     */
92
+    protected $connection;
93
+
94
+    protected $eventDispatcher;
95
+
96
+    /** @var QuerySearchHelper */
97
+    protected $querySearchHelper;
98
+
99
+    /**
100
+     * @param IStorage $storage
101
+     */
102
+    public function __construct(IStorage $storage) {
103
+        $this->storageId = $storage->getId();
104
+        $this->storage = $storage;
105
+        if (strlen($this->storageId) > 64) {
106
+            $this->storageId = md5($this->storageId);
107
+        }
108
+
109
+        $this->storageCache = new Storage($storage);
110
+        $this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
111
+        $this->connection = \OC::$server->getDatabaseConnection();
112
+        $this->eventDispatcher = \OC::$server->getEventDispatcher();
113
+        $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
114
+    }
115
+
116
+    private function getQueryBuilder() {
117
+        return new CacheQueryBuilder(
118
+            $this->connection,
119
+            \OC::$server->getSystemConfig(),
120
+            \OC::$server->getLogger(),
121
+            $this
122
+        );
123
+    }
124
+
125
+    /**
126
+     * Get the numeric storage id for this cache's storage
127
+     *
128
+     * @return int
129
+     */
130
+    public function getNumericStorageId() {
131
+        return $this->storageCache->getNumericId();
132
+    }
133
+
134
+    /**
135
+     * get the stored metadata of a file or folder
136
+     *
137
+     * @param string | int $file either the path of a file or folder or the file id for a file or folder
138
+     * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
139
+     */
140
+    public function get($file) {
141
+        $query = $this->getQueryBuilder();
142
+        $query->selectFileCache();
143
+
144
+        if (is_string($file) or $file == '') {
145
+            // normalize file
146
+            $file = $this->normalize($file);
147
+
148
+            $query->whereStorageId()
149
+                ->wherePath($file);
150
+        } else { //file id
151
+            $query->whereFileId($file);
152
+        }
153
+
154
+        $data = $query->execute()->fetch();
155
+
156
+        //merge partial data
157
+        if (!$data and is_string($file) and isset($this->partial[$file])) {
158
+            return $this->partial[$file];
159
+        } elseif (!$data) {
160
+            return $data;
161
+        } else {
162
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
163
+        }
164
+    }
165
+
166
+    /**
167
+     * Create a CacheEntry from database row
168
+     *
169
+     * @param array $data
170
+     * @param IMimeTypeLoader $mimetypeLoader
171
+     * @return CacheEntry
172
+     */
173
+    public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
174
+        //fix types
175
+        $data['fileid'] = (int)$data['fileid'];
176
+        $data['parent'] = (int)$data['parent'];
177
+        $data['size'] = 0 + $data['size'];
178
+        $data['mtime'] = (int)$data['mtime'];
179
+        $data['storage_mtime'] = (int)$data['storage_mtime'];
180
+        $data['encryptedVersion'] = (int)$data['encrypted'];
181
+        $data['encrypted'] = (bool)$data['encrypted'];
182
+        $data['storage_id'] = $data['storage'];
183
+        $data['storage'] = (int)$data['storage'];
184
+        $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
185
+        $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
186
+        if ($data['storage_mtime'] == 0) {
187
+            $data['storage_mtime'] = $data['mtime'];
188
+        }
189
+        $data['permissions'] = (int)$data['permissions'];
190
+        if (isset($data['creation_time'])) {
191
+            $data['creation_time'] = (int) $data['creation_time'];
192
+        }
193
+        if (isset($data['upload_time'])) {
194
+            $data['upload_time'] = (int) $data['upload_time'];
195
+        }
196
+        return new CacheEntry($data);
197
+    }
198
+
199
+    /**
200
+     * get the metadata of all files stored in $folder
201
+     *
202
+     * @param string $folder
203
+     * @return ICacheEntry[]
204
+     */
205
+    public function getFolderContents($folder) {
206
+        $fileId = $this->getId($folder);
207
+        return $this->getFolderContentsById($fileId);
208
+    }
209
+
210
+    /**
211
+     * get the metadata of all files stored in $folder
212
+     *
213
+     * @param int $fileId the file id of the folder
214
+     * @return ICacheEntry[]
215
+     */
216
+    public function getFolderContentsById($fileId) {
217
+        if ($fileId > -1) {
218
+            $query = $this->getQueryBuilder();
219
+            $query->selectFileCache()
220
+                ->whereParent($fileId)
221
+                ->orderBy('name', 'ASC');
222
+
223
+            $files = $query->execute()->fetchAll();
224
+            return array_map(function (array $data) {
225
+                return self::cacheEntryFromData($data, $this->mimetypeLoader);
226
+            }, $files);
227
+        }
228
+        return [];
229
+    }
230
+
231
+    /**
232
+     * insert or update meta data for a file or folder
233
+     *
234
+     * @param string $file
235
+     * @param array $data
236
+     *
237
+     * @return int file id
238
+     * @throws \RuntimeException
239
+     */
240
+    public function put($file, array $data) {
241
+        if (($id = $this->getId($file)) > -1) {
242
+            $this->update($id, $data);
243
+            return $id;
244
+        } else {
245
+            return $this->insert($file, $data);
246
+        }
247
+    }
248
+
249
+    /**
250
+     * insert meta data for a new file or folder
251
+     *
252
+     * @param string $file
253
+     * @param array $data
254
+     *
255
+     * @return int file id
256
+     * @throws \RuntimeException
257
+     */
258
+    public function insert($file, array $data) {
259
+        // normalize file
260
+        $file = $this->normalize($file);
261
+
262
+        if (isset($this->partial[$file])) { //add any saved partial data
263
+            $data = array_merge($this->partial[$file], $data);
264
+            unset($this->partial[$file]);
265
+        }
266
+
267
+        $requiredFields = ['size', 'mtime', 'mimetype'];
268
+        foreach ($requiredFields as $field) {
269
+            if (!isset($data[$field])) { //data not complete save as partial and return
270
+                $this->partial[$file] = $data;
271
+                return -1;
272
+            }
273
+        }
274
+
275
+        $data['path'] = $file;
276
+        if (!isset($data['parent'])) {
277
+            $data['parent'] = $this->getParentId($file);
278
+        }
279
+        $data['name'] = basename($file);
280
+
281
+        [$values, $extensionValues] = $this->normalizeData($data);
282
+        $values['storage'] = $this->getNumericStorageId();
283
+
284
+        try {
285
+            $builder = $this->connection->getQueryBuilder();
286
+            $builder->insert('filecache');
287
+
288
+            foreach ($values as $column => $value) {
289
+                $builder->setValue($column, $builder->createNamedParameter($value));
290
+            }
291
+
292
+            if ($builder->execute()) {
293
+                $fileId = $builder->getLastInsertId();
294
+
295
+                if (count($extensionValues)) {
296
+                    $query = $this->getQueryBuilder();
297
+                    $query->insert('filecache_extended');
298
+
299
+                    $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
300
+                    foreach ($extensionValues as $column => $value) {
301
+                        $query->setValue($column, $query->createNamedParameter($value));
302
+                    }
303
+                    $query->execute();
304
+                }
305
+
306
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
307
+                return $fileId;
308
+            }
309
+        } catch (UniqueConstraintViolationException $e) {
310
+            // entry exists already
311
+            if ($this->connection->inTransaction()) {
312
+                $this->connection->commit();
313
+                $this->connection->beginTransaction();
314
+            }
315
+        }
316
+
317
+        // The file was created in the mean time
318
+        if (($id = $this->getId($file)) > -1) {
319
+            $this->update($id, $data);
320
+            return $id;
321
+        } else {
322
+            throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
323
+        }
324
+    }
325
+
326
+    /**
327
+     * update the metadata of an existing file or folder in the cache
328
+     *
329
+     * @param int $id the fileid of the existing file or folder
330
+     * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
331
+     */
332
+    public function update($id, array $data) {
333
+        if (isset($data['path'])) {
334
+            // normalize path
335
+            $data['path'] = $this->normalize($data['path']);
336
+        }
337
+
338
+        if (isset($data['name'])) {
339
+            // normalize path
340
+            $data['name'] = $this->normalize($data['name']);
341
+        }
342
+
343
+        [$values, $extensionValues] = $this->normalizeData($data);
344
+
345
+        if (count($values)) {
346
+            $query = $this->getQueryBuilder();
347
+
348
+            $query->update('filecache')
349
+                ->whereFileId($id)
350
+                ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
351
+                    return $query->expr()->orX(
352
+                        $query->expr()->neq($key, $query->createNamedParameter($value)),
353
+                        $query->expr()->isNull($key)
354
+                    );
355
+                }, array_keys($values), array_values($values))));
356
+
357
+            foreach ($values as $key => $value) {
358
+                $query->set($key, $query->createNamedParameter($value));
359
+            }
360
+
361
+            $query->execute();
362
+        }
363
+
364
+        if (count($extensionValues)) {
365
+            try {
366
+                $query = $this->getQueryBuilder();
367
+                $query->insert('filecache_extended');
368
+
369
+                $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
370
+                foreach ($extensionValues as $column => $value) {
371
+                    $query->setValue($column, $query->createNamedParameter($value));
372
+                }
373
+
374
+                $query->execute();
375
+            } catch (UniqueConstraintViolationException $e) {
376
+                $query = $this->getQueryBuilder();
377
+                $query->update('filecache_extended')
378
+                    ->whereFileId($id)
379
+                    ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
380
+                        return $query->expr()->orX(
381
+                            $query->expr()->neq($key, $query->createNamedParameter($value)),
382
+                            $query->expr()->isNull($key)
383
+                        );
384
+                    }, array_keys($extensionValues), array_values($extensionValues))));
385
+
386
+                foreach ($extensionValues as $key => $value) {
387
+                    $query->set($key, $query->createNamedParameter($value));
388
+                }
389
+
390
+                $query->execute();
391
+            }
392
+        }
393
+
394
+        $path = $this->getPathById($id);
395
+        // path can still be null if the file doesn't exist
396
+        if ($path !== null) {
397
+            $this->eventDispatcher->dispatch(CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
398
+        }
399
+    }
400
+
401
+    /**
402
+     * extract query parts and params array from data array
403
+     *
404
+     * @param array $data
405
+     * @return array
406
+     */
407
+    protected function normalizeData(array $data): array {
408
+        $fields = [
409
+            'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
410
+            'etag', 'permissions', 'checksum', 'storage'];
411
+        $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
412
+
413
+        $doNotCopyStorageMTime = false;
414
+        if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
415
+            // this horrific magic tells it to not copy storage_mtime to mtime
416
+            unset($data['mtime']);
417
+            $doNotCopyStorageMTime = true;
418
+        }
419
+
420
+        $params = [];
421
+        $extensionParams = [];
422
+        foreach ($data as $name => $value) {
423
+            if (array_search($name, $fields) !== false) {
424
+                if ($name === 'path') {
425
+                    $params['path_hash'] = md5($value);
426
+                } elseif ($name === 'mimetype') {
427
+                    $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
428
+                    $value = $this->mimetypeLoader->getId($value);
429
+                } elseif ($name === 'storage_mtime') {
430
+                    if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
431
+                        $params['mtime'] = $value;
432
+                    }
433
+                } elseif ($name === 'encrypted') {
434
+                    if (isset($data['encryptedVersion'])) {
435
+                        $value = $data['encryptedVersion'];
436
+                    } else {
437
+                        // Boolean to integer conversion
438
+                        $value = $value ? 1 : 0;
439
+                    }
440
+                }
441
+                $params[$name] = $value;
442
+            }
443
+            if (array_search($name, $extensionFields) !== false) {
444
+                $extensionParams[$name] = $value;
445
+            }
446
+        }
447
+        return [$params, array_filter($extensionParams)];
448
+    }
449
+
450
+    /**
451
+     * get the file id for a file
452
+     *
453
+     * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
454
+     *
455
+     * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
456
+     *
457
+     * @param string $file
458
+     * @return int
459
+     */
460
+    public function getId($file) {
461
+        // normalize file
462
+        $file = $this->normalize($file);
463
+
464
+        $query = $this->getQueryBuilder();
465
+        $query->select('fileid')
466
+            ->from('filecache')
467
+            ->whereStorageId()
468
+            ->wherePath($file);
469
+
470
+        $id = $query->execute()->fetchColumn();
471
+        return $id === false ? -1 : (int)$id;
472
+    }
473
+
474
+    /**
475
+     * get the id of the parent folder of a file
476
+     *
477
+     * @param string $file
478
+     * @return int
479
+     */
480
+    public function getParentId($file) {
481
+        if ($file === '') {
482
+            return -1;
483
+        } else {
484
+            $parent = $this->getParentPath($file);
485
+            return (int)$this->getId($parent);
486
+        }
487
+    }
488
+
489
+    private function getParentPath($path) {
490
+        $parent = dirname($path);
491
+        if ($parent === '.') {
492
+            $parent = '';
493
+        }
494
+        return $parent;
495
+    }
496
+
497
+    /**
498
+     * check if a file is available in the cache
499
+     *
500
+     * @param string $file
501
+     * @return bool
502
+     */
503
+    public function inCache($file) {
504
+        return $this->getId($file) != -1;
505
+    }
506
+
507
+    /**
508
+     * remove a file or folder from the cache
509
+     *
510
+     * when removing a folder from the cache all files and folders inside the folder will be removed as well
511
+     *
512
+     * @param string $file
513
+     */
514
+    public function remove($file) {
515
+        $entry = $this->get($file);
516
+
517
+        if ($entry) {
518
+            $query = $this->getQueryBuilder();
519
+            $query->delete('filecache')
520
+                ->whereFileId($entry->getId());
521
+            $query->execute();
522
+
523
+            $query = $this->getQueryBuilder();
524
+            $query->delete('filecache_extended')
525
+                ->whereFileId($entry->getId());
526
+            $query->execute();
527
+
528
+            if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
529
+                $this->removeChildren($entry);
530
+            }
531
+        }
532
+    }
533
+
534
+    /**
535
+     * Get all sub folders of a folder
536
+     *
537
+     * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
538
+     * @return ICacheEntry[] the cache entries for the subfolders
539
+     */
540
+    private function getSubFolders(ICacheEntry $entry) {
541
+        $children = $this->getFolderContentsById($entry->getId());
542
+        return array_filter($children, function ($child) {
543
+            return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
544
+        });
545
+    }
546
+
547
+    /**
548
+     * Recursively remove all children of a folder
549
+     *
550
+     * @param ICacheEntry $entry the cache entry of the folder to remove the children of
551
+     * @throws \OC\DatabaseException
552
+     */
553
+    private function removeChildren(ICacheEntry $entry) {
554
+        $parentIds = [$entry->getId()];
555
+        $queue = [$entry->getId()];
556
+
557
+        // we walk depth first trough the file tree, removing all filecache_extended attributes while we walk
558
+        // and collecting all folder ids to later use to delete the filecache entries
559
+        while ($entryId = array_pop($queue)) {
560
+            $children = $this->getFolderContentsById($entryId);
561
+            $childIds = array_map(function (ICacheEntry $cacheEntry) {
562
+                return $cacheEntry->getId();
563
+            }, $children);
564
+
565
+            $query = $this->getQueryBuilder();
566
+            $query->delete('filecache_extended')
567
+                ->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
568
+            $query->execute();
569
+
570
+            /** @var ICacheEntry[] $childFolders */
571
+            $childFolders = array_filter($children, function ($child) {
572
+                return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
573
+            });
574
+            foreach ($childFolders as $folder) {
575
+                $parentIds[] = $folder->getId();
576
+                $queue[] = $folder->getId();
577
+            }
578
+        }
579
+
580
+        $query = $this->getQueryBuilder();
581
+        $query->delete('filecache')
582
+            ->whereParentIn($parentIds);
583
+        $query->execute();
584
+    }
585
+
586
+    /**
587
+     * Move a file or folder in the cache
588
+     *
589
+     * @param string $source
590
+     * @param string $target
591
+     */
592
+    public function move($source, $target) {
593
+        $this->moveFromCache($this, $source, $target);
594
+    }
595
+
596
+    /**
597
+     * Get the storage id and path needed for a move
598
+     *
599
+     * @param string $path
600
+     * @return array [$storageId, $internalPath]
601
+     */
602
+    protected function getMoveInfo($path) {
603
+        return [$this->getNumericStorageId(), $path];
604
+    }
605
+
606
+    /**
607
+     * Move a file or folder in the cache
608
+     *
609
+     * @param \OCP\Files\Cache\ICache $sourceCache
610
+     * @param string $sourcePath
611
+     * @param string $targetPath
612
+     * @throws \OC\DatabaseException
613
+     * @throws \Exception if the given storages have an invalid id
614
+     */
615
+    public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
616
+        if ($sourceCache instanceof Cache) {
617
+            // normalize source and target
618
+            $sourcePath = $this->normalize($sourcePath);
619
+            $targetPath = $this->normalize($targetPath);
620
+
621
+            $sourceData = $sourceCache->get($sourcePath);
622
+            $sourceId = $sourceData['fileid'];
623
+            $newParentId = $this->getParentId($targetPath);
624
+
625
+            [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
626
+            [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
627
+
628
+            if (is_null($sourceStorageId) || $sourceStorageId === false) {
629
+                throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
630
+            }
631
+            if (is_null($targetStorageId) || $targetStorageId === false) {
632
+                throw new \Exception('Invalid target storage id: ' . $targetStorageId);
633
+            }
634
+
635
+            $this->connection->beginTransaction();
636
+            if ($sourceData['mimetype'] === 'httpd/unix-directory') {
637
+                //update all child entries
638
+                $sourceLength = mb_strlen($sourcePath);
639
+                $query = $this->connection->getQueryBuilder();
640
+
641
+                $fun = $query->func();
642
+                $newPathFunction = $fun->concat(
643
+                    $query->createNamedParameter($targetPath),
644
+                    $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
645
+                );
646
+                $query->update('filecache')
647
+                    ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
648
+                    ->set('path_hash', $fun->md5($newPathFunction))
649
+                    ->set('path', $newPathFunction)
650
+                    ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
651
+                    ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
652
+
653
+                try {
654
+                    $query->execute();
655
+                } catch (\OC\DatabaseException $e) {
656
+                    $this->connection->rollBack();
657
+                    throw $e;
658
+                }
659
+            }
660
+
661
+            $query = $this->getQueryBuilder();
662
+            $query->update('filecache')
663
+                ->set('storage', $query->createNamedParameter($targetStorageId))
664
+                ->set('path', $query->createNamedParameter($targetPath))
665
+                ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
666
+                ->set('name', $query->createNamedParameter(basename($targetPath)))
667
+                ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
668
+                ->whereFileId($sourceId);
669
+            $query->execute();
670
+
671
+            $this->connection->commit();
672
+        } else {
673
+            $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
674
+        }
675
+    }
676
+
677
+    /**
678
+     * remove all entries for files that are stored on the storage from the cache
679
+     */
680
+    public function clear() {
681
+        $query = $this->getQueryBuilder();
682
+        $query->delete('filecache')
683
+            ->whereStorageId();
684
+        $query->execute();
685
+
686
+        $query = $this->connection->getQueryBuilder();
687
+        $query->delete('storages')
688
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
689
+        $query->execute();
690
+    }
691
+
692
+    /**
693
+     * Get the scan status of a file
694
+     *
695
+     * - Cache::NOT_FOUND: File is not in the cache
696
+     * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
697
+     * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
698
+     * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
699
+     *
700
+     * @param string $file
701
+     *
702
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
703
+     */
704
+    public function getStatus($file) {
705
+        // normalize file
706
+        $file = $this->normalize($file);
707
+
708
+        $query = $this->getQueryBuilder();
709
+        $query->select('size')
710
+            ->from('filecache')
711
+            ->whereStorageId()
712
+            ->wherePath($file);
713
+        $size = $query->execute()->fetchColumn();
714
+        if ($size !== false) {
715
+            if ((int)$size === -1) {
716
+                return self::SHALLOW;
717
+            } else {
718
+                return self::COMPLETE;
719
+            }
720
+        } else {
721
+            if (isset($this->partial[$file])) {
722
+                return self::PARTIAL;
723
+            } else {
724
+                return self::NOT_FOUND;
725
+            }
726
+        }
727
+    }
728
+
729
+    /**
730
+     * search for files matching $pattern
731
+     *
732
+     * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
733
+     * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
734
+     */
735
+    public function search($pattern) {
736
+        // normalize pattern
737
+        $pattern = $this->normalize($pattern);
738
+
739
+        if ($pattern === '%%') {
740
+            return [];
741
+        }
742
+
743
+        $query = $this->getQueryBuilder();
744
+        $query->selectFileCache()
745
+            ->whereStorageId()
746
+            ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
747
+
748
+        return array_map(function (array $data) {
749
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
750
+        }, $query->execute()->fetchAll());
751
+    }
752
+
753
+    /**
754
+     * @param Statement $result
755
+     * @return CacheEntry[]
756
+     */
757
+    private function searchResultToCacheEntries(Statement $result) {
758
+        $files = $result->fetchAll();
759
+
760
+        return array_map(function (array $data) {
761
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
762
+        }, $files);
763
+    }
764
+
765
+    /**
766
+     * search for files by mimetype
767
+     *
768
+     * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
769
+     *        where it will search for all mimetypes in the group ('image/*')
770
+     * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
771
+     */
772
+    public function searchByMime($mimetype) {
773
+        $mimeId = $this->mimetypeLoader->getId($mimetype);
774
+
775
+        $query = $this->getQueryBuilder();
776
+        $query->selectFileCache()
777
+            ->whereStorageId();
778
+
779
+        if (strpos($mimetype, '/')) {
780
+            $query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
781
+        } else {
782
+            $query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
783
+        }
784
+
785
+        return array_map(function (array $data) {
786
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
787
+        }, $query->execute()->fetchAll());
788
+    }
789
+
790
+    public function searchQuery(ISearchQuery $searchQuery) {
791
+        $builder = $this->getQueryBuilder();
792
+
793
+        $query = $builder->selectFileCache('file');
794
+
795
+        $query->whereStorageId();
796
+
797
+        if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
798
+            $query
799
+                ->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
800
+                ->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
801
+                    $builder->expr()->eq('tagmap.type', 'tag.type'),
802
+                    $builder->expr()->eq('tagmap.categoryid', 'tag.id')
803
+                ))
804
+                ->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
805
+                ->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
806
+        }
807
+
808
+        $searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
809
+        if ($searchExpr) {
810
+            $query->andWhere($searchExpr);
811
+        }
812
+
813
+        if ($searchQuery->limitToHome() && ($this instanceof HomeCache)) {
814
+            $query->andWhere($builder->expr()->like('path', $query->expr()->literal('files/%')));
815
+        }
816
+
817
+        $this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
818
+
819
+        if ($searchQuery->getLimit()) {
820
+            $query->setMaxResults($searchQuery->getLimit());
821
+        }
822
+        if ($searchQuery->getOffset()) {
823
+            $query->setFirstResult($searchQuery->getOffset());
824
+        }
825
+
826
+        $result = $query->execute();
827
+        return $this->searchResultToCacheEntries($result);
828
+    }
829
+
830
+    /**
831
+     * Re-calculate the folder size and the size of all parent folders
832
+     *
833
+     * @param string|boolean $path
834
+     * @param array $data (optional) meta data of the folder
835
+     */
836
+    public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
837
+        $this->calculateFolderSize($path, $data);
838
+        if ($path !== '') {
839
+            $parent = dirname($path);
840
+            if ($parent === '.' or $parent === '/') {
841
+                $parent = '';
842
+            }
843
+            if ($isBackgroundScan) {
844
+                $parentData = $this->get($parent);
845
+                if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
846
+                    $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
847
+                }
848
+            } else {
849
+                $this->correctFolderSize($parent);
850
+            }
851
+        }
852
+    }
853
+
854
+    /**
855
+     * get the incomplete count that shares parent $folder
856
+     *
857
+     * @param int $fileId the file id of the folder
858
+     * @return int
859
+     */
860
+    public function getIncompleteChildrenCount($fileId) {
861
+        if ($fileId > -1) {
862
+            $query = $this->getQueryBuilder();
863
+            $query->select($query->func()->count())
864
+                ->from('filecache')
865
+                ->whereParent($fileId)
866
+                ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
867
+
868
+            return (int)$query->execute()->fetchColumn();
869
+        }
870
+        return -1;
871
+    }
872
+
873
+    /**
874
+     * calculate the size of a folder and set it in the cache
875
+     *
876
+     * @param string $path
877
+     * @param array $entry (optional) meta data of the folder
878
+     * @return int
879
+     */
880
+    public function calculateFolderSize($path, $entry = null) {
881
+        $totalSize = 0;
882
+        if (is_null($entry) or !isset($entry['fileid'])) {
883
+            $entry = $this->get($path);
884
+        }
885
+        if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
886
+            $id = $entry['fileid'];
887
+
888
+            $query = $this->getQueryBuilder();
889
+            $query->selectAlias($query->func()->sum('size'), 'f1')
890
+                ->selectAlias($query->func()->min('size'), 'f2')
891
+                ->from('filecache')
892
+                ->whereStorageId()
893
+                ->whereParent($id);
894
+
895
+            if ($row = $query->execute()->fetch()) {
896
+                [$sum, $min] = array_values($row);
897
+                $sum = 0 + $sum;
898
+                $min = 0 + $min;
899
+                if ($min === -1) {
900
+                    $totalSize = $min;
901
+                } else {
902
+                    $totalSize = $sum;
903
+                }
904
+                if ($entry['size'] !== $totalSize) {
905
+                    $this->update($id, ['size' => $totalSize]);
906
+                }
907
+            }
908
+        }
909
+        return $totalSize;
910
+    }
911
+
912
+    /**
913
+     * get all file ids on the files on the storage
914
+     *
915
+     * @return int[]
916
+     */
917
+    public function getAll() {
918
+        $query = $this->getQueryBuilder();
919
+        $query->select('fileid')
920
+            ->from('filecache')
921
+            ->whereStorageId();
922
+
923
+        return array_map(function ($id) {
924
+            return (int)$id;
925
+        }, $query->execute()->fetchAll(\PDO::FETCH_COLUMN));
926
+    }
927
+
928
+    /**
929
+     * find a folder in the cache which has not been fully scanned
930
+     *
931
+     * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
932
+     * use the one with the highest id gives the best result with the background scanner, since that is most
933
+     * likely the folder where we stopped scanning previously
934
+     *
935
+     * @return string|bool the path of the folder or false when no folder matched
936
+     */
937
+    public function getIncomplete() {
938
+        $query = $this->getQueryBuilder();
939
+        $query->select('path')
940
+            ->from('filecache')
941
+            ->whereStorageId()
942
+            ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
943
+            ->orderBy('fileid', 'DESC');
944
+
945
+        return $query->execute()->fetchColumn();
946
+    }
947
+
948
+    /**
949
+     * get the path of a file on this storage by it's file id
950
+     *
951
+     * @param int $id the file id of the file or folder to search
952
+     * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
953
+     */
954
+    public function getPathById($id) {
955
+        $query = $this->getQueryBuilder();
956
+        $query->select('path')
957
+            ->from('filecache')
958
+            ->whereStorageId()
959
+            ->whereFileId($id);
960
+
961
+        $path = $query->execute()->fetchColumn();
962
+        return $path === false ? null : $path;
963
+    }
964
+
965
+    /**
966
+     * get the storage id of the storage for a file and the internal path of the file
967
+     * unlike getPathById this does not limit the search to files on this storage and
968
+     * instead does a global search in the cache table
969
+     *
970
+     * @param int $id
971
+     * @return array first element holding the storage id, second the path
972
+     * @deprecated use getPathById() instead
973
+     */
974
+    public static function getById($id) {
975
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
976
+        $query->select('path', 'storage')
977
+            ->from('filecache')
978
+            ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
979
+        if ($row = $query->execute()->fetch()) {
980
+            $numericId = $row['storage'];
981
+            $path = $row['path'];
982
+        } else {
983
+            return null;
984
+        }
985
+
986
+        if ($id = Storage::getStorageId($numericId)) {
987
+            return [$id, $path];
988
+        } else {
989
+            return null;
990
+        }
991
+    }
992
+
993
+    /**
994
+     * normalize the given path
995
+     *
996
+     * @param string $path
997
+     * @return string
998
+     */
999
+    public function normalize($path) {
1000
+        return trim(\OC_Util::normalizeUnicode($path), '/');
1001
+    }
1002 1002
 }
Please login to merge, or discard this patch.
lib/private/DB/Adapter.php 2 patches
Indentation   +95 added lines, -95 removed lines patch added patch discarded remove patch
@@ -38,108 +38,108 @@
 block discarded – undo
38 38
  */
39 39
 class Adapter {
40 40
 
41
-	/**
42
-	 * @var \OC\DB\Connection $conn
43
-	 */
44
-	protected $conn;
41
+    /**
42
+     * @var \OC\DB\Connection $conn
43
+     */
44
+    protected $conn;
45 45
 
46
-	public function __construct($conn) {
47
-		$this->conn = $conn;
48
-	}
46
+    public function __construct($conn) {
47
+        $this->conn = $conn;
48
+    }
49 49
 
50
-	/**
51
-	 * @param string $table name
52
-	 * @return int id of last insert statement
53
-	 */
54
-	public function lastInsertId($table) {
55
-		return $this->conn->realLastInsertId($table);
56
-	}
50
+    /**
51
+     * @param string $table name
52
+     * @return int id of last insert statement
53
+     */
54
+    public function lastInsertId($table) {
55
+        return $this->conn->realLastInsertId($table);
56
+    }
57 57
 
58
-	/**
59
-	 * @param string $statement that needs to be changed so the db can handle it
60
-	 * @return string changed statement
61
-	 */
62
-	public function fixupStatement($statement) {
63
-		return $statement;
64
-	}
58
+    /**
59
+     * @param string $statement that needs to be changed so the db can handle it
60
+     * @return string changed statement
61
+     */
62
+    public function fixupStatement($statement) {
63
+        return $statement;
64
+    }
65 65
 
66
-	/**
67
-	 * Create an exclusive read+write lock on a table
68
-	 *
69
-	 * @param string $tableName
70
-	 * @since 9.1.0
71
-	 */
72
-	public function lockTable($tableName) {
73
-		$this->conn->beginTransaction();
74
-		$this->conn->executeUpdate('LOCK TABLE `' .$tableName . '` IN EXCLUSIVE MODE');
75
-	}
66
+    /**
67
+     * Create an exclusive read+write lock on a table
68
+     *
69
+     * @param string $tableName
70
+     * @since 9.1.0
71
+     */
72
+    public function lockTable($tableName) {
73
+        $this->conn->beginTransaction();
74
+        $this->conn->executeUpdate('LOCK TABLE `' .$tableName . '` IN EXCLUSIVE MODE');
75
+    }
76 76
 
77
-	/**
78
-	 * Release a previous acquired lock again
79
-	 *
80
-	 * @since 9.1.0
81
-	 */
82
-	public function unlockTable() {
83
-		$this->conn->commit();
84
-	}
77
+    /**
78
+     * Release a previous acquired lock again
79
+     *
80
+     * @since 9.1.0
81
+     */
82
+    public function unlockTable() {
83
+        $this->conn->commit();
84
+    }
85 85
 
86
-	/**
87
-	 * Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
88
-	 * it is needed that there is also a unique constraint on the values. Then this method will
89
-	 * catch the exception and return 0.
90
-	 *
91
-	 * @param string $table The table name (will replace *PREFIX* with the actual prefix)
92
-	 * @param array $input data that should be inserted into the table  (column name => value)
93
-	 * @param array|null $compare List of values that should be checked for "if not exists"
94
-	 *				If this is null or an empty array, all keys of $input will be compared
95
-	 *				Please note: text fields (clob) must not be used in the compare array
96
-	 * @return int number of inserted rows
97
-	 * @throws \Doctrine\DBAL\DBALException
98
-	 * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
99
-	 */
100
-	public function insertIfNotExist($table, $input, array $compare = null) {
101
-		if (empty($compare)) {
102
-			$compare = array_keys($input);
103
-		}
104
-		$query = 'INSERT INTO `' .$table . '` (`'
105
-			. implode('`,`', array_keys($input)) . '`) SELECT '
106
-			. str_repeat('?,', count($input)-1).'? ' // Is there a prettier alternative?
107
-			. 'FROM `' . $table . '` WHERE ';
86
+    /**
87
+     * Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
88
+     * it is needed that there is also a unique constraint on the values. Then this method will
89
+     * catch the exception and return 0.
90
+     *
91
+     * @param string $table The table name (will replace *PREFIX* with the actual prefix)
92
+     * @param array $input data that should be inserted into the table  (column name => value)
93
+     * @param array|null $compare List of values that should be checked for "if not exists"
94
+     *				If this is null or an empty array, all keys of $input will be compared
95
+     *				Please note: text fields (clob) must not be used in the compare array
96
+     * @return int number of inserted rows
97
+     * @throws \Doctrine\DBAL\DBALException
98
+     * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
99
+     */
100
+    public function insertIfNotExist($table, $input, array $compare = null) {
101
+        if (empty($compare)) {
102
+            $compare = array_keys($input);
103
+        }
104
+        $query = 'INSERT INTO `' .$table . '` (`'
105
+            . implode('`,`', array_keys($input)) . '`) SELECT '
106
+            . str_repeat('?,', count($input)-1).'? ' // Is there a prettier alternative?
107
+            . 'FROM `' . $table . '` WHERE ';
108 108
 
109
-		$inserts = array_values($input);
110
-		foreach ($compare as $key) {
111
-			$query .= '`' . $key . '`';
112
-			if (is_null($input[$key])) {
113
-				$query .= ' IS NULL AND ';
114
-			} else {
115
-				$inserts[] = $input[$key];
116
-				$query .= ' = ? AND ';
117
-			}
118
-		}
119
-		$query = substr($query, 0, -5);
120
-		$query .= ' HAVING COUNT(*) = 0';
109
+        $inserts = array_values($input);
110
+        foreach ($compare as $key) {
111
+            $query .= '`' . $key . '`';
112
+            if (is_null($input[$key])) {
113
+                $query .= ' IS NULL AND ';
114
+            } else {
115
+                $inserts[] = $input[$key];
116
+                $query .= ' = ? AND ';
117
+            }
118
+        }
119
+        $query = substr($query, 0, -5);
120
+        $query .= ' HAVING COUNT(*) = 0';
121 121
 
122
-		try {
123
-			return $this->conn->executeUpdate($query, $inserts);
124
-		} catch (UniqueConstraintViolationException $e) {
125
-			// if this is thrown then a concurrent insert happened between the insert and the sub-select in the insert, that should have avoided it
126
-			// it's fine to ignore this then
127
-			//
128
-			// more discussions about this can be found at https://github.com/nextcloud/server/pull/12315
129
-			return 0;
130
-		}
131
-	}
122
+        try {
123
+            return $this->conn->executeUpdate($query, $inserts);
124
+        } catch (UniqueConstraintViolationException $e) {
125
+            // if this is thrown then a concurrent insert happened between the insert and the sub-select in the insert, that should have avoided it
126
+            // it's fine to ignore this then
127
+            //
128
+            // more discussions about this can be found at https://github.com/nextcloud/server/pull/12315
129
+            return 0;
130
+        }
131
+    }
132 132
 
133
-	public function insertIgnoreConflict(string $table,array $values) : int {
134
-		try {
135
-			$builder = $this->conn->getQueryBuilder();
136
-			$builder->insert($table);
137
-			foreach ($values as $key => $value) {
138
-				$builder->setValue($key, $builder->createNamedParameter($value));
139
-			}
140
-			return $builder->execute();
141
-		} catch (UniqueConstraintViolationException $e) {
142
-			return 0;
143
-		}
144
-	}
133
+    public function insertIgnoreConflict(string $table,array $values) : int {
134
+        try {
135
+            $builder = $this->conn->getQueryBuilder();
136
+            $builder->insert($table);
137
+            foreach ($values as $key => $value) {
138
+                $builder->setValue($key, $builder->createNamedParameter($value));
139
+            }
140
+            return $builder->execute();
141
+        } catch (UniqueConstraintViolationException $e) {
142
+            return 0;
143
+        }
144
+    }
145 145
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -71,7 +71,7 @@  discard block
 block discarded – undo
71 71
 	 */
72 72
 	public function lockTable($tableName) {
73 73
 		$this->conn->beginTransaction();
74
-		$this->conn->executeUpdate('LOCK TABLE `' .$tableName . '` IN EXCLUSIVE MODE');
74
+		$this->conn->executeUpdate('LOCK TABLE `'.$tableName.'` IN EXCLUSIVE MODE');
75 75
 	}
76 76
 
77 77
 	/**
@@ -101,14 +101,14 @@  discard block
 block discarded – undo
101 101
 		if (empty($compare)) {
102 102
 			$compare = array_keys($input);
103 103
 		}
104
-		$query = 'INSERT INTO `' .$table . '` (`'
105
-			. implode('`,`', array_keys($input)) . '`) SELECT '
106
-			. str_repeat('?,', count($input)-1).'? ' // Is there a prettier alternative?
107
-			. 'FROM `' . $table . '` WHERE ';
104
+		$query = 'INSERT INTO `'.$table.'` (`'
105
+			. implode('`,`', array_keys($input)).'`) SELECT '
106
+			. str_repeat('?,', count($input) - 1).'? ' // Is there a prettier alternative?
107
+			. 'FROM `'.$table.'` WHERE ';
108 108
 
109 109
 		$inserts = array_values($input);
110 110
 		foreach ($compare as $key) {
111
-			$query .= '`' . $key . '`';
111
+			$query .= '`'.$key.'`';
112 112
 			if (is_null($input[$key])) {
113 113
 				$query .= ' IS NULL AND ';
114 114
 			} else {
@@ -130,7 +130,7 @@  discard block
 block discarded – undo
130 130
 		}
131 131
 	}
132 132
 
133
-	public function insertIgnoreConflict(string $table,array $values) : int {
133
+	public function insertIgnoreConflict(string $table, array $values) : int {
134 134
 		try {
135 135
 			$builder = $this->conn->getQueryBuilder();
136 136
 			$builder->insert($table);
Please login to merge, or discard this patch.
lib/private/DB/Connection.php 1 patch
Indentation   +402 added lines, -402 removed lines patch added patch discarded remove patch
@@ -47,406 +47,406 @@
 block discarded – undo
47 47
 use OCP\PreConditionNotMetException;
48 48
 
49 49
 class Connection extends ReconnectWrapper implements IDBConnection {
50
-	/**
51
-	 * @var string $tablePrefix
52
-	 */
53
-	protected $tablePrefix;
54
-
55
-	/**
56
-	 * @var \OC\DB\Adapter $adapter
57
-	 */
58
-	protected $adapter;
59
-
60
-	protected $lockedTable = null;
61
-
62
-	public function connect() {
63
-		try {
64
-			return parent::connect();
65
-		} catch (DBALException $e) {
66
-			// throw a new exception to prevent leaking info from the stacktrace
67
-			throw new DBALException('Failed to connect to the database: ' . $e->getMessage(), $e->getCode());
68
-		}
69
-	}
70
-
71
-	/**
72
-	 * Returns a QueryBuilder for the connection.
73
-	 *
74
-	 * @return \OCP\DB\QueryBuilder\IQueryBuilder
75
-	 */
76
-	public function getQueryBuilder() {
77
-		return new QueryBuilder(
78
-			$this,
79
-			\OC::$server->getSystemConfig(),
80
-			\OC::$server->getLogger()
81
-		);
82
-	}
83
-
84
-	/**
85
-	 * Gets the QueryBuilder for the connection.
86
-	 *
87
-	 * @return \Doctrine\DBAL\Query\QueryBuilder
88
-	 * @deprecated please use $this->getQueryBuilder() instead
89
-	 */
90
-	public function createQueryBuilder() {
91
-		$backtrace = $this->getCallerBacktrace();
92
-		\OC::$server->getLogger()->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
93
-		return parent::createQueryBuilder();
94
-	}
95
-
96
-	/**
97
-	 * Gets the ExpressionBuilder for the connection.
98
-	 *
99
-	 * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
100
-	 * @deprecated please use $this->getQueryBuilder()->expr() instead
101
-	 */
102
-	public function getExpressionBuilder() {
103
-		$backtrace = $this->getCallerBacktrace();
104
-		\OC::$server->getLogger()->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
105
-		return parent::getExpressionBuilder();
106
-	}
107
-
108
-	/**
109
-	 * Get the file and line that called the method where `getCallerBacktrace()` was used
110
-	 *
111
-	 * @return string
112
-	 */
113
-	protected function getCallerBacktrace() {
114
-		$traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
115
-
116
-		// 0 is the method where we use `getCallerBacktrace`
117
-		// 1 is the target method which uses the method we want to log
118
-		if (isset($traces[1])) {
119
-			return $traces[1]['file'] . ':' . $traces[1]['line'];
120
-		}
121
-
122
-		return '';
123
-	}
124
-
125
-	/**
126
-	 * @return string
127
-	 */
128
-	public function getPrefix() {
129
-		return $this->tablePrefix;
130
-	}
131
-
132
-	/**
133
-	 * Initializes a new instance of the Connection class.
134
-	 *
135
-	 * @param array $params  The connection parameters.
136
-	 * @param \Doctrine\DBAL\Driver $driver
137
-	 * @param \Doctrine\DBAL\Configuration $config
138
-	 * @param \Doctrine\Common\EventManager $eventManager
139
-	 * @throws \Exception
140
-	 */
141
-	public function __construct(array $params, Driver $driver, Configuration $config = null,
142
-		EventManager $eventManager = null) {
143
-		if (!isset($params['adapter'])) {
144
-			throw new \Exception('adapter not set');
145
-		}
146
-		if (!isset($params['tablePrefix'])) {
147
-			throw new \Exception('tablePrefix not set');
148
-		}
149
-		parent::__construct($params, $driver, $config, $eventManager);
150
-		$this->adapter = new $params['adapter']($this);
151
-		$this->tablePrefix = $params['tablePrefix'];
152
-	}
153
-
154
-	/**
155
-	 * Prepares an SQL statement.
156
-	 *
157
-	 * @param string $statement The SQL statement to prepare.
158
-	 * @param int $limit
159
-	 * @param int $offset
160
-	 * @return \Doctrine\DBAL\Driver\Statement The prepared statement.
161
-	 */
162
-	public function prepare($statement, $limit=null, $offset=null) {
163
-		if ($limit === -1) {
164
-			$limit = null;
165
-		}
166
-		if (!is_null($limit)) {
167
-			$platform = $this->getDatabasePlatform();
168
-			$statement = $platform->modifyLimitQuery($statement, $limit, $offset);
169
-		}
170
-		$statement = $this->replaceTablePrefix($statement);
171
-		$statement = $this->adapter->fixupStatement($statement);
172
-
173
-		return parent::prepare($statement);
174
-	}
175
-
176
-	/**
177
-	 * Executes an, optionally parametrized, SQL query.
178
-	 *
179
-	 * If the query is parametrized, a prepared statement is used.
180
-	 * If an SQLLogger is configured, the execution is logged.
181
-	 *
182
-	 * @param string                                      $query  The SQL query to execute.
183
-	 * @param array                                       $params The parameters to bind to the query, if any.
184
-	 * @param array                                       $types  The types the previous parameters are in.
185
-	 * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp    The query cache profile, optional.
186
-	 *
187
-	 * @return \Doctrine\DBAL\Driver\Statement The executed statement.
188
-	 *
189
-	 * @throws \Doctrine\DBAL\DBALException
190
-	 */
191
-	public function executeQuery($query, array $params = [], $types = [], QueryCacheProfile $qcp = null) {
192
-		$query = $this->replaceTablePrefix($query);
193
-		$query = $this->adapter->fixupStatement($query);
194
-		return parent::executeQuery($query, $params, $types, $qcp);
195
-	}
196
-
197
-	/**
198
-	 * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
199
-	 * and returns the number of affected rows.
200
-	 *
201
-	 * This method supports PDO binding types as well as DBAL mapping types.
202
-	 *
203
-	 * @param string $query  The SQL query.
204
-	 * @param array  $params The query parameters.
205
-	 * @param array  $types  The parameter types.
206
-	 *
207
-	 * @return integer The number of affected rows.
208
-	 *
209
-	 * @throws \Doctrine\DBAL\DBALException
210
-	 */
211
-	public function executeUpdate($query, array $params = [], array $types = []) {
212
-		$query = $this->replaceTablePrefix($query);
213
-		$query = $this->adapter->fixupStatement($query);
214
-		return parent::executeUpdate($query, $params, $types);
215
-	}
216
-
217
-	/**
218
-	 * Returns the ID of the last inserted row, or the last value from a sequence object,
219
-	 * depending on the underlying driver.
220
-	 *
221
-	 * Note: This method may not return a meaningful or consistent result across different drivers,
222
-	 * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
223
-	 * columns or sequences.
224
-	 *
225
-	 * @param string $seqName Name of the sequence object from which the ID should be returned.
226
-	 * @return string A string representation of the last inserted ID.
227
-	 */
228
-	public function lastInsertId($seqName = null) {
229
-		if ($seqName) {
230
-			$seqName = $this->replaceTablePrefix($seqName);
231
-		}
232
-		return $this->adapter->lastInsertId($seqName);
233
-	}
234
-
235
-	// internal use
236
-	public function realLastInsertId($seqName = null) {
237
-		return parent::lastInsertId($seqName);
238
-	}
239
-
240
-	/**
241
-	 * Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
242
-	 * it is needed that there is also a unique constraint on the values. Then this method will
243
-	 * catch the exception and return 0.
244
-	 *
245
-	 * @param string $table The table name (will replace *PREFIX* with the actual prefix)
246
-	 * @param array $input data that should be inserted into the table  (column name => value)
247
-	 * @param array|null $compare List of values that should be checked for "if not exists"
248
-	 *				If this is null or an empty array, all keys of $input will be compared
249
-	 *				Please note: text fields (clob) must not be used in the compare array
250
-	 * @return int number of inserted rows
251
-	 * @throws \Doctrine\DBAL\DBALException
252
-	 * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
253
-	 */
254
-	public function insertIfNotExist($table, $input, array $compare = null) {
255
-		return $this->adapter->insertIfNotExist($table, $input, $compare);
256
-	}
257
-
258
-	public function insertIgnoreConflict(string $table, array $values) : int {
259
-		return $this->adapter->insertIgnoreConflict($table, $values);
260
-	}
261
-
262
-	private function getType($value) {
263
-		if (is_bool($value)) {
264
-			return IQueryBuilder::PARAM_BOOL;
265
-		} elseif (is_int($value)) {
266
-			return IQueryBuilder::PARAM_INT;
267
-		} else {
268
-			return IQueryBuilder::PARAM_STR;
269
-		}
270
-	}
271
-
272
-	/**
273
-	 * Insert or update a row value
274
-	 *
275
-	 * @param string $table
276
-	 * @param array $keys (column name => value)
277
-	 * @param array $values (column name => value)
278
-	 * @param array $updatePreconditionValues ensure values match preconditions (column name => value)
279
-	 * @return int number of new rows
280
-	 * @throws \Doctrine\DBAL\DBALException
281
-	 * @throws PreConditionNotMetException
282
-	 */
283
-	public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) {
284
-		try {
285
-			$insertQb = $this->getQueryBuilder();
286
-			$insertQb->insert($table)
287
-				->values(
288
-					array_map(function ($value) use ($insertQb) {
289
-						return $insertQb->createNamedParameter($value, $this->getType($value));
290
-					}, array_merge($keys, $values))
291
-				);
292
-			return $insertQb->execute();
293
-		} catch (ConstraintViolationException $e) {
294
-			// value already exists, try update
295
-			$updateQb = $this->getQueryBuilder();
296
-			$updateQb->update($table);
297
-			foreach ($values as $name => $value) {
298
-				$updateQb->set($name, $updateQb->createNamedParameter($value, $this->getType($value)));
299
-			}
300
-			$where = $updateQb->expr()->andX();
301
-			$whereValues = array_merge($keys, $updatePreconditionValues);
302
-			foreach ($whereValues as $name => $value) {
303
-				$where->add($updateQb->expr()->eq(
304
-					$name,
305
-					$updateQb->createNamedParameter($value, $this->getType($value)),
306
-					$this->getType($value)
307
-				));
308
-			}
309
-			$updateQb->where($where);
310
-			$affected = $updateQb->execute();
311
-
312
-			if ($affected === 0 && !empty($updatePreconditionValues)) {
313
-				throw new PreConditionNotMetException();
314
-			}
315
-
316
-			return 0;
317
-		}
318
-	}
319
-
320
-	/**
321
-	 * Create an exclusive read+write lock on a table
322
-	 *
323
-	 * @param string $tableName
324
-	 * @throws \BadMethodCallException When trying to acquire a second lock
325
-	 * @since 9.1.0
326
-	 */
327
-	public function lockTable($tableName) {
328
-		if ($this->lockedTable !== null) {
329
-			throw new \BadMethodCallException('Can not lock a new table until the previous lock is released.');
330
-		}
331
-
332
-		$tableName = $this->tablePrefix . $tableName;
333
-		$this->lockedTable = $tableName;
334
-		$this->adapter->lockTable($tableName);
335
-	}
336
-
337
-	/**
338
-	 * Release a previous acquired lock again
339
-	 *
340
-	 * @since 9.1.0
341
-	 */
342
-	public function unlockTable() {
343
-		$this->adapter->unlockTable();
344
-		$this->lockedTable = null;
345
-	}
346
-
347
-	/**
348
-	 * returns the error code and message as a string for logging
349
-	 * works with DoctrineException
350
-	 * @return string
351
-	 */
352
-	public function getError() {
353
-		$msg = $this->errorCode() . ': ';
354
-		$errorInfo = $this->errorInfo();
355
-		if (is_array($errorInfo)) {
356
-			$msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
357
-			$msg .= 'Driver Code = '.$errorInfo[1] . ', ';
358
-			$msg .= 'Driver Message = '.$errorInfo[2];
359
-		}
360
-		return $msg;
361
-	}
362
-
363
-	/**
364
-	 * Drop a table from the database if it exists
365
-	 *
366
-	 * @param string $table table name without the prefix
367
-	 */
368
-	public function dropTable($table) {
369
-		$table = $this->tablePrefix . trim($table);
370
-		$schema = $this->getSchemaManager();
371
-		if ($schema->tablesExist([$table])) {
372
-			$schema->dropTable($table);
373
-		}
374
-	}
375
-
376
-	/**
377
-	 * Check if a table exists
378
-	 *
379
-	 * @param string $table table name without the prefix
380
-	 * @return bool
381
-	 */
382
-	public function tableExists($table) {
383
-		$table = $this->tablePrefix . trim($table);
384
-		$schema = $this->getSchemaManager();
385
-		return $schema->tablesExist([$table]);
386
-	}
387
-
388
-	// internal use
389
-	/**
390
-	 * @param string $statement
391
-	 * @return string
392
-	 */
393
-	protected function replaceTablePrefix($statement) {
394
-		return str_replace('*PREFIX*', $this->tablePrefix, $statement);
395
-	}
396
-
397
-	/**
398
-	 * Check if a transaction is active
399
-	 *
400
-	 * @return bool
401
-	 * @since 8.2.0
402
-	 */
403
-	public function inTransaction() {
404
-		return $this->getTransactionNestingLevel() > 0;
405
-	}
406
-
407
-	/**
408
-	 * Escape a parameter to be used in a LIKE query
409
-	 *
410
-	 * @param string $param
411
-	 * @return string
412
-	 */
413
-	public function escapeLikeParameter($param) {
414
-		return addcslashes($param, '\\_%');
415
-	}
416
-
417
-	/**
418
-	 * Check whether or not the current database support 4byte wide unicode
419
-	 *
420
-	 * @return bool
421
-	 * @since 11.0.0
422
-	 */
423
-	public function supports4ByteText() {
424
-		if (!$this->getDatabasePlatform() instanceof MySqlPlatform) {
425
-			return true;
426
-		}
427
-		return $this->getParams()['charset'] === 'utf8mb4';
428
-	}
429
-
430
-
431
-	/**
432
-	 * Create the schema of the connected database
433
-	 *
434
-	 * @return Schema
435
-	 */
436
-	public function createSchema() {
437
-		$schemaManager = new MDB2SchemaManager($this);
438
-		$migrator = $schemaManager->getMigrator();
439
-		return $migrator->createSchema();
440
-	}
441
-
442
-	/**
443
-	 * Migrate the database to the given schema
444
-	 *
445
-	 * @param Schema $toSchema
446
-	 */
447
-	public function migrateToSchema(Schema $toSchema) {
448
-		$schemaManager = new MDB2SchemaManager($this);
449
-		$migrator = $schemaManager->getMigrator();
450
-		$migrator->migrate($toSchema);
451
-	}
50
+    /**
51
+     * @var string $tablePrefix
52
+     */
53
+    protected $tablePrefix;
54
+
55
+    /**
56
+     * @var \OC\DB\Adapter $adapter
57
+     */
58
+    protected $adapter;
59
+
60
+    protected $lockedTable = null;
61
+
62
+    public function connect() {
63
+        try {
64
+            return parent::connect();
65
+        } catch (DBALException $e) {
66
+            // throw a new exception to prevent leaking info from the stacktrace
67
+            throw new DBALException('Failed to connect to the database: ' . $e->getMessage(), $e->getCode());
68
+        }
69
+    }
70
+
71
+    /**
72
+     * Returns a QueryBuilder for the connection.
73
+     *
74
+     * @return \OCP\DB\QueryBuilder\IQueryBuilder
75
+     */
76
+    public function getQueryBuilder() {
77
+        return new QueryBuilder(
78
+            $this,
79
+            \OC::$server->getSystemConfig(),
80
+            \OC::$server->getLogger()
81
+        );
82
+    }
83
+
84
+    /**
85
+     * Gets the QueryBuilder for the connection.
86
+     *
87
+     * @return \Doctrine\DBAL\Query\QueryBuilder
88
+     * @deprecated please use $this->getQueryBuilder() instead
89
+     */
90
+    public function createQueryBuilder() {
91
+        $backtrace = $this->getCallerBacktrace();
92
+        \OC::$server->getLogger()->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
93
+        return parent::createQueryBuilder();
94
+    }
95
+
96
+    /**
97
+     * Gets the ExpressionBuilder for the connection.
98
+     *
99
+     * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
100
+     * @deprecated please use $this->getQueryBuilder()->expr() instead
101
+     */
102
+    public function getExpressionBuilder() {
103
+        $backtrace = $this->getCallerBacktrace();
104
+        \OC::$server->getLogger()->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
105
+        return parent::getExpressionBuilder();
106
+    }
107
+
108
+    /**
109
+     * Get the file and line that called the method where `getCallerBacktrace()` was used
110
+     *
111
+     * @return string
112
+     */
113
+    protected function getCallerBacktrace() {
114
+        $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
115
+
116
+        // 0 is the method where we use `getCallerBacktrace`
117
+        // 1 is the target method which uses the method we want to log
118
+        if (isset($traces[1])) {
119
+            return $traces[1]['file'] . ':' . $traces[1]['line'];
120
+        }
121
+
122
+        return '';
123
+    }
124
+
125
+    /**
126
+     * @return string
127
+     */
128
+    public function getPrefix() {
129
+        return $this->tablePrefix;
130
+    }
131
+
132
+    /**
133
+     * Initializes a new instance of the Connection class.
134
+     *
135
+     * @param array $params  The connection parameters.
136
+     * @param \Doctrine\DBAL\Driver $driver
137
+     * @param \Doctrine\DBAL\Configuration $config
138
+     * @param \Doctrine\Common\EventManager $eventManager
139
+     * @throws \Exception
140
+     */
141
+    public function __construct(array $params, Driver $driver, Configuration $config = null,
142
+        EventManager $eventManager = null) {
143
+        if (!isset($params['adapter'])) {
144
+            throw new \Exception('adapter not set');
145
+        }
146
+        if (!isset($params['tablePrefix'])) {
147
+            throw new \Exception('tablePrefix not set');
148
+        }
149
+        parent::__construct($params, $driver, $config, $eventManager);
150
+        $this->adapter = new $params['adapter']($this);
151
+        $this->tablePrefix = $params['tablePrefix'];
152
+    }
153
+
154
+    /**
155
+     * Prepares an SQL statement.
156
+     *
157
+     * @param string $statement The SQL statement to prepare.
158
+     * @param int $limit
159
+     * @param int $offset
160
+     * @return \Doctrine\DBAL\Driver\Statement The prepared statement.
161
+     */
162
+    public function prepare($statement, $limit=null, $offset=null) {
163
+        if ($limit === -1) {
164
+            $limit = null;
165
+        }
166
+        if (!is_null($limit)) {
167
+            $platform = $this->getDatabasePlatform();
168
+            $statement = $platform->modifyLimitQuery($statement, $limit, $offset);
169
+        }
170
+        $statement = $this->replaceTablePrefix($statement);
171
+        $statement = $this->adapter->fixupStatement($statement);
172
+
173
+        return parent::prepare($statement);
174
+    }
175
+
176
+    /**
177
+     * Executes an, optionally parametrized, SQL query.
178
+     *
179
+     * If the query is parametrized, a prepared statement is used.
180
+     * If an SQLLogger is configured, the execution is logged.
181
+     *
182
+     * @param string                                      $query  The SQL query to execute.
183
+     * @param array                                       $params The parameters to bind to the query, if any.
184
+     * @param array                                       $types  The types the previous parameters are in.
185
+     * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp    The query cache profile, optional.
186
+     *
187
+     * @return \Doctrine\DBAL\Driver\Statement The executed statement.
188
+     *
189
+     * @throws \Doctrine\DBAL\DBALException
190
+     */
191
+    public function executeQuery($query, array $params = [], $types = [], QueryCacheProfile $qcp = null) {
192
+        $query = $this->replaceTablePrefix($query);
193
+        $query = $this->adapter->fixupStatement($query);
194
+        return parent::executeQuery($query, $params, $types, $qcp);
195
+    }
196
+
197
+    /**
198
+     * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
199
+     * and returns the number of affected rows.
200
+     *
201
+     * This method supports PDO binding types as well as DBAL mapping types.
202
+     *
203
+     * @param string $query  The SQL query.
204
+     * @param array  $params The query parameters.
205
+     * @param array  $types  The parameter types.
206
+     *
207
+     * @return integer The number of affected rows.
208
+     *
209
+     * @throws \Doctrine\DBAL\DBALException
210
+     */
211
+    public function executeUpdate($query, array $params = [], array $types = []) {
212
+        $query = $this->replaceTablePrefix($query);
213
+        $query = $this->adapter->fixupStatement($query);
214
+        return parent::executeUpdate($query, $params, $types);
215
+    }
216
+
217
+    /**
218
+     * Returns the ID of the last inserted row, or the last value from a sequence object,
219
+     * depending on the underlying driver.
220
+     *
221
+     * Note: This method may not return a meaningful or consistent result across different drivers,
222
+     * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
223
+     * columns or sequences.
224
+     *
225
+     * @param string $seqName Name of the sequence object from which the ID should be returned.
226
+     * @return string A string representation of the last inserted ID.
227
+     */
228
+    public function lastInsertId($seqName = null) {
229
+        if ($seqName) {
230
+            $seqName = $this->replaceTablePrefix($seqName);
231
+        }
232
+        return $this->adapter->lastInsertId($seqName);
233
+    }
234
+
235
+    // internal use
236
+    public function realLastInsertId($seqName = null) {
237
+        return parent::lastInsertId($seqName);
238
+    }
239
+
240
+    /**
241
+     * Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
242
+     * it is needed that there is also a unique constraint on the values. Then this method will
243
+     * catch the exception and return 0.
244
+     *
245
+     * @param string $table The table name (will replace *PREFIX* with the actual prefix)
246
+     * @param array $input data that should be inserted into the table  (column name => value)
247
+     * @param array|null $compare List of values that should be checked for "if not exists"
248
+     *				If this is null or an empty array, all keys of $input will be compared
249
+     *				Please note: text fields (clob) must not be used in the compare array
250
+     * @return int number of inserted rows
251
+     * @throws \Doctrine\DBAL\DBALException
252
+     * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
253
+     */
254
+    public function insertIfNotExist($table, $input, array $compare = null) {
255
+        return $this->adapter->insertIfNotExist($table, $input, $compare);
256
+    }
257
+
258
+    public function insertIgnoreConflict(string $table, array $values) : int {
259
+        return $this->adapter->insertIgnoreConflict($table, $values);
260
+    }
261
+
262
+    private function getType($value) {
263
+        if (is_bool($value)) {
264
+            return IQueryBuilder::PARAM_BOOL;
265
+        } elseif (is_int($value)) {
266
+            return IQueryBuilder::PARAM_INT;
267
+        } else {
268
+            return IQueryBuilder::PARAM_STR;
269
+        }
270
+    }
271
+
272
+    /**
273
+     * Insert or update a row value
274
+     *
275
+     * @param string $table
276
+     * @param array $keys (column name => value)
277
+     * @param array $values (column name => value)
278
+     * @param array $updatePreconditionValues ensure values match preconditions (column name => value)
279
+     * @return int number of new rows
280
+     * @throws \Doctrine\DBAL\DBALException
281
+     * @throws PreConditionNotMetException
282
+     */
283
+    public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) {
284
+        try {
285
+            $insertQb = $this->getQueryBuilder();
286
+            $insertQb->insert($table)
287
+                ->values(
288
+                    array_map(function ($value) use ($insertQb) {
289
+                        return $insertQb->createNamedParameter($value, $this->getType($value));
290
+                    }, array_merge($keys, $values))
291
+                );
292
+            return $insertQb->execute();
293
+        } catch (ConstraintViolationException $e) {
294
+            // value already exists, try update
295
+            $updateQb = $this->getQueryBuilder();
296
+            $updateQb->update($table);
297
+            foreach ($values as $name => $value) {
298
+                $updateQb->set($name, $updateQb->createNamedParameter($value, $this->getType($value)));
299
+            }
300
+            $where = $updateQb->expr()->andX();
301
+            $whereValues = array_merge($keys, $updatePreconditionValues);
302
+            foreach ($whereValues as $name => $value) {
303
+                $where->add($updateQb->expr()->eq(
304
+                    $name,
305
+                    $updateQb->createNamedParameter($value, $this->getType($value)),
306
+                    $this->getType($value)
307
+                ));
308
+            }
309
+            $updateQb->where($where);
310
+            $affected = $updateQb->execute();
311
+
312
+            if ($affected === 0 && !empty($updatePreconditionValues)) {
313
+                throw new PreConditionNotMetException();
314
+            }
315
+
316
+            return 0;
317
+        }
318
+    }
319
+
320
+    /**
321
+     * Create an exclusive read+write lock on a table
322
+     *
323
+     * @param string $tableName
324
+     * @throws \BadMethodCallException When trying to acquire a second lock
325
+     * @since 9.1.0
326
+     */
327
+    public function lockTable($tableName) {
328
+        if ($this->lockedTable !== null) {
329
+            throw new \BadMethodCallException('Can not lock a new table until the previous lock is released.');
330
+        }
331
+
332
+        $tableName = $this->tablePrefix . $tableName;
333
+        $this->lockedTable = $tableName;
334
+        $this->adapter->lockTable($tableName);
335
+    }
336
+
337
+    /**
338
+     * Release a previous acquired lock again
339
+     *
340
+     * @since 9.1.0
341
+     */
342
+    public function unlockTable() {
343
+        $this->adapter->unlockTable();
344
+        $this->lockedTable = null;
345
+    }
346
+
347
+    /**
348
+     * returns the error code and message as a string for logging
349
+     * works with DoctrineException
350
+     * @return string
351
+     */
352
+    public function getError() {
353
+        $msg = $this->errorCode() . ': ';
354
+        $errorInfo = $this->errorInfo();
355
+        if (is_array($errorInfo)) {
356
+            $msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
357
+            $msg .= 'Driver Code = '.$errorInfo[1] . ', ';
358
+            $msg .= 'Driver Message = '.$errorInfo[2];
359
+        }
360
+        return $msg;
361
+    }
362
+
363
+    /**
364
+     * Drop a table from the database if it exists
365
+     *
366
+     * @param string $table table name without the prefix
367
+     */
368
+    public function dropTable($table) {
369
+        $table = $this->tablePrefix . trim($table);
370
+        $schema = $this->getSchemaManager();
371
+        if ($schema->tablesExist([$table])) {
372
+            $schema->dropTable($table);
373
+        }
374
+    }
375
+
376
+    /**
377
+     * Check if a table exists
378
+     *
379
+     * @param string $table table name without the prefix
380
+     * @return bool
381
+     */
382
+    public function tableExists($table) {
383
+        $table = $this->tablePrefix . trim($table);
384
+        $schema = $this->getSchemaManager();
385
+        return $schema->tablesExist([$table]);
386
+    }
387
+
388
+    // internal use
389
+    /**
390
+     * @param string $statement
391
+     * @return string
392
+     */
393
+    protected function replaceTablePrefix($statement) {
394
+        return str_replace('*PREFIX*', $this->tablePrefix, $statement);
395
+    }
396
+
397
+    /**
398
+     * Check if a transaction is active
399
+     *
400
+     * @return bool
401
+     * @since 8.2.0
402
+     */
403
+    public function inTransaction() {
404
+        return $this->getTransactionNestingLevel() > 0;
405
+    }
406
+
407
+    /**
408
+     * Escape a parameter to be used in a LIKE query
409
+     *
410
+     * @param string $param
411
+     * @return string
412
+     */
413
+    public function escapeLikeParameter($param) {
414
+        return addcslashes($param, '\\_%');
415
+    }
416
+
417
+    /**
418
+     * Check whether or not the current database support 4byte wide unicode
419
+     *
420
+     * @return bool
421
+     * @since 11.0.0
422
+     */
423
+    public function supports4ByteText() {
424
+        if (!$this->getDatabasePlatform() instanceof MySqlPlatform) {
425
+            return true;
426
+        }
427
+        return $this->getParams()['charset'] === 'utf8mb4';
428
+    }
429
+
430
+
431
+    /**
432
+     * Create the schema of the connected database
433
+     *
434
+     * @return Schema
435
+     */
436
+    public function createSchema() {
437
+        $schemaManager = new MDB2SchemaManager($this);
438
+        $migrator = $schemaManager->getMigrator();
439
+        return $migrator->createSchema();
440
+    }
441
+
442
+    /**
443
+     * Migrate the database to the given schema
444
+     *
445
+     * @param Schema $toSchema
446
+     */
447
+    public function migrateToSchema(Schema $toSchema) {
448
+        $schemaManager = new MDB2SchemaManager($this);
449
+        $migrator = $schemaManager->getMigrator();
450
+        $migrator->migrate($toSchema);
451
+    }
452 452
 }
Please login to merge, or discard this patch.
lib/private/DB/AdapterPgSql.php 2 patches
Indentation   +32 added lines, -32 removed lines patch added patch discarded remove patch
@@ -28,43 +28,43 @@
 block discarded – undo
28 28
 namespace OC\DB;
29 29
 
30 30
 class AdapterPgSql extends Adapter {
31
-	protected $compatModePre9_5 = null;
31
+    protected $compatModePre9_5 = null;
32 32
 
33
-	public function lastInsertId($table) {
34
-		return $this->conn->fetchColumn('SELECT lastval()');
35
-	}
33
+    public function lastInsertId($table) {
34
+        return $this->conn->fetchColumn('SELECT lastval()');
35
+    }
36 36
 
37
-	public const UNIX_TIMESTAMP_REPLACEMENT = 'cast(extract(epoch from current_timestamp) as integer)';
38
-	public function fixupStatement($statement) {
39
-		$statement = str_replace('`', '"', $statement);
40
-		$statement = str_ireplace('UNIX_TIMESTAMP()', self::UNIX_TIMESTAMP_REPLACEMENT, $statement);
41
-		return $statement;
42
-	}
37
+    public const UNIX_TIMESTAMP_REPLACEMENT = 'cast(extract(epoch from current_timestamp) as integer)';
38
+    public function fixupStatement($statement) {
39
+        $statement = str_replace('`', '"', $statement);
40
+        $statement = str_ireplace('UNIX_TIMESTAMP()', self::UNIX_TIMESTAMP_REPLACEMENT, $statement);
41
+        return $statement;
42
+    }
43 43
 
44
-	public function insertIgnoreConflict(string $table,array $values) : int {
45
-		if ($this->isPre9_5CompatMode() === true) {
46
-			return parent::insertIgnoreConflict($table, $values);
47
-		}
44
+    public function insertIgnoreConflict(string $table,array $values) : int {
45
+        if ($this->isPre9_5CompatMode() === true) {
46
+            return parent::insertIgnoreConflict($table, $values);
47
+        }
48 48
 
49
-		// "upsert" is only available since PgSQL 9.5, but the generic way
50
-		// would leave error logs in the DB.
51
-		$builder = $this->conn->getQueryBuilder();
52
-		$builder->insert($table);
53
-		foreach ($values as $key => $value) {
54
-			$builder->setValue($key, $builder->createNamedParameter($value));
55
-		}
56
-		$queryString = $builder->getSQL() . ' ON CONFLICT DO NOTHING';
57
-		return $this->conn->executeUpdate($queryString, $builder->getParameters(), $builder->getParameterTypes());
58
-	}
49
+        // "upsert" is only available since PgSQL 9.5, but the generic way
50
+        // would leave error logs in the DB.
51
+        $builder = $this->conn->getQueryBuilder();
52
+        $builder->insert($table);
53
+        foreach ($values as $key => $value) {
54
+            $builder->setValue($key, $builder->createNamedParameter($value));
55
+        }
56
+        $queryString = $builder->getSQL() . ' ON CONFLICT DO NOTHING';
57
+        return $this->conn->executeUpdate($queryString, $builder->getParameters(), $builder->getParameterTypes());
58
+    }
59 59
 
60
-	protected function isPre9_5CompatMode(): bool {
61
-		if ($this->compatModePre9_5 !== null) {
62
-			return $this->compatModePre9_5;
63
-		}
60
+    protected function isPre9_5CompatMode(): bool {
61
+        if ($this->compatModePre9_5 !== null) {
62
+            return $this->compatModePre9_5;
63
+        }
64 64
 
65
-		$version = $this->conn->fetchColumn('SHOW SERVER_VERSION');
66
-		$this->compatModePre9_5 = version_compare($version, '9.5', '<');
65
+        $version = $this->conn->fetchColumn('SHOW SERVER_VERSION');
66
+        $this->compatModePre9_5 = version_compare($version, '9.5', '<');
67 67
 
68
-		return $this->compatModePre9_5;
69
-	}
68
+        return $this->compatModePre9_5;
69
+    }
70 70
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -41,7 +41,7 @@  discard block
 block discarded – undo
41 41
 		return $statement;
42 42
 	}
43 43
 
44
-	public function insertIgnoreConflict(string $table,array $values) : int {
44
+	public function insertIgnoreConflict(string $table, array $values) : int {
45 45
 		if ($this->isPre9_5CompatMode() === true) {
46 46
 			return parent::insertIgnoreConflict($table, $values);
47 47
 		}
@@ -53,7 +53,7 @@  discard block
 block discarded – undo
53 53
 		foreach ($values as $key => $value) {
54 54
 			$builder->setValue($key, $builder->createNamedParameter($value));
55 55
 		}
56
-		$queryString = $builder->getSQL() . ' ON CONFLICT DO NOTHING';
56
+		$queryString = $builder->getSQL().' ON CONFLICT DO NOTHING';
57 57
 		return $this->conn->executeUpdate($queryString, $builder->getParameters(), $builder->getParameterTypes());
58 58
 	}
59 59
 
Please login to merge, or discard this patch.
lib/private/Repair/RepairInvalidShares.php 1 patch
Indentation   +85 added lines, -85 removed lines patch added patch discarded remove patch
@@ -33,89 +33,89 @@
 block discarded – undo
33 33
  * Repairs shares with invalid data
34 34
  */
35 35
 class RepairInvalidShares implements IRepairStep {
36
-	public const CHUNK_SIZE = 200;
37
-
38
-	/** @var \OCP\IConfig */
39
-	protected $config;
40
-
41
-	/** @var \OCP\IDBConnection */
42
-	protected $connection;
43
-
44
-	/**
45
-	 * @param \OCP\IConfig $config
46
-	 * @param \OCP\IDBConnection $connection
47
-	 */
48
-	public function __construct($config, $connection) {
49
-		$this->connection = $connection;
50
-		$this->config = $config;
51
-	}
52
-
53
-	public function getName() {
54
-		return 'Repair invalid shares';
55
-	}
56
-
57
-	/**
58
-	 * Adjust file share permissions
59
-	 */
60
-	private function adjustFileSharePermissions(IOutput $out) {
61
-		$mask = \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_SHARE;
62
-		$builder = $this->connection->getQueryBuilder();
63
-
64
-		$permsFunc = $builder->expr()->bitwiseAnd('permissions', $mask);
65
-		$builder
66
-			->update('share')
67
-			->set('permissions', $permsFunc)
68
-			->where($builder->expr()->eq('item_type', $builder->expr()->literal('file')))
69
-			->andWhere($builder->expr()->neq('permissions', $permsFunc));
70
-
71
-		$updatedEntries = $builder->execute();
72
-		if ($updatedEntries > 0) {
73
-			$out->info('Fixed file share permissions for ' . $updatedEntries . ' shares');
74
-		}
75
-	}
76
-
77
-	/**
78
-	 * Remove shares where the parent share does not exist anymore
79
-	 */
80
-	private function removeSharesNonExistingParent(IOutput $out) {
81
-		$deletedEntries = 0;
82
-
83
-		$query = $this->connection->getQueryBuilder();
84
-		$query->select('s1.parent')
85
-			->from('share', 's1')
86
-			->where($query->expr()->isNotNull('s1.parent'))
87
-				->andWhere($query->expr()->isNull('s2.id'))
88
-			->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id'))
89
-			->groupBy('s1.parent')
90
-			->setMaxResults(self::CHUNK_SIZE);
91
-
92
-		$deleteQuery = $this->connection->getQueryBuilder();
93
-		$deleteQuery->delete('share')
94
-			->where($deleteQuery->expr()->eq('parent', $deleteQuery->createParameter('parent')));
95
-
96
-		$deletedInLastChunk = self::CHUNK_SIZE;
97
-		while ($deletedInLastChunk === self::CHUNK_SIZE) {
98
-			$deletedInLastChunk = 0;
99
-			$result = $query->execute();
100
-			while ($row = $result->fetch()) {
101
-				$deletedInLastChunk++;
102
-				$deletedEntries += $deleteQuery->setParameter('parent', (int) $row['parent'])
103
-					->execute();
104
-			}
105
-			$result->closeCursor();
106
-		}
107
-
108
-		if ($deletedEntries) {
109
-			$out->info('Removed ' . $deletedEntries . ' shares where the parent did not exist');
110
-		}
111
-	}
112
-
113
-	public function run(IOutput $out) {
114
-		$ocVersionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0');
115
-		if (version_compare($ocVersionFromBeforeUpdate, '12.0.0.11', '<')) {
116
-			$this->adjustFileSharePermissions($out);
117
-		}
118
-
119
-		$this->removeSharesNonExistingParent($out);
120
-	}
36
+    public const CHUNK_SIZE = 200;
37
+
38
+    /** @var \OCP\IConfig */
39
+    protected $config;
40
+
41
+    /** @var \OCP\IDBConnection */
42
+    protected $connection;
43
+
44
+    /**
45
+     * @param \OCP\IConfig $config
46
+     * @param \OCP\IDBConnection $connection
47
+     */
48
+    public function __construct($config, $connection) {
49
+        $this->connection = $connection;
50
+        $this->config = $config;
51
+    }
52
+
53
+    public function getName() {
54
+        return 'Repair invalid shares';
55
+    }
56
+
57
+    /**
58
+     * Adjust file share permissions
59
+     */
60
+    private function adjustFileSharePermissions(IOutput $out) {
61
+        $mask = \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_SHARE;
62
+        $builder = $this->connection->getQueryBuilder();
63
+
64
+        $permsFunc = $builder->expr()->bitwiseAnd('permissions', $mask);
65
+        $builder
66
+            ->update('share')
67
+            ->set('permissions', $permsFunc)
68
+            ->where($builder->expr()->eq('item_type', $builder->expr()->literal('file')))
69
+            ->andWhere($builder->expr()->neq('permissions', $permsFunc));
70
+
71
+        $updatedEntries = $builder->execute();
72
+        if ($updatedEntries > 0) {
73
+            $out->info('Fixed file share permissions for ' . $updatedEntries . ' shares');
74
+        }
75
+    }
76
+
77
+    /**
78
+     * Remove shares where the parent share does not exist anymore
79
+     */
80
+    private function removeSharesNonExistingParent(IOutput $out) {
81
+        $deletedEntries = 0;
82
+
83
+        $query = $this->connection->getQueryBuilder();
84
+        $query->select('s1.parent')
85
+            ->from('share', 's1')
86
+            ->where($query->expr()->isNotNull('s1.parent'))
87
+                ->andWhere($query->expr()->isNull('s2.id'))
88
+            ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id'))
89
+            ->groupBy('s1.parent')
90
+            ->setMaxResults(self::CHUNK_SIZE);
91
+
92
+        $deleteQuery = $this->connection->getQueryBuilder();
93
+        $deleteQuery->delete('share')
94
+            ->where($deleteQuery->expr()->eq('parent', $deleteQuery->createParameter('parent')));
95
+
96
+        $deletedInLastChunk = self::CHUNK_SIZE;
97
+        while ($deletedInLastChunk === self::CHUNK_SIZE) {
98
+            $deletedInLastChunk = 0;
99
+            $result = $query->execute();
100
+            while ($row = $result->fetch()) {
101
+                $deletedInLastChunk++;
102
+                $deletedEntries += $deleteQuery->setParameter('parent', (int) $row['parent'])
103
+                    ->execute();
104
+            }
105
+            $result->closeCursor();
106
+        }
107
+
108
+        if ($deletedEntries) {
109
+            $out->info('Removed ' . $deletedEntries . ' shares where the parent did not exist');
110
+        }
111
+    }
112
+
113
+    public function run(IOutput $out) {
114
+        $ocVersionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0');
115
+        if (version_compare($ocVersionFromBeforeUpdate, '12.0.0.11', '<')) {
116
+            $this->adjustFileSharePermissions($out);
117
+        }
118
+
119
+        $this->removeSharesNonExistingParent($out);
120
+    }
121 121
 }
Please login to merge, or discard this patch.