Passed
Push — master ( e81fdf...9f1d49 )
by Robin
16:12 queued 13s
created
lib/private/Share/Share.php 1 patch
Indentation   +933 added lines, -933 removed lines patch added patch discarded remove patch
@@ -50,937 +50,937 @@
 block discarded – undo
50 50
  *  - post_shared
51 51
  */
52 52
 class Share extends Constants {
53
-	/** CRUDS permissions (Create, Read, Update, Delete, Share) using a bitmask
54
-	 * Construct permissions for share() and setPermissions with Or (|) e.g.
55
-	 * Give user read and update permissions: PERMISSION_READ | PERMISSION_UPDATE
56
-	 *
57
-	 * Check if permission is granted with And (&) e.g. Check if delete is
58
-	 * granted: if ($permissions & PERMISSION_DELETE)
59
-	 *
60
-	 * Remove permissions with And (&) and Not (~) e.g. Remove the update
61
-	 * permission: $permissions &= ~PERMISSION_UPDATE
62
-	 *
63
-	 * Apps are required to handle permissions on their own, this class only
64
-	 * stores and manages the permissions of shares
65
-	 *
66
-	 * @see lib/public/Constants.php
67
-	 */
68
-
69
-	/**
70
-	 * Register a sharing backend class that implements OCP\Share_Backend for an item type
71
-	 *
72
-	 * @param string $itemType Item type
73
-	 * @param string $class Backend class
74
-	 * @param string $collectionOf (optional) Depends on item type
75
-	 * @param array $supportedFileExtensions (optional) List of supported file extensions if this item type depends on files
76
-	 * @return boolean true if backend is registered or false if error
77
-	 */
78
-	public static function registerBackend($itemType, $class, $collectionOf = null, $supportedFileExtensions = null) {
79
-		if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_enabled', 'yes') == 'yes') {
80
-			if (!isset(self::$backendTypes[$itemType])) {
81
-				self::$backendTypes[$itemType] = [
82
-					'class' => $class,
83
-					'collectionOf' => $collectionOf,
84
-					'supportedFileExtensions' => $supportedFileExtensions
85
-				];
86
-				return true;
87
-			}
88
-			\OC::$server->get(LoggerInterface::class)->warning(
89
-				'Sharing backend '.$class.' not registered, '.self::$backendTypes[$itemType]['class']
90
-				.' is already registered for '.$itemType,
91
-				['app' => 'files_sharing']);
92
-		}
93
-		return false;
94
-	}
95
-
96
-	/**
97
-	 * Get the items of item type shared with the current user
98
-	 *
99
-	 * @param string $itemType
100
-	 * @param int $format (optional) Format type must be defined by the backend
101
-	 * @param mixed $parameters (optional)
102
-	 * @param int $limit Number of items to return (optional) Returns all by default
103
-	 * @param boolean $includeCollections (optional)
104
-	 * @return mixed Return depends on format
105
-	 * @deprecated TESTS ONLY - this methods is only used by tests
106
-	 * called like this:
107
-	 * \OC\Share\Share::getItemsSharedWith('folder'); (apps/files_sharing/tests/UpdaterTest.php)
108
-	 */
109
-	public static function getItemsSharedWith() {
110
-		return self::getItems('folder', null, self::$shareTypeUserAndGroups, \OC_User::getUser());
111
-	}
112
-
113
-	/**
114
-	 * Get the items of item type shared with a user
115
-	 *
116
-	 * @param string $itemType
117
-	 * @param string $user id for which user we want the shares
118
-	 * @param int $format (optional) Format type must be defined by the backend
119
-	 * @param mixed $parameters (optional)
120
-	 * @param int $limit Number of items to return (optional) Returns all by default
121
-	 * @param boolean $includeCollections (optional)
122
-	 * @return mixed Return depends on format
123
-	 * @deprecated TESTS ONLY - this methods is only used by tests
124
-	 * called like this:
125
-	 * \OC\Share\Share::getItemsSharedWithUser('test', $shareWith); (tests/lib/Share/Backend.php)
126
-	 */
127
-	public static function getItemsSharedWithUser($itemType, $user) {
128
-		return self::getItems('test', null, self::$shareTypeUserAndGroups, $user);
129
-	}
130
-
131
-	/**
132
-	 * Get the item of item type shared with a given user by source
133
-	 *
134
-	 * @param string $itemType
135
-	 * @param string $itemSource
136
-	 * @param ?string $user User to whom the item was shared
137
-	 * @param ?string $owner Owner of the share
138
-	 * @param ?int $shareType only look for a specific share type
139
-	 * @return array Return list of items with file_target, permissions and expiration
140
-	 * @throws Exception
141
-	 */
142
-	public static function getItemSharedWithUser(string $itemType, string $itemSource, ?string $user = null, ?string $owner = null, ?int $shareType = null) {
143
-		$shares = [];
144
-		$fileDependent = $itemType === 'file' || $itemType === 'folder';
145
-		$qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent);
146
-		$qb->from('share', 's');
147
-		if ($fileDependent) {
148
-			$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'));
149
-			$qb->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
150
-			$column = 'file_source';
151
-		} else {
152
-			$column = 'item_source';
153
-		}
154
-
155
-		$qb->where($qb->expr()->eq($column, $qb->createNamedParameter($itemSource)))
156
-			->andWhere($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType)));
157
-
158
-		// for link shares $user === null
159
-		if ($user !== null) {
160
-			$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($user)));
161
-		}
162
-
163
-		if ($shareType !== null) {
164
-			$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT)));
165
-		}
166
-
167
-		if ($owner !== null) {
168
-			$qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($owner)));
169
-		}
170
-
171
-		$result = $qb->executeQuery();
172
-		while ($row = $result->fetch()) {
173
-			if ($fileDependent && !self::isFileReachable($row['path'], $row['storage_id'])) {
174
-				continue;
175
-			}
176
-			if ($fileDependent && (int)$row['file_parent'] === -1) {
177
-				// if it is a mount point we need to get the path from the mount manager
178
-				$mountManager = \OC\Files\Filesystem::getMountManager();
179
-				$mountPoint = $mountManager->findByStorageId($row['storage_id']);
180
-				if (!empty($mountPoint)) {
181
-					$path = $mountPoint[0]->getMountPoint();
182
-					$path = trim($path, '/');
183
-					$path = substr($path, strlen($owner) + 1); //normalize path to 'files/foo.txt`
184
-					$row['path'] = $path;
185
-				} else {
186
-					\OC::$server->get(LoggerInterface::class)->warning(
187
-						'Could not resolve mount point for ' . $row['storage_id'],
188
-						['app' => 'OCP\Share']
189
-					);
190
-				}
191
-			}
192
-			$shares[] = $row;
193
-		}
194
-		$result->closeCursor();
195
-
196
-		// if we didn't found a result then let's look for a group share.
197
-		if (empty($shares) && $user !== null) {
198
-			$userObject = \OC::$server->getUserManager()->get($user);
199
-			$groups = [];
200
-			if ($userObject) {
201
-				$groups = \OC::$server->getGroupManager()->getUserGroupIds($userObject);
202
-			}
203
-
204
-			if (!empty($groups)) {
205
-				$qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent);
206
-				$qb->from('share', 's');
207
-
208
-				if ($fileDependent) {
209
-					$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'))
210
-						->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
211
-				}
212
-
213
-				$qb->where($qb->expr()->eq($column, $qb->createNamedParameter($itemSource)))
214
-					->andWhere($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType, IQueryBuilder::PARAM_STR)))
215
-					->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)));
216
-
217
-				if ($owner !== null) {
218
-					$qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($owner)));
219
-				}
220
-				$result = $qb->executeQuery();
221
-
222
-				while ($row = $result->fetch()) {
223
-					$shares[] = $row;
224
-				}
225
-				$result->closeCursor();
226
-			}
227
-		}
228
-
229
-		return $shares;
230
-	}
231
-
232
-	/**
233
-	 * Get the shared item of item type owned by the current user
234
-	 *
235
-	 * @param string $itemType
236
-	 * @param string $itemSource
237
-	 * @param int $format (optional) Format type must be defined by the backend
238
-	 * @param mixed $parameters
239
-	 * @param boolean $includeCollections
240
-	 * @return mixed Return depends on format
241
-	 *
242
-	 * Refactoring notes:
243
-	 *   * defacto $parameters and $format is always the default and therefore is removed in the subsequent call
244
-	 */
245
-	public static function getItemShared($itemType, $itemSource, $format = self::FORMAT_NONE,
246
-										 $parameters = null, $includeCollections = false) {
247
-		return self::getItems($itemType, $itemSource, null, null, \OC_User::getUser(), self::FORMAT_NONE,
248
-			null, -1, $includeCollections);
249
-	}
250
-
251
-	/**
252
-	 * Get the backend class for the specified item type
253
-	 *
254
-	 * @param string $itemType
255
-	 * @return \OCP\Share_Backend
256
-	 * @throws \Exception
257
-	 */
258
-	public static function getBackend($itemType) {
259
-		$l = \OC::$server->getL10N('lib');
260
-		$logger = \OC::$server->get(LoggerInterface::class);
261
-		if (isset(self::$backends[$itemType])) {
262
-			return self::$backends[$itemType];
263
-		} elseif (isset(self::$backendTypes[$itemType]['class'])) {
264
-			$class = self::$backendTypes[$itemType]['class'];
265
-			if (class_exists($class)) {
266
-				self::$backends[$itemType] = new $class;
267
-				if (!(self::$backends[$itemType] instanceof \OCP\Share_Backend)) {
268
-					$message = 'Sharing backend %s must implement the interface OCP\Share_Backend';
269
-					$message_t = $l->t('Sharing backend %s must implement the interface OCP\Share_Backend', [$class]);
270
-					$logger->error(sprintf($message, $class), ['app' => 'OCP\Share']);
271
-					throw new \Exception($message_t);
272
-				}
273
-				return self::$backends[$itemType];
274
-			} else {
275
-				$message = 'Sharing backend %s not found';
276
-				$message_t = $l->t('Sharing backend %s not found', [$class]);
277
-				$logger->error(sprintf($message, $class), ['app' => 'OCP\Share']);
278
-				throw new \Exception($message_t);
279
-			}
280
-		}
281
-		$message = 'Sharing backend for %s not found';
282
-		$message_t = $l->t('Sharing backend for %s not found', [$itemType]);
283
-		$logger->error(sprintf($message, $itemType), ['app' => 'OCP\Share']);
284
-		throw new \Exception($message_t);
285
-	}
286
-
287
-	/**
288
-	 * Check if resharing is allowed
289
-	 *
290
-	 * @return boolean true if allowed or false
291
-	 *
292
-	 * Resharing is allowed by default if not configured
293
-	 */
294
-	public static function isResharingAllowed() {
295
-		if (!isset(self::$isResharingAllowed)) {
296
-			if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_allow_resharing', 'yes') == 'yes') {
297
-				self::$isResharingAllowed = true;
298
-			} else {
299
-				self::$isResharingAllowed = false;
300
-			}
301
-		}
302
-		return self::$isResharingAllowed;
303
-	}
304
-
305
-	/**
306
-	 * Get a list of collection item types for the specified item type
307
-	 *
308
-	 * @param string $itemType
309
-	 * @return array|false
310
-	 */
311
-	private static function getCollectionItemTypes(string $itemType) {
312
-		$collectionTypes = [$itemType];
313
-		foreach (self::$backendTypes as $type => $backend) {
314
-			if (in_array($backend['collectionOf'], $collectionTypes)) {
315
-				$collectionTypes[] = $type;
316
-			}
317
-		}
318
-		// TODO Add option for collections to be collection of themselves, only 'folder' does it now...
319
-		if (isset(self::$backendTypes[$itemType]) && (!self::getBackend($itemType) instanceof \OCP\Share_Backend_Collection || $itemType != 'folder')) {
320
-			unset($collectionTypes[0]);
321
-		}
322
-		// Return array if collections were found or the item type is a
323
-		// collection itself - collections can be inside collections
324
-		if (count($collectionTypes) > 0) {
325
-			return $collectionTypes;
326
-		}
327
-		return false;
328
-	}
329
-
330
-	/**
331
-	 * Get shared items from the database
332
-	 *
333
-	 * @param string $itemType
334
-	 * @param string $item Item source or target (optional)
335
-	 * @param int $shareType SHARE_TYPE_USER, SHARE_TYPE_GROUP, SHARE_TYPE_LINK, $shareTypeUserAndGroups, or $shareTypeGroupUserUnique
336
-	 * @param string $shareWith User or group the item is being shared with
337
-	 * @param string $uidOwner User that is the owner of shared items (optional)
338
-	 * @param int $format Format to convert items to with formatItems() (optional)
339
-	 * @param mixed $parameters to pass to formatItems() (optional)
340
-	 * @param int $limit Number of items to return, -1 to return all matches (optional)
341
-	 * @param boolean $includeCollections Include collection item types (optional)
342
-	 * @param boolean $itemShareWithBySource (optional)
343
-	 * @param boolean $checkExpireDate
344
-	 * @return array
345
-	 *
346
-	 * See public functions getItem(s)... for parameter usage
347
-	 *
348
-	 * Refactoring notes:
349
-	 *   * defacto $limit, $itemsShareWithBySource, $checkExpireDate, $parameters and $format is always the default and therefore is removed in the subsequent call
350
-	 */
351
-	public static function getItems($itemType, ?string $item = null, ?int $shareType = null, $shareWith = null,
352
-									$uidOwner = null, $format = self::FORMAT_NONE, $parameters = null, $limit = -1,
353
-									$includeCollections = false, $itemShareWithBySource = false, $checkExpireDate = true) {
354
-		if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_enabled', 'yes') != 'yes') {
355
-			return [];
356
-		}
357
-		$fileDependent = $itemType == 'file' || $itemType == 'folder';
358
-		$qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent, $uidOwner);
359
-		$qb->from('share', 's');
360
-
361
-		$backend = self::getBackend($itemType);
362
-		$collectionTypes = false;
363
-		// Get filesystem root to add it to the file target and remove from the
364
-		// file source, match file_source with the file cache
365
-		if ($fileDependent) {
366
-			if (!is_null($uidOwner)) {
367
-				$root = \OC\Files\Filesystem::getRoot();
368
-			} else {
369
-				$root = '';
370
-			}
371
-			if (isset($item)) {
372
-				$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'));
373
-			} else {
374
-				$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->andX(
375
-					$qb->expr()->eq('file_source', 'f.fileid'),
376
-					$qb->expr()->isNotNull('file_target')
377
-				));
378
-			}
379
-			$qb->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
380
-		} else {
381
-			$root = '';
382
-			$collectionTypes = self::getCollectionItemTypes($itemType);
383
-			if ($includeCollections && !isset($item) && $collectionTypes) {
384
-				// If includeCollections is true, find collections of this item type, e.g. a music album contains songs
385
-				if (!in_array($itemType, $collectionTypes)) {
386
-					$itemTypes = array_merge([$itemType], $collectionTypes);
387
-				} else {
388
-					$itemTypes = $collectionTypes;
389
-				}
390
-				$qb->where($qb->expr()->in('item_type', $qb->createNamedParameter($itemTypes, IQueryBuilder::PARAM_STR_ARRAY)));
391
-			} else {
392
-				$qb->where($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType)));
393
-			}
394
-		}
395
-		if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
396
-			$qb->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK, IQueryBuilder::PARAM_INT)));
397
-		}
398
-		if (isset($shareType)) {
399
-			// Include all user and group items
400
-			if ($shareType === self::$shareTypeUserAndGroups && isset($shareWith)) {
401
-				$qb->andWhere($qb->expr()->andX(
402
-					$qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, self::$shareTypeGroupUserUnique], IQueryBuilder::PARAM_INT_ARRAY)),
403
-					$qb->expr()->eq('share_with', $qb->createNamedParameter($shareWith))
404
-				));
405
-
406
-				$user = \OC::$server->getUserManager()->get($shareWith);
407
-				$groups = [];
408
-				if ($user) {
409
-					$groups = \OC::$server->getGroupManager()->getUserGroupIds($user);
410
-				}
411
-				if (!empty($groups)) {
412
-					$qb->orWhere($qb->expr()->andX(
413
-						$qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP, IQueryBuilder::PARAM_INT)),
414
-						$qb->expr()->in('share_with', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))
415
-					));
416
-				}
417
-
418
-				// Don't include own group shares
419
-				$qb->andWhere($qb->expr()->neq('uid_owner', $qb->createNamedParameter($shareWith)));
420
-			} else {
421
-				$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT)));
422
-				if (isset($shareWith)) {
423
-					$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($shareWith, IQueryBuilder::PARAM_STR)));
424
-				}
425
-			}
426
-		}
427
-		if (isset($uidOwner)) {
428
-			$qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uidOwner)));
429
-			if (!isset($shareType)) {
430
-				// Prevent unique user targets for group shares from being selected
431
-				$qb->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(self::$shareTypeGroupUserUnique, IQueryBuilder::PARAM_INT)));
432
-			}
433
-			if ($fileDependent) {
434
-				$column = 'file_source';
435
-			} else {
436
-				$column = 'item_source';
437
-			}
438
-		} else {
439
-			if ($fileDependent) {
440
-				$column = 'file_target';
441
-			} else {
442
-				$column = 'item_target';
443
-			}
444
-		}
445
-		if (isset($item)) {
446
-			$collectionTypes = self::getCollectionItemTypes($itemType);
447
-			// If looking for own shared items, check item_source else check item_target
448
-			if (isset($uidOwner)) {
449
-				// If item type is a file, file source needs to be checked in case the item was converted
450
-				if ($fileDependent) {
451
-					$expr = $qb->expr()->eq('file_source', $qb->createNamedParameter($item));
452
-					$column = 'file_source';
453
-				} else {
454
-					$expr = $qb->expr()->eq('item_source', $qb->createNamedParameter($item));
455
-					$column = 'item_source';
456
-				}
457
-			} else {
458
-				if ($fileDependent) {
459
-					$item = \OC\Files\Filesystem::normalizePath($item);
460
-					$expr = $qb->expr()->eq('file_target', $qb->createNamedParameter($item));
461
-				} else {
462
-					$expr = $qb->expr()->eq('item_target', $qb->createNamedParameter($item));
463
-				}
464
-			}
465
-			if ($includeCollections && $collectionTypes && !in_array('folder', $collectionTypes)) {
466
-				$qb->andWhere($qb->expr()->orX(
467
-					$expr,
468
-					$qb->expr()->in('item_type', $qb->createNamedParameter($collectionTypes, IQueryBuilder::PARAM_STR_ARRAY))
469
-				));
470
-			} else {
471
-				$qb->andWhere($expr);
472
-			}
473
-		}
474
-		$qb->orderBy('s.id', 'ASC');
475
-		try {
476
-			$result = $qb->executeQuery();
477
-		} catch (\Exception $e) {
478
-			\OCP\Server::get(LoggerInterface::class)->error(
479
-				'Error while selecting shares: ' . $qb->getSQL(),
480
-				[
481
-					'app' => 'files_sharing',
482
-					'exception' => $e
483
-				]);
484
-			throw new \RuntimeException('Wrong SQL query', 500, $e);
485
-		}
486
-
487
-		$root = strlen($root);
488
-		$items = [];
489
-		$targets = [];
490
-		$switchedItems = [];
491
-		$mounts = [];
492
-		while ($row = $result->fetch()) {
493
-			//var_dump($row);
494
-			self::transformDBResults($row);
495
-			// Filter out duplicate group shares for users with unique targets
496
-			if ($fileDependent && !self::isFileReachable($row['path'], $row['storage_id'])) {
497
-				continue;
498
-			}
499
-			if ($row['share_type'] == self::$shareTypeGroupUserUnique && isset($items[$row['parent']])) {
500
-				$row['share_type'] = IShare::TYPE_GROUP;
501
-				$row['unique_name'] = true; // remember that we use a unique name for this user
502
-				$row['share_with'] = $items[$row['parent']]['share_with'];
503
-				// if the group share was unshared from the user we keep the permission, otherwise
504
-				// we take the permission from the parent because this is always the up-to-date
505
-				// permission for the group share
506
-				if ($row['permissions'] > 0) {
507
-					$row['permissions'] = $items[$row['parent']]['permissions'];
508
-				}
509
-				// Remove the parent group share
510
-				unset($items[$row['parent']]);
511
-				if ($row['permissions'] == 0) {
512
-					continue;
513
-				}
514
-			} elseif (!isset($uidOwner)) {
515
-				// Check if the same target already exists
516
-				if (isset($targets[$row['id']])) {
517
-					// Check if the same owner shared with the user twice
518
-					// through a group and user share - this is allowed
519
-					$id = $targets[$row['id']];
520
-					if (isset($items[$id]) && $items[$id]['uid_owner'] == $row['uid_owner']) {
521
-						// Switch to group share type to ensure resharing conditions aren't bypassed
522
-						if ($items[$id]['share_type'] != IShare::TYPE_GROUP) {
523
-							$items[$id]['share_type'] = IShare::TYPE_GROUP;
524
-							$items[$id]['share_with'] = $row['share_with'];
525
-						}
526
-						// Switch ids if sharing permission is granted on only
527
-						// one share to ensure correct parent is used if resharing
528
-						if (~(int)$items[$id]['permissions'] & \OCP\Constants::PERMISSION_SHARE
529
-							&& (int)$row['permissions'] & \OCP\Constants::PERMISSION_SHARE) {
530
-							$items[$row['id']] = $items[$id];
531
-							$switchedItems[$id] = $row['id'];
532
-							unset($items[$id]);
533
-							$id = $row['id'];
534
-						}
535
-						$items[$id]['permissions'] |= (int)$row['permissions'];
536
-					}
537
-					continue;
538
-				} elseif (!empty($row['parent'])) {
539
-					$targets[$row['parent']] = $row['id'];
540
-				}
541
-			}
542
-			// Remove root from file source paths if retrieving own shared items
543
-			if (isset($uidOwner) && isset($row['path'])) {
544
-				if (isset($row['parent'])) {
545
-					$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
546
-					$query->select('file_target')
547
-						->from('share')
548
-						->where($query->expr()->eq('id', $query->createNamedParameter($row['parent'])));
549
-
550
-					$parentRow = false;
551
-					try {
552
-						$parentResult = $query->executeQuery();
553
-						$parentRow = $parentResult->fetchOne();
554
-						$parentResult->closeCursor();
555
-
556
-						$tmpPath = $parentRow['file_target'];
557
-						// find the right position where the row path continues from the target path
558
-						$pos = strrpos($row['path'], $parentRow['file_target']);
559
-						$subPath = substr($row['path'], $pos);
560
-						$splitPath = explode('/', $subPath);
561
-						foreach (array_slice($splitPath, 2) as $pathPart) {
562
-							$tmpPath = $tmpPath . '/' . $pathPart;
563
-						}
564
-						$row['path'] = $tmpPath;
565
-					} catch (Exception $e) {
566
-						\OCP\Server::get(LoggerInterface::class)
567
-							->error('Can\'t select parent :' . $e->getMessage() . ' query=' . $query->getSQL(), [
568
-								'exception' => $e,
569
-								'app' => 'core'
570
-							]);
571
-					}
572
-				} else {
573
-					if (!isset($mounts[$row['storage']])) {
574
-						$mountPoints = \OC\Files\Filesystem::getMountByNumericId($row['storage']);
575
-						if (is_array($mountPoints) && !empty($mountPoints)) {
576
-							$mounts[$row['storage']] = current($mountPoints);
577
-						}
578
-					}
579
-					if (!empty($mounts[$row['storage']])) {
580
-						$path = $mounts[$row['storage']]->getMountPoint() . $row['path'];
581
-						$relPath = substr($path, $root); // path relative to data/user
582
-						$row['path'] = rtrim($relPath, '/');
583
-					}
584
-				}
585
-			}
586
-
587
-			// Check if resharing is allowed, if not remove share permission
588
-			if (isset($row['permissions']) && (!self::isResharingAllowed() | \OCP\Util::isSharingDisabledForUser())) {
589
-				$row['permissions'] &= ~\OCP\Constants::PERMISSION_SHARE;
590
-			}
591
-			// Add display names to result
592
-			$row['share_with_displayname'] = $row['share_with'];
593
-			if (isset($row['share_with']) && $row['share_with'] != '' &&
594
-				$row['share_type'] === IShare::TYPE_USER) {
595
-				$shareWithUser = \OC::$server->getUserManager()->get($row['share_with']);
596
-				$row['share_with_displayname'] = $shareWithUser === null ? $row['share_with'] : $shareWithUser->getDisplayName();
597
-			} elseif (isset($row['share_with']) && $row['share_with'] != '' &&
598
-				$row['share_type'] === IShare::TYPE_REMOTE) {
599
-				$addressBookEntries = \OC::$server->getContactsManager()->search($row['share_with'], ['CLOUD'], [
600
-					'limit' => 1,
601
-					'enumeration' => false,
602
-					'fullmatch' => false,
603
-					'strict_search' => true,
604
-				]);
605
-				foreach ($addressBookEntries as $entry) {
606
-					foreach ($entry['CLOUD'] as $cloudID) {
607
-						if ($cloudID === $row['share_with']) {
608
-							$row['share_with_displayname'] = $entry['FN'];
609
-						}
610
-					}
611
-				}
612
-			}
613
-			if (isset($row['uid_owner']) && $row['uid_owner'] != '') {
614
-				$ownerUser = \OC::$server->getUserManager()->get($row['uid_owner']);
615
-				$row['displayname_owner'] = $ownerUser === null ? $row['uid_owner'] : $ownerUser->getDisplayName();
616
-			}
617
-
618
-			if ($row['permissions'] > 0) {
619
-				$items[$row['id']] = $row;
620
-			}
621
-		}
622
-		$result->closeCursor();
623
-
624
-		// group items if we are looking for items shared with the current user
625
-		if (isset($shareWith) && $shareWith === \OC_User::getUser()) {
626
-			$items = self::groupItems($items, $itemType);
627
-		}
628
-
629
-		if (!empty($items)) {
630
-			$collectionItems = [];
631
-			foreach ($items as &$row) {
632
-				// Check if this is a collection of the requested item type
633
-				if ($includeCollections && $collectionTypes && $row['item_type'] !== 'folder' && in_array($row['item_type'], $collectionTypes)) {
634
-					if (($collectionBackend = self::getBackend($row['item_type']))
635
-						&& $collectionBackend instanceof \OCP\Share_Backend_Collection) {
636
-						// Collections can be inside collections, check if the item is a collection
637
-						if (isset($item) && $row['item_type'] == $itemType && $row[$column] == $item) {
638
-							$collectionItems[] = $row;
639
-						} else {
640
-							$collection = [];
641
-							$collection['item_type'] = $row['item_type'];
642
-							if ($row['item_type'] == 'file' || $row['item_type'] == 'folder') {
643
-								$collection['path'] = basename($row['path']);
644
-							}
645
-							$row['collection'] = $collection;
646
-							// Fetch all the children sources
647
-							$children = $collectionBackend->getChildren($row[$column]);
648
-							foreach ($children as $child) {
649
-								$childItem = $row;
650
-								$childItem['item_type'] = $itemType;
651
-								if ($row['item_type'] != 'file' && $row['item_type'] != 'folder') {
652
-									$childItem['item_source'] = $child['source'];
653
-									$childItem['item_target'] = $child['target'];
654
-								}
655
-								if ($backend instanceof \OCP\Share_Backend_File_Dependent) {
656
-									if ($row['item_type'] == 'file' || $row['item_type'] == 'folder') {
657
-										$childItem['file_source'] = $child['source'];
658
-									} else { // TODO is this really needed if we already know that we use the file backend?
659
-										$meta = \OC\Files\Filesystem::getFileInfo($child['file_path']);
660
-										$childItem['file_source'] = $meta['fileid'];
661
-									}
662
-									$childItem['file_target'] =
663
-										\OC\Files\Filesystem::normalizePath($child['file_path']);
664
-								}
665
-								if (isset($item)) {
666
-									if ($childItem[$column] == $item) {
667
-										$collectionItems[] = $childItem;
668
-									}
669
-								} else {
670
-									$collectionItems[] = $childItem;
671
-								}
672
-							}
673
-						}
674
-					}
675
-					// Remove collection item
676
-					$toRemove = $row['id'];
677
-					if (array_key_exists($toRemove, $switchedItems)) {
678
-						$toRemove = $switchedItems[$toRemove];
679
-					}
680
-					unset($items[$toRemove]);
681
-				} elseif ($includeCollections && $collectionTypes && in_array($row['item_type'], $collectionTypes)) {
682
-					// FIXME: Thats a dirty hack to improve file sharing performance,
683
-					// see github issue #10588 for more details
684
-					// Need to find a solution which works for all back-ends
685
-					$collectionBackend = self::getBackend($row['item_type']);
686
-					$sharedParents = $collectionBackend->getParents($row['item_source']);
687
-					foreach ($sharedParents as $parent) {
688
-						$collectionItems[] = $parent;
689
-					}
690
-				}
691
-			}
692
-			if (!empty($collectionItems)) {
693
-				$collectionItems = array_unique($collectionItems, SORT_REGULAR);
694
-				$items = array_merge($items, $collectionItems);
695
-			}
696
-
697
-			// filter out invalid items, these can appear when subshare entries exist
698
-			// for a group in which the requested user isn't a member any more
699
-			$items = array_filter($items, function ($item) {
700
-				return $item['share_type'] !== self::$shareTypeGroupUserUnique;
701
-			});
702
-
703
-			return self::formatResult($items, $column, $backend);
704
-		} elseif ($includeCollections && $collectionTypes && in_array('folder', $collectionTypes)) {
705
-			// FIXME: Thats a dirty hack to improve file sharing performance,
706
-			// see github issue #10588 for more details
707
-			// Need to find a solution which works for all back-ends
708
-			$collectionItems = [];
709
-			$collectionBackend = self::getBackend('folder');
710
-			$sharedParents = $collectionBackend->getParents($item, $shareWith, $uidOwner);
711
-			foreach ($sharedParents as $parent) {
712
-				$collectionItems[] = $parent;
713
-			}
714
-			return self::formatResult($collectionItems, $column, $backend);
715
-		}
716
-
717
-		return [];
718
-	}
719
-
720
-	/**
721
-	 * group items with link to the same source
722
-	 *
723
-	 * @param array $items
724
-	 * @param string $itemType
725
-	 * @return array of grouped items
726
-	 */
727
-	protected static function groupItems($items, $itemType) {
728
-		$fileSharing = $itemType === 'file' || $itemType === 'folder';
729
-
730
-		$result = [];
731
-
732
-		foreach ($items as $item) {
733
-			$grouped = false;
734
-			foreach ($result as $key => $r) {
735
-				// for file/folder shares we need to compare file_source, otherwise we compare item_source
736
-				// only group shares if they already point to the same target, otherwise the file where shared
737
-				// before grouping of shares was added. In this case we don't group them to avoid confusions
738
-				if (($fileSharing && $item['file_source'] === $r['file_source'] && $item['file_target'] === $r['file_target']) ||
739
-					(!$fileSharing && $item['item_source'] === $r['item_source'] && $item['item_target'] === $r['item_target'])) {
740
-					// add the first item to the list of grouped shares
741
-					if (!isset($result[$key]['grouped'])) {
742
-						$result[$key]['grouped'][] = $result[$key];
743
-					}
744
-					$result[$key]['permissions'] = (int)$item['permissions'] | (int)$r['permissions'];
745
-					$result[$key]['grouped'][] = $item;
746
-					$grouped = true;
747
-					break;
748
-				}
749
-			}
750
-
751
-			if (!$grouped) {
752
-				$result[] = $item;
753
-			}
754
-		}
755
-
756
-		return $result;
757
-	}
758
-
759
-	/**
760
-	 * Construct select statement
761
-	 *
762
-	 * @param bool $fileDependent ist it a file/folder share or a general share
763
-	 */
764
-	private static function getSelectStatement(int $format, bool $fileDependent, ?string $uidOwner = null): IQueryBuilder {
765
-		/** @var IDBConnection $connection */
766
-		$connection = \OC::$server->get(IDBConnection::class);
767
-		$qb = $connection->getQueryBuilder();
768
-		if ($format == self::FORMAT_STATUSES) {
769
-			if ($fileDependent) {
770
-				return $qb->select(
771
-					's.id',
772
-					's.parent',
773
-					'share_type',
774
-					'path',
775
-					'storage',
776
-					'share_with',
777
-					'uid_owner',
778
-					'file_source',
779
-					'stime',
780
-					's.permissions',
781
-					'uid_initiator'
782
-				)->selectAlias('st.id', 'storage_id')
783
-					->selectAlias('f.parent', 'file_parent');
784
-			}
785
-			return $qb->select('id', 'parent', 'share_type', 'share_with', 'uid_owner', 'item_source', 'stime', 's.permissions');
786
-		}
787
-
788
-		if (isset($uidOwner)) {
789
-			if ($fileDependent) {
790
-				return $qb->select(
791
-					's.id',
792
-					'item_type',
793
-					'item_source',
794
-					's.parent',
795
-					'share_type',
796
-					'share_with',
797
-					'file_source',
798
-					'file_target',
799
-					'path',
800
-					's.permissions',
801
-					'stime',
802
-					'expiration',
803
-					'token',
804
-					'storage',
805
-					'mail_send',
806
-					'uid_owner',
807
-					'uid_initiator'
808
-				)->selectAlias('st.id', 'storage_id')
809
-					->selectAlias('f.parent', 'file_parent');
810
-			}
811
-			return $qb->select('id', 'item_type', 'item_source', 'parent', 'share_type',
812
-				'share_with', 'uid_owner', 'file_source', 'stime', 's.permissions',
813
-				'expiration', 'token', 'mail_send');
814
-		}
815
-
816
-		if ($fileDependent) {
817
-			if ($format == File::FORMAT_GET_FOLDER_CONTENTS || $format == File::FORMAT_FILE_APP_ROOT) {
818
-				return $qb->select(
819
-					's.id',
820
-					'item_type',
821
-					'item_source',
822
-					's.parent',
823
-					'uid_owner',
824
-					'share_type',
825
-					'share_with',
826
-					'file_source',
827
-					'path',
828
-					'file_target',
829
-					's.permissions',
830
-					'stime',
831
-					'expiration',
832
-					'storage',
833
-					'name',
834
-					'mtime',
835
-					'mimepart',
836
-					'size',
837
-					'encrypted',
838
-					'etag',
839
-					'mail_send'
840
-				)->selectAlias('f.parent', 'file_parent');
841
-			}
842
-			return $qb->select(
843
-				's.id',
844
-				'item_type',
845
-				'item_source',
846
-				'item_target',
847
-				's.parent',
848
-				'share_type',
849
-				'share_with',
850
-				'uid_owner',
851
-				'file_source',
852
-				'path',
853
-				'file_target',
854
-				's.permissions',
855
-				'stime',
856
-				'expiration',
857
-				'token',
858
-				'storage',
859
-				'mail_send',
860
-			)->selectAlias('f.parent', 'file_parent')
861
-				->selectAlias('st.id', 'storage_id');
862
-		}
863
-		return $qb->select('*');
864
-	}
865
-
866
-
867
-	/**
868
-	 * transform db results
869
-	 *
870
-	 * @param array $row result
871
-	 */
872
-	private static function transformDBResults(&$row) {
873
-		if (isset($row['id'])) {
874
-			$row['id'] = (int)$row['id'];
875
-		}
876
-		if (isset($row['share_type'])) {
877
-			$row['share_type'] = (int)$row['share_type'];
878
-		}
879
-		if (isset($row['parent'])) {
880
-			$row['parent'] = (int)$row['parent'];
881
-		}
882
-		if (isset($row['file_parent'])) {
883
-			$row['file_parent'] = (int)$row['file_parent'];
884
-		}
885
-		if (isset($row['file_source'])) {
886
-			$row['file_source'] = (int)$row['file_source'];
887
-		}
888
-		if (isset($row['permissions'])) {
889
-			$row['permissions'] = (int)$row['permissions'];
890
-		}
891
-		if (isset($row['storage'])) {
892
-			$row['storage'] = (int)$row['storage'];
893
-		}
894
-		if (isset($row['stime'])) {
895
-			$row['stime'] = (int)$row['stime'];
896
-		}
897
-		if (isset($row['expiration']) && $row['share_type'] !== IShare::TYPE_LINK) {
898
-			// discard expiration date for non-link shares, which might have been
899
-			// set by ancient bugs
900
-			$row['expiration'] = null;
901
-		}
902
-	}
903
-
904
-	/**
905
-	 * format result
906
-	 *
907
-	 * @param array $items result
908
-	 * @param string $column is it a file share or a general share ('file_target' or 'item_target')
909
-	 * @param \OCP\Share_Backend $backend sharing backend
910
-	 * @param int $format
911
-	 * @param array $parameters additional format parameters
912
-	 * @return array format result
913
-	 */
914
-	private static function formatResult($items, $column, $backend, $format = self::FORMAT_NONE, $parameters = null) {
915
-		if ($format === self::FORMAT_NONE) {
916
-			return $items;
917
-		} elseif ($format === self::FORMAT_STATUSES) {
918
-			$statuses = [];
919
-			foreach ($items as $item) {
920
-				if ($item['share_type'] === IShare::TYPE_LINK) {
921
-					if ($item['uid_initiator'] !== \OC::$server->getUserSession()->getUser()->getUID()) {
922
-						continue;
923
-					}
924
-					$statuses[$item[$column]]['link'] = true;
925
-				} elseif (!isset($statuses[$item[$column]])) {
926
-					$statuses[$item[$column]]['link'] = false;
927
-				}
928
-				if (!empty($item['file_target'])) {
929
-					$statuses[$item[$column]]['path'] = $item['path'];
930
-				}
931
-			}
932
-			return $statuses;
933
-		} else {
934
-			return $backend->formatItems($items, $format, $parameters);
935
-		}
936
-	}
937
-
938
-	/**
939
-	 * remove protocol from URL
940
-	 *
941
-	 * @param string $url
942
-	 * @return string
943
-	 */
944
-	public static function removeProtocolFromUrl($url) {
945
-		if (str_starts_with($url, 'https://')) {
946
-			return substr($url, strlen('https://'));
947
-		} elseif (str_starts_with($url, 'http://')) {
948
-			return substr($url, strlen('http://'));
949
-		}
950
-
951
-		return $url;
952
-	}
953
-
954
-
955
-	/**
956
-	 * @return int
957
-	 */
958
-	public static function getExpireInterval() {
959
-		return (int)\OC::$server->getConfig()->getAppValue('core', 'shareapi_expire_after_n_days', '7');
960
-	}
961
-
962
-	/**
963
-	 * Checks whether the given path is reachable for the given owner
964
-	 *
965
-	 * @param string $path path relative to files
966
-	 * @param string $ownerStorageId storage id of the owner
967
-	 *
968
-	 * @return boolean true if file is reachable, false otherwise
969
-	 */
970
-	private static function isFileReachable($path, $ownerStorageId) {
971
-		// if outside the home storage, file is always considered reachable
972
-		if (!(substr($ownerStorageId, 0, 6) === 'home::' ||
973
-			substr($ownerStorageId, 0, 13) === 'object::user:'
974
-		)) {
975
-			return true;
976
-		}
977
-
978
-		// if inside the home storage, the file has to be under "/files/"
979
-		$path = ltrim($path, '/');
980
-		if (substr($path, 0, 6) === 'files/') {
981
-			return true;
982
-		}
983
-
984
-		return false;
985
-	}
53
+    /** CRUDS permissions (Create, Read, Update, Delete, Share) using a bitmask
54
+     * Construct permissions for share() and setPermissions with Or (|) e.g.
55
+     * Give user read and update permissions: PERMISSION_READ | PERMISSION_UPDATE
56
+     *
57
+     * Check if permission is granted with And (&) e.g. Check if delete is
58
+     * granted: if ($permissions & PERMISSION_DELETE)
59
+     *
60
+     * Remove permissions with And (&) and Not (~) e.g. Remove the update
61
+     * permission: $permissions &= ~PERMISSION_UPDATE
62
+     *
63
+     * Apps are required to handle permissions on their own, this class only
64
+     * stores and manages the permissions of shares
65
+     *
66
+     * @see lib/public/Constants.php
67
+     */
68
+
69
+    /**
70
+     * Register a sharing backend class that implements OCP\Share_Backend for an item type
71
+     *
72
+     * @param string $itemType Item type
73
+     * @param string $class Backend class
74
+     * @param string $collectionOf (optional) Depends on item type
75
+     * @param array $supportedFileExtensions (optional) List of supported file extensions if this item type depends on files
76
+     * @return boolean true if backend is registered or false if error
77
+     */
78
+    public static function registerBackend($itemType, $class, $collectionOf = null, $supportedFileExtensions = null) {
79
+        if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_enabled', 'yes') == 'yes') {
80
+            if (!isset(self::$backendTypes[$itemType])) {
81
+                self::$backendTypes[$itemType] = [
82
+                    'class' => $class,
83
+                    'collectionOf' => $collectionOf,
84
+                    'supportedFileExtensions' => $supportedFileExtensions
85
+                ];
86
+                return true;
87
+            }
88
+            \OC::$server->get(LoggerInterface::class)->warning(
89
+                'Sharing backend '.$class.' not registered, '.self::$backendTypes[$itemType]['class']
90
+                .' is already registered for '.$itemType,
91
+                ['app' => 'files_sharing']);
92
+        }
93
+        return false;
94
+    }
95
+
96
+    /**
97
+     * Get the items of item type shared with the current user
98
+     *
99
+     * @param string $itemType
100
+     * @param int $format (optional) Format type must be defined by the backend
101
+     * @param mixed $parameters (optional)
102
+     * @param int $limit Number of items to return (optional) Returns all by default
103
+     * @param boolean $includeCollections (optional)
104
+     * @return mixed Return depends on format
105
+     * @deprecated TESTS ONLY - this methods is only used by tests
106
+     * called like this:
107
+     * \OC\Share\Share::getItemsSharedWith('folder'); (apps/files_sharing/tests/UpdaterTest.php)
108
+     */
109
+    public static function getItemsSharedWith() {
110
+        return self::getItems('folder', null, self::$shareTypeUserAndGroups, \OC_User::getUser());
111
+    }
112
+
113
+    /**
114
+     * Get the items of item type shared with a user
115
+     *
116
+     * @param string $itemType
117
+     * @param string $user id for which user we want the shares
118
+     * @param int $format (optional) Format type must be defined by the backend
119
+     * @param mixed $parameters (optional)
120
+     * @param int $limit Number of items to return (optional) Returns all by default
121
+     * @param boolean $includeCollections (optional)
122
+     * @return mixed Return depends on format
123
+     * @deprecated TESTS ONLY - this methods is only used by tests
124
+     * called like this:
125
+     * \OC\Share\Share::getItemsSharedWithUser('test', $shareWith); (tests/lib/Share/Backend.php)
126
+     */
127
+    public static function getItemsSharedWithUser($itemType, $user) {
128
+        return self::getItems('test', null, self::$shareTypeUserAndGroups, $user);
129
+    }
130
+
131
+    /**
132
+     * Get the item of item type shared with a given user by source
133
+     *
134
+     * @param string $itemType
135
+     * @param string $itemSource
136
+     * @param ?string $user User to whom the item was shared
137
+     * @param ?string $owner Owner of the share
138
+     * @param ?int $shareType only look for a specific share type
139
+     * @return array Return list of items with file_target, permissions and expiration
140
+     * @throws Exception
141
+     */
142
+    public static function getItemSharedWithUser(string $itemType, string $itemSource, ?string $user = null, ?string $owner = null, ?int $shareType = null) {
143
+        $shares = [];
144
+        $fileDependent = $itemType === 'file' || $itemType === 'folder';
145
+        $qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent);
146
+        $qb->from('share', 's');
147
+        if ($fileDependent) {
148
+            $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'));
149
+            $qb->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
150
+            $column = 'file_source';
151
+        } else {
152
+            $column = 'item_source';
153
+        }
154
+
155
+        $qb->where($qb->expr()->eq($column, $qb->createNamedParameter($itemSource)))
156
+            ->andWhere($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType)));
157
+
158
+        // for link shares $user === null
159
+        if ($user !== null) {
160
+            $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($user)));
161
+        }
162
+
163
+        if ($shareType !== null) {
164
+            $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT)));
165
+        }
166
+
167
+        if ($owner !== null) {
168
+            $qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($owner)));
169
+        }
170
+
171
+        $result = $qb->executeQuery();
172
+        while ($row = $result->fetch()) {
173
+            if ($fileDependent && !self::isFileReachable($row['path'], $row['storage_id'])) {
174
+                continue;
175
+            }
176
+            if ($fileDependent && (int)$row['file_parent'] === -1) {
177
+                // if it is a mount point we need to get the path from the mount manager
178
+                $mountManager = \OC\Files\Filesystem::getMountManager();
179
+                $mountPoint = $mountManager->findByStorageId($row['storage_id']);
180
+                if (!empty($mountPoint)) {
181
+                    $path = $mountPoint[0]->getMountPoint();
182
+                    $path = trim($path, '/');
183
+                    $path = substr($path, strlen($owner) + 1); //normalize path to 'files/foo.txt`
184
+                    $row['path'] = $path;
185
+                } else {
186
+                    \OC::$server->get(LoggerInterface::class)->warning(
187
+                        'Could not resolve mount point for ' . $row['storage_id'],
188
+                        ['app' => 'OCP\Share']
189
+                    );
190
+                }
191
+            }
192
+            $shares[] = $row;
193
+        }
194
+        $result->closeCursor();
195
+
196
+        // if we didn't found a result then let's look for a group share.
197
+        if (empty($shares) && $user !== null) {
198
+            $userObject = \OC::$server->getUserManager()->get($user);
199
+            $groups = [];
200
+            if ($userObject) {
201
+                $groups = \OC::$server->getGroupManager()->getUserGroupIds($userObject);
202
+            }
203
+
204
+            if (!empty($groups)) {
205
+                $qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent);
206
+                $qb->from('share', 's');
207
+
208
+                if ($fileDependent) {
209
+                    $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'))
210
+                        ->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
211
+                }
212
+
213
+                $qb->where($qb->expr()->eq($column, $qb->createNamedParameter($itemSource)))
214
+                    ->andWhere($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType, IQueryBuilder::PARAM_STR)))
215
+                    ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)));
216
+
217
+                if ($owner !== null) {
218
+                    $qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($owner)));
219
+                }
220
+                $result = $qb->executeQuery();
221
+
222
+                while ($row = $result->fetch()) {
223
+                    $shares[] = $row;
224
+                }
225
+                $result->closeCursor();
226
+            }
227
+        }
228
+
229
+        return $shares;
230
+    }
231
+
232
+    /**
233
+     * Get the shared item of item type owned by the current user
234
+     *
235
+     * @param string $itemType
236
+     * @param string $itemSource
237
+     * @param int $format (optional) Format type must be defined by the backend
238
+     * @param mixed $parameters
239
+     * @param boolean $includeCollections
240
+     * @return mixed Return depends on format
241
+     *
242
+     * Refactoring notes:
243
+     *   * defacto $parameters and $format is always the default and therefore is removed in the subsequent call
244
+     */
245
+    public static function getItemShared($itemType, $itemSource, $format = self::FORMAT_NONE,
246
+                                            $parameters = null, $includeCollections = false) {
247
+        return self::getItems($itemType, $itemSource, null, null, \OC_User::getUser(), self::FORMAT_NONE,
248
+            null, -1, $includeCollections);
249
+    }
250
+
251
+    /**
252
+     * Get the backend class for the specified item type
253
+     *
254
+     * @param string $itemType
255
+     * @return \OCP\Share_Backend
256
+     * @throws \Exception
257
+     */
258
+    public static function getBackend($itemType) {
259
+        $l = \OC::$server->getL10N('lib');
260
+        $logger = \OC::$server->get(LoggerInterface::class);
261
+        if (isset(self::$backends[$itemType])) {
262
+            return self::$backends[$itemType];
263
+        } elseif (isset(self::$backendTypes[$itemType]['class'])) {
264
+            $class = self::$backendTypes[$itemType]['class'];
265
+            if (class_exists($class)) {
266
+                self::$backends[$itemType] = new $class;
267
+                if (!(self::$backends[$itemType] instanceof \OCP\Share_Backend)) {
268
+                    $message = 'Sharing backend %s must implement the interface OCP\Share_Backend';
269
+                    $message_t = $l->t('Sharing backend %s must implement the interface OCP\Share_Backend', [$class]);
270
+                    $logger->error(sprintf($message, $class), ['app' => 'OCP\Share']);
271
+                    throw new \Exception($message_t);
272
+                }
273
+                return self::$backends[$itemType];
274
+            } else {
275
+                $message = 'Sharing backend %s not found';
276
+                $message_t = $l->t('Sharing backend %s not found', [$class]);
277
+                $logger->error(sprintf($message, $class), ['app' => 'OCP\Share']);
278
+                throw new \Exception($message_t);
279
+            }
280
+        }
281
+        $message = 'Sharing backend for %s not found';
282
+        $message_t = $l->t('Sharing backend for %s not found', [$itemType]);
283
+        $logger->error(sprintf($message, $itemType), ['app' => 'OCP\Share']);
284
+        throw new \Exception($message_t);
285
+    }
286
+
287
+    /**
288
+     * Check if resharing is allowed
289
+     *
290
+     * @return boolean true if allowed or false
291
+     *
292
+     * Resharing is allowed by default if not configured
293
+     */
294
+    public static function isResharingAllowed() {
295
+        if (!isset(self::$isResharingAllowed)) {
296
+            if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_allow_resharing', 'yes') == 'yes') {
297
+                self::$isResharingAllowed = true;
298
+            } else {
299
+                self::$isResharingAllowed = false;
300
+            }
301
+        }
302
+        return self::$isResharingAllowed;
303
+    }
304
+
305
+    /**
306
+     * Get a list of collection item types for the specified item type
307
+     *
308
+     * @param string $itemType
309
+     * @return array|false
310
+     */
311
+    private static function getCollectionItemTypes(string $itemType) {
312
+        $collectionTypes = [$itemType];
313
+        foreach (self::$backendTypes as $type => $backend) {
314
+            if (in_array($backend['collectionOf'], $collectionTypes)) {
315
+                $collectionTypes[] = $type;
316
+            }
317
+        }
318
+        // TODO Add option for collections to be collection of themselves, only 'folder' does it now...
319
+        if (isset(self::$backendTypes[$itemType]) && (!self::getBackend($itemType) instanceof \OCP\Share_Backend_Collection || $itemType != 'folder')) {
320
+            unset($collectionTypes[0]);
321
+        }
322
+        // Return array if collections were found or the item type is a
323
+        // collection itself - collections can be inside collections
324
+        if (count($collectionTypes) > 0) {
325
+            return $collectionTypes;
326
+        }
327
+        return false;
328
+    }
329
+
330
+    /**
331
+     * Get shared items from the database
332
+     *
333
+     * @param string $itemType
334
+     * @param string $item Item source or target (optional)
335
+     * @param int $shareType SHARE_TYPE_USER, SHARE_TYPE_GROUP, SHARE_TYPE_LINK, $shareTypeUserAndGroups, or $shareTypeGroupUserUnique
336
+     * @param string $shareWith User or group the item is being shared with
337
+     * @param string $uidOwner User that is the owner of shared items (optional)
338
+     * @param int $format Format to convert items to with formatItems() (optional)
339
+     * @param mixed $parameters to pass to formatItems() (optional)
340
+     * @param int $limit Number of items to return, -1 to return all matches (optional)
341
+     * @param boolean $includeCollections Include collection item types (optional)
342
+     * @param boolean $itemShareWithBySource (optional)
343
+     * @param boolean $checkExpireDate
344
+     * @return array
345
+     *
346
+     * See public functions getItem(s)... for parameter usage
347
+     *
348
+     * Refactoring notes:
349
+     *   * defacto $limit, $itemsShareWithBySource, $checkExpireDate, $parameters and $format is always the default and therefore is removed in the subsequent call
350
+     */
351
+    public static function getItems($itemType, ?string $item = null, ?int $shareType = null, $shareWith = null,
352
+                                    $uidOwner = null, $format = self::FORMAT_NONE, $parameters = null, $limit = -1,
353
+                                    $includeCollections = false, $itemShareWithBySource = false, $checkExpireDate = true) {
354
+        if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_enabled', 'yes') != 'yes') {
355
+            return [];
356
+        }
357
+        $fileDependent = $itemType == 'file' || $itemType == 'folder';
358
+        $qb = self::getSelectStatement(self::FORMAT_NONE, $fileDependent, $uidOwner);
359
+        $qb->from('share', 's');
360
+
361
+        $backend = self::getBackend($itemType);
362
+        $collectionTypes = false;
363
+        // Get filesystem root to add it to the file target and remove from the
364
+        // file source, match file_source with the file cache
365
+        if ($fileDependent) {
366
+            if (!is_null($uidOwner)) {
367
+                $root = \OC\Files\Filesystem::getRoot();
368
+            } else {
369
+                $root = '';
370
+            }
371
+            if (isset($item)) {
372
+                $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('file_source', 'f.fileid'));
373
+            } else {
374
+                $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->andX(
375
+                    $qb->expr()->eq('file_source', 'f.fileid'),
376
+                    $qb->expr()->isNotNull('file_target')
377
+                ));
378
+            }
379
+            $qb->innerJoin('s', 'storages', 'st', $qb->expr()->eq('numeric_id', 'f.storage'));
380
+        } else {
381
+            $root = '';
382
+            $collectionTypes = self::getCollectionItemTypes($itemType);
383
+            if ($includeCollections && !isset($item) && $collectionTypes) {
384
+                // If includeCollections is true, find collections of this item type, e.g. a music album contains songs
385
+                if (!in_array($itemType, $collectionTypes)) {
386
+                    $itemTypes = array_merge([$itemType], $collectionTypes);
387
+                } else {
388
+                    $itemTypes = $collectionTypes;
389
+                }
390
+                $qb->where($qb->expr()->in('item_type', $qb->createNamedParameter($itemTypes, IQueryBuilder::PARAM_STR_ARRAY)));
391
+            } else {
392
+                $qb->where($qb->expr()->eq('item_type', $qb->createNamedParameter($itemType)));
393
+            }
394
+        }
395
+        if (\OC::$server->getConfig()->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
396
+            $qb->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK, IQueryBuilder::PARAM_INT)));
397
+        }
398
+        if (isset($shareType)) {
399
+            // Include all user and group items
400
+            if ($shareType === self::$shareTypeUserAndGroups && isset($shareWith)) {
401
+                $qb->andWhere($qb->expr()->andX(
402
+                    $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, self::$shareTypeGroupUserUnique], IQueryBuilder::PARAM_INT_ARRAY)),
403
+                    $qb->expr()->eq('share_with', $qb->createNamedParameter($shareWith))
404
+                ));
405
+
406
+                $user = \OC::$server->getUserManager()->get($shareWith);
407
+                $groups = [];
408
+                if ($user) {
409
+                    $groups = \OC::$server->getGroupManager()->getUserGroupIds($user);
410
+                }
411
+                if (!empty($groups)) {
412
+                    $qb->orWhere($qb->expr()->andX(
413
+                        $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP, IQueryBuilder::PARAM_INT)),
414
+                        $qb->expr()->in('share_with', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))
415
+                    ));
416
+                }
417
+
418
+                // Don't include own group shares
419
+                $qb->andWhere($qb->expr()->neq('uid_owner', $qb->createNamedParameter($shareWith)));
420
+            } else {
421
+                $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT)));
422
+                if (isset($shareWith)) {
423
+                    $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($shareWith, IQueryBuilder::PARAM_STR)));
424
+                }
425
+            }
426
+        }
427
+        if (isset($uidOwner)) {
428
+            $qb->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uidOwner)));
429
+            if (!isset($shareType)) {
430
+                // Prevent unique user targets for group shares from being selected
431
+                $qb->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(self::$shareTypeGroupUserUnique, IQueryBuilder::PARAM_INT)));
432
+            }
433
+            if ($fileDependent) {
434
+                $column = 'file_source';
435
+            } else {
436
+                $column = 'item_source';
437
+            }
438
+        } else {
439
+            if ($fileDependent) {
440
+                $column = 'file_target';
441
+            } else {
442
+                $column = 'item_target';
443
+            }
444
+        }
445
+        if (isset($item)) {
446
+            $collectionTypes = self::getCollectionItemTypes($itemType);
447
+            // If looking for own shared items, check item_source else check item_target
448
+            if (isset($uidOwner)) {
449
+                // If item type is a file, file source needs to be checked in case the item was converted
450
+                if ($fileDependent) {
451
+                    $expr = $qb->expr()->eq('file_source', $qb->createNamedParameter($item));
452
+                    $column = 'file_source';
453
+                } else {
454
+                    $expr = $qb->expr()->eq('item_source', $qb->createNamedParameter($item));
455
+                    $column = 'item_source';
456
+                }
457
+            } else {
458
+                if ($fileDependent) {
459
+                    $item = \OC\Files\Filesystem::normalizePath($item);
460
+                    $expr = $qb->expr()->eq('file_target', $qb->createNamedParameter($item));
461
+                } else {
462
+                    $expr = $qb->expr()->eq('item_target', $qb->createNamedParameter($item));
463
+                }
464
+            }
465
+            if ($includeCollections && $collectionTypes && !in_array('folder', $collectionTypes)) {
466
+                $qb->andWhere($qb->expr()->orX(
467
+                    $expr,
468
+                    $qb->expr()->in('item_type', $qb->createNamedParameter($collectionTypes, IQueryBuilder::PARAM_STR_ARRAY))
469
+                ));
470
+            } else {
471
+                $qb->andWhere($expr);
472
+            }
473
+        }
474
+        $qb->orderBy('s.id', 'ASC');
475
+        try {
476
+            $result = $qb->executeQuery();
477
+        } catch (\Exception $e) {
478
+            \OCP\Server::get(LoggerInterface::class)->error(
479
+                'Error while selecting shares: ' . $qb->getSQL(),
480
+                [
481
+                    'app' => 'files_sharing',
482
+                    'exception' => $e
483
+                ]);
484
+            throw new \RuntimeException('Wrong SQL query', 500, $e);
485
+        }
486
+
487
+        $root = strlen($root);
488
+        $items = [];
489
+        $targets = [];
490
+        $switchedItems = [];
491
+        $mounts = [];
492
+        while ($row = $result->fetch()) {
493
+            //var_dump($row);
494
+            self::transformDBResults($row);
495
+            // Filter out duplicate group shares for users with unique targets
496
+            if ($fileDependent && !self::isFileReachable($row['path'], $row['storage_id'])) {
497
+                continue;
498
+            }
499
+            if ($row['share_type'] == self::$shareTypeGroupUserUnique && isset($items[$row['parent']])) {
500
+                $row['share_type'] = IShare::TYPE_GROUP;
501
+                $row['unique_name'] = true; // remember that we use a unique name for this user
502
+                $row['share_with'] = $items[$row['parent']]['share_with'];
503
+                // if the group share was unshared from the user we keep the permission, otherwise
504
+                // we take the permission from the parent because this is always the up-to-date
505
+                // permission for the group share
506
+                if ($row['permissions'] > 0) {
507
+                    $row['permissions'] = $items[$row['parent']]['permissions'];
508
+                }
509
+                // Remove the parent group share
510
+                unset($items[$row['parent']]);
511
+                if ($row['permissions'] == 0) {
512
+                    continue;
513
+                }
514
+            } elseif (!isset($uidOwner)) {
515
+                // Check if the same target already exists
516
+                if (isset($targets[$row['id']])) {
517
+                    // Check if the same owner shared with the user twice
518
+                    // through a group and user share - this is allowed
519
+                    $id = $targets[$row['id']];
520
+                    if (isset($items[$id]) && $items[$id]['uid_owner'] == $row['uid_owner']) {
521
+                        // Switch to group share type to ensure resharing conditions aren't bypassed
522
+                        if ($items[$id]['share_type'] != IShare::TYPE_GROUP) {
523
+                            $items[$id]['share_type'] = IShare::TYPE_GROUP;
524
+                            $items[$id]['share_with'] = $row['share_with'];
525
+                        }
526
+                        // Switch ids if sharing permission is granted on only
527
+                        // one share to ensure correct parent is used if resharing
528
+                        if (~(int)$items[$id]['permissions'] & \OCP\Constants::PERMISSION_SHARE
529
+                            && (int)$row['permissions'] & \OCP\Constants::PERMISSION_SHARE) {
530
+                            $items[$row['id']] = $items[$id];
531
+                            $switchedItems[$id] = $row['id'];
532
+                            unset($items[$id]);
533
+                            $id = $row['id'];
534
+                        }
535
+                        $items[$id]['permissions'] |= (int)$row['permissions'];
536
+                    }
537
+                    continue;
538
+                } elseif (!empty($row['parent'])) {
539
+                    $targets[$row['parent']] = $row['id'];
540
+                }
541
+            }
542
+            // Remove root from file source paths if retrieving own shared items
543
+            if (isset($uidOwner) && isset($row['path'])) {
544
+                if (isset($row['parent'])) {
545
+                    $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
546
+                    $query->select('file_target')
547
+                        ->from('share')
548
+                        ->where($query->expr()->eq('id', $query->createNamedParameter($row['parent'])));
549
+
550
+                    $parentRow = false;
551
+                    try {
552
+                        $parentResult = $query->executeQuery();
553
+                        $parentRow = $parentResult->fetchOne();
554
+                        $parentResult->closeCursor();
555
+
556
+                        $tmpPath = $parentRow['file_target'];
557
+                        // find the right position where the row path continues from the target path
558
+                        $pos = strrpos($row['path'], $parentRow['file_target']);
559
+                        $subPath = substr($row['path'], $pos);
560
+                        $splitPath = explode('/', $subPath);
561
+                        foreach (array_slice($splitPath, 2) as $pathPart) {
562
+                            $tmpPath = $tmpPath . '/' . $pathPart;
563
+                        }
564
+                        $row['path'] = $tmpPath;
565
+                    } catch (Exception $e) {
566
+                        \OCP\Server::get(LoggerInterface::class)
567
+                            ->error('Can\'t select parent :' . $e->getMessage() . ' query=' . $query->getSQL(), [
568
+                                'exception' => $e,
569
+                                'app' => 'core'
570
+                            ]);
571
+                    }
572
+                } else {
573
+                    if (!isset($mounts[$row['storage']])) {
574
+                        $mountPoints = \OC\Files\Filesystem::getMountByNumericId($row['storage']);
575
+                        if (is_array($mountPoints) && !empty($mountPoints)) {
576
+                            $mounts[$row['storage']] = current($mountPoints);
577
+                        }
578
+                    }
579
+                    if (!empty($mounts[$row['storage']])) {
580
+                        $path = $mounts[$row['storage']]->getMountPoint() . $row['path'];
581
+                        $relPath = substr($path, $root); // path relative to data/user
582
+                        $row['path'] = rtrim($relPath, '/');
583
+                    }
584
+                }
585
+            }
586
+
587
+            // Check if resharing is allowed, if not remove share permission
588
+            if (isset($row['permissions']) && (!self::isResharingAllowed() | \OCP\Util::isSharingDisabledForUser())) {
589
+                $row['permissions'] &= ~\OCP\Constants::PERMISSION_SHARE;
590
+            }
591
+            // Add display names to result
592
+            $row['share_with_displayname'] = $row['share_with'];
593
+            if (isset($row['share_with']) && $row['share_with'] != '' &&
594
+                $row['share_type'] === IShare::TYPE_USER) {
595
+                $shareWithUser = \OC::$server->getUserManager()->get($row['share_with']);
596
+                $row['share_with_displayname'] = $shareWithUser === null ? $row['share_with'] : $shareWithUser->getDisplayName();
597
+            } elseif (isset($row['share_with']) && $row['share_with'] != '' &&
598
+                $row['share_type'] === IShare::TYPE_REMOTE) {
599
+                $addressBookEntries = \OC::$server->getContactsManager()->search($row['share_with'], ['CLOUD'], [
600
+                    'limit' => 1,
601
+                    'enumeration' => false,
602
+                    'fullmatch' => false,
603
+                    'strict_search' => true,
604
+                ]);
605
+                foreach ($addressBookEntries as $entry) {
606
+                    foreach ($entry['CLOUD'] as $cloudID) {
607
+                        if ($cloudID === $row['share_with']) {
608
+                            $row['share_with_displayname'] = $entry['FN'];
609
+                        }
610
+                    }
611
+                }
612
+            }
613
+            if (isset($row['uid_owner']) && $row['uid_owner'] != '') {
614
+                $ownerUser = \OC::$server->getUserManager()->get($row['uid_owner']);
615
+                $row['displayname_owner'] = $ownerUser === null ? $row['uid_owner'] : $ownerUser->getDisplayName();
616
+            }
617
+
618
+            if ($row['permissions'] > 0) {
619
+                $items[$row['id']] = $row;
620
+            }
621
+        }
622
+        $result->closeCursor();
623
+
624
+        // group items if we are looking for items shared with the current user
625
+        if (isset($shareWith) && $shareWith === \OC_User::getUser()) {
626
+            $items = self::groupItems($items, $itemType);
627
+        }
628
+
629
+        if (!empty($items)) {
630
+            $collectionItems = [];
631
+            foreach ($items as &$row) {
632
+                // Check if this is a collection of the requested item type
633
+                if ($includeCollections && $collectionTypes && $row['item_type'] !== 'folder' && in_array($row['item_type'], $collectionTypes)) {
634
+                    if (($collectionBackend = self::getBackend($row['item_type']))
635
+                        && $collectionBackend instanceof \OCP\Share_Backend_Collection) {
636
+                        // Collections can be inside collections, check if the item is a collection
637
+                        if (isset($item) && $row['item_type'] == $itemType && $row[$column] == $item) {
638
+                            $collectionItems[] = $row;
639
+                        } else {
640
+                            $collection = [];
641
+                            $collection['item_type'] = $row['item_type'];
642
+                            if ($row['item_type'] == 'file' || $row['item_type'] == 'folder') {
643
+                                $collection['path'] = basename($row['path']);
644
+                            }
645
+                            $row['collection'] = $collection;
646
+                            // Fetch all the children sources
647
+                            $children = $collectionBackend->getChildren($row[$column]);
648
+                            foreach ($children as $child) {
649
+                                $childItem = $row;
650
+                                $childItem['item_type'] = $itemType;
651
+                                if ($row['item_type'] != 'file' && $row['item_type'] != 'folder') {
652
+                                    $childItem['item_source'] = $child['source'];
653
+                                    $childItem['item_target'] = $child['target'];
654
+                                }
655
+                                if ($backend instanceof \OCP\Share_Backend_File_Dependent) {
656
+                                    if ($row['item_type'] == 'file' || $row['item_type'] == 'folder') {
657
+                                        $childItem['file_source'] = $child['source'];
658
+                                    } else { // TODO is this really needed if we already know that we use the file backend?
659
+                                        $meta = \OC\Files\Filesystem::getFileInfo($child['file_path']);
660
+                                        $childItem['file_source'] = $meta['fileid'];
661
+                                    }
662
+                                    $childItem['file_target'] =
663
+                                        \OC\Files\Filesystem::normalizePath($child['file_path']);
664
+                                }
665
+                                if (isset($item)) {
666
+                                    if ($childItem[$column] == $item) {
667
+                                        $collectionItems[] = $childItem;
668
+                                    }
669
+                                } else {
670
+                                    $collectionItems[] = $childItem;
671
+                                }
672
+                            }
673
+                        }
674
+                    }
675
+                    // Remove collection item
676
+                    $toRemove = $row['id'];
677
+                    if (array_key_exists($toRemove, $switchedItems)) {
678
+                        $toRemove = $switchedItems[$toRemove];
679
+                    }
680
+                    unset($items[$toRemove]);
681
+                } elseif ($includeCollections && $collectionTypes && in_array($row['item_type'], $collectionTypes)) {
682
+                    // FIXME: Thats a dirty hack to improve file sharing performance,
683
+                    // see github issue #10588 for more details
684
+                    // Need to find a solution which works for all back-ends
685
+                    $collectionBackend = self::getBackend($row['item_type']);
686
+                    $sharedParents = $collectionBackend->getParents($row['item_source']);
687
+                    foreach ($sharedParents as $parent) {
688
+                        $collectionItems[] = $parent;
689
+                    }
690
+                }
691
+            }
692
+            if (!empty($collectionItems)) {
693
+                $collectionItems = array_unique($collectionItems, SORT_REGULAR);
694
+                $items = array_merge($items, $collectionItems);
695
+            }
696
+
697
+            // filter out invalid items, these can appear when subshare entries exist
698
+            // for a group in which the requested user isn't a member any more
699
+            $items = array_filter($items, function ($item) {
700
+                return $item['share_type'] !== self::$shareTypeGroupUserUnique;
701
+            });
702
+
703
+            return self::formatResult($items, $column, $backend);
704
+        } elseif ($includeCollections && $collectionTypes && in_array('folder', $collectionTypes)) {
705
+            // FIXME: Thats a dirty hack to improve file sharing performance,
706
+            // see github issue #10588 for more details
707
+            // Need to find a solution which works for all back-ends
708
+            $collectionItems = [];
709
+            $collectionBackend = self::getBackend('folder');
710
+            $sharedParents = $collectionBackend->getParents($item, $shareWith, $uidOwner);
711
+            foreach ($sharedParents as $parent) {
712
+                $collectionItems[] = $parent;
713
+            }
714
+            return self::formatResult($collectionItems, $column, $backend);
715
+        }
716
+
717
+        return [];
718
+    }
719
+
720
+    /**
721
+     * group items with link to the same source
722
+     *
723
+     * @param array $items
724
+     * @param string $itemType
725
+     * @return array of grouped items
726
+     */
727
+    protected static function groupItems($items, $itemType) {
728
+        $fileSharing = $itemType === 'file' || $itemType === 'folder';
729
+
730
+        $result = [];
731
+
732
+        foreach ($items as $item) {
733
+            $grouped = false;
734
+            foreach ($result as $key => $r) {
735
+                // for file/folder shares we need to compare file_source, otherwise we compare item_source
736
+                // only group shares if they already point to the same target, otherwise the file where shared
737
+                // before grouping of shares was added. In this case we don't group them to avoid confusions
738
+                if (($fileSharing && $item['file_source'] === $r['file_source'] && $item['file_target'] === $r['file_target']) ||
739
+                    (!$fileSharing && $item['item_source'] === $r['item_source'] && $item['item_target'] === $r['item_target'])) {
740
+                    // add the first item to the list of grouped shares
741
+                    if (!isset($result[$key]['grouped'])) {
742
+                        $result[$key]['grouped'][] = $result[$key];
743
+                    }
744
+                    $result[$key]['permissions'] = (int)$item['permissions'] | (int)$r['permissions'];
745
+                    $result[$key]['grouped'][] = $item;
746
+                    $grouped = true;
747
+                    break;
748
+                }
749
+            }
750
+
751
+            if (!$grouped) {
752
+                $result[] = $item;
753
+            }
754
+        }
755
+
756
+        return $result;
757
+    }
758
+
759
+    /**
760
+     * Construct select statement
761
+     *
762
+     * @param bool $fileDependent ist it a file/folder share or a general share
763
+     */
764
+    private static function getSelectStatement(int $format, bool $fileDependent, ?string $uidOwner = null): IQueryBuilder {
765
+        /** @var IDBConnection $connection */
766
+        $connection = \OC::$server->get(IDBConnection::class);
767
+        $qb = $connection->getQueryBuilder();
768
+        if ($format == self::FORMAT_STATUSES) {
769
+            if ($fileDependent) {
770
+                return $qb->select(
771
+                    's.id',
772
+                    's.parent',
773
+                    'share_type',
774
+                    'path',
775
+                    'storage',
776
+                    'share_with',
777
+                    'uid_owner',
778
+                    'file_source',
779
+                    'stime',
780
+                    's.permissions',
781
+                    'uid_initiator'
782
+                )->selectAlias('st.id', 'storage_id')
783
+                    ->selectAlias('f.parent', 'file_parent');
784
+            }
785
+            return $qb->select('id', 'parent', 'share_type', 'share_with', 'uid_owner', 'item_source', 'stime', 's.permissions');
786
+        }
787
+
788
+        if (isset($uidOwner)) {
789
+            if ($fileDependent) {
790
+                return $qb->select(
791
+                    's.id',
792
+                    'item_type',
793
+                    'item_source',
794
+                    's.parent',
795
+                    'share_type',
796
+                    'share_with',
797
+                    'file_source',
798
+                    'file_target',
799
+                    'path',
800
+                    's.permissions',
801
+                    'stime',
802
+                    'expiration',
803
+                    'token',
804
+                    'storage',
805
+                    'mail_send',
806
+                    'uid_owner',
807
+                    'uid_initiator'
808
+                )->selectAlias('st.id', 'storage_id')
809
+                    ->selectAlias('f.parent', 'file_parent');
810
+            }
811
+            return $qb->select('id', 'item_type', 'item_source', 'parent', 'share_type',
812
+                'share_with', 'uid_owner', 'file_source', 'stime', 's.permissions',
813
+                'expiration', 'token', 'mail_send');
814
+        }
815
+
816
+        if ($fileDependent) {
817
+            if ($format == File::FORMAT_GET_FOLDER_CONTENTS || $format == File::FORMAT_FILE_APP_ROOT) {
818
+                return $qb->select(
819
+                    's.id',
820
+                    'item_type',
821
+                    'item_source',
822
+                    's.parent',
823
+                    'uid_owner',
824
+                    'share_type',
825
+                    'share_with',
826
+                    'file_source',
827
+                    'path',
828
+                    'file_target',
829
+                    's.permissions',
830
+                    'stime',
831
+                    'expiration',
832
+                    'storage',
833
+                    'name',
834
+                    'mtime',
835
+                    'mimepart',
836
+                    'size',
837
+                    'encrypted',
838
+                    'etag',
839
+                    'mail_send'
840
+                )->selectAlias('f.parent', 'file_parent');
841
+            }
842
+            return $qb->select(
843
+                's.id',
844
+                'item_type',
845
+                'item_source',
846
+                'item_target',
847
+                's.parent',
848
+                'share_type',
849
+                'share_with',
850
+                'uid_owner',
851
+                'file_source',
852
+                'path',
853
+                'file_target',
854
+                's.permissions',
855
+                'stime',
856
+                'expiration',
857
+                'token',
858
+                'storage',
859
+                'mail_send',
860
+            )->selectAlias('f.parent', 'file_parent')
861
+                ->selectAlias('st.id', 'storage_id');
862
+        }
863
+        return $qb->select('*');
864
+    }
865
+
866
+
867
+    /**
868
+     * transform db results
869
+     *
870
+     * @param array $row result
871
+     */
872
+    private static function transformDBResults(&$row) {
873
+        if (isset($row['id'])) {
874
+            $row['id'] = (int)$row['id'];
875
+        }
876
+        if (isset($row['share_type'])) {
877
+            $row['share_type'] = (int)$row['share_type'];
878
+        }
879
+        if (isset($row['parent'])) {
880
+            $row['parent'] = (int)$row['parent'];
881
+        }
882
+        if (isset($row['file_parent'])) {
883
+            $row['file_parent'] = (int)$row['file_parent'];
884
+        }
885
+        if (isset($row['file_source'])) {
886
+            $row['file_source'] = (int)$row['file_source'];
887
+        }
888
+        if (isset($row['permissions'])) {
889
+            $row['permissions'] = (int)$row['permissions'];
890
+        }
891
+        if (isset($row['storage'])) {
892
+            $row['storage'] = (int)$row['storage'];
893
+        }
894
+        if (isset($row['stime'])) {
895
+            $row['stime'] = (int)$row['stime'];
896
+        }
897
+        if (isset($row['expiration']) && $row['share_type'] !== IShare::TYPE_LINK) {
898
+            // discard expiration date for non-link shares, which might have been
899
+            // set by ancient bugs
900
+            $row['expiration'] = null;
901
+        }
902
+    }
903
+
904
+    /**
905
+     * format result
906
+     *
907
+     * @param array $items result
908
+     * @param string $column is it a file share or a general share ('file_target' or 'item_target')
909
+     * @param \OCP\Share_Backend $backend sharing backend
910
+     * @param int $format
911
+     * @param array $parameters additional format parameters
912
+     * @return array format result
913
+     */
914
+    private static function formatResult($items, $column, $backend, $format = self::FORMAT_NONE, $parameters = null) {
915
+        if ($format === self::FORMAT_NONE) {
916
+            return $items;
917
+        } elseif ($format === self::FORMAT_STATUSES) {
918
+            $statuses = [];
919
+            foreach ($items as $item) {
920
+                if ($item['share_type'] === IShare::TYPE_LINK) {
921
+                    if ($item['uid_initiator'] !== \OC::$server->getUserSession()->getUser()->getUID()) {
922
+                        continue;
923
+                    }
924
+                    $statuses[$item[$column]]['link'] = true;
925
+                } elseif (!isset($statuses[$item[$column]])) {
926
+                    $statuses[$item[$column]]['link'] = false;
927
+                }
928
+                if (!empty($item['file_target'])) {
929
+                    $statuses[$item[$column]]['path'] = $item['path'];
930
+                }
931
+            }
932
+            return $statuses;
933
+        } else {
934
+            return $backend->formatItems($items, $format, $parameters);
935
+        }
936
+    }
937
+
938
+    /**
939
+     * remove protocol from URL
940
+     *
941
+     * @param string $url
942
+     * @return string
943
+     */
944
+    public static function removeProtocolFromUrl($url) {
945
+        if (str_starts_with($url, 'https://')) {
946
+            return substr($url, strlen('https://'));
947
+        } elseif (str_starts_with($url, 'http://')) {
948
+            return substr($url, strlen('http://'));
949
+        }
950
+
951
+        return $url;
952
+    }
953
+
954
+
955
+    /**
956
+     * @return int
957
+     */
958
+    public static function getExpireInterval() {
959
+        return (int)\OC::$server->getConfig()->getAppValue('core', 'shareapi_expire_after_n_days', '7');
960
+    }
961
+
962
+    /**
963
+     * Checks whether the given path is reachable for the given owner
964
+     *
965
+     * @param string $path path relative to files
966
+     * @param string $ownerStorageId storage id of the owner
967
+     *
968
+     * @return boolean true if file is reachable, false otherwise
969
+     */
970
+    private static function isFileReachable($path, $ownerStorageId) {
971
+        // if outside the home storage, file is always considered reachable
972
+        if (!(substr($ownerStorageId, 0, 6) === 'home::' ||
973
+            substr($ownerStorageId, 0, 13) === 'object::user:'
974
+        )) {
975
+            return true;
976
+        }
977
+
978
+        // if inside the home storage, the file has to be under "/files/"
979
+        $path = ltrim($path, '/');
980
+        if (substr($path, 0, 6) === 'files/') {
981
+            return true;
982
+        }
983
+
984
+        return false;
985
+    }
986 986
 }
Please login to merge, or discard this patch.
lib/private/Preview/Generator.php 1 patch
Indentation   +601 added lines, -601 removed lines patch added patch discarded remove patch
@@ -48,619 +48,619 @@
 block discarded – undo
48 48
 use Symfony\Component\EventDispatcher\GenericEvent;
49 49
 
50 50
 class Generator {
51
-	public const SEMAPHORE_ID_ALL = 0x0a11;
52
-	public const SEMAPHORE_ID_NEW = 0x07ea;
53
-
54
-	/** @var IPreview */
55
-	private $previewManager;
56
-	/** @var IConfig */
57
-	private $config;
58
-	/** @var IAppData */
59
-	private $appData;
60
-	/** @var GeneratorHelper */
61
-	private $helper;
62
-	/** @var EventDispatcherInterface */
63
-	private $legacyEventDispatcher;
64
-	/** @var IEventDispatcher */
65
-	private $eventDispatcher;
66
-
67
-	public function __construct(
68
-		IConfig $config,
69
-		IPreview $previewManager,
70
-		IAppData $appData,
71
-		GeneratorHelper $helper,
72
-		EventDispatcherInterface $legacyEventDispatcher,
73
-		IEventDispatcher $eventDispatcher
74
-	) {
75
-		$this->config = $config;
76
-		$this->previewManager = $previewManager;
77
-		$this->appData = $appData;
78
-		$this->helper = $helper;
79
-		$this->legacyEventDispatcher = $legacyEventDispatcher;
80
-		$this->eventDispatcher = $eventDispatcher;
81
-	}
82
-
83
-	/**
84
-	 * Returns a preview of a file
85
-	 *
86
-	 * The cache is searched first and if nothing usable was found then a preview is
87
-	 * generated by one of the providers
88
-	 *
89
-	 * @param File $file
90
-	 * @param int $width
91
-	 * @param int $height
92
-	 * @param bool $crop
93
-	 * @param string $mode
94
-	 * @param string $mimeType
95
-	 * @return ISimpleFile
96
-	 * @throws NotFoundException
97
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
98
-	 */
99
-	public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
100
-		$specification = [
101
-			'width' => $width,
102
-			'height' => $height,
103
-			'crop' => $crop,
104
-			'mode' => $mode,
105
-		];
106
-
107
-		$this->legacyEventDispatcher->dispatch(
108
-			IPreview::EVENT,
109
-			new GenericEvent($file, $specification)
110
-		);
111
-		$this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent(
112
-			$file
113
-		));
114
-
115
-		// since we only ask for one preview, and the generate method return the last one it created, it returns the one we want
116
-		return $this->generatePreviews($file, [$specification], $mimeType);
117
-	}
118
-
119
-	/**
120
-	 * Generates previews of a file
121
-	 *
122
-	 * @param File $file
123
-	 * @param non-empty-array $specifications
124
-	 * @param string $mimeType
125
-	 * @return ISimpleFile the last preview that was generated
126
-	 * @throws NotFoundException
127
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
128
-	 */
129
-	public function generatePreviews(File $file, array $specifications, $mimeType = null) {
130
-		//Make sure that we can read the file
131
-		if (!$file->isReadable()) {
132
-			throw new NotFoundException('Cannot read file');
133
-		}
134
-
135
-		if ($mimeType === null) {
136
-			$mimeType = $file->getMimeType();
137
-		}
138
-
139
-		$previewFolder = $this->getPreviewFolder($file);
140
-		// List every existing preview first instead of trying to find them one by one
141
-		$previewFiles = $previewFolder->getDirectoryListing();
142
-
143
-		$previewVersion = '';
144
-		if ($file instanceof IVersionedPreviewFile) {
145
-			$previewVersion = $file->getPreviewVersion() . '-';
146
-		}
147
-
148
-		// If imaginary is enabled, and we request a small thumbnail,
149
-		// let's not generate the max preview for performance reasons
150
-		if (count($specifications) === 1
151
-			&& ($specifications[0]['width'] <= 256 || $specifications[0]['height'] <= 256)
152
-			&& preg_match(Imaginary::supportedMimeTypes(), $mimeType)
153
-			&& $this->config->getSystemValueString('preview_imaginary_url', 'invalid') !== 'invalid') {
154
-			$crop = $specifications[0]['crop'] ?? false;
155
-			$preview = $this->getSmallImagePreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion, $crop);
156
-
157
-			if ($preview->getSize() === 0) {
158
-				$preview->delete();
159
-				throw new NotFoundException('Cached preview size 0, invalid!');
160
-			}
161
-
162
-			return $preview;
163
-		}
164
-
165
-		// Get the max preview and infer the max preview sizes from that
166
-		$maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion);
167
-		$maxPreviewImage = null; // only load the image when we need it
168
-		if ($maxPreview->getSize() === 0) {
169
-			$maxPreview->delete();
170
-			throw new NotFoundException('Max preview size 0, invalid!');
171
-		}
172
-
173
-		[$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion);
174
-
175
-		if ($maxWidth <= 0 || $maxHeight <= 0) {
176
-			throw new NotFoundException('The maximum preview sizes are zero or less pixels');
177
-		}
178
-
179
-		$preview = null;
180
-
181
-		foreach ($specifications as $specification) {
182
-			$width = $specification['width'] ?? -1;
183
-			$height = $specification['height'] ?? -1;
184
-			$crop = $specification['crop'] ?? false;
185
-			$mode = $specification['mode'] ?? IPreview::MODE_FILL;
186
-
187
-			// If both width and height are -1 we just want the max preview
188
-			if ($width === -1 && $height === -1) {
189
-				$width = $maxWidth;
190
-				$height = $maxHeight;
191
-			}
192
-
193
-			// Calculate the preview size
194
-			[$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
195
-
196
-			// No need to generate a preview that is just the max preview
197
-			if ($width === $maxWidth && $height === $maxHeight) {
198
-				// ensure correct return value if this was the last one
199
-				$preview = $maxPreview;
200
-				continue;
201
-			}
202
-
203
-			// Try to get a cached preview. Else generate (and store) one
204
-			try {
205
-				try {
206
-					$preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
207
-				} catch (NotFoundException $e) {
208
-					if (!$this->previewManager->isMimeSupported($mimeType)) {
209
-						throw new NotFoundException();
210
-					}
211
-
212
-					if ($maxPreviewImage === null) {
213
-						$maxPreviewImage = $this->helper->getImage($maxPreview);
214
-					}
215
-
216
-					$preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion);
217
-					// New file, augment our array
218
-					$previewFiles[] = $preview;
219
-				}
220
-			} catch (\InvalidArgumentException $e) {
221
-				throw new NotFoundException("", 0, $e);
222
-			}
223
-
224
-			if ($preview->getSize() === 0) {
225
-				$preview->delete();
226
-				throw new NotFoundException('Cached preview size 0, invalid!');
227
-			}
228
-		}
229
-		assert($preview !== null);
230
-
231
-		// Free memory being used by the embedded image resource.  Without this the image is kept in memory indefinitely.
232
-		// Garbage Collection does NOT free this memory.  We have to do it ourselves.
233
-		if ($maxPreviewImage instanceof \OCP\Image) {
234
-			$maxPreviewImage->destroy();
235
-		}
236
-
237
-		return $preview;
238
-	}
239
-
240
-	/**
241
-	 * Generate a small image straight away without generating a max preview first
242
-	 * Preview generated is 256x256
243
-	 *
244
-	 * @param ISimpleFile[] $previewFiles
245
-	 *
246
-	 * @throws NotFoundException
247
-	 */
248
-	private function getSmallImagePreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, string $mimeType, string $prefix, bool $crop): ISimpleFile {
249
-		$width = 256;
250
-		$height = 256;
251
-
252
-		try {
253
-			return $this->getCachedPreview($previewFiles, $width, $height, $crop, $mimeType, $prefix);
254
-		} catch (NotFoundException $e) {
255
-			return $this->generateProviderPreview($previewFolder, $file, $width, $height, $crop, false, $mimeType, $prefix);
256
-		}
257
-	}
258
-
259
-	/**
260
-	 * Acquire a semaphore of the specified id and concurrency, blocking if necessary.
261
-	 * Return an identifier of the semaphore on success, which can be used to release it via
262
-	 * {@see Generator::unguardWithSemaphore()}.
263
-	 *
264
-	 * @param int $semId
265
-	 * @param int $concurrency
266
-	 * @return false|resource the semaphore on success or false on failure
267
-	 */
268
-	public static function guardWithSemaphore(int $semId, int $concurrency) {
269
-		if (!extension_loaded('sysvsem')) {
270
-			return false;
271
-		}
272
-		$sem = sem_get($semId, $concurrency);
273
-		if ($sem === false) {
274
-			return false;
275
-		}
276
-		if (!sem_acquire($sem)) {
277
-			return false;
278
-		}
279
-		return $sem;
280
-	}
281
-
282
-	/**
283
-	 * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}.
284
-	 *
285
-	 * @param resource|bool $semId the semaphore identifier returned by guardWithSemaphore
286
-	 * @return bool
287
-	 */
288
-	public static function unguardWithSemaphore($semId): bool {
289
-		if (!is_resource($semId) || !extension_loaded('sysvsem')) {
290
-			return false;
291
-		}
292
-		return sem_release($semId);
293
-	}
294
-
295
-	/**
296
-	 * Get the number of concurrent threads supported by the host.
297
-	 *
298
-	 * @return int number of concurrent threads, or 0 if it cannot be determined
299
-	 */
300
-	public static function getHardwareConcurrency(): int {
301
-		static $width;
302
-		if (!isset($width)) {
303
-			if (is_file("/proc/cpuinfo")) {
304
-				$width = substr_count(file_get_contents("/proc/cpuinfo"), "processor");
305
-			} else {
306
-				$width = 0;
307
-			}
308
-		}
309
-		return $width;
310
-	}
311
-
312
-	/**
313
-	 * Get number of concurrent preview generations from system config
314
-	 *
315
-	 * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`,
316
-	 * are available. If not set, the default values are determined with the hardware concurrency
317
-	 * of the host. In case the hardware concurrency cannot be determined, or the user sets an
318
-	 * invalid value, fallback values are:
319
-	 * For new images whose previews do not exist and need to be generated, 4;
320
-	 * For all preview generation requests, 8.
321
-	 * Value of `preview_concurrency_all` should be greater than or equal to that of
322
-	 * `preview_concurrency_new`, otherwise, the latter is returned.
323
-	 *
324
-	 * @param string $type either `preview_concurrency_new` or `preview_concurrency_all`
325
-	 * @return int number of concurrent preview generations, or -1 if $type is invalid
326
-	 */
327
-	public function getNumConcurrentPreviews(string $type): int {
328
-		static $cached = array();
329
-		if (array_key_exists($type, $cached)) {
330
-			return $cached[$type];
331
-		}
332
-
333
-		$hardwareConcurrency = self::getHardwareConcurrency();
334
-		switch ($type) {
335
-			case "preview_concurrency_all":
336
-				$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8;
337
-				$concurrency_all = $this->config->getSystemValueInt($type, $fallback);
338
-				$concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new");
339
-				$cached[$type] = max($concurrency_all, $concurrency_new);
340
-				break;
341
-			case "preview_concurrency_new":
342
-				$fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4;
343
-				$cached[$type] = $this->config->getSystemValueInt($type, $fallback);
344
-				break;
345
-			default:
346
-				return -1;
347
-		}
348
-		return $cached[$type];
349
-	}
350
-
351
-	/**
352
-	 * @param ISimpleFolder $previewFolder
353
-	 * @param ISimpleFile[] $previewFiles
354
-	 * @param File $file
355
-	 * @param string $mimeType
356
-	 * @param string $prefix
357
-	 * @return ISimpleFile
358
-	 * @throws NotFoundException
359
-	 */
360
-	private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) {
361
-		// We don't know the max preview size, so we can't use getCachedPreview.
362
-		// It might have been generated with a higher resolution than the current value.
363
-		foreach ($previewFiles as $node) {
364
-			$name = $node->getName();
365
-			if (($prefix === '' || str_starts_with($name, $prefix)) && strpos($name, 'max')) {
366
-				return $node;
367
-			}
368
-		}
369
-
370
-		$maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096);
371
-		$maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096);
372
-
373
-		return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix);
374
-	}
375
-
376
-	private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) {
377
-		$previewProviders = $this->previewManager->getProviders();
378
-		foreach ($previewProviders as $supportedMimeType => $providers) {
379
-			// Filter out providers that does not support this mime
380
-			if (!preg_match($supportedMimeType, $mimeType)) {
381
-				continue;
382
-			}
383
-
384
-			foreach ($providers as $providerClosure) {
385
-				$provider = $this->helper->getProvider($providerClosure);
386
-				if (!($provider instanceof IProviderV2)) {
387
-					continue;
388
-				}
389
-
390
-				if (!$provider->isAvailable($file)) {
391
-					continue;
392
-				}
393
-
394
-				$previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
395
-				$sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
396
-				try {
397
-					$preview = $this->helper->getThumbnail($provider, $file, $width, $height);
398
-				} finally {
399
-					self::unguardWithSemaphore($sem);
400
-				}
401
-
402
-				if (!($preview instanceof IImage)) {
403
-					continue;
404
-				}
405
-
406
-				$path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix);
407
-				try {
408
-					$file = $previewFolder->newFile($path);
409
-					if ($preview instanceof IStreamImage) {
410
-						$file->putContent($preview->resource());
411
-					} else {
412
-						$file->putContent($preview->data());
413
-					}
414
-				} catch (NotPermittedException $e) {
415
-					throw new NotFoundException();
416
-				}
417
-
418
-				return $file;
419
-			}
420
-		}
421
-
422
-		throw new NotFoundException('No provider successfully handled the preview generation');
423
-	}
424
-
425
-	/**
426
-	 * @param ISimpleFile $file
427
-	 * @param string $prefix
428
-	 * @return int[]
429
-	 */
430
-	private function getPreviewSize(ISimpleFile $file, string $prefix = '') {
431
-		$size = explode('-', substr($file->getName(), strlen($prefix)));
432
-		return [(int)$size[0], (int)$size[1]];
433
-	}
434
-
435
-	/**
436
-	 * @param int $width
437
-	 * @param int $height
438
-	 * @param bool $crop
439
-	 * @param bool $max
440
-	 * @param string $mimeType
441
-	 * @param string $prefix
442
-	 * @return string
443
-	 */
444
-	private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) {
445
-		$path = $prefix . (string)$width . '-' . (string)$height;
446
-		if ($crop) {
447
-			$path .= '-crop';
448
-		}
449
-		if ($max) {
450
-			$path .= '-max';
451
-		}
452
-
453
-		$ext = $this->getExtension($mimeType);
454
-		$path .= '.' . $ext;
455
-		return $path;
456
-	}
457
-
458
-
459
-	/**
460
-	 * @param int $width
461
-	 * @param int $height
462
-	 * @param bool $crop
463
-	 * @param string $mode
464
-	 * @param int $maxWidth
465
-	 * @param int $maxHeight
466
-	 * @return int[]
467
-	 */
468
-	private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
469
-		/*
51
+    public const SEMAPHORE_ID_ALL = 0x0a11;
52
+    public const SEMAPHORE_ID_NEW = 0x07ea;
53
+
54
+    /** @var IPreview */
55
+    private $previewManager;
56
+    /** @var IConfig */
57
+    private $config;
58
+    /** @var IAppData */
59
+    private $appData;
60
+    /** @var GeneratorHelper */
61
+    private $helper;
62
+    /** @var EventDispatcherInterface */
63
+    private $legacyEventDispatcher;
64
+    /** @var IEventDispatcher */
65
+    private $eventDispatcher;
66
+
67
+    public function __construct(
68
+        IConfig $config,
69
+        IPreview $previewManager,
70
+        IAppData $appData,
71
+        GeneratorHelper $helper,
72
+        EventDispatcherInterface $legacyEventDispatcher,
73
+        IEventDispatcher $eventDispatcher
74
+    ) {
75
+        $this->config = $config;
76
+        $this->previewManager = $previewManager;
77
+        $this->appData = $appData;
78
+        $this->helper = $helper;
79
+        $this->legacyEventDispatcher = $legacyEventDispatcher;
80
+        $this->eventDispatcher = $eventDispatcher;
81
+    }
82
+
83
+    /**
84
+     * Returns a preview of a file
85
+     *
86
+     * The cache is searched first and if nothing usable was found then a preview is
87
+     * generated by one of the providers
88
+     *
89
+     * @param File $file
90
+     * @param int $width
91
+     * @param int $height
92
+     * @param bool $crop
93
+     * @param string $mode
94
+     * @param string $mimeType
95
+     * @return ISimpleFile
96
+     * @throws NotFoundException
97
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
98
+     */
99
+    public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
100
+        $specification = [
101
+            'width' => $width,
102
+            'height' => $height,
103
+            'crop' => $crop,
104
+            'mode' => $mode,
105
+        ];
106
+
107
+        $this->legacyEventDispatcher->dispatch(
108
+            IPreview::EVENT,
109
+            new GenericEvent($file, $specification)
110
+        );
111
+        $this->eventDispatcher->dispatchTyped(new BeforePreviewFetchedEvent(
112
+            $file
113
+        ));
114
+
115
+        // since we only ask for one preview, and the generate method return the last one it created, it returns the one we want
116
+        return $this->generatePreviews($file, [$specification], $mimeType);
117
+    }
118
+
119
+    /**
120
+     * Generates previews of a file
121
+     *
122
+     * @param File $file
123
+     * @param non-empty-array $specifications
124
+     * @param string $mimeType
125
+     * @return ISimpleFile the last preview that was generated
126
+     * @throws NotFoundException
127
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
128
+     */
129
+    public function generatePreviews(File $file, array $specifications, $mimeType = null) {
130
+        //Make sure that we can read the file
131
+        if (!$file->isReadable()) {
132
+            throw new NotFoundException('Cannot read file');
133
+        }
134
+
135
+        if ($mimeType === null) {
136
+            $mimeType = $file->getMimeType();
137
+        }
138
+
139
+        $previewFolder = $this->getPreviewFolder($file);
140
+        // List every existing preview first instead of trying to find them one by one
141
+        $previewFiles = $previewFolder->getDirectoryListing();
142
+
143
+        $previewVersion = '';
144
+        if ($file instanceof IVersionedPreviewFile) {
145
+            $previewVersion = $file->getPreviewVersion() . '-';
146
+        }
147
+
148
+        // If imaginary is enabled, and we request a small thumbnail,
149
+        // let's not generate the max preview for performance reasons
150
+        if (count($specifications) === 1
151
+            && ($specifications[0]['width'] <= 256 || $specifications[0]['height'] <= 256)
152
+            && preg_match(Imaginary::supportedMimeTypes(), $mimeType)
153
+            && $this->config->getSystemValueString('preview_imaginary_url', 'invalid') !== 'invalid') {
154
+            $crop = $specifications[0]['crop'] ?? false;
155
+            $preview = $this->getSmallImagePreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion, $crop);
156
+
157
+            if ($preview->getSize() === 0) {
158
+                $preview->delete();
159
+                throw new NotFoundException('Cached preview size 0, invalid!');
160
+            }
161
+
162
+            return $preview;
163
+        }
164
+
165
+        // Get the max preview and infer the max preview sizes from that
166
+        $maxPreview = $this->getMaxPreview($previewFolder, $previewFiles, $file, $mimeType, $previewVersion);
167
+        $maxPreviewImage = null; // only load the image when we need it
168
+        if ($maxPreview->getSize() === 0) {
169
+            $maxPreview->delete();
170
+            throw new NotFoundException('Max preview size 0, invalid!');
171
+        }
172
+
173
+        [$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion);
174
+
175
+        if ($maxWidth <= 0 || $maxHeight <= 0) {
176
+            throw new NotFoundException('The maximum preview sizes are zero or less pixels');
177
+        }
178
+
179
+        $preview = null;
180
+
181
+        foreach ($specifications as $specification) {
182
+            $width = $specification['width'] ?? -1;
183
+            $height = $specification['height'] ?? -1;
184
+            $crop = $specification['crop'] ?? false;
185
+            $mode = $specification['mode'] ?? IPreview::MODE_FILL;
186
+
187
+            // If both width and height are -1 we just want the max preview
188
+            if ($width === -1 && $height === -1) {
189
+                $width = $maxWidth;
190
+                $height = $maxHeight;
191
+            }
192
+
193
+            // Calculate the preview size
194
+            [$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
195
+
196
+            // No need to generate a preview that is just the max preview
197
+            if ($width === $maxWidth && $height === $maxHeight) {
198
+                // ensure correct return value if this was the last one
199
+                $preview = $maxPreview;
200
+                continue;
201
+            }
202
+
203
+            // Try to get a cached preview. Else generate (and store) one
204
+            try {
205
+                try {
206
+                    $preview = $this->getCachedPreview($previewFiles, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
207
+                } catch (NotFoundException $e) {
208
+                    if (!$this->previewManager->isMimeSupported($mimeType)) {
209
+                        throw new NotFoundException();
210
+                    }
211
+
212
+                    if ($maxPreviewImage === null) {
213
+                        $maxPreviewImage = $this->helper->getImage($maxPreview);
214
+                    }
215
+
216
+                    $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion);
217
+                    // New file, augment our array
218
+                    $previewFiles[] = $preview;
219
+                }
220
+            } catch (\InvalidArgumentException $e) {
221
+                throw new NotFoundException("", 0, $e);
222
+            }
223
+
224
+            if ($preview->getSize() === 0) {
225
+                $preview->delete();
226
+                throw new NotFoundException('Cached preview size 0, invalid!');
227
+            }
228
+        }
229
+        assert($preview !== null);
230
+
231
+        // Free memory being used by the embedded image resource.  Without this the image is kept in memory indefinitely.
232
+        // Garbage Collection does NOT free this memory.  We have to do it ourselves.
233
+        if ($maxPreviewImage instanceof \OCP\Image) {
234
+            $maxPreviewImage->destroy();
235
+        }
236
+
237
+        return $preview;
238
+    }
239
+
240
+    /**
241
+     * Generate a small image straight away without generating a max preview first
242
+     * Preview generated is 256x256
243
+     *
244
+     * @param ISimpleFile[] $previewFiles
245
+     *
246
+     * @throws NotFoundException
247
+     */
248
+    private function getSmallImagePreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, string $mimeType, string $prefix, bool $crop): ISimpleFile {
249
+        $width = 256;
250
+        $height = 256;
251
+
252
+        try {
253
+            return $this->getCachedPreview($previewFiles, $width, $height, $crop, $mimeType, $prefix);
254
+        } catch (NotFoundException $e) {
255
+            return $this->generateProviderPreview($previewFolder, $file, $width, $height, $crop, false, $mimeType, $prefix);
256
+        }
257
+    }
258
+
259
+    /**
260
+     * Acquire a semaphore of the specified id and concurrency, blocking if necessary.
261
+     * Return an identifier of the semaphore on success, which can be used to release it via
262
+     * {@see Generator::unguardWithSemaphore()}.
263
+     *
264
+     * @param int $semId
265
+     * @param int $concurrency
266
+     * @return false|resource the semaphore on success or false on failure
267
+     */
268
+    public static function guardWithSemaphore(int $semId, int $concurrency) {
269
+        if (!extension_loaded('sysvsem')) {
270
+            return false;
271
+        }
272
+        $sem = sem_get($semId, $concurrency);
273
+        if ($sem === false) {
274
+            return false;
275
+        }
276
+        if (!sem_acquire($sem)) {
277
+            return false;
278
+        }
279
+        return $sem;
280
+    }
281
+
282
+    /**
283
+     * Releases the semaphore acquired from {@see Generator::guardWithSemaphore()}.
284
+     *
285
+     * @param resource|bool $semId the semaphore identifier returned by guardWithSemaphore
286
+     * @return bool
287
+     */
288
+    public static function unguardWithSemaphore($semId): bool {
289
+        if (!is_resource($semId) || !extension_loaded('sysvsem')) {
290
+            return false;
291
+        }
292
+        return sem_release($semId);
293
+    }
294
+
295
+    /**
296
+     * Get the number of concurrent threads supported by the host.
297
+     *
298
+     * @return int number of concurrent threads, or 0 if it cannot be determined
299
+     */
300
+    public static function getHardwareConcurrency(): int {
301
+        static $width;
302
+        if (!isset($width)) {
303
+            if (is_file("/proc/cpuinfo")) {
304
+                $width = substr_count(file_get_contents("/proc/cpuinfo"), "processor");
305
+            } else {
306
+                $width = 0;
307
+            }
308
+        }
309
+        return $width;
310
+    }
311
+
312
+    /**
313
+     * Get number of concurrent preview generations from system config
314
+     *
315
+     * Two config entries, `preview_concurrency_new` and `preview_concurrency_all`,
316
+     * are available. If not set, the default values are determined with the hardware concurrency
317
+     * of the host. In case the hardware concurrency cannot be determined, or the user sets an
318
+     * invalid value, fallback values are:
319
+     * For new images whose previews do not exist and need to be generated, 4;
320
+     * For all preview generation requests, 8.
321
+     * Value of `preview_concurrency_all` should be greater than or equal to that of
322
+     * `preview_concurrency_new`, otherwise, the latter is returned.
323
+     *
324
+     * @param string $type either `preview_concurrency_new` or `preview_concurrency_all`
325
+     * @return int number of concurrent preview generations, or -1 if $type is invalid
326
+     */
327
+    public function getNumConcurrentPreviews(string $type): int {
328
+        static $cached = array();
329
+        if (array_key_exists($type, $cached)) {
330
+            return $cached[$type];
331
+        }
332
+
333
+        $hardwareConcurrency = self::getHardwareConcurrency();
334
+        switch ($type) {
335
+            case "preview_concurrency_all":
336
+                $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8;
337
+                $concurrency_all = $this->config->getSystemValueInt($type, $fallback);
338
+                $concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new");
339
+                $cached[$type] = max($concurrency_all, $concurrency_new);
340
+                break;
341
+            case "preview_concurrency_new":
342
+                $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4;
343
+                $cached[$type] = $this->config->getSystemValueInt($type, $fallback);
344
+                break;
345
+            default:
346
+                return -1;
347
+        }
348
+        return $cached[$type];
349
+    }
350
+
351
+    /**
352
+     * @param ISimpleFolder $previewFolder
353
+     * @param ISimpleFile[] $previewFiles
354
+     * @param File $file
355
+     * @param string $mimeType
356
+     * @param string $prefix
357
+     * @return ISimpleFile
358
+     * @throws NotFoundException
359
+     */
360
+    private function getMaxPreview(ISimpleFolder $previewFolder, array $previewFiles, File $file, $mimeType, $prefix) {
361
+        // We don't know the max preview size, so we can't use getCachedPreview.
362
+        // It might have been generated with a higher resolution than the current value.
363
+        foreach ($previewFiles as $node) {
364
+            $name = $node->getName();
365
+            if (($prefix === '' || str_starts_with($name, $prefix)) && strpos($name, 'max')) {
366
+                return $node;
367
+            }
368
+        }
369
+
370
+        $maxWidth = $this->config->getSystemValueInt('preview_max_x', 4096);
371
+        $maxHeight = $this->config->getSystemValueInt('preview_max_y', 4096);
372
+
373
+        return $this->generateProviderPreview($previewFolder, $file, $maxWidth, $maxHeight, false, true, $mimeType, $prefix);
374
+    }
375
+
376
+    private function generateProviderPreview(ISimpleFolder $previewFolder, File $file, int $width, int $height, bool $crop, bool $max, string $mimeType, string $prefix) {
377
+        $previewProviders = $this->previewManager->getProviders();
378
+        foreach ($previewProviders as $supportedMimeType => $providers) {
379
+            // Filter out providers that does not support this mime
380
+            if (!preg_match($supportedMimeType, $mimeType)) {
381
+                continue;
382
+            }
383
+
384
+            foreach ($providers as $providerClosure) {
385
+                $provider = $this->helper->getProvider($providerClosure);
386
+                if (!($provider instanceof IProviderV2)) {
387
+                    continue;
388
+                }
389
+
390
+                if (!$provider->isAvailable($file)) {
391
+                    continue;
392
+                }
393
+
394
+                $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
395
+                $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
396
+                try {
397
+                    $preview = $this->helper->getThumbnail($provider, $file, $width, $height);
398
+                } finally {
399
+                    self::unguardWithSemaphore($sem);
400
+                }
401
+
402
+                if (!($preview instanceof IImage)) {
403
+                    continue;
404
+                }
405
+
406
+                $path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix);
407
+                try {
408
+                    $file = $previewFolder->newFile($path);
409
+                    if ($preview instanceof IStreamImage) {
410
+                        $file->putContent($preview->resource());
411
+                    } else {
412
+                        $file->putContent($preview->data());
413
+                    }
414
+                } catch (NotPermittedException $e) {
415
+                    throw new NotFoundException();
416
+                }
417
+
418
+                return $file;
419
+            }
420
+        }
421
+
422
+        throw new NotFoundException('No provider successfully handled the preview generation');
423
+    }
424
+
425
+    /**
426
+     * @param ISimpleFile $file
427
+     * @param string $prefix
428
+     * @return int[]
429
+     */
430
+    private function getPreviewSize(ISimpleFile $file, string $prefix = '') {
431
+        $size = explode('-', substr($file->getName(), strlen($prefix)));
432
+        return [(int)$size[0], (int)$size[1]];
433
+    }
434
+
435
+    /**
436
+     * @param int $width
437
+     * @param int $height
438
+     * @param bool $crop
439
+     * @param bool $max
440
+     * @param string $mimeType
441
+     * @param string $prefix
442
+     * @return string
443
+     */
444
+    private function generatePath($width, $height, $crop, $max, $mimeType, $prefix) {
445
+        $path = $prefix . (string)$width . '-' . (string)$height;
446
+        if ($crop) {
447
+            $path .= '-crop';
448
+        }
449
+        if ($max) {
450
+            $path .= '-max';
451
+        }
452
+
453
+        $ext = $this->getExtension($mimeType);
454
+        $path .= '.' . $ext;
455
+        return $path;
456
+    }
457
+
458
+
459
+    /**
460
+     * @param int $width
461
+     * @param int $height
462
+     * @param bool $crop
463
+     * @param string $mode
464
+     * @param int $maxWidth
465
+     * @param int $maxHeight
466
+     * @return int[]
467
+     */
468
+    private function calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight) {
469
+        /*
470 470
 		 * If we are not cropping we have to make sure the requested image
471 471
 		 * respects the aspect ratio of the original.
472 472
 		 */
473
-		if (!$crop) {
474
-			$ratio = $maxHeight / $maxWidth;
473
+        if (!$crop) {
474
+            $ratio = $maxHeight / $maxWidth;
475 475
 
476
-			if ($width === -1) {
477
-				$width = $height / $ratio;
478
-			}
479
-			if ($height === -1) {
480
-				$height = $width * $ratio;
481
-			}
476
+            if ($width === -1) {
477
+                $width = $height / $ratio;
478
+            }
479
+            if ($height === -1) {
480
+                $height = $width * $ratio;
481
+            }
482 482
 
483
-			$ratioH = $height / $maxHeight;
484
-			$ratioW = $width / $maxWidth;
483
+            $ratioH = $height / $maxHeight;
484
+            $ratioW = $width / $maxWidth;
485 485
 
486
-			/*
486
+            /*
487 487
 			 * Fill means that the $height and $width are the max
488 488
 			 * Cover means min.
489 489
 			 */
490
-			if ($mode === IPreview::MODE_FILL) {
491
-				if ($ratioH > $ratioW) {
492
-					$height = $width * $ratio;
493
-				} else {
494
-					$width = $height / $ratio;
495
-				}
496
-			} elseif ($mode === IPreview::MODE_COVER) {
497
-				if ($ratioH > $ratioW) {
498
-					$width = $height / $ratio;
499
-				} else {
500
-					$height = $width * $ratio;
501
-				}
502
-			}
503
-		}
504
-
505
-		if ($height !== $maxHeight && $width !== $maxWidth) {
506
-			/*
490
+            if ($mode === IPreview::MODE_FILL) {
491
+                if ($ratioH > $ratioW) {
492
+                    $height = $width * $ratio;
493
+                } else {
494
+                    $width = $height / $ratio;
495
+                }
496
+            } elseif ($mode === IPreview::MODE_COVER) {
497
+                if ($ratioH > $ratioW) {
498
+                    $width = $height / $ratio;
499
+                } else {
500
+                    $height = $width * $ratio;
501
+                }
502
+            }
503
+        }
504
+
505
+        if ($height !== $maxHeight && $width !== $maxWidth) {
506
+            /*
507 507
 			 * Scale to the nearest power of four
508 508
 			 */
509
-			$pow4height = 4 ** ceil(log($height) / log(4));
510
-			$pow4width = 4 ** ceil(log($width) / log(4));
511
-
512
-			// Minimum size is 64
513
-			$pow4height = max($pow4height, 64);
514
-			$pow4width = max($pow4width, 64);
515
-
516
-			$ratioH = $height / $pow4height;
517
-			$ratioW = $width / $pow4width;
518
-
519
-			if ($ratioH < $ratioW) {
520
-				$width = $pow4width;
521
-				$height /= $ratioW;
522
-			} else {
523
-				$height = $pow4height;
524
-				$width /= $ratioH;
525
-			}
526
-		}
527
-
528
-		/*
509
+            $pow4height = 4 ** ceil(log($height) / log(4));
510
+            $pow4width = 4 ** ceil(log($width) / log(4));
511
+
512
+            // Minimum size is 64
513
+            $pow4height = max($pow4height, 64);
514
+            $pow4width = max($pow4width, 64);
515
+
516
+            $ratioH = $height / $pow4height;
517
+            $ratioW = $width / $pow4width;
518
+
519
+            if ($ratioH < $ratioW) {
520
+                $width = $pow4width;
521
+                $height /= $ratioW;
522
+            } else {
523
+                $height = $pow4height;
524
+                $width /= $ratioH;
525
+            }
526
+        }
527
+
528
+        /*
529 529
 		 * Make sure the requested height and width fall within the max
530 530
 		 * of the preview.
531 531
 		 */
532
-		if ($height > $maxHeight) {
533
-			$ratio = $height / $maxHeight;
534
-			$height = $maxHeight;
535
-			$width /= $ratio;
536
-		}
537
-		if ($width > $maxWidth) {
538
-			$ratio = $width / $maxWidth;
539
-			$width = $maxWidth;
540
-			$height /= $ratio;
541
-		}
542
-
543
-		return [(int)round($width), (int)round($height)];
544
-	}
545
-
546
-	/**
547
-	 * @param ISimpleFolder $previewFolder
548
-	 * @param ISimpleFile $maxPreview
549
-	 * @param int $width
550
-	 * @param int $height
551
-	 * @param bool $crop
552
-	 * @param int $maxWidth
553
-	 * @param int $maxHeight
554
-	 * @param string $prefix
555
-	 * @return ISimpleFile
556
-	 * @throws NotFoundException
557
-	 * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
558
-	 */
559
-	private function generatePreview(ISimpleFolder $previewFolder, IImage $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) {
560
-		$preview = $maxPreview;
561
-		if (!$preview->valid()) {
562
-			throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
563
-		}
564
-
565
-		$previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
566
-		$sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
567
-		try {
568
-			if ($crop) {
569
-				if ($height !== $preview->height() && $width !== $preview->width()) {
570
-					//Resize
571
-					$widthR = $preview->width() / $width;
572
-					$heightR = $preview->height() / $height;
573
-
574
-					if ($widthR > $heightR) {
575
-						$scaleH = $height;
576
-						$scaleW = $maxWidth / $heightR;
577
-					} else {
578
-						$scaleH = $maxHeight / $widthR;
579
-						$scaleW = $width;
580
-					}
581
-					$preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
582
-				}
583
-				$cropX = (int)floor(abs($width - $preview->width()) * 0.5);
584
-				$cropY = (int)floor(abs($height - $preview->height()) * 0.5);
585
-				$preview = $preview->cropCopy($cropX, $cropY, $width, $height);
586
-			} else {
587
-				$preview = $maxPreview->resizeCopy(max($width, $height));
588
-			}
589
-		} finally {
590
-			self::unguardWithSemaphore($sem);
591
-		}
592
-
593
-
594
-		$path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix);
595
-		try {
596
-			$file = $previewFolder->newFile($path);
597
-			$file->putContent($preview->data());
598
-		} catch (NotPermittedException $e) {
599
-			throw new NotFoundException();
600
-		}
601
-
602
-		return $file;
603
-	}
604
-
605
-	/**
606
-	 * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing()
607
-	 * @param int $width
608
-	 * @param int $height
609
-	 * @param bool $crop
610
-	 * @param string $mimeType
611
-	 * @param string $prefix
612
-	 * @return ISimpleFile
613
-	 *
614
-	 * @throws NotFoundException
615
-	 */
616
-	private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) {
617
-		$path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix);
618
-		foreach ($files as $file) {
619
-			if ($file->getName() === $path) {
620
-				return $file;
621
-			}
622
-		}
623
-		throw new NotFoundException();
624
-	}
625
-
626
-	/**
627
-	 * Get the specific preview folder for this file
628
-	 *
629
-	 * @param File $file
630
-	 * @return ISimpleFolder
631
-	 *
632
-	 * @throws InvalidPathException
633
-	 * @throws NotFoundException
634
-	 * @throws NotPermittedException
635
-	 */
636
-	private function getPreviewFolder(File $file) {
637
-		// Obtain file id outside of try catch block to prevent the creation of an existing folder
638
-		$fileId = (string)$file->getId();
639
-
640
-		try {
641
-			$folder = $this->appData->getFolder($fileId);
642
-		} catch (NotFoundException $e) {
643
-			$folder = $this->appData->newFolder($fileId);
644
-		}
645
-
646
-		return $folder;
647
-	}
648
-
649
-	/**
650
-	 * @param string $mimeType
651
-	 * @return null|string
652
-	 * @throws \InvalidArgumentException
653
-	 */
654
-	private function getExtension($mimeType) {
655
-		switch ($mimeType) {
656
-			case 'image/png':
657
-				return 'png';
658
-			case 'image/jpeg':
659
-				return 'jpg';
660
-			case 'image/gif':
661
-				return 'gif';
662
-			default:
663
-				throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"');
664
-		}
665
-	}
532
+        if ($height > $maxHeight) {
533
+            $ratio = $height / $maxHeight;
534
+            $height = $maxHeight;
535
+            $width /= $ratio;
536
+        }
537
+        if ($width > $maxWidth) {
538
+            $ratio = $width / $maxWidth;
539
+            $width = $maxWidth;
540
+            $height /= $ratio;
541
+        }
542
+
543
+        return [(int)round($width), (int)round($height)];
544
+    }
545
+
546
+    /**
547
+     * @param ISimpleFolder $previewFolder
548
+     * @param ISimpleFile $maxPreview
549
+     * @param int $width
550
+     * @param int $height
551
+     * @param bool $crop
552
+     * @param int $maxWidth
553
+     * @param int $maxHeight
554
+     * @param string $prefix
555
+     * @return ISimpleFile
556
+     * @throws NotFoundException
557
+     * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
558
+     */
559
+    private function generatePreview(ISimpleFolder $previewFolder, IImage $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) {
560
+        $preview = $maxPreview;
561
+        if (!$preview->valid()) {
562
+            throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
563
+        }
564
+
565
+        $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new');
566
+        $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency);
567
+        try {
568
+            if ($crop) {
569
+                if ($height !== $preview->height() && $width !== $preview->width()) {
570
+                    //Resize
571
+                    $widthR = $preview->width() / $width;
572
+                    $heightR = $preview->height() / $height;
573
+
574
+                    if ($widthR > $heightR) {
575
+                        $scaleH = $height;
576
+                        $scaleW = $maxWidth / $heightR;
577
+                    } else {
578
+                        $scaleH = $maxHeight / $widthR;
579
+                        $scaleW = $width;
580
+                    }
581
+                    $preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
582
+                }
583
+                $cropX = (int)floor(abs($width - $preview->width()) * 0.5);
584
+                $cropY = (int)floor(abs($height - $preview->height()) * 0.5);
585
+                $preview = $preview->cropCopy($cropX, $cropY, $width, $height);
586
+            } else {
587
+                $preview = $maxPreview->resizeCopy(max($width, $height));
588
+            }
589
+        } finally {
590
+            self::unguardWithSemaphore($sem);
591
+        }
592
+
593
+
594
+        $path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix);
595
+        try {
596
+            $file = $previewFolder->newFile($path);
597
+            $file->putContent($preview->data());
598
+        } catch (NotPermittedException $e) {
599
+            throw new NotFoundException();
600
+        }
601
+
602
+        return $file;
603
+    }
604
+
605
+    /**
606
+     * @param ISimpleFile[] $files Array of FileInfo, as the result of getDirectoryListing()
607
+     * @param int $width
608
+     * @param int $height
609
+     * @param bool $crop
610
+     * @param string $mimeType
611
+     * @param string $prefix
612
+     * @return ISimpleFile
613
+     *
614
+     * @throws NotFoundException
615
+     */
616
+    private function getCachedPreview($files, $width, $height, $crop, $mimeType, $prefix) {
617
+        $path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix);
618
+        foreach ($files as $file) {
619
+            if ($file->getName() === $path) {
620
+                return $file;
621
+            }
622
+        }
623
+        throw new NotFoundException();
624
+    }
625
+
626
+    /**
627
+     * Get the specific preview folder for this file
628
+     *
629
+     * @param File $file
630
+     * @return ISimpleFolder
631
+     *
632
+     * @throws InvalidPathException
633
+     * @throws NotFoundException
634
+     * @throws NotPermittedException
635
+     */
636
+    private function getPreviewFolder(File $file) {
637
+        // Obtain file id outside of try catch block to prevent the creation of an existing folder
638
+        $fileId = (string)$file->getId();
639
+
640
+        try {
641
+            $folder = $this->appData->getFolder($fileId);
642
+        } catch (NotFoundException $e) {
643
+            $folder = $this->appData->newFolder($fileId);
644
+        }
645
+
646
+        return $folder;
647
+    }
648
+
649
+    /**
650
+     * @param string $mimeType
651
+     * @return null|string
652
+     * @throws \InvalidArgumentException
653
+     */
654
+    private function getExtension($mimeType) {
655
+        switch ($mimeType) {
656
+            case 'image/png':
657
+                return 'png';
658
+            case 'image/jpeg':
659
+                return 'jpg';
660
+            case 'image/gif':
661
+                return 'gif';
662
+            default:
663
+                throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"');
664
+        }
665
+    }
666 666
 }
Please login to merge, or discard this patch.
lib/private/App/DependencyAnalyzer.php 2 patches
Indentation   +367 added lines, -367 removed lines patch added patch discarded remove patch
@@ -33,371 +33,371 @@
 block discarded – undo
33 33
 use OCP\IL10N;
34 34
 
35 35
 class DependencyAnalyzer {
36
-	/** @var Platform */
37
-	private $platform;
38
-	/** @var \OCP\IL10N */
39
-	private $l;
40
-	/** @var array */
41
-	private $appInfo;
42
-
43
-	/**
44
-	 * @param Platform $platform
45
-	 * @param \OCP\IL10N $l
46
-	 */
47
-	public function __construct(Platform $platform, IL10N $l) {
48
-		$this->platform = $platform;
49
-		$this->l = $l;
50
-	}
51
-
52
-	/**
53
-	 * @param array $app
54
-	 * @returns array of missing dependencies
55
-	 */
56
-	public function analyze(array $app, bool $ignoreMax = false) {
57
-		$this->appInfo = $app;
58
-		if (isset($app['dependencies'])) {
59
-			$dependencies = $app['dependencies'];
60
-		} else {
61
-			$dependencies = [];
62
-		}
63
-
64
-		return array_merge(
65
-			$this->analyzeArchitecture($dependencies),
66
-			$this->analyzePhpVersion($dependencies),
67
-			$this->analyzeDatabases($dependencies),
68
-			$this->analyzeCommands($dependencies),
69
-			$this->analyzeLibraries($dependencies),
70
-			$this->analyzeOS($dependencies),
71
-			$this->analyzeOC($dependencies, $app, $ignoreMax)
72
-		);
73
-	}
74
-
75
-	public function isMarkedCompatible(array $app): bool {
76
-		if (isset($app['dependencies'])) {
77
-			$dependencies = $app['dependencies'];
78
-		} else {
79
-			$dependencies = [];
80
-		}
81
-
82
-		$maxVersion = $this->getMaxVersion($dependencies, $app);
83
-		if ($maxVersion === null) {
84
-			return true;
85
-		}
86
-		return !$this->compareBigger($this->platform->getOcVersion(), $maxVersion);
87
-	}
88
-
89
-	/**
90
-	 * Truncates both versions to the lowest common version, e.g.
91
-	 * 5.1.2.3 and 5.1 will be turned into 5.1 and 5.1,
92
-	 * 5.2.6.5 and 5.1 will be turned into 5.2 and 5.1
93
-	 * @param string $first
94
-	 * @param string $second
95
-	 * @return string[] first element is the first version, second element is the
96
-	 * second version
97
-	 */
98
-	private function normalizeVersions($first, $second) {
99
-		$first = explode('.', $first);
100
-		$second = explode('.', $second);
101
-
102
-		// get both arrays to the same minimum size
103
-		$length = min(count($second), count($first));
104
-		$first = array_slice($first, 0, $length);
105
-		$second = array_slice($second, 0, $length);
106
-
107
-		return [implode('.', $first), implode('.', $second)];
108
-	}
109
-
110
-	/**
111
-	 * Parameters will be normalized and then passed into version_compare
112
-	 * in the same order they are specified in the method header
113
-	 * @param string $first
114
-	 * @param string $second
115
-	 * @param string $operator
116
-	 * @return bool result similar to version_compare
117
-	 */
118
-	private function compare($first, $second, $operator) {
119
-		// we can't normalize versions if one of the given parameters is not a
120
-		// version string but null. In case one parameter is null normalization
121
-		// will therefore be skipped
122
-		if ($first !== null && $second !== null) {
123
-			[$first, $second] = $this->normalizeVersions($first, $second);
124
-		}
125
-
126
-		return version_compare($first, $second, $operator);
127
-	}
128
-
129
-	/**
130
-	 * Checks if a version is bigger than another version
131
-	 * @param string $first
132
-	 * @param string $second
133
-	 * @return bool true if the first version is bigger than the second
134
-	 */
135
-	private function compareBigger($first, $second) {
136
-		return $this->compare($first, $second, '>');
137
-	}
138
-
139
-	/**
140
-	 * Checks if a version is smaller than another version
141
-	 * @param string $first
142
-	 * @param string $second
143
-	 * @return bool true if the first version is smaller than the second
144
-	 */
145
-	private function compareSmaller($first, $second) {
146
-		return $this->compare($first, $second, '<');
147
-	}
148
-
149
-	/**
150
-	 * @param array $dependencies
151
-	 * @return array
152
-	 */
153
-	private function analyzePhpVersion(array $dependencies) {
154
-		$missing = [];
155
-		if (isset($dependencies['php']['@attributes']['min-version'])) {
156
-			$minVersion = $dependencies['php']['@attributes']['min-version'];
157
-			if ($this->compareSmaller($this->platform->getPhpVersion(), $minVersion)) {
158
-				$missing[] = $this->l->t('PHP %s or higher is required.', [$minVersion]);
159
-			}
160
-		}
161
-		if (isset($dependencies['php']['@attributes']['max-version'])) {
162
-			$maxVersion = $dependencies['php']['@attributes']['max-version'];
163
-			if ($this->compareBigger($this->platform->getPhpVersion(), $maxVersion)) {
164
-				$missing[] = $this->l->t('PHP with a version lower than %s is required.', [$maxVersion]);
165
-			}
166
-		}
167
-		if (isset($dependencies['php']['@attributes']['min-int-size'])) {
168
-			$intSize = $dependencies['php']['@attributes']['min-int-size'];
169
-			if ($intSize > $this->platform->getIntSize() * 8) {
170
-				$missing[] = $this->l->t('%sbit or higher PHP required.', [$intSize]);
171
-			}
172
-		}
173
-		return $missing;
174
-	}
175
-
176
-	private function analyzeArchitecture(array $dependencies) {
177
-		$missing = [];
178
-		if (!isset($dependencies['architecture'])) {
179
-			return $missing;
180
-		}
181
-
182
-		$supportedArchitectures = $dependencies['architecture'];
183
-		if (empty($supportedArchitectures)) {
184
-			return $missing;
185
-		}
186
-		if (!is_array($supportedArchitectures)) {
187
-			$supportedArchitectures = [$supportedArchitectures];
188
-		}
189
-		$supportedArchitectures = array_map(function ($architecture) {
190
-			return $this->getValue($architecture);
191
-		}, $supportedArchitectures);
192
-		$currentArchitecture = $this->platform->getArchitecture();
193
-		if (!in_array($currentArchitecture, $supportedArchitectures, true)) {
194
-			$missing[] = $this->l->t('The following architectures are supported: %s', [implode(', ', $supportedArchitectures)]);
195
-		}
196
-		return $missing;
197
-	}
198
-
199
-	/**
200
-	 * @param array $dependencies
201
-	 * @return array
202
-	 */
203
-	private function analyzeDatabases(array $dependencies) {
204
-		$missing = [];
205
-		if (!isset($dependencies['database'])) {
206
-			return $missing;
207
-		}
208
-
209
-		$supportedDatabases = $dependencies['database'];
210
-		if (empty($supportedDatabases)) {
211
-			return $missing;
212
-		}
213
-		if (!is_array($supportedDatabases)) {
214
-			$supportedDatabases = [$supportedDatabases];
215
-		}
216
-		$supportedDatabases = array_map(function ($db) {
217
-			return $this->getValue($db);
218
-		}, $supportedDatabases);
219
-		$currentDatabase = $this->platform->getDatabase();
220
-		if (!in_array($currentDatabase, $supportedDatabases)) {
221
-			$missing[] = $this->l->t('The following databases are supported: %s', [implode(', ', $supportedDatabases)]);
222
-		}
223
-		return $missing;
224
-	}
225
-
226
-	/**
227
-	 * @param array $dependencies
228
-	 * @return array
229
-	 */
230
-	private function analyzeCommands(array $dependencies) {
231
-		$missing = [];
232
-		if (!isset($dependencies['command'])) {
233
-			return $missing;
234
-		}
235
-
236
-		$commands = $dependencies['command'];
237
-		if (!is_array($commands)) {
238
-			$commands = [$commands];
239
-		}
240
-		if (isset($commands['@value'])) {
241
-			$commands = [$commands];
242
-		}
243
-		$os = $this->platform->getOS();
244
-		foreach ($commands as $command) {
245
-			if (isset($command['@attributes']['os']) && $command['@attributes']['os'] !== $os) {
246
-				continue;
247
-			}
248
-			$commandName = $this->getValue($command);
249
-			if (!$this->platform->isCommandKnown($commandName)) {
250
-				$missing[] = $this->l->t('The command line tool %s could not be found', [$commandName]);
251
-			}
252
-		}
253
-		return $missing;
254
-	}
255
-
256
-	/**
257
-	 * @param array $dependencies
258
-	 * @return array
259
-	 */
260
-	private function analyzeLibraries(array $dependencies) {
261
-		$missing = [];
262
-		if (!isset($dependencies['lib'])) {
263
-			return $missing;
264
-		}
265
-
266
-		$libs = $dependencies['lib'];
267
-		if (!is_array($libs)) {
268
-			$libs = [$libs];
269
-		}
270
-		if (isset($libs['@value'])) {
271
-			$libs = [$libs];
272
-		}
273
-		foreach ($libs as $lib) {
274
-			$libName = $this->getValue($lib);
275
-			$libVersion = $this->platform->getLibraryVersion($libName);
276
-			if (is_null($libVersion)) {
277
-				$missing[] = $this->l->t('The library %s is not available.', [$libName]);
278
-				continue;
279
-			}
280
-
281
-			if (is_array($lib)) {
282
-				if (isset($lib['@attributes']['min-version'])) {
283
-					$minVersion = $lib['@attributes']['min-version'];
284
-					if ($this->compareSmaller($libVersion, $minVersion)) {
285
-						$missing[] = $this->l->t('Library %1$s with a version higher than %2$s is required - available version %3$s.',
286
-							[$libName, $minVersion, $libVersion]);
287
-					}
288
-				}
289
-				if (isset($lib['@attributes']['max-version'])) {
290
-					$maxVersion = $lib['@attributes']['max-version'];
291
-					if ($this->compareBigger($libVersion, $maxVersion)) {
292
-						$missing[] = $this->l->t('Library %1$s with a version lower than %2$s is required - available version %3$s.',
293
-							[$libName, $maxVersion, $libVersion]);
294
-					}
295
-				}
296
-			}
297
-		}
298
-		return $missing;
299
-	}
300
-
301
-	/**
302
-	 * @param array $dependencies
303
-	 * @return array
304
-	 */
305
-	private function analyzeOS(array $dependencies) {
306
-		$missing = [];
307
-		if (!isset($dependencies['os'])) {
308
-			return $missing;
309
-		}
310
-
311
-		$oss = $dependencies['os'];
312
-		if (empty($oss)) {
313
-			return $missing;
314
-		}
315
-		if (is_array($oss)) {
316
-			$oss = array_map(function ($os) {
317
-				return $this->getValue($os);
318
-			}, $oss);
319
-		} else {
320
-			$oss = [$oss];
321
-		}
322
-		$currentOS = $this->platform->getOS();
323
-		if (!in_array($currentOS, $oss)) {
324
-			$missing[] = $this->l->t('The following platforms are supported: %s', [implode(', ', $oss)]);
325
-		}
326
-		return $missing;
327
-	}
328
-
329
-	/**
330
-	 * @param array $dependencies
331
-	 * @param array $appInfo
332
-	 * @return array
333
-	 */
334
-	private function analyzeOC(array $dependencies, array $appInfo, bool $ignoreMax) {
335
-		$missing = [];
336
-		$minVersion = null;
337
-		if (isset($dependencies['nextcloud']['@attributes']['min-version'])) {
338
-			$minVersion = $dependencies['nextcloud']['@attributes']['min-version'];
339
-		} elseif (isset($dependencies['owncloud']['@attributes']['min-version'])) {
340
-			$minVersion = $dependencies['owncloud']['@attributes']['min-version'];
341
-		} elseif (isset($appInfo['requiremin'])) {
342
-			$minVersion = $appInfo['requiremin'];
343
-		} elseif (isset($appInfo['require'])) {
344
-			$minVersion = $appInfo['require'];
345
-		}
346
-		$maxVersion = $this->getMaxVersion($dependencies, $appInfo);
347
-
348
-		if (!is_null($minVersion)) {
349
-			if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) {
350
-				$missing[] = $this->l->t('Server version %s or higher is required.', [$this->toVisibleVersion($minVersion)]);
351
-			}
352
-		}
353
-		if (!$ignoreMax && !is_null($maxVersion)) {
354
-			if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) {
355
-				$missing[] = $this->l->t('Server version %s or lower is required.', [$this->toVisibleVersion($maxVersion)]);
356
-			}
357
-		}
358
-		return $missing;
359
-	}
360
-
361
-	private function getMaxVersion(array $dependencies, array $appInfo): ?string {
362
-		if (isset($dependencies['nextcloud']['@attributes']['max-version'])) {
363
-			return $dependencies['nextcloud']['@attributes']['max-version'];
364
-		}
365
-		if (isset($dependencies['owncloud']['@attributes']['max-version'])) {
366
-			return $dependencies['owncloud']['@attributes']['max-version'];
367
-		}
368
-		if (isset($appInfo['requiremax'])) {
369
-			return $appInfo['requiremax'];
370
-		}
371
-
372
-		return null;
373
-	}
374
-
375
-	/**
376
-	 * Map the internal version number to the Nextcloud version
377
-	 *
378
-	 * @param string $version
379
-	 * @return string
380
-	 */
381
-	protected function toVisibleVersion($version) {
382
-		switch ($version) {
383
-			case '9.1':
384
-				return '10';
385
-			default:
386
-				if (str_starts_with($version, '9.1.')) {
387
-					$version = '10.0.' . substr($version, 4);
388
-				}
389
-				return $version;
390
-		}
391
-	}
392
-
393
-	/**
394
-	 * @param $element
395
-	 * @return mixed
396
-	 */
397
-	private function getValue($element) {
398
-		if (isset($element['@value'])) {
399
-			return $element['@value'];
400
-		}
401
-		return (string)$element;
402
-	}
36
+    /** @var Platform */
37
+    private $platform;
38
+    /** @var \OCP\IL10N */
39
+    private $l;
40
+    /** @var array */
41
+    private $appInfo;
42
+
43
+    /**
44
+     * @param Platform $platform
45
+     * @param \OCP\IL10N $l
46
+     */
47
+    public function __construct(Platform $platform, IL10N $l) {
48
+        $this->platform = $platform;
49
+        $this->l = $l;
50
+    }
51
+
52
+    /**
53
+     * @param array $app
54
+     * @returns array of missing dependencies
55
+     */
56
+    public function analyze(array $app, bool $ignoreMax = false) {
57
+        $this->appInfo = $app;
58
+        if (isset($app['dependencies'])) {
59
+            $dependencies = $app['dependencies'];
60
+        } else {
61
+            $dependencies = [];
62
+        }
63
+
64
+        return array_merge(
65
+            $this->analyzeArchitecture($dependencies),
66
+            $this->analyzePhpVersion($dependencies),
67
+            $this->analyzeDatabases($dependencies),
68
+            $this->analyzeCommands($dependencies),
69
+            $this->analyzeLibraries($dependencies),
70
+            $this->analyzeOS($dependencies),
71
+            $this->analyzeOC($dependencies, $app, $ignoreMax)
72
+        );
73
+    }
74
+
75
+    public function isMarkedCompatible(array $app): bool {
76
+        if (isset($app['dependencies'])) {
77
+            $dependencies = $app['dependencies'];
78
+        } else {
79
+            $dependencies = [];
80
+        }
81
+
82
+        $maxVersion = $this->getMaxVersion($dependencies, $app);
83
+        if ($maxVersion === null) {
84
+            return true;
85
+        }
86
+        return !$this->compareBigger($this->platform->getOcVersion(), $maxVersion);
87
+    }
88
+
89
+    /**
90
+     * Truncates both versions to the lowest common version, e.g.
91
+     * 5.1.2.3 and 5.1 will be turned into 5.1 and 5.1,
92
+     * 5.2.6.5 and 5.1 will be turned into 5.2 and 5.1
93
+     * @param string $first
94
+     * @param string $second
95
+     * @return string[] first element is the first version, second element is the
96
+     * second version
97
+     */
98
+    private function normalizeVersions($first, $second) {
99
+        $first = explode('.', $first);
100
+        $second = explode('.', $second);
101
+
102
+        // get both arrays to the same minimum size
103
+        $length = min(count($second), count($first));
104
+        $first = array_slice($first, 0, $length);
105
+        $second = array_slice($second, 0, $length);
106
+
107
+        return [implode('.', $first), implode('.', $second)];
108
+    }
109
+
110
+    /**
111
+     * Parameters will be normalized and then passed into version_compare
112
+     * in the same order they are specified in the method header
113
+     * @param string $first
114
+     * @param string $second
115
+     * @param string $operator
116
+     * @return bool result similar to version_compare
117
+     */
118
+    private function compare($first, $second, $operator) {
119
+        // we can't normalize versions if one of the given parameters is not a
120
+        // version string but null. In case one parameter is null normalization
121
+        // will therefore be skipped
122
+        if ($first !== null && $second !== null) {
123
+            [$first, $second] = $this->normalizeVersions($first, $second);
124
+        }
125
+
126
+        return version_compare($first, $second, $operator);
127
+    }
128
+
129
+    /**
130
+     * Checks if a version is bigger than another version
131
+     * @param string $first
132
+     * @param string $second
133
+     * @return bool true if the first version is bigger than the second
134
+     */
135
+    private function compareBigger($first, $second) {
136
+        return $this->compare($first, $second, '>');
137
+    }
138
+
139
+    /**
140
+     * Checks if a version is smaller than another version
141
+     * @param string $first
142
+     * @param string $second
143
+     * @return bool true if the first version is smaller than the second
144
+     */
145
+    private function compareSmaller($first, $second) {
146
+        return $this->compare($first, $second, '<');
147
+    }
148
+
149
+    /**
150
+     * @param array $dependencies
151
+     * @return array
152
+     */
153
+    private function analyzePhpVersion(array $dependencies) {
154
+        $missing = [];
155
+        if (isset($dependencies['php']['@attributes']['min-version'])) {
156
+            $minVersion = $dependencies['php']['@attributes']['min-version'];
157
+            if ($this->compareSmaller($this->platform->getPhpVersion(), $minVersion)) {
158
+                $missing[] = $this->l->t('PHP %s or higher is required.', [$minVersion]);
159
+            }
160
+        }
161
+        if (isset($dependencies['php']['@attributes']['max-version'])) {
162
+            $maxVersion = $dependencies['php']['@attributes']['max-version'];
163
+            if ($this->compareBigger($this->platform->getPhpVersion(), $maxVersion)) {
164
+                $missing[] = $this->l->t('PHP with a version lower than %s is required.', [$maxVersion]);
165
+            }
166
+        }
167
+        if (isset($dependencies['php']['@attributes']['min-int-size'])) {
168
+            $intSize = $dependencies['php']['@attributes']['min-int-size'];
169
+            if ($intSize > $this->platform->getIntSize() * 8) {
170
+                $missing[] = $this->l->t('%sbit or higher PHP required.', [$intSize]);
171
+            }
172
+        }
173
+        return $missing;
174
+    }
175
+
176
+    private function analyzeArchitecture(array $dependencies) {
177
+        $missing = [];
178
+        if (!isset($dependencies['architecture'])) {
179
+            return $missing;
180
+        }
181
+
182
+        $supportedArchitectures = $dependencies['architecture'];
183
+        if (empty($supportedArchitectures)) {
184
+            return $missing;
185
+        }
186
+        if (!is_array($supportedArchitectures)) {
187
+            $supportedArchitectures = [$supportedArchitectures];
188
+        }
189
+        $supportedArchitectures = array_map(function ($architecture) {
190
+            return $this->getValue($architecture);
191
+        }, $supportedArchitectures);
192
+        $currentArchitecture = $this->platform->getArchitecture();
193
+        if (!in_array($currentArchitecture, $supportedArchitectures, true)) {
194
+            $missing[] = $this->l->t('The following architectures are supported: %s', [implode(', ', $supportedArchitectures)]);
195
+        }
196
+        return $missing;
197
+    }
198
+
199
+    /**
200
+     * @param array $dependencies
201
+     * @return array
202
+     */
203
+    private function analyzeDatabases(array $dependencies) {
204
+        $missing = [];
205
+        if (!isset($dependencies['database'])) {
206
+            return $missing;
207
+        }
208
+
209
+        $supportedDatabases = $dependencies['database'];
210
+        if (empty($supportedDatabases)) {
211
+            return $missing;
212
+        }
213
+        if (!is_array($supportedDatabases)) {
214
+            $supportedDatabases = [$supportedDatabases];
215
+        }
216
+        $supportedDatabases = array_map(function ($db) {
217
+            return $this->getValue($db);
218
+        }, $supportedDatabases);
219
+        $currentDatabase = $this->platform->getDatabase();
220
+        if (!in_array($currentDatabase, $supportedDatabases)) {
221
+            $missing[] = $this->l->t('The following databases are supported: %s', [implode(', ', $supportedDatabases)]);
222
+        }
223
+        return $missing;
224
+    }
225
+
226
+    /**
227
+     * @param array $dependencies
228
+     * @return array
229
+     */
230
+    private function analyzeCommands(array $dependencies) {
231
+        $missing = [];
232
+        if (!isset($dependencies['command'])) {
233
+            return $missing;
234
+        }
235
+
236
+        $commands = $dependencies['command'];
237
+        if (!is_array($commands)) {
238
+            $commands = [$commands];
239
+        }
240
+        if (isset($commands['@value'])) {
241
+            $commands = [$commands];
242
+        }
243
+        $os = $this->platform->getOS();
244
+        foreach ($commands as $command) {
245
+            if (isset($command['@attributes']['os']) && $command['@attributes']['os'] !== $os) {
246
+                continue;
247
+            }
248
+            $commandName = $this->getValue($command);
249
+            if (!$this->platform->isCommandKnown($commandName)) {
250
+                $missing[] = $this->l->t('The command line tool %s could not be found', [$commandName]);
251
+            }
252
+        }
253
+        return $missing;
254
+    }
255
+
256
+    /**
257
+     * @param array $dependencies
258
+     * @return array
259
+     */
260
+    private function analyzeLibraries(array $dependencies) {
261
+        $missing = [];
262
+        if (!isset($dependencies['lib'])) {
263
+            return $missing;
264
+        }
265
+
266
+        $libs = $dependencies['lib'];
267
+        if (!is_array($libs)) {
268
+            $libs = [$libs];
269
+        }
270
+        if (isset($libs['@value'])) {
271
+            $libs = [$libs];
272
+        }
273
+        foreach ($libs as $lib) {
274
+            $libName = $this->getValue($lib);
275
+            $libVersion = $this->platform->getLibraryVersion($libName);
276
+            if (is_null($libVersion)) {
277
+                $missing[] = $this->l->t('The library %s is not available.', [$libName]);
278
+                continue;
279
+            }
280
+
281
+            if (is_array($lib)) {
282
+                if (isset($lib['@attributes']['min-version'])) {
283
+                    $minVersion = $lib['@attributes']['min-version'];
284
+                    if ($this->compareSmaller($libVersion, $minVersion)) {
285
+                        $missing[] = $this->l->t('Library %1$s with a version higher than %2$s is required - available version %3$s.',
286
+                            [$libName, $minVersion, $libVersion]);
287
+                    }
288
+                }
289
+                if (isset($lib['@attributes']['max-version'])) {
290
+                    $maxVersion = $lib['@attributes']['max-version'];
291
+                    if ($this->compareBigger($libVersion, $maxVersion)) {
292
+                        $missing[] = $this->l->t('Library %1$s with a version lower than %2$s is required - available version %3$s.',
293
+                            [$libName, $maxVersion, $libVersion]);
294
+                    }
295
+                }
296
+            }
297
+        }
298
+        return $missing;
299
+    }
300
+
301
+    /**
302
+     * @param array $dependencies
303
+     * @return array
304
+     */
305
+    private function analyzeOS(array $dependencies) {
306
+        $missing = [];
307
+        if (!isset($dependencies['os'])) {
308
+            return $missing;
309
+        }
310
+
311
+        $oss = $dependencies['os'];
312
+        if (empty($oss)) {
313
+            return $missing;
314
+        }
315
+        if (is_array($oss)) {
316
+            $oss = array_map(function ($os) {
317
+                return $this->getValue($os);
318
+            }, $oss);
319
+        } else {
320
+            $oss = [$oss];
321
+        }
322
+        $currentOS = $this->platform->getOS();
323
+        if (!in_array($currentOS, $oss)) {
324
+            $missing[] = $this->l->t('The following platforms are supported: %s', [implode(', ', $oss)]);
325
+        }
326
+        return $missing;
327
+    }
328
+
329
+    /**
330
+     * @param array $dependencies
331
+     * @param array $appInfo
332
+     * @return array
333
+     */
334
+    private function analyzeOC(array $dependencies, array $appInfo, bool $ignoreMax) {
335
+        $missing = [];
336
+        $minVersion = null;
337
+        if (isset($dependencies['nextcloud']['@attributes']['min-version'])) {
338
+            $minVersion = $dependencies['nextcloud']['@attributes']['min-version'];
339
+        } elseif (isset($dependencies['owncloud']['@attributes']['min-version'])) {
340
+            $minVersion = $dependencies['owncloud']['@attributes']['min-version'];
341
+        } elseif (isset($appInfo['requiremin'])) {
342
+            $minVersion = $appInfo['requiremin'];
343
+        } elseif (isset($appInfo['require'])) {
344
+            $minVersion = $appInfo['require'];
345
+        }
346
+        $maxVersion = $this->getMaxVersion($dependencies, $appInfo);
347
+
348
+        if (!is_null($minVersion)) {
349
+            if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) {
350
+                $missing[] = $this->l->t('Server version %s or higher is required.', [$this->toVisibleVersion($minVersion)]);
351
+            }
352
+        }
353
+        if (!$ignoreMax && !is_null($maxVersion)) {
354
+            if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) {
355
+                $missing[] = $this->l->t('Server version %s or lower is required.', [$this->toVisibleVersion($maxVersion)]);
356
+            }
357
+        }
358
+        return $missing;
359
+    }
360
+
361
+    private function getMaxVersion(array $dependencies, array $appInfo): ?string {
362
+        if (isset($dependencies['nextcloud']['@attributes']['max-version'])) {
363
+            return $dependencies['nextcloud']['@attributes']['max-version'];
364
+        }
365
+        if (isset($dependencies['owncloud']['@attributes']['max-version'])) {
366
+            return $dependencies['owncloud']['@attributes']['max-version'];
367
+        }
368
+        if (isset($appInfo['requiremax'])) {
369
+            return $appInfo['requiremax'];
370
+        }
371
+
372
+        return null;
373
+    }
374
+
375
+    /**
376
+     * Map the internal version number to the Nextcloud version
377
+     *
378
+     * @param string $version
379
+     * @return string
380
+     */
381
+    protected function toVisibleVersion($version) {
382
+        switch ($version) {
383
+            case '9.1':
384
+                return '10';
385
+            default:
386
+                if (str_starts_with($version, '9.1.')) {
387
+                    $version = '10.0.' . substr($version, 4);
388
+                }
389
+                return $version;
390
+        }
391
+    }
392
+
393
+    /**
394
+     * @param $element
395
+     * @return mixed
396
+     */
397
+    private function getValue($element) {
398
+        if (isset($element['@value'])) {
399
+            return $element['@value'];
400
+        }
401
+        return (string)$element;
402
+    }
403 403
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -186,7 +186,7 @@  discard block
 block discarded – undo
186 186
 		if (!is_array($supportedArchitectures)) {
187 187
 			$supportedArchitectures = [$supportedArchitectures];
188 188
 		}
189
-		$supportedArchitectures = array_map(function ($architecture) {
189
+		$supportedArchitectures = array_map(function($architecture) {
190 190
 			return $this->getValue($architecture);
191 191
 		}, $supportedArchitectures);
192 192
 		$currentArchitecture = $this->platform->getArchitecture();
@@ -213,7 +213,7 @@  discard block
 block discarded – undo
213 213
 		if (!is_array($supportedDatabases)) {
214 214
 			$supportedDatabases = [$supportedDatabases];
215 215
 		}
216
-		$supportedDatabases = array_map(function ($db) {
216
+		$supportedDatabases = array_map(function($db) {
217 217
 			return $this->getValue($db);
218 218
 		}, $supportedDatabases);
219 219
 		$currentDatabase = $this->platform->getDatabase();
@@ -313,7 +313,7 @@  discard block
 block discarded – undo
313 313
 			return $missing;
314 314
 		}
315 315
 		if (is_array($oss)) {
316
-			$oss = array_map(function ($os) {
316
+			$oss = array_map(function($os) {
317 317
 				return $this->getValue($os);
318 318
 			}, $oss);
319 319
 		} else {
@@ -384,7 +384,7 @@  discard block
 block discarded – undo
384 384
 				return '10';
385 385
 			default:
386 386
 				if (str_starts_with($version, '9.1.')) {
387
-					$version = '10.0.' . substr($version, 4);
387
+					$version = '10.0.'.substr($version, 4);
388 388
 				}
389 389
 				return $version;
390 390
 		}
@@ -398,6 +398,6 @@  discard block
 block discarded – undo
398 398
 		if (isset($element['@value'])) {
399 399
 			return $element['@value'];
400 400
 		}
401
-		return (string)$element;
401
+		return (string) $element;
402 402
 	}
403 403
 }
Please login to merge, or discard this patch.
lib/private/App/AppStore/Fetcher/AppFetcher.php 1 patch
Indentation   +157 added lines, -157 removed lines patch added patch discarded remove patch
@@ -39,161 +39,161 @@
 block discarded – undo
39 39
 use Psr\Log\LoggerInterface;
40 40
 
41 41
 class AppFetcher extends Fetcher {
42
-	/** @var CompareVersion */
43
-	private $compareVersion;
44
-
45
-	/** @var IRegistry */
46
-	protected $registry;
47
-
48
-	/** @var bool */
49
-	private $ignoreMaxVersion;
50
-
51
-	public function __construct(Factory $appDataFactory,
52
-								IClientService $clientService,
53
-								ITimeFactory $timeFactory,
54
-								IConfig $config,
55
-								CompareVersion $compareVersion,
56
-								LoggerInterface $logger,
57
-								IRegistry $registry) {
58
-		parent::__construct(
59
-			$appDataFactory,
60
-			$clientService,
61
-			$timeFactory,
62
-			$config,
63
-			$logger,
64
-			$registry
65
-		);
66
-
67
-		$this->compareVersion = $compareVersion;
68
-		$this->registry = $registry;
69
-
70
-		$this->fileName = 'apps.json';
71
-		$this->endpointName = 'apps.json';
72
-		$this->ignoreMaxVersion = true;
73
-	}
74
-
75
-	/**
76
-	 * Only returns the latest compatible app release in the releases array
77
-	 *
78
-	 * @param string $ETag
79
-	 * @param string $content
80
-	 * @param bool [$allowUnstable] Allow unstable releases
81
-	 *
82
-	 * @return array
83
-	 */
84
-	protected function fetch($ETag, $content, $allowUnstable = false) {
85
-		/** @var mixed[] $response */
86
-		$response = parent::fetch($ETag, $content);
87
-
88
-		if (empty($response)) {
89
-			return [];
90
-		}
91
-
92
-		$allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
93
-		$allowNightly = $allowUnstable || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
94
-
95
-		foreach ($response['data'] as $dataKey => $app) {
96
-			$releases = [];
97
-
98
-			// Filter all compatible releases
99
-			foreach ($app['releases'] as $release) {
100
-				// Exclude all nightly and pre-releases if required
101
-				if (($allowNightly || $release['isNightly'] === false)
102
-					&& ($allowPreReleases || !str_contains($release['version'], '-'))) {
103
-					// Exclude all versions not compatible with the current version
104
-					try {
105
-						$versionParser = new VersionParser();
106
-						$serverVersion = $versionParser->getVersion($release['rawPlatformVersionSpec']);
107
-						$ncVersion = $this->getVersion();
108
-						$minServerVersion = $serverVersion->getMinimumVersion();
109
-						$maxServerVersion = $serverVersion->getMaximumVersion();
110
-						$minFulfilled = $this->compareVersion->isCompatible($ncVersion, $minServerVersion, '>=');
111
-						$maxFulfilled = $maxServerVersion !== '' &&
112
-							$this->compareVersion->isCompatible($ncVersion, $maxServerVersion, '<=');
113
-						$isPhpCompatible = true;
114
-						if (($release['rawPhpVersionSpec'] ?? '*') !== '*') {
115
-							$phpVersion = $versionParser->getVersion($release['rawPhpVersionSpec']);
116
-							$minPhpVersion = $phpVersion->getMinimumVersion();
117
-							$maxPhpVersion = $phpVersion->getMaximumVersion();
118
-							$minPhpFulfilled = $minPhpVersion === '' || $this->compareVersion->isCompatible(
119
-								PHP_VERSION,
120
-								$minPhpVersion,
121
-								'>='
122
-							);
123
-							$maxPhpFulfilled = $maxPhpVersion === '' || $this->compareVersion->isCompatible(
124
-								PHP_VERSION,
125
-								$maxPhpVersion,
126
-								'<='
127
-							);
128
-
129
-							$isPhpCompatible = $minPhpFulfilled && $maxPhpFulfilled;
130
-						}
131
-						if ($minFulfilled && ($this->ignoreMaxVersion || $maxFulfilled) && $isPhpCompatible) {
132
-							$releases[] = $release;
133
-						}
134
-					} catch (\InvalidArgumentException $e) {
135
-						$this->logger->warning($e->getMessage(), [
136
-							'exception' => $e,
137
-						]);
138
-					}
139
-				}
140
-			}
141
-
142
-			if (empty($releases)) {
143
-				// Remove apps that don't have a matching release
144
-				$response['data'][$dataKey] = [];
145
-				continue;
146
-			}
147
-
148
-			// Get the highest version
149
-			$versions = [];
150
-			foreach ($releases as $release) {
151
-				$versions[] = $release['version'];
152
-			}
153
-			usort($versions, function ($version1, $version2) {
154
-				return version_compare($version1, $version2);
155
-			});
156
-			$versions = array_reverse($versions);
157
-			if (isset($versions[0])) {
158
-				$highestVersion = $versions[0];
159
-				foreach ($releases as $release) {
160
-					if ((string)$release['version'] === (string)$highestVersion) {
161
-						$response['data'][$dataKey]['releases'] = [$release];
162
-						break;
163
-					}
164
-				}
165
-			}
166
-		}
167
-
168
-		$response['data'] = array_values(array_filter($response['data']));
169
-		return $response;
170
-	}
171
-
172
-	/**
173
-	 * @param string $version
174
-	 * @param string $fileName
175
-	 * @param bool $ignoreMaxVersion
176
-	 */
177
-	public function setVersion(string $version, string $fileName = 'apps.json', bool $ignoreMaxVersion = true) {
178
-		parent::setVersion($version);
179
-		$this->fileName = $fileName;
180
-		$this->ignoreMaxVersion = $ignoreMaxVersion;
181
-	}
182
-
183
-
184
-	public function get($allowUnstable = false) {
185
-		$allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
186
-
187
-		$apps = parent::get($allowPreReleases);
188
-		$allowList = $this->config->getSystemValue('appsallowlist');
189
-
190
-		// If the admin specified a allow list, filter apps from the appstore
191
-		if (is_array($allowList) && $this->registry->delegateHasValidSubscription()) {
192
-			return array_filter($apps, function ($app) use ($allowList) {
193
-				return in_array($app['id'], $allowList);
194
-			});
195
-		}
196
-
197
-		return $apps;
198
-	}
42
+    /** @var CompareVersion */
43
+    private $compareVersion;
44
+
45
+    /** @var IRegistry */
46
+    protected $registry;
47
+
48
+    /** @var bool */
49
+    private $ignoreMaxVersion;
50
+
51
+    public function __construct(Factory $appDataFactory,
52
+                                IClientService $clientService,
53
+                                ITimeFactory $timeFactory,
54
+                                IConfig $config,
55
+                                CompareVersion $compareVersion,
56
+                                LoggerInterface $logger,
57
+                                IRegistry $registry) {
58
+        parent::__construct(
59
+            $appDataFactory,
60
+            $clientService,
61
+            $timeFactory,
62
+            $config,
63
+            $logger,
64
+            $registry
65
+        );
66
+
67
+        $this->compareVersion = $compareVersion;
68
+        $this->registry = $registry;
69
+
70
+        $this->fileName = 'apps.json';
71
+        $this->endpointName = 'apps.json';
72
+        $this->ignoreMaxVersion = true;
73
+    }
74
+
75
+    /**
76
+     * Only returns the latest compatible app release in the releases array
77
+     *
78
+     * @param string $ETag
79
+     * @param string $content
80
+     * @param bool [$allowUnstable] Allow unstable releases
81
+     *
82
+     * @return array
83
+     */
84
+    protected function fetch($ETag, $content, $allowUnstable = false) {
85
+        /** @var mixed[] $response */
86
+        $response = parent::fetch($ETag, $content);
87
+
88
+        if (empty($response)) {
89
+            return [];
90
+        }
91
+
92
+        $allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
93
+        $allowNightly = $allowUnstable || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
94
+
95
+        foreach ($response['data'] as $dataKey => $app) {
96
+            $releases = [];
97
+
98
+            // Filter all compatible releases
99
+            foreach ($app['releases'] as $release) {
100
+                // Exclude all nightly and pre-releases if required
101
+                if (($allowNightly || $release['isNightly'] === false)
102
+                    && ($allowPreReleases || !str_contains($release['version'], '-'))) {
103
+                    // Exclude all versions not compatible with the current version
104
+                    try {
105
+                        $versionParser = new VersionParser();
106
+                        $serverVersion = $versionParser->getVersion($release['rawPlatformVersionSpec']);
107
+                        $ncVersion = $this->getVersion();
108
+                        $minServerVersion = $serverVersion->getMinimumVersion();
109
+                        $maxServerVersion = $serverVersion->getMaximumVersion();
110
+                        $minFulfilled = $this->compareVersion->isCompatible($ncVersion, $minServerVersion, '>=');
111
+                        $maxFulfilled = $maxServerVersion !== '' &&
112
+                            $this->compareVersion->isCompatible($ncVersion, $maxServerVersion, '<=');
113
+                        $isPhpCompatible = true;
114
+                        if (($release['rawPhpVersionSpec'] ?? '*') !== '*') {
115
+                            $phpVersion = $versionParser->getVersion($release['rawPhpVersionSpec']);
116
+                            $minPhpVersion = $phpVersion->getMinimumVersion();
117
+                            $maxPhpVersion = $phpVersion->getMaximumVersion();
118
+                            $minPhpFulfilled = $minPhpVersion === '' || $this->compareVersion->isCompatible(
119
+                                PHP_VERSION,
120
+                                $minPhpVersion,
121
+                                '>='
122
+                            );
123
+                            $maxPhpFulfilled = $maxPhpVersion === '' || $this->compareVersion->isCompatible(
124
+                                PHP_VERSION,
125
+                                $maxPhpVersion,
126
+                                '<='
127
+                            );
128
+
129
+                            $isPhpCompatible = $minPhpFulfilled && $maxPhpFulfilled;
130
+                        }
131
+                        if ($minFulfilled && ($this->ignoreMaxVersion || $maxFulfilled) && $isPhpCompatible) {
132
+                            $releases[] = $release;
133
+                        }
134
+                    } catch (\InvalidArgumentException $e) {
135
+                        $this->logger->warning($e->getMessage(), [
136
+                            'exception' => $e,
137
+                        ]);
138
+                    }
139
+                }
140
+            }
141
+
142
+            if (empty($releases)) {
143
+                // Remove apps that don't have a matching release
144
+                $response['data'][$dataKey] = [];
145
+                continue;
146
+            }
147
+
148
+            // Get the highest version
149
+            $versions = [];
150
+            foreach ($releases as $release) {
151
+                $versions[] = $release['version'];
152
+            }
153
+            usort($versions, function ($version1, $version2) {
154
+                return version_compare($version1, $version2);
155
+            });
156
+            $versions = array_reverse($versions);
157
+            if (isset($versions[0])) {
158
+                $highestVersion = $versions[0];
159
+                foreach ($releases as $release) {
160
+                    if ((string)$release['version'] === (string)$highestVersion) {
161
+                        $response['data'][$dataKey]['releases'] = [$release];
162
+                        break;
163
+                    }
164
+                }
165
+            }
166
+        }
167
+
168
+        $response['data'] = array_values(array_filter($response['data']));
169
+        return $response;
170
+    }
171
+
172
+    /**
173
+     * @param string $version
174
+     * @param string $fileName
175
+     * @param bool $ignoreMaxVersion
176
+     */
177
+    public function setVersion(string $version, string $fileName = 'apps.json', bool $ignoreMaxVersion = true) {
178
+        parent::setVersion($version);
179
+        $this->fileName = $fileName;
180
+        $this->ignoreMaxVersion = $ignoreMaxVersion;
181
+    }
182
+
183
+
184
+    public function get($allowUnstable = false) {
185
+        $allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
186
+
187
+        $apps = parent::get($allowPreReleases);
188
+        $allowList = $this->config->getSystemValue('appsallowlist');
189
+
190
+        // If the admin specified a allow list, filter apps from the appstore
191
+        if (is_array($allowList) && $this->registry->delegateHasValidSubscription()) {
192
+            return array_filter($apps, function ($app) use ($allowList) {
193
+                return in_array($app['id'], $allowList);
194
+            });
195
+        }
196
+
197
+        return $apps;
198
+    }
199 199
 }
Please login to merge, or discard this patch.
lib/private/App/AppStore/Version/VersionParser.php 1 patch
Indentation   +48 added lines, -48 removed lines patch added patch discarded remove patch
@@ -30,56 +30,56 @@
 block discarded – undo
30 30
  * @package OC\App\AppStore
31 31
  */
32 32
 class VersionParser {
33
-	/**
34
-	 * @param string $versionString
35
-	 * @return bool
36
-	 */
37
-	private function isValidVersionString($versionString) {
38
-		return (bool)preg_match('/^[0-9.]+$/', $versionString);
39
-	}
33
+    /**
34
+     * @param string $versionString
35
+     * @return bool
36
+     */
37
+    private function isValidVersionString($versionString) {
38
+        return (bool)preg_match('/^[0-9.]+$/', $versionString);
39
+    }
40 40
 
41
-	/**
42
-	 * Returns the version for a version string
43
-	 *
44
-	 * @param string $versionSpec
45
-	 * @return Version
46
-	 * @throws \Exception If the version cannot be parsed
47
-	 */
48
-	public function getVersion($versionSpec) {
49
-		// * indicates that the version is compatible with all versions
50
-		if ($versionSpec === '*') {
51
-			return new Version('', '');
52
-		}
41
+    /**
42
+     * Returns the version for a version string
43
+     *
44
+     * @param string $versionSpec
45
+     * @return Version
46
+     * @throws \Exception If the version cannot be parsed
47
+     */
48
+    public function getVersion($versionSpec) {
49
+        // * indicates that the version is compatible with all versions
50
+        if ($versionSpec === '*') {
51
+            return new Version('', '');
52
+        }
53 53
 
54
-		// Count the amount of =, if it is one then it's either maximum or minimum
55
-		// version. If it is two then it is maximum and minimum.
56
-		$versionElements = explode(' ', $versionSpec);
57
-		$firstVersion = isset($versionElements[0]) ? $versionElements[0] : '';
58
-		$firstVersionNumber = substr($firstVersion, 2);
59
-		$secondVersion = isset($versionElements[1]) ? $versionElements[1] : '';
60
-		$secondVersionNumber = substr($secondVersion, 2);
54
+        // Count the amount of =, if it is one then it's either maximum or minimum
55
+        // version. If it is two then it is maximum and minimum.
56
+        $versionElements = explode(' ', $versionSpec);
57
+        $firstVersion = isset($versionElements[0]) ? $versionElements[0] : '';
58
+        $firstVersionNumber = substr($firstVersion, 2);
59
+        $secondVersion = isset($versionElements[1]) ? $versionElements[1] : '';
60
+        $secondVersionNumber = substr($secondVersion, 2);
61 61
 
62
-		switch (count($versionElements)) {
63
-			case 1:
64
-				if (!$this->isValidVersionString($firstVersionNumber)) {
65
-					break;
66
-				}
67
-				if (str_starts_with($firstVersion, '>')) {
68
-					return new Version($firstVersionNumber, '');
69
-				}
70
-				return new Version('', $firstVersionNumber);
71
-			case 2:
72
-				if (!$this->isValidVersionString($firstVersionNumber) || !$this->isValidVersionString($secondVersionNumber)) {
73
-					break;
74
-				}
75
-				return new Version($firstVersionNumber, $secondVersionNumber);
76
-		}
62
+        switch (count($versionElements)) {
63
+            case 1:
64
+                if (!$this->isValidVersionString($firstVersionNumber)) {
65
+                    break;
66
+                }
67
+                if (str_starts_with($firstVersion, '>')) {
68
+                    return new Version($firstVersionNumber, '');
69
+                }
70
+                return new Version('', $firstVersionNumber);
71
+            case 2:
72
+                if (!$this->isValidVersionString($firstVersionNumber) || !$this->isValidVersionString($secondVersionNumber)) {
73
+                    break;
74
+                }
75
+                return new Version($firstVersionNumber, $secondVersionNumber);
76
+        }
77 77
 
78
-		throw new \Exception(
79
-			sprintf(
80
-				'Version cannot be parsed: %s',
81
-				$versionSpec
82
-			)
83
-		);
84
-	}
78
+        throw new \Exception(
79
+            sprintf(
80
+                'Version cannot be parsed: %s',
81
+                $versionSpec
82
+            )
83
+        );
84
+    }
85 85
 }
Please login to merge, or discard this patch.
lib/private/L10N/Factory.php 1 patch
Indentation   +626 added lines, -626 removed lines patch added patch discarded remove patch
@@ -54,630 +54,630 @@
 block discarded – undo
54 54
  * A factory that generates language instances
55 55
  */
56 56
 class Factory implements IFactory {
57
-	/** @var string */
58
-	protected $requestLanguage = '';
59
-
60
-	/**
61
-	 * cached instances
62
-	 * @var array Structure: Lang => App => \OCP\IL10N
63
-	 */
64
-	protected $instances = [];
65
-
66
-	/**
67
-	 * @var array Structure: App => string[]
68
-	 */
69
-	protected $availableLanguages = [];
70
-
71
-	/**
72
-	 * @var array
73
-	 */
74
-	protected $localeCache = [];
75
-
76
-	/**
77
-	 * @var array
78
-	 */
79
-	protected $availableLocales = [];
80
-
81
-	/**
82
-	 * @var array Structure: string => callable
83
-	 */
84
-	protected $pluralFunctions = [];
85
-
86
-	public const COMMON_LANGUAGE_CODES = [
87
-		'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
88
-		'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
89
-	];
90
-
91
-	/** @var IConfig */
92
-	protected $config;
93
-
94
-	/** @var IRequest */
95
-	protected $request;
96
-
97
-	/** @var IUserSession */
98
-	protected IUserSession $userSession;
99
-
100
-	private ICache $cache;
101
-
102
-	/** @var string */
103
-	protected $serverRoot;
104
-
105
-	/**
106
-	 * @param IConfig $config
107
-	 * @param IRequest $request
108
-	 * @param IUserSession $userSession
109
-	 * @param string $serverRoot
110
-	 */
111
-	public function __construct(
112
-		IConfig $config,
113
-		IRequest $request,
114
-		IUserSession $userSession,
115
-		ICacheFactory $cacheFactory,
116
-		$serverRoot
117
-	) {
118
-		$this->config = $config;
119
-		$this->request = $request;
120
-		$this->userSession = $userSession;
121
-		$this->cache = $cacheFactory->createLocal('L10NFactory');
122
-		$this->serverRoot = $serverRoot;
123
-	}
124
-
125
-	/**
126
-	 * Get a language instance
127
-	 *
128
-	 * @param string $app
129
-	 * @param string|null $lang
130
-	 * @param string|null $locale
131
-	 * @return \OCP\IL10N
132
-	 */
133
-	public function get($app, $lang = null, $locale = null) {
134
-		return new LazyL10N(function () use ($app, $lang, $locale) {
135
-			$app = \OC_App::cleanAppId($app);
136
-			if ($lang !== null) {
137
-				$lang = str_replace(['\0', '/', '\\', '..'], '', $lang);
138
-			}
139
-
140
-			$forceLang = $this->config->getSystemValue('force_language', false);
141
-			if (is_string($forceLang)) {
142
-				$lang = $forceLang;
143
-			}
144
-
145
-			$forceLocale = $this->config->getSystemValue('force_locale', false);
146
-			if (is_string($forceLocale)) {
147
-				$locale = $forceLocale;
148
-			}
149
-
150
-			if ($lang === null || !$this->languageExists($app, $lang)) {
151
-				$lang = $this->findLanguage($app);
152
-			}
153
-
154
-			if ($locale === null || !$this->localeExists($locale)) {
155
-				$locale = $this->findLocale($lang);
156
-			}
157
-
158
-			if (!isset($this->instances[$lang][$app])) {
159
-				$this->instances[$lang][$app] = new L10N(
160
-					$this,
161
-					$app,
162
-					$lang,
163
-					$locale,
164
-					$this->getL10nFilesForApp($app, $lang)
165
-				);
166
-			}
167
-
168
-			return $this->instances[$lang][$app];
169
-		});
170
-	}
171
-
172
-	/**
173
-	 * Find the best language
174
-	 *
175
-	 * @param string|null $appId App id or null for core
176
-	 *
177
-	 * @return string language If nothing works it returns 'en'
178
-	 */
179
-	public function findLanguage(?string $appId = null): string {
180
-		// Step 1: Forced language always has precedence over anything else
181
-		$forceLang = $this->config->getSystemValue('force_language', false);
182
-		if (is_string($forceLang)) {
183
-			$this->requestLanguage = $forceLang;
184
-		}
185
-
186
-		// Step 2: Return cached language
187
-		if ($this->requestLanguage !== '' && $this->languageExists($appId, $this->requestLanguage)) {
188
-			return $this->requestLanguage;
189
-		}
190
-
191
-		/**
192
-		 * Step 3: At this point Nextcloud might not yet be installed and thus the lookup
193
-		 * in the preferences table might fail. For this reason we need to check
194
-		 * whether the instance has already been installed
195
-		 *
196
-		 * @link https://github.com/owncloud/core/issues/21955
197
-		 */
198
-		if ($this->config->getSystemValueBool('installed', false)) {
199
-			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
200
-			if (!is_null($userId)) {
201
-				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
202
-			} else {
203
-				$userLang = null;
204
-			}
205
-		} else {
206
-			$userId = null;
207
-			$userLang = null;
208
-		}
209
-		if ($userLang) {
210
-			$this->requestLanguage = $userLang;
211
-			if ($this->languageExists($appId, $userLang)) {
212
-				return $userLang;
213
-			}
214
-		}
215
-
216
-		// Step 4: Check the request headers
217
-		try {
218
-			// Try to get the language from the Request
219
-			$lang = $this->getLanguageFromRequest($appId);
220
-			if ($userId !== null && $appId === null && !$userLang) {
221
-				$this->config->setUserValue($userId, 'core', 'lang', $lang);
222
-			}
223
-			return $lang;
224
-		} catch (LanguageNotFoundException $e) {
225
-			// Finding language from request failed fall back to default language
226
-			$defaultLanguage = $this->config->getSystemValue('default_language', false);
227
-			if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
228
-				return $defaultLanguage;
229
-			}
230
-		}
231
-
232
-		// Step 5: fall back to English
233
-		return 'en';
234
-	}
235
-
236
-	public function findGenericLanguage(string $appId = null): string {
237
-		// Step 1: Forced language always has precedence over anything else
238
-		$forcedLanguage = $this->config->getSystemValue('force_language', false);
239
-		if ($forcedLanguage !== false) {
240
-			return $forcedLanguage;
241
-		}
242
-
243
-		// Step 2: Check if we have a default language
244
-		$defaultLanguage = $this->config->getSystemValue('default_language', false);
245
-		if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
246
-			return $defaultLanguage;
247
-		}
248
-
249
-		// Step 3.1: Check if Nextcloud is already installed before we try to access user info
250
-		if (!$this->config->getSystemValueBool('installed', false)) {
251
-			return 'en';
252
-		}
253
-		// Step 3.2: Check the current user (if any) for their preferred language
254
-		$user = $this->userSession->getUser();
255
-		if ($user !== null) {
256
-			$userLang = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
257
-			if ($userLang !== null) {
258
-				return $userLang;
259
-			}
260
-		}
261
-
262
-		// Step 4: Check the request headers
263
-		try {
264
-			return $this->getLanguageFromRequest($appId);
265
-		} catch (LanguageNotFoundException $e) {
266
-			// Ignore and continue
267
-		}
268
-
269
-		// Step 5: fall back to English
270
-		return 'en';
271
-	}
272
-
273
-	/**
274
-	 * find the best locale
275
-	 *
276
-	 * @param string $lang
277
-	 * @return null|string
278
-	 */
279
-	public function findLocale($lang = null) {
280
-		$forceLocale = $this->config->getSystemValue('force_locale', false);
281
-		if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
282
-			return $forceLocale;
283
-		}
284
-
285
-		if ($this->config->getSystemValueBool('installed', false)) {
286
-			$userId = null !== $this->userSession->getUser() ? $this->userSession->getUser()->getUID() :  null;
287
-			$userLocale = null;
288
-			if (null !== $userId) {
289
-				$userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
290
-			}
291
-		} else {
292
-			$userId = null;
293
-			$userLocale = null;
294
-		}
295
-
296
-		if ($userLocale && $this->localeExists($userLocale)) {
297
-			return $userLocale;
298
-		}
299
-
300
-		// Default : use system default locale
301
-		$defaultLocale = $this->config->getSystemValue('default_locale', false);
302
-		if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
303
-			return $defaultLocale;
304
-		}
305
-
306
-		// If no user locale set, use lang as locale
307
-		if (null !== $lang && $this->localeExists($lang)) {
308
-			return $lang;
309
-		}
310
-
311
-		// At last, return USA
312
-		return 'en_US';
313
-	}
314
-
315
-	/**
316
-	 * find the matching lang from the locale
317
-	 *
318
-	 * @param string $app
319
-	 * @param string $locale
320
-	 * @return null|string
321
-	 */
322
-	public function findLanguageFromLocale(string $app = 'core', string $locale = null) {
323
-		if ($this->languageExists($app, $locale)) {
324
-			return $locale;
325
-		}
326
-
327
-		// Try to split e.g: fr_FR => fr
328
-		$locale = explode('_', $locale)[0];
329
-		if ($this->languageExists($app, $locale)) {
330
-			return $locale;
331
-		}
332
-	}
333
-
334
-	/**
335
-	 * Find all available languages for an app
336
-	 *
337
-	 * @param string|null $app App id or null for core
338
-	 * @return string[] an array of available languages
339
-	 */
340
-	public function findAvailableLanguages($app = null): array {
341
-		$key = $app;
342
-		if ($key === null) {
343
-			$key = 'null';
344
-		}
345
-
346
-		if ($availableLanguages = $this->cache->get($key)) {
347
-			$this->availableLanguages[$key] = $availableLanguages;
348
-		}
349
-
350
-		// also works with null as key
351
-		if (!empty($this->availableLanguages[$key])) {
352
-			return $this->availableLanguages[$key];
353
-		}
354
-
355
-		$available = ['en']; //english is always available
356
-		$dir = $this->findL10nDir($app);
357
-		if (is_dir($dir)) {
358
-			$files = scandir($dir);
359
-			if ($files !== false) {
360
-				foreach ($files as $file) {
361
-					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
362
-						$available[] = substr($file, 0, -5);
363
-					}
364
-				}
365
-			}
366
-		}
367
-
368
-		// merge with translations from theme
369
-		$theme = $this->config->getSystemValueString('theme');
370
-		if (!empty($theme)) {
371
-			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
372
-
373
-			if (is_dir($themeDir)) {
374
-				$files = scandir($themeDir);
375
-				if ($files !== false) {
376
-					foreach ($files as $file) {
377
-						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
378
-							$available[] = substr($file, 0, -5);
379
-						}
380
-					}
381
-				}
382
-			}
383
-		}
384
-
385
-		$this->availableLanguages[$key] = $available;
386
-		$this->cache->set($key, $available, 60);
387
-		return $available;
388
-	}
389
-
390
-	/**
391
-	 * @return array|mixed
392
-	 */
393
-	public function findAvailableLocales() {
394
-		if (!empty($this->availableLocales)) {
395
-			return $this->availableLocales;
396
-		}
397
-
398
-		$localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
399
-		$this->availableLocales = \json_decode($localeData, true);
400
-
401
-		return $this->availableLocales;
402
-	}
403
-
404
-	/**
405
-	 * @param string|null $app App id or null for core
406
-	 * @param string $lang
407
-	 * @return bool
408
-	 */
409
-	public function languageExists($app, $lang) {
410
-		if ($lang === 'en') { //english is always available
411
-			return true;
412
-		}
413
-
414
-		$languages = $this->findAvailableLanguages($app);
415
-		return in_array($lang, $languages);
416
-	}
417
-
418
-	public function getLanguageIterator(IUser $user = null): ILanguageIterator {
419
-		$user = $user ?? $this->userSession->getUser();
420
-		if ($user === null) {
421
-			throw new \RuntimeException('Failed to get an IUser instance');
422
-		}
423
-		return new LanguageIterator($user, $this->config);
424
-	}
425
-
426
-	/**
427
-	 * Return the language to use when sending something to a user
428
-	 *
429
-	 * @param IUser|null $user
430
-	 * @return string
431
-	 * @since 20.0.0
432
-	 */
433
-	public function getUserLanguage(IUser $user = null): string {
434
-		$language = $this->config->getSystemValue('force_language', false);
435
-		if ($language !== false) {
436
-			return $language;
437
-		}
438
-
439
-		if ($user instanceof IUser) {
440
-			$language = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
441
-			if ($language !== null) {
442
-				return $language;
443
-			}
444
-
445
-			// Use language from request
446
-			if ($this->userSession->getUser() instanceof IUser &&
447
-				$user->getUID() === $this->userSession->getUser()->getUID()) {
448
-				try {
449
-					return $this->getLanguageFromRequest();
450
-				} catch (LanguageNotFoundException $e) {
451
-				}
452
-			}
453
-		}
454
-
455
-		return $this->config->getSystemValueString('default_language', 'en');
456
-	}
457
-
458
-	/**
459
-	 * @param string $locale
460
-	 * @return bool
461
-	 */
462
-	public function localeExists($locale) {
463
-		if ($locale === 'en') { //english is always available
464
-			return true;
465
-		}
466
-
467
-		if ($this->localeCache === []) {
468
-			$locales = $this->findAvailableLocales();
469
-			foreach ($locales as $l) {
470
-				$this->localeCache[$l['code']] = true;
471
-			}
472
-		}
473
-
474
-		return isset($this->localeCache[$locale]);
475
-	}
476
-
477
-	/**
478
-	 * @throws LanguageNotFoundException
479
-	 */
480
-	private function getLanguageFromRequest(?string $app = null): string {
481
-		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
482
-		if ($header !== '') {
483
-			$available = $this->findAvailableLanguages($app);
484
-
485
-			// E.g. make sure that 'de' is before 'de_DE'.
486
-			sort($available);
487
-
488
-			$preferences = preg_split('/,\s*/', strtolower($header));
489
-			foreach ($preferences as $preference) {
490
-				[$preferred_language] = explode(';', $preference);
491
-				$preferred_language = str_replace('-', '_', $preferred_language);
492
-
493
-				foreach ($available as $available_language) {
494
-					if ($preferred_language === strtolower($available_language)) {
495
-						return $this->respectDefaultLanguage($app, $available_language);
496
-					}
497
-				}
498
-
499
-				// Fallback from de_De to de
500
-				foreach ($available as $available_language) {
501
-					if (substr($preferred_language, 0, 2) === $available_language) {
502
-						return $available_language;
503
-					}
504
-				}
505
-			}
506
-		}
507
-
508
-		throw new LanguageNotFoundException();
509
-	}
510
-
511
-	/**
512
-	 * if default language is set to de_DE (formal German) this should be
513
-	 * preferred to 'de' (non-formal German) if possible
514
-	 */
515
-	protected function respectDefaultLanguage(?string $app, string $lang): string {
516
-		$result = $lang;
517
-		$defaultLanguage = $this->config->getSystemValue('default_language', false);
518
-
519
-		// use formal version of german ("Sie" instead of "Du") if the default
520
-		// language is set to 'de_DE' if possible
521
-		if (
522
-			is_string($defaultLanguage) &&
523
-			strtolower($lang) === 'de' &&
524
-			strtolower($defaultLanguage) === 'de_de' &&
525
-			$this->languageExists($app, 'de_DE')
526
-		) {
527
-			$result = 'de_DE';
528
-		}
529
-
530
-		return $result;
531
-	}
532
-
533
-	/**
534
-	 * Checks if $sub is a subdirectory of $parent
535
-	 *
536
-	 * @param string $sub
537
-	 * @param string $parent
538
-	 * @return bool
539
-	 */
540
-	private function isSubDirectory($sub, $parent) {
541
-		// Check whether $sub contains no ".."
542
-		if (str_contains($sub, '..')) {
543
-			return false;
544
-		}
545
-
546
-		// Check whether $sub is a subdirectory of $parent
547
-		if (str_starts_with($sub, $parent)) {
548
-			return true;
549
-		}
550
-
551
-		return false;
552
-	}
553
-
554
-	/**
555
-	 * Get a list of language files that should be loaded
556
-	 *
557
-	 * @param string $app
558
-	 * @param string $lang
559
-	 * @return string[]
560
-	 */
561
-	// FIXME This method is only public, until OC_L10N does not need it anymore,
562
-	// FIXME This is also the reason, why it is not in the public interface
563
-	public function getL10nFilesForApp($app, $lang) {
564
-		$languageFiles = [];
565
-
566
-		$i18nDir = $this->findL10nDir($app);
567
-		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
568
-
569
-		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
570
-				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
571
-				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/'))
572
-			&& file_exists($transFile)
573
-		) {
574
-			// load the translations file
575
-			$languageFiles[] = $transFile;
576
-		}
577
-
578
-		// merge with translations from theme
579
-		$theme = $this->config->getSystemValueString('theme');
580
-		if (!empty($theme)) {
581
-			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
582
-			if (file_exists($transFile)) {
583
-				$languageFiles[] = $transFile;
584
-			}
585
-		}
586
-
587
-		return $languageFiles;
588
-	}
589
-
590
-	/**
591
-	 * find the l10n directory
592
-	 *
593
-	 * @param string $app App id or empty string for core
594
-	 * @return string directory
595
-	 */
596
-	protected function findL10nDir($app = null) {
597
-		if (in_array($app, ['core', 'lib'])) {
598
-			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
599
-				return $this->serverRoot . '/' . $app . '/l10n/';
600
-			}
601
-		} elseif ($app && \OC_App::getAppPath($app) !== false) {
602
-			// Check if the app is in the app folder
603
-			return \OC_App::getAppPath($app) . '/l10n/';
604
-		}
605
-		return $this->serverRoot . '/core/l10n/';
606
-	}
607
-
608
-	/**
609
-	 * @inheritDoc
610
-	 */
611
-	public function getLanguages(): array {
612
-		$forceLanguage = $this->config->getSystemValue('force_language', false);
613
-		if ($forceLanguage !== false) {
614
-			$l = $this->get('lib', $forceLanguage);
615
-			$potentialName = $l->t('__language_name__');
616
-
617
-			return [
618
-				'commonLanguages' => [[
619
-					'code' => $forceLanguage,
620
-					'name' => $potentialName,
621
-				]],
622
-				'otherLanguages' => [],
623
-			];
624
-		}
625
-
626
-		$languageCodes = $this->findAvailableLanguages();
627
-
628
-		$commonLanguages = [];
629
-		$otherLanguages = [];
630
-
631
-		foreach ($languageCodes as $lang) {
632
-			$l = $this->get('lib', $lang);
633
-			// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
634
-			$potentialName = $l->t('__language_name__');
635
-			if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') { //first check if the language name is in the translation file
636
-				$ln = [
637
-					'code' => $lang,
638
-					'name' => $potentialName
639
-				];
640
-			} elseif ($lang === 'en') {
641
-				$ln = [
642
-					'code' => $lang,
643
-					'name' => 'English (US)'
644
-				];
645
-			} else { //fallback to language code
646
-				$ln = [
647
-					'code' => $lang,
648
-					'name' => $lang
649
-				];
650
-			}
651
-
652
-			// put appropriate languages into appropriate arrays, to print them sorted
653
-			// common languages -> divider -> other languages
654
-			if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
655
-				$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
656
-			} else {
657
-				$otherLanguages[] = $ln;
658
-			}
659
-		}
660
-
661
-		ksort($commonLanguages);
662
-
663
-		// sort now by displayed language not the iso-code
664
-		usort($otherLanguages, function ($a, $b) {
665
-			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
666
-				// If a doesn't have a name, but b does, list b before a
667
-				return 1;
668
-			}
669
-			if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
670
-				// If a does have a name, but b doesn't, list a before b
671
-				return -1;
672
-			}
673
-			// Otherwise compare the names
674
-			return strcmp($a['name'], $b['name']);
675
-		});
676
-
677
-		return [
678
-			// reset indexes
679
-			'commonLanguages' => array_values($commonLanguages),
680
-			'otherLanguages' => $otherLanguages
681
-		];
682
-	}
57
+    /** @var string */
58
+    protected $requestLanguage = '';
59
+
60
+    /**
61
+     * cached instances
62
+     * @var array Structure: Lang => App => \OCP\IL10N
63
+     */
64
+    protected $instances = [];
65
+
66
+    /**
67
+     * @var array Structure: App => string[]
68
+     */
69
+    protected $availableLanguages = [];
70
+
71
+    /**
72
+     * @var array
73
+     */
74
+    protected $localeCache = [];
75
+
76
+    /**
77
+     * @var array
78
+     */
79
+    protected $availableLocales = [];
80
+
81
+    /**
82
+     * @var array Structure: string => callable
83
+     */
84
+    protected $pluralFunctions = [];
85
+
86
+    public const COMMON_LANGUAGE_CODES = [
87
+        'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
88
+        'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
89
+    ];
90
+
91
+    /** @var IConfig */
92
+    protected $config;
93
+
94
+    /** @var IRequest */
95
+    protected $request;
96
+
97
+    /** @var IUserSession */
98
+    protected IUserSession $userSession;
99
+
100
+    private ICache $cache;
101
+
102
+    /** @var string */
103
+    protected $serverRoot;
104
+
105
+    /**
106
+     * @param IConfig $config
107
+     * @param IRequest $request
108
+     * @param IUserSession $userSession
109
+     * @param string $serverRoot
110
+     */
111
+    public function __construct(
112
+        IConfig $config,
113
+        IRequest $request,
114
+        IUserSession $userSession,
115
+        ICacheFactory $cacheFactory,
116
+        $serverRoot
117
+    ) {
118
+        $this->config = $config;
119
+        $this->request = $request;
120
+        $this->userSession = $userSession;
121
+        $this->cache = $cacheFactory->createLocal('L10NFactory');
122
+        $this->serverRoot = $serverRoot;
123
+    }
124
+
125
+    /**
126
+     * Get a language instance
127
+     *
128
+     * @param string $app
129
+     * @param string|null $lang
130
+     * @param string|null $locale
131
+     * @return \OCP\IL10N
132
+     */
133
+    public function get($app, $lang = null, $locale = null) {
134
+        return new LazyL10N(function () use ($app, $lang, $locale) {
135
+            $app = \OC_App::cleanAppId($app);
136
+            if ($lang !== null) {
137
+                $lang = str_replace(['\0', '/', '\\', '..'], '', $lang);
138
+            }
139
+
140
+            $forceLang = $this->config->getSystemValue('force_language', false);
141
+            if (is_string($forceLang)) {
142
+                $lang = $forceLang;
143
+            }
144
+
145
+            $forceLocale = $this->config->getSystemValue('force_locale', false);
146
+            if (is_string($forceLocale)) {
147
+                $locale = $forceLocale;
148
+            }
149
+
150
+            if ($lang === null || !$this->languageExists($app, $lang)) {
151
+                $lang = $this->findLanguage($app);
152
+            }
153
+
154
+            if ($locale === null || !$this->localeExists($locale)) {
155
+                $locale = $this->findLocale($lang);
156
+            }
157
+
158
+            if (!isset($this->instances[$lang][$app])) {
159
+                $this->instances[$lang][$app] = new L10N(
160
+                    $this,
161
+                    $app,
162
+                    $lang,
163
+                    $locale,
164
+                    $this->getL10nFilesForApp($app, $lang)
165
+                );
166
+            }
167
+
168
+            return $this->instances[$lang][$app];
169
+        });
170
+    }
171
+
172
+    /**
173
+     * Find the best language
174
+     *
175
+     * @param string|null $appId App id or null for core
176
+     *
177
+     * @return string language If nothing works it returns 'en'
178
+     */
179
+    public function findLanguage(?string $appId = null): string {
180
+        // Step 1: Forced language always has precedence over anything else
181
+        $forceLang = $this->config->getSystemValue('force_language', false);
182
+        if (is_string($forceLang)) {
183
+            $this->requestLanguage = $forceLang;
184
+        }
185
+
186
+        // Step 2: Return cached language
187
+        if ($this->requestLanguage !== '' && $this->languageExists($appId, $this->requestLanguage)) {
188
+            return $this->requestLanguage;
189
+        }
190
+
191
+        /**
192
+         * Step 3: At this point Nextcloud might not yet be installed and thus the lookup
193
+         * in the preferences table might fail. For this reason we need to check
194
+         * whether the instance has already been installed
195
+         *
196
+         * @link https://github.com/owncloud/core/issues/21955
197
+         */
198
+        if ($this->config->getSystemValueBool('installed', false)) {
199
+            $userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
200
+            if (!is_null($userId)) {
201
+                $userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
202
+            } else {
203
+                $userLang = null;
204
+            }
205
+        } else {
206
+            $userId = null;
207
+            $userLang = null;
208
+        }
209
+        if ($userLang) {
210
+            $this->requestLanguage = $userLang;
211
+            if ($this->languageExists($appId, $userLang)) {
212
+                return $userLang;
213
+            }
214
+        }
215
+
216
+        // Step 4: Check the request headers
217
+        try {
218
+            // Try to get the language from the Request
219
+            $lang = $this->getLanguageFromRequest($appId);
220
+            if ($userId !== null && $appId === null && !$userLang) {
221
+                $this->config->setUserValue($userId, 'core', 'lang', $lang);
222
+            }
223
+            return $lang;
224
+        } catch (LanguageNotFoundException $e) {
225
+            // Finding language from request failed fall back to default language
226
+            $defaultLanguage = $this->config->getSystemValue('default_language', false);
227
+            if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
228
+                return $defaultLanguage;
229
+            }
230
+        }
231
+
232
+        // Step 5: fall back to English
233
+        return 'en';
234
+    }
235
+
236
+    public function findGenericLanguage(string $appId = null): string {
237
+        // Step 1: Forced language always has precedence over anything else
238
+        $forcedLanguage = $this->config->getSystemValue('force_language', false);
239
+        if ($forcedLanguage !== false) {
240
+            return $forcedLanguage;
241
+        }
242
+
243
+        // Step 2: Check if we have a default language
244
+        $defaultLanguage = $this->config->getSystemValue('default_language', false);
245
+        if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) {
246
+            return $defaultLanguage;
247
+        }
248
+
249
+        // Step 3.1: Check if Nextcloud is already installed before we try to access user info
250
+        if (!$this->config->getSystemValueBool('installed', false)) {
251
+            return 'en';
252
+        }
253
+        // Step 3.2: Check the current user (if any) for their preferred language
254
+        $user = $this->userSession->getUser();
255
+        if ($user !== null) {
256
+            $userLang = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
257
+            if ($userLang !== null) {
258
+                return $userLang;
259
+            }
260
+        }
261
+
262
+        // Step 4: Check the request headers
263
+        try {
264
+            return $this->getLanguageFromRequest($appId);
265
+        } catch (LanguageNotFoundException $e) {
266
+            // Ignore and continue
267
+        }
268
+
269
+        // Step 5: fall back to English
270
+        return 'en';
271
+    }
272
+
273
+    /**
274
+     * find the best locale
275
+     *
276
+     * @param string $lang
277
+     * @return null|string
278
+     */
279
+    public function findLocale($lang = null) {
280
+        $forceLocale = $this->config->getSystemValue('force_locale', false);
281
+        if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
282
+            return $forceLocale;
283
+        }
284
+
285
+        if ($this->config->getSystemValueBool('installed', false)) {
286
+            $userId = null !== $this->userSession->getUser() ? $this->userSession->getUser()->getUID() :  null;
287
+            $userLocale = null;
288
+            if (null !== $userId) {
289
+                $userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
290
+            }
291
+        } else {
292
+            $userId = null;
293
+            $userLocale = null;
294
+        }
295
+
296
+        if ($userLocale && $this->localeExists($userLocale)) {
297
+            return $userLocale;
298
+        }
299
+
300
+        // Default : use system default locale
301
+        $defaultLocale = $this->config->getSystemValue('default_locale', false);
302
+        if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
303
+            return $defaultLocale;
304
+        }
305
+
306
+        // If no user locale set, use lang as locale
307
+        if (null !== $lang && $this->localeExists($lang)) {
308
+            return $lang;
309
+        }
310
+
311
+        // At last, return USA
312
+        return 'en_US';
313
+    }
314
+
315
+    /**
316
+     * find the matching lang from the locale
317
+     *
318
+     * @param string $app
319
+     * @param string $locale
320
+     * @return null|string
321
+     */
322
+    public function findLanguageFromLocale(string $app = 'core', string $locale = null) {
323
+        if ($this->languageExists($app, $locale)) {
324
+            return $locale;
325
+        }
326
+
327
+        // Try to split e.g: fr_FR => fr
328
+        $locale = explode('_', $locale)[0];
329
+        if ($this->languageExists($app, $locale)) {
330
+            return $locale;
331
+        }
332
+    }
333
+
334
+    /**
335
+     * Find all available languages for an app
336
+     *
337
+     * @param string|null $app App id or null for core
338
+     * @return string[] an array of available languages
339
+     */
340
+    public function findAvailableLanguages($app = null): array {
341
+        $key = $app;
342
+        if ($key === null) {
343
+            $key = 'null';
344
+        }
345
+
346
+        if ($availableLanguages = $this->cache->get($key)) {
347
+            $this->availableLanguages[$key] = $availableLanguages;
348
+        }
349
+
350
+        // also works with null as key
351
+        if (!empty($this->availableLanguages[$key])) {
352
+            return $this->availableLanguages[$key];
353
+        }
354
+
355
+        $available = ['en']; //english is always available
356
+        $dir = $this->findL10nDir($app);
357
+        if (is_dir($dir)) {
358
+            $files = scandir($dir);
359
+            if ($files !== false) {
360
+                foreach ($files as $file) {
361
+                    if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
362
+                        $available[] = substr($file, 0, -5);
363
+                    }
364
+                }
365
+            }
366
+        }
367
+
368
+        // merge with translations from theme
369
+        $theme = $this->config->getSystemValueString('theme');
370
+        if (!empty($theme)) {
371
+            $themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
372
+
373
+            if (is_dir($themeDir)) {
374
+                $files = scandir($themeDir);
375
+                if ($files !== false) {
376
+                    foreach ($files as $file) {
377
+                        if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
378
+                            $available[] = substr($file, 0, -5);
379
+                        }
380
+                    }
381
+                }
382
+            }
383
+        }
384
+
385
+        $this->availableLanguages[$key] = $available;
386
+        $this->cache->set($key, $available, 60);
387
+        return $available;
388
+    }
389
+
390
+    /**
391
+     * @return array|mixed
392
+     */
393
+    public function findAvailableLocales() {
394
+        if (!empty($this->availableLocales)) {
395
+            return $this->availableLocales;
396
+        }
397
+
398
+        $localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
399
+        $this->availableLocales = \json_decode($localeData, true);
400
+
401
+        return $this->availableLocales;
402
+    }
403
+
404
+    /**
405
+     * @param string|null $app App id or null for core
406
+     * @param string $lang
407
+     * @return bool
408
+     */
409
+    public function languageExists($app, $lang) {
410
+        if ($lang === 'en') { //english is always available
411
+            return true;
412
+        }
413
+
414
+        $languages = $this->findAvailableLanguages($app);
415
+        return in_array($lang, $languages);
416
+    }
417
+
418
+    public function getLanguageIterator(IUser $user = null): ILanguageIterator {
419
+        $user = $user ?? $this->userSession->getUser();
420
+        if ($user === null) {
421
+            throw new \RuntimeException('Failed to get an IUser instance');
422
+        }
423
+        return new LanguageIterator($user, $this->config);
424
+    }
425
+
426
+    /**
427
+     * Return the language to use when sending something to a user
428
+     *
429
+     * @param IUser|null $user
430
+     * @return string
431
+     * @since 20.0.0
432
+     */
433
+    public function getUserLanguage(IUser $user = null): string {
434
+        $language = $this->config->getSystemValue('force_language', false);
435
+        if ($language !== false) {
436
+            return $language;
437
+        }
438
+
439
+        if ($user instanceof IUser) {
440
+            $language = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
441
+            if ($language !== null) {
442
+                return $language;
443
+            }
444
+
445
+            // Use language from request
446
+            if ($this->userSession->getUser() instanceof IUser &&
447
+                $user->getUID() === $this->userSession->getUser()->getUID()) {
448
+                try {
449
+                    return $this->getLanguageFromRequest();
450
+                } catch (LanguageNotFoundException $e) {
451
+                }
452
+            }
453
+        }
454
+
455
+        return $this->config->getSystemValueString('default_language', 'en');
456
+    }
457
+
458
+    /**
459
+     * @param string $locale
460
+     * @return bool
461
+     */
462
+    public function localeExists($locale) {
463
+        if ($locale === 'en') { //english is always available
464
+            return true;
465
+        }
466
+
467
+        if ($this->localeCache === []) {
468
+            $locales = $this->findAvailableLocales();
469
+            foreach ($locales as $l) {
470
+                $this->localeCache[$l['code']] = true;
471
+            }
472
+        }
473
+
474
+        return isset($this->localeCache[$locale]);
475
+    }
476
+
477
+    /**
478
+     * @throws LanguageNotFoundException
479
+     */
480
+    private function getLanguageFromRequest(?string $app = null): string {
481
+        $header = $this->request->getHeader('ACCEPT_LANGUAGE');
482
+        if ($header !== '') {
483
+            $available = $this->findAvailableLanguages($app);
484
+
485
+            // E.g. make sure that 'de' is before 'de_DE'.
486
+            sort($available);
487
+
488
+            $preferences = preg_split('/,\s*/', strtolower($header));
489
+            foreach ($preferences as $preference) {
490
+                [$preferred_language] = explode(';', $preference);
491
+                $preferred_language = str_replace('-', '_', $preferred_language);
492
+
493
+                foreach ($available as $available_language) {
494
+                    if ($preferred_language === strtolower($available_language)) {
495
+                        return $this->respectDefaultLanguage($app, $available_language);
496
+                    }
497
+                }
498
+
499
+                // Fallback from de_De to de
500
+                foreach ($available as $available_language) {
501
+                    if (substr($preferred_language, 0, 2) === $available_language) {
502
+                        return $available_language;
503
+                    }
504
+                }
505
+            }
506
+        }
507
+
508
+        throw new LanguageNotFoundException();
509
+    }
510
+
511
+    /**
512
+     * if default language is set to de_DE (formal German) this should be
513
+     * preferred to 'de' (non-formal German) if possible
514
+     */
515
+    protected function respectDefaultLanguage(?string $app, string $lang): string {
516
+        $result = $lang;
517
+        $defaultLanguage = $this->config->getSystemValue('default_language', false);
518
+
519
+        // use formal version of german ("Sie" instead of "Du") if the default
520
+        // language is set to 'de_DE' if possible
521
+        if (
522
+            is_string($defaultLanguage) &&
523
+            strtolower($lang) === 'de' &&
524
+            strtolower($defaultLanguage) === 'de_de' &&
525
+            $this->languageExists($app, 'de_DE')
526
+        ) {
527
+            $result = 'de_DE';
528
+        }
529
+
530
+        return $result;
531
+    }
532
+
533
+    /**
534
+     * Checks if $sub is a subdirectory of $parent
535
+     *
536
+     * @param string $sub
537
+     * @param string $parent
538
+     * @return bool
539
+     */
540
+    private function isSubDirectory($sub, $parent) {
541
+        // Check whether $sub contains no ".."
542
+        if (str_contains($sub, '..')) {
543
+            return false;
544
+        }
545
+
546
+        // Check whether $sub is a subdirectory of $parent
547
+        if (str_starts_with($sub, $parent)) {
548
+            return true;
549
+        }
550
+
551
+        return false;
552
+    }
553
+
554
+    /**
555
+     * Get a list of language files that should be loaded
556
+     *
557
+     * @param string $app
558
+     * @param string $lang
559
+     * @return string[]
560
+     */
561
+    // FIXME This method is only public, until OC_L10N does not need it anymore,
562
+    // FIXME This is also the reason, why it is not in the public interface
563
+    public function getL10nFilesForApp($app, $lang) {
564
+        $languageFiles = [];
565
+
566
+        $i18nDir = $this->findL10nDir($app);
567
+        $transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
568
+
569
+        if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
570
+                || $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
571
+                || $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/'))
572
+            && file_exists($transFile)
573
+        ) {
574
+            // load the translations file
575
+            $languageFiles[] = $transFile;
576
+        }
577
+
578
+        // merge with translations from theme
579
+        $theme = $this->config->getSystemValueString('theme');
580
+        if (!empty($theme)) {
581
+            $transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
582
+            if (file_exists($transFile)) {
583
+                $languageFiles[] = $transFile;
584
+            }
585
+        }
586
+
587
+        return $languageFiles;
588
+    }
589
+
590
+    /**
591
+     * find the l10n directory
592
+     *
593
+     * @param string $app App id or empty string for core
594
+     * @return string directory
595
+     */
596
+    protected function findL10nDir($app = null) {
597
+        if (in_array($app, ['core', 'lib'])) {
598
+            if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
599
+                return $this->serverRoot . '/' . $app . '/l10n/';
600
+            }
601
+        } elseif ($app && \OC_App::getAppPath($app) !== false) {
602
+            // Check if the app is in the app folder
603
+            return \OC_App::getAppPath($app) . '/l10n/';
604
+        }
605
+        return $this->serverRoot . '/core/l10n/';
606
+    }
607
+
608
+    /**
609
+     * @inheritDoc
610
+     */
611
+    public function getLanguages(): array {
612
+        $forceLanguage = $this->config->getSystemValue('force_language', false);
613
+        if ($forceLanguage !== false) {
614
+            $l = $this->get('lib', $forceLanguage);
615
+            $potentialName = $l->t('__language_name__');
616
+
617
+            return [
618
+                'commonLanguages' => [[
619
+                    'code' => $forceLanguage,
620
+                    'name' => $potentialName,
621
+                ]],
622
+                'otherLanguages' => [],
623
+            ];
624
+        }
625
+
626
+        $languageCodes = $this->findAvailableLanguages();
627
+
628
+        $commonLanguages = [];
629
+        $otherLanguages = [];
630
+
631
+        foreach ($languageCodes as $lang) {
632
+            $l = $this->get('lib', $lang);
633
+            // TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
634
+            $potentialName = $l->t('__language_name__');
635
+            if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') { //first check if the language name is in the translation file
636
+                $ln = [
637
+                    'code' => $lang,
638
+                    'name' => $potentialName
639
+                ];
640
+            } elseif ($lang === 'en') {
641
+                $ln = [
642
+                    'code' => $lang,
643
+                    'name' => 'English (US)'
644
+                ];
645
+            } else { //fallback to language code
646
+                $ln = [
647
+                    'code' => $lang,
648
+                    'name' => $lang
649
+                ];
650
+            }
651
+
652
+            // put appropriate languages into appropriate arrays, to print them sorted
653
+            // common languages -> divider -> other languages
654
+            if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
655
+                $commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
656
+            } else {
657
+                $otherLanguages[] = $ln;
658
+            }
659
+        }
660
+
661
+        ksort($commonLanguages);
662
+
663
+        // sort now by displayed language not the iso-code
664
+        usort($otherLanguages, function ($a, $b) {
665
+            if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
666
+                // If a doesn't have a name, but b does, list b before a
667
+                return 1;
668
+            }
669
+            if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
670
+                // If a does have a name, but b doesn't, list a before b
671
+                return -1;
672
+            }
673
+            // Otherwise compare the names
674
+            return strcmp($a['name'], $b['name']);
675
+        });
676
+
677
+        return [
678
+            // reset indexes
679
+            'commonLanguages' => array_values($commonLanguages),
680
+            'otherLanguages' => $otherLanguages
681
+        ];
682
+    }
683 683
 }
Please login to merge, or discard this patch.
lib/private/L10N/L10NString.php 1 patch
Indentation   +50 added lines, -50 removed lines patch added patch discarded remove patch
@@ -28,67 +28,67 @@
 block discarded – undo
28 28
 namespace OC\L10N;
29 29
 
30 30
 class L10NString implements \JsonSerializable {
31
-	/** @var L10N */
32
-	protected $l10n;
31
+    /** @var L10N */
32
+    protected $l10n;
33 33
 
34
-	/** @var string */
35
-	protected $text;
34
+    /** @var string */
35
+    protected $text;
36 36
 
37
-	/** @var array */
38
-	protected $parameters;
37
+    /** @var array */
38
+    protected $parameters;
39 39
 
40
-	/** @var integer */
41
-	protected $count;
40
+    /** @var integer */
41
+    protected $count;
42 42
 
43
-	/**
44
-	 * @param L10N $l10n
45
-	 * @param string|string[] $text
46
-	 * @param array $parameters
47
-	 * @param int $count
48
-	 */
49
-	public function __construct(L10N $l10n, $text, array $parameters, int $count = 1) {
50
-		$this->l10n = $l10n;
51
-		$this->text = $text;
52
-		$this->parameters = $parameters;
53
-		$this->count = $count;
54
-	}
43
+    /**
44
+     * @param L10N $l10n
45
+     * @param string|string[] $text
46
+     * @param array $parameters
47
+     * @param int $count
48
+     */
49
+    public function __construct(L10N $l10n, $text, array $parameters, int $count = 1) {
50
+        $this->l10n = $l10n;
51
+        $this->text = $text;
52
+        $this->parameters = $parameters;
53
+        $this->count = $count;
54
+    }
55 55
 
56
-	public function __toString(): string {
57
-		$translations = $this->l10n->getTranslations();
58
-		$identityTranslator = $this->l10n->getIdentityTranslator();
56
+    public function __toString(): string {
57
+        $translations = $this->l10n->getTranslations();
58
+        $identityTranslator = $this->l10n->getIdentityTranslator();
59 59
 
60
-		// Use the indexed version as per \Symfony\Contracts\Translation\TranslatorInterface
61
-		$identity = $this->text;
62
-		if (array_key_exists($this->text, $translations)) {
63
-			$identity = $translations[$this->text];
64
-		}
60
+        // Use the indexed version as per \Symfony\Contracts\Translation\TranslatorInterface
61
+        $identity = $this->text;
62
+        if (array_key_exists($this->text, $translations)) {
63
+            $identity = $translations[$this->text];
64
+        }
65 65
 
66
-		if (is_array($identity)) {
67
-			$pipeCheck = implode('', $identity);
68
-			if (str_contains($pipeCheck, '|')) {
69
-				return 'Can not use pipe character in translations';
70
-			}
66
+        if (is_array($identity)) {
67
+            $pipeCheck = implode('', $identity);
68
+            if (str_contains($pipeCheck, '|')) {
69
+                return 'Can not use pipe character in translations';
70
+            }
71 71
 
72
-			$identity = implode('|', $identity);
73
-		} elseif (str_contains($identity, '|')) {
74
-			return 'Can not use pipe character in translations';
75
-		}
72
+            $identity = implode('|', $identity);
73
+        } elseif (str_contains($identity, '|')) {
74
+            return 'Can not use pipe character in translations';
75
+        }
76 76
 
77
-		$beforeIdentity = $identity;
78
-		$identity = str_replace('%n', '%count%', $identity);
77
+        $beforeIdentity = $identity;
78
+        $identity = str_replace('%n', '%count%', $identity);
79 79
 
80
-		$parameters = [];
81
-		if ($beforeIdentity !== $identity) {
82
-			$parameters = ['%count%' => $this->count];
83
-		}
80
+        $parameters = [];
81
+        if ($beforeIdentity !== $identity) {
82
+            $parameters = ['%count%' => $this->count];
83
+        }
84 84
 
85
-		// $count as %count% as per \Symfony\Contracts\Translation\TranslatorInterface
86
-		$text = $identityTranslator->trans($identity, $parameters);
85
+        // $count as %count% as per \Symfony\Contracts\Translation\TranslatorInterface
86
+        $text = $identityTranslator->trans($identity, $parameters);
87 87
 
88
-		return vsprintf($text, $this->parameters);
89
-	}
88
+        return vsprintf($text, $this->parameters);
89
+    }
90 90
 
91
-	public function jsonSerialize(): string {
92
-		return $this->__toString();
93
-	}
91
+    public function jsonSerialize(): string {
92
+        return $this->__toString();
93
+    }
94 94
 }
Please login to merge, or discard this patch.
lib/private/DB/QueryBuilder/QueryBuilder.php 1 patch
Indentation   +1300 added lines, -1300 removed lines patch added patch discarded remove patch
@@ -56,1304 +56,1304 @@
 block discarded – undo
56 56
 use Psr\Log\LoggerInterface;
57 57
 
58 58
 class QueryBuilder implements IQueryBuilder {
59
-	/** @var ConnectionAdapter */
60
-	private $connection;
61
-
62
-	/** @var SystemConfig */
63
-	private $systemConfig;
64
-
65
-	private LoggerInterface $logger;
66
-
67
-	/** @var \Doctrine\DBAL\Query\QueryBuilder */
68
-	private $queryBuilder;
69
-
70
-	/** @var QuoteHelper */
71
-	private $helper;
72
-
73
-	/** @var bool */
74
-	private $automaticTablePrefix = true;
75
-
76
-	/** @var string */
77
-	protected $lastInsertedTable;
78
-
79
-	/**
80
-	 * Initializes a new QueryBuilder.
81
-	 *
82
-	 * @param ConnectionAdapter $connection
83
-	 * @param SystemConfig $systemConfig
84
-	 */
85
-	public function __construct(ConnectionAdapter $connection, SystemConfig $systemConfig, LoggerInterface $logger) {
86
-		$this->connection = $connection;
87
-		$this->systemConfig = $systemConfig;
88
-		$this->logger = $logger;
89
-		$this->queryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection->getInner());
90
-		$this->helper = new QuoteHelper();
91
-	}
92
-
93
-	/**
94
-	 * Enable/disable automatic prefixing of table names with the oc_ prefix
95
-	 *
96
-	 * @param bool $enabled If set to true table names will be prefixed with the
97
-	 * owncloud database prefix automatically.
98
-	 * @since 8.2.0
99
-	 */
100
-	public function automaticTablePrefix($enabled) {
101
-		$this->automaticTablePrefix = (bool) $enabled;
102
-	}
103
-
104
-	/**
105
-	 * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
106
-	 * This producer method is intended for convenient inline usage. Example:
107
-	 *
108
-	 * <code>
109
-	 *     $qb = $conn->getQueryBuilder()
110
-	 *         ->select('u')
111
-	 *         ->from('users', 'u')
112
-	 *         ->where($qb->expr()->eq('u.id', 1));
113
-	 * </code>
114
-	 *
115
-	 * For more complex expression construction, consider storing the expression
116
-	 * builder object in a local variable.
117
-	 *
118
-	 * @return \OCP\DB\QueryBuilder\IExpressionBuilder
119
-	 */
120
-	public function expr() {
121
-		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
122
-			return new OCIExpressionBuilder($this->connection, $this);
123
-		}
124
-		if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
125
-			return new PgSqlExpressionBuilder($this->connection, $this);
126
-		}
127
-		if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
128
-			return new MySqlExpressionBuilder($this->connection, $this);
129
-		}
130
-		if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
131
-			return new SqliteExpressionBuilder($this->connection, $this);
132
-		}
133
-
134
-		return new ExpressionBuilder($this->connection, $this);
135
-	}
136
-
137
-	/**
138
-	 * Gets an FunctionBuilder used for object-oriented construction of query functions.
139
-	 * This producer method is intended for convenient inline usage. Example:
140
-	 *
141
-	 * <code>
142
-	 *     $qb = $conn->getQueryBuilder()
143
-	 *         ->select('u')
144
-	 *         ->from('users', 'u')
145
-	 *         ->where($qb->fun()->md5('u.id'));
146
-	 * </code>
147
-	 *
148
-	 * For more complex function construction, consider storing the function
149
-	 * builder object in a local variable.
150
-	 *
151
-	 * @return \OCP\DB\QueryBuilder\IFunctionBuilder
152
-	 */
153
-	public function func() {
154
-		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
155
-			return new OCIFunctionBuilder($this->connection, $this, $this->helper);
156
-		}
157
-		if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
158
-			return new SqliteFunctionBuilder($this->connection, $this, $this->helper);
159
-		}
160
-		if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
161
-			return new PgSqlFunctionBuilder($this->connection, $this, $this->helper);
162
-		}
163
-
164
-		return new FunctionBuilder($this->connection, $this, $this->helper);
165
-	}
166
-
167
-	/**
168
-	 * Gets the type of the currently built query.
169
-	 *
170
-	 * @return integer
171
-	 */
172
-	public function getType() {
173
-		return $this->queryBuilder->getType();
174
-	}
175
-
176
-	/**
177
-	 * Gets the associated DBAL Connection for this query builder.
178
-	 *
179
-	 * @return \OCP\IDBConnection
180
-	 */
181
-	public function getConnection() {
182
-		return $this->connection;
183
-	}
184
-
185
-	/**
186
-	 * Gets the state of this query builder instance.
187
-	 *
188
-	 * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
189
-	 */
190
-	public function getState() {
191
-		return $this->queryBuilder->getState();
192
-	}
193
-
194
-	/**
195
-	 * Executes this query using the bound parameters and their types.
196
-	 *
197
-	 * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate}
198
-	 * for insert, update and delete statements.
199
-	 *
200
-	 * @return IResult|int
201
-	 */
202
-	public function execute() {
203
-		if ($this->systemConfig->getValue('log_query', false)) {
204
-			try {
205
-				$params = [];
206
-				foreach ($this->getParameters() as $placeholder => $value) {
207
-					if ($value instanceof \DateTime) {
208
-						$params[] = $placeholder . ' => DateTime:\'' . $value->format('c') . '\'';
209
-					} elseif (is_array($value)) {
210
-						$params[] = $placeholder . ' => (\'' . implode('\', \'', $value) . '\')';
211
-					} else {
212
-						$params[] = $placeholder . ' => \'' . $value . '\'';
213
-					}
214
-				}
215
-				if (empty($params)) {
216
-					$this->logger->debug('DB QueryBuilder: \'{query}\'', [
217
-						'query' => $this->getSQL(),
218
-						'app' => 'core',
219
-					]);
220
-				} else {
221
-					$this->logger->debug('DB QueryBuilder: \'{query}\' with parameters: {params}', [
222
-						'query' => $this->getSQL(),
223
-						'params' => implode(', ', $params),
224
-						'app' => 'core',
225
-					]);
226
-				}
227
-			} catch (\Error $e) {
228
-				// likely an error during conversion of $value to string
229
-				$this->logger->error('DB QueryBuilder: error trying to log SQL query', ['exception' => $e]);
230
-			}
231
-		}
232
-
233
-		if (!empty($this->getQueryPart('select'))) {
234
-			$select = $this->getQueryPart('select');
235
-			$hasSelectAll = array_filter($select, static function ($s) {
236
-				return $s === '*';
237
-			});
238
-			$hasSelectSpecific = array_filter($select, static function ($s) {
239
-				return $s !== '*';
240
-			});
241
-
242
-			if (empty($hasSelectAll) === empty($hasSelectSpecific)) {
243
-				$exception = new QueryException('Query is selecting * and specific values in the same query. This is not supported in Oracle.');
244
-				$this->logger->error($exception->getMessage(), [
245
-					'query' => $this->getSQL(),
246
-					'app' => 'core',
247
-					'exception' => $exception,
248
-				]);
249
-			}
250
-		}
251
-
252
-		$numberOfParameters = 0;
253
-		$hasTooLargeArrayParameter = false;
254
-		foreach ($this->getParameters() as $parameter) {
255
-			if (is_array($parameter)) {
256
-				$count = count($parameter);
257
-				$numberOfParameters += $count;
258
-				$hasTooLargeArrayParameter = $hasTooLargeArrayParameter || ($count > 1000);
259
-			}
260
-		}
261
-
262
-		if ($hasTooLargeArrayParameter) {
263
-			$exception = new QueryException('More than 1000 expressions in a list are not allowed on Oracle.');
264
-			$this->logger->error($exception->getMessage(), [
265
-				'query' => $this->getSQL(),
266
-				'app' => 'core',
267
-				'exception' => $exception,
268
-			]);
269
-		}
270
-
271
-		if ($numberOfParameters > 65535) {
272
-			$exception = new QueryException('The number of parameters must not exceed 65535. Restriction by PostgreSQL.');
273
-			$this->logger->error($exception->getMessage(), [
274
-				'query' => $this->getSQL(),
275
-				'app' => 'core',
276
-				'exception' => $exception,
277
-			]);
278
-		}
279
-
280
-		$result = $this->queryBuilder->execute();
281
-		if (is_int($result)) {
282
-			return $result;
283
-		}
284
-		return new ResultAdapter($result);
285
-	}
286
-
287
-	public function executeQuery(): IResult {
288
-		if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
289
-			throw new \RuntimeException('Invalid query type, expected SELECT query');
290
-		}
291
-
292
-		try {
293
-			$result = $this->execute();
294
-		} catch (\Doctrine\DBAL\Exception $e) {
295
-			throw \OC\DB\Exceptions\DbalException::wrap($e);
296
-		}
297
-
298
-		if ($result instanceof IResult) {
299
-			return $result;
300
-		}
301
-
302
-		throw new \RuntimeException('Invalid return type for query');
303
-	}
304
-
305
-	/**
306
-	 * Monkey-patched compatibility layer for apps that were adapted for Nextcloud 22 before
307
-	 * the first beta, where executeStatement was named executeUpdate.
308
-	 *
309
-	 * Static analysis should catch those misuses, but until then let's try to keep things
310
-	 * running.
311
-	 *
312
-	 * @internal
313
-	 * @deprecated
314
-	 * @todo drop ASAP
315
-	 */
316
-	public function executeUpdate(): int {
317
-		return $this->executeStatement();
318
-	}
319
-
320
-	public function executeStatement(): int {
321
-		if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
322
-			throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
323
-		}
324
-
325
-		try {
326
-			$result = $this->execute();
327
-		} catch (\Doctrine\DBAL\Exception $e) {
328
-			throw \OC\DB\Exceptions\DbalException::wrap($e);
329
-		}
330
-
331
-		if (!is_int($result)) {
332
-			throw new \RuntimeException('Invalid return type for statement');
333
-		}
334
-
335
-		return $result;
336
-	}
337
-
338
-
339
-	/**
340
-	 * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
341
-	 *
342
-	 * <code>
343
-	 *     $qb = $conn->getQueryBuilder()
344
-	 *         ->select('u')
345
-	 *         ->from('User', 'u')
346
-	 *     echo $qb->getSQL(); // SELECT u FROM User u
347
-	 * </code>
348
-	 *
349
-	 * @return string The SQL query string.
350
-	 */
351
-	public function getSQL() {
352
-		return $this->queryBuilder->getSQL();
353
-	}
354
-
355
-	/**
356
-	 * Sets a query parameter for the query being constructed.
357
-	 *
358
-	 * <code>
359
-	 *     $qb = $conn->getQueryBuilder()
360
-	 *         ->select('u')
361
-	 *         ->from('users', 'u')
362
-	 *         ->where('u.id = :user_id')
363
-	 *         ->setParameter(':user_id', 1);
364
-	 * </code>
365
-	 *
366
-	 * @param string|integer $key The parameter position or name.
367
-	 * @param mixed $value The parameter value.
368
-	 * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants.
369
-	 *
370
-	 * @return $this This QueryBuilder instance.
371
-	 */
372
-	public function setParameter($key, $value, $type = null) {
373
-		$this->queryBuilder->setParameter($key, $value, $type);
374
-
375
-		return $this;
376
-	}
377
-
378
-	/**
379
-	 * Sets a collection of query parameters for the query being constructed.
380
-	 *
381
-	 * <code>
382
-	 *     $qb = $conn->getQueryBuilder()
383
-	 *         ->select('u')
384
-	 *         ->from('users', 'u')
385
-	 *         ->where('u.id = :user_id1 OR u.id = :user_id2')
386
-	 *         ->setParameters(array(
387
-	 *             ':user_id1' => 1,
388
-	 *             ':user_id2' => 2
389
-	 *         ));
390
-	 * </code>
391
-	 *
392
-	 * @param array $params The query parameters to set.
393
-	 * @param array $types The query parameters types to set.
394
-	 *
395
-	 * @return $this This QueryBuilder instance.
396
-	 */
397
-	public function setParameters(array $params, array $types = []) {
398
-		$this->queryBuilder->setParameters($params, $types);
399
-
400
-		return $this;
401
-	}
402
-
403
-	/**
404
-	 * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
405
-	 *
406
-	 * @return array The currently defined query parameters indexed by parameter index or name.
407
-	 */
408
-	public function getParameters() {
409
-		return $this->queryBuilder->getParameters();
410
-	}
411
-
412
-	/**
413
-	 * Gets a (previously set) query parameter of the query being constructed.
414
-	 *
415
-	 * @param mixed $key The key (index or name) of the bound parameter.
416
-	 *
417
-	 * @return mixed The value of the bound parameter.
418
-	 */
419
-	public function getParameter($key) {
420
-		return $this->queryBuilder->getParameter($key);
421
-	}
422
-
423
-	/**
424
-	 * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
425
-	 *
426
-	 * @return array The currently defined query parameter types indexed by parameter index or name.
427
-	 */
428
-	public function getParameterTypes() {
429
-		return $this->queryBuilder->getParameterTypes();
430
-	}
431
-
432
-	/**
433
-	 * Gets a (previously set) query parameter type of the query being constructed.
434
-	 *
435
-	 * @param mixed $key The key (index or name) of the bound parameter type.
436
-	 *
437
-	 * @return mixed The value of the bound parameter type.
438
-	 */
439
-	public function getParameterType($key) {
440
-		return $this->queryBuilder->getParameterType($key);
441
-	}
442
-
443
-	/**
444
-	 * Sets the position of the first result to retrieve (the "offset").
445
-	 *
446
-	 * @param int $firstResult The first result to return.
447
-	 *
448
-	 * @return $this This QueryBuilder instance.
449
-	 */
450
-	public function setFirstResult($firstResult) {
451
-		$this->queryBuilder->setFirstResult((int) $firstResult);
452
-
453
-		return $this;
454
-	}
455
-
456
-	/**
457
-	 * Gets the position of the first result the query object was set to retrieve (the "offset").
458
-	 * Returns 0 if {@link setFirstResult} was not applied to this QueryBuilder.
459
-	 *
460
-	 * @return int The position of the first result.
461
-	 */
462
-	public function getFirstResult() {
463
-		return $this->queryBuilder->getFirstResult();
464
-	}
465
-
466
-	/**
467
-	 * Sets the maximum number of results to retrieve (the "limit").
468
-	 *
469
-	 * NOTE: Setting max results to "0" will cause mixed behaviour. While most
470
-	 * of the databases will just return an empty result set, Oracle will return
471
-	 * all entries.
472
-	 *
473
-	 * @param int|null $maxResults The maximum number of results to retrieve.
474
-	 *
475
-	 * @return $this This QueryBuilder instance.
476
-	 */
477
-	public function setMaxResults($maxResults) {
478
-		if ($maxResults === null) {
479
-			$this->queryBuilder->setMaxResults($maxResults);
480
-		} else {
481
-			$this->queryBuilder->setMaxResults((int) $maxResults);
482
-		}
483
-
484
-		return $this;
485
-	}
486
-
487
-	/**
488
-	 * Gets the maximum number of results the query object was set to retrieve (the "limit").
489
-	 * Returns NULL if {@link setMaxResults} was not applied to this query builder.
490
-	 *
491
-	 * @return int|null The maximum number of results.
492
-	 */
493
-	public function getMaxResults() {
494
-		return $this->queryBuilder->getMaxResults();
495
-	}
496
-
497
-	/**
498
-	 * Specifies an item that is to be returned in the query result.
499
-	 * Replaces any previously specified selections, if any.
500
-	 *
501
-	 * <code>
502
-	 *     $qb = $conn->getQueryBuilder()
503
-	 *         ->select('u.id', 'p.id')
504
-	 *         ->from('users', 'u')
505
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
506
-	 * </code>
507
-	 *
508
-	 * @param mixed ...$selects The selection expressions.
509
-	 *
510
-	 * '@return $this This QueryBuilder instance.
511
-	 */
512
-	public function select(...$selects) {
513
-		if (count($selects) === 1 && is_array($selects[0])) {
514
-			$selects = $selects[0];
515
-		}
516
-
517
-		$this->queryBuilder->select(
518
-			$this->helper->quoteColumnNames($selects)
519
-		);
520
-
521
-		return $this;
522
-	}
523
-
524
-	/**
525
-	 * Specifies an item that is to be returned with a different name in the query result.
526
-	 *
527
-	 * <code>
528
-	 *     $qb = $conn->getQueryBuilder()
529
-	 *         ->selectAlias('u.id', 'user_id')
530
-	 *         ->from('users', 'u')
531
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
532
-	 * </code>
533
-	 *
534
-	 * @param mixed $select The selection expressions.
535
-	 * @param string $alias The column alias used in the constructed query.
536
-	 *
537
-	 * @return $this This QueryBuilder instance.
538
-	 */
539
-	public function selectAlias($select, $alias) {
540
-		$this->queryBuilder->addSelect(
541
-			$this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias)
542
-		);
543
-
544
-		return $this;
545
-	}
546
-
547
-	/**
548
-	 * Specifies an item that is to be returned uniquely in the query result.
549
-	 *
550
-	 * <code>
551
-	 *     $qb = $conn->getQueryBuilder()
552
-	 *         ->selectDistinct('type')
553
-	 *         ->from('users');
554
-	 * </code>
555
-	 *
556
-	 * @param mixed $select The selection expressions.
557
-	 *
558
-	 * @return $this This QueryBuilder instance.
559
-	 */
560
-	public function selectDistinct($select) {
561
-		if (!is_array($select)) {
562
-			$select = [$select];
563
-		}
564
-
565
-		$quotedSelect = $this->helper->quoteColumnNames($select);
566
-
567
-		$this->queryBuilder->addSelect(
568
-			'DISTINCT ' . implode(', ', $quotedSelect)
569
-		);
570
-
571
-		return $this;
572
-	}
573
-
574
-	/**
575
-	 * Adds an item that is to be returned in the query result.
576
-	 *
577
-	 * <code>
578
-	 *     $qb = $conn->getQueryBuilder()
579
-	 *         ->select('u.id')
580
-	 *         ->addSelect('p.id')
581
-	 *         ->from('users', 'u')
582
-	 *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
583
-	 * </code>
584
-	 *
585
-	 * @param mixed ...$selects The selection expression.
586
-	 *
587
-	 * @return $this This QueryBuilder instance.
588
-	 */
589
-	public function addSelect(...$selects) {
590
-		if (count($selects) === 1 && is_array($selects[0])) {
591
-			$selects = $selects[0];
592
-		}
593
-
594
-		$this->queryBuilder->addSelect(
595
-			$this->helper->quoteColumnNames($selects)
596
-		);
597
-
598
-		return $this;
599
-	}
600
-
601
-	/**
602
-	 * Turns the query being built into a bulk delete query that ranges over
603
-	 * a certain table.
604
-	 *
605
-	 * <code>
606
-	 *     $qb = $conn->getQueryBuilder()
607
-	 *         ->delete('users', 'u')
608
-	 *         ->where('u.id = :user_id');
609
-	 *         ->setParameter(':user_id', 1);
610
-	 * </code>
611
-	 *
612
-	 * @param string $delete The table whose rows are subject to the deletion.
613
-	 * @param string $alias The table alias used in the constructed query.
614
-	 *
615
-	 * @return $this This QueryBuilder instance.
616
-	 */
617
-	public function delete($delete = null, $alias = null) {
618
-		$this->queryBuilder->delete(
619
-			$this->getTableName($delete),
620
-			$alias
621
-		);
622
-
623
-		return $this;
624
-	}
625
-
626
-	/**
627
-	 * Turns the query being built into a bulk update query that ranges over
628
-	 * a certain table
629
-	 *
630
-	 * <code>
631
-	 *     $qb = $conn->getQueryBuilder()
632
-	 *         ->update('users', 'u')
633
-	 *         ->set('u.password', md5('password'))
634
-	 *         ->where('u.id = ?');
635
-	 * </code>
636
-	 *
637
-	 * @param string $update The table whose rows are subject to the update.
638
-	 * @param string $alias The table alias used in the constructed query.
639
-	 *
640
-	 * @return $this This QueryBuilder instance.
641
-	 */
642
-	public function update($update = null, $alias = null) {
643
-		$this->queryBuilder->update(
644
-			$this->getTableName($update),
645
-			$alias
646
-		);
647
-
648
-		return $this;
649
-	}
650
-
651
-	/**
652
-	 * Turns the query being built into an insert query that inserts into
653
-	 * a certain table
654
-	 *
655
-	 * <code>
656
-	 *     $qb = $conn->getQueryBuilder()
657
-	 *         ->insert('users')
658
-	 *         ->values(
659
-	 *             array(
660
-	 *                 'name' => '?',
661
-	 *                 'password' => '?'
662
-	 *             )
663
-	 *         );
664
-	 * </code>
665
-	 *
666
-	 * @param string $insert The table into which the rows should be inserted.
667
-	 *
668
-	 * @return $this This QueryBuilder instance.
669
-	 */
670
-	public function insert($insert = null) {
671
-		$this->queryBuilder->insert(
672
-			$this->getTableName($insert)
673
-		);
674
-
675
-		$this->lastInsertedTable = $insert;
676
-
677
-		return $this;
678
-	}
679
-
680
-	/**
681
-	 * Creates and adds a query root corresponding to the table identified by the
682
-	 * given alias, forming a cartesian product with any existing query roots.
683
-	 *
684
-	 * <code>
685
-	 *     $qb = $conn->getQueryBuilder()
686
-	 *         ->select('u.id')
687
-	 *         ->from('users', 'u')
688
-	 * </code>
689
-	 *
690
-	 * @param string|IQueryFunction $from The table.
691
-	 * @param string|null $alias The alias of the table.
692
-	 *
693
-	 * @return $this This QueryBuilder instance.
694
-	 */
695
-	public function from($from, $alias = null) {
696
-		$this->queryBuilder->from(
697
-			$this->getTableName($from),
698
-			$this->quoteAlias($alias)
699
-		);
700
-
701
-		return $this;
702
-	}
703
-
704
-	/**
705
-	 * Creates and adds a join to the query.
706
-	 *
707
-	 * <code>
708
-	 *     $qb = $conn->getQueryBuilder()
709
-	 *         ->select('u.name')
710
-	 *         ->from('users', 'u')
711
-	 *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
712
-	 * </code>
713
-	 *
714
-	 * @param string $fromAlias The alias that points to a from clause.
715
-	 * @param string $join The table name to join.
716
-	 * @param string $alias The alias of the join table.
717
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
718
-	 *
719
-	 * @return $this This QueryBuilder instance.
720
-	 */
721
-	public function join($fromAlias, $join, $alias, $condition = null) {
722
-		$this->queryBuilder->join(
723
-			$this->quoteAlias($fromAlias),
724
-			$this->getTableName($join),
725
-			$this->quoteAlias($alias),
726
-			$condition
727
-		);
728
-
729
-		return $this;
730
-	}
731
-
732
-	/**
733
-	 * Creates and adds a join to the query.
734
-	 *
735
-	 * <code>
736
-	 *     $qb = $conn->getQueryBuilder()
737
-	 *         ->select('u.name')
738
-	 *         ->from('users', 'u')
739
-	 *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
740
-	 * </code>
741
-	 *
742
-	 * @param string $fromAlias The alias that points to a from clause.
743
-	 * @param string $join The table name to join.
744
-	 * @param string $alias The alias of the join table.
745
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
746
-	 *
747
-	 * @return $this This QueryBuilder instance.
748
-	 */
749
-	public function innerJoin($fromAlias, $join, $alias, $condition = null) {
750
-		$this->queryBuilder->innerJoin(
751
-			$this->quoteAlias($fromAlias),
752
-			$this->getTableName($join),
753
-			$this->quoteAlias($alias),
754
-			$condition
755
-		);
756
-
757
-		return $this;
758
-	}
759
-
760
-	/**
761
-	 * Creates and adds a left join to the query.
762
-	 *
763
-	 * <code>
764
-	 *     $qb = $conn->getQueryBuilder()
765
-	 *         ->select('u.name')
766
-	 *         ->from('users', 'u')
767
-	 *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
768
-	 * </code>
769
-	 *
770
-	 * @param string $fromAlias The alias that points to a from clause.
771
-	 * @param string $join The table name to join.
772
-	 * @param string $alias The alias of the join table.
773
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
774
-	 *
775
-	 * @return $this This QueryBuilder instance.
776
-	 */
777
-	public function leftJoin($fromAlias, $join, $alias, $condition = null) {
778
-		$this->queryBuilder->leftJoin(
779
-			$this->quoteAlias($fromAlias),
780
-			$this->getTableName($join),
781
-			$this->quoteAlias($alias),
782
-			$condition
783
-		);
784
-
785
-		return $this;
786
-	}
787
-
788
-	/**
789
-	 * Creates and adds a right join to the query.
790
-	 *
791
-	 * <code>
792
-	 *     $qb = $conn->getQueryBuilder()
793
-	 *         ->select('u.name')
794
-	 *         ->from('users', 'u')
795
-	 *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
796
-	 * </code>
797
-	 *
798
-	 * @param string $fromAlias The alias that points to a from clause.
799
-	 * @param string $join The table name to join.
800
-	 * @param string $alias The alias of the join table.
801
-	 * @param string|ICompositeExpression|null $condition The condition for the join.
802
-	 *
803
-	 * @return $this This QueryBuilder instance.
804
-	 */
805
-	public function rightJoin($fromAlias, $join, $alias, $condition = null) {
806
-		$this->queryBuilder->rightJoin(
807
-			$this->quoteAlias($fromAlias),
808
-			$this->getTableName($join),
809
-			$this->quoteAlias($alias),
810
-			$condition
811
-		);
812
-
813
-		return $this;
814
-	}
815
-
816
-	/**
817
-	 * Sets a new value for a column in a bulk update query.
818
-	 *
819
-	 * <code>
820
-	 *     $qb = $conn->getQueryBuilder()
821
-	 *         ->update('users', 'u')
822
-	 *         ->set('u.password', md5('password'))
823
-	 *         ->where('u.id = ?');
824
-	 * </code>
825
-	 *
826
-	 * @param string $key The column to set.
827
-	 * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc.
828
-	 *
829
-	 * @return $this This QueryBuilder instance.
830
-	 */
831
-	public function set($key, $value) {
832
-		$this->queryBuilder->set(
833
-			$this->helper->quoteColumnName($key),
834
-			$this->helper->quoteColumnName($value)
835
-		);
836
-
837
-		return $this;
838
-	}
839
-
840
-	/**
841
-	 * Specifies one or more restrictions to the query result.
842
-	 * Replaces any previously specified restrictions, if any.
843
-	 *
844
-	 * <code>
845
-	 *     $qb = $conn->getQueryBuilder()
846
-	 *         ->select('u.name')
847
-	 *         ->from('users', 'u')
848
-	 *         ->where('u.id = ?');
849
-	 *
850
-	 *     // You can optionally programmatically build and/or expressions
851
-	 *     $qb = $conn->getQueryBuilder();
852
-	 *
853
-	 *     $or = $qb->expr()->orx();
854
-	 *     $or->add($qb->expr()->eq('u.id', 1));
855
-	 *     $or->add($qb->expr()->eq('u.id', 2));
856
-	 *
857
-	 *     $qb->update('users', 'u')
858
-	 *         ->set('u.password', md5('password'))
859
-	 *         ->where($or);
860
-	 * </code>
861
-	 *
862
-	 * @param mixed ...$predicates The restriction predicates.
863
-	 *
864
-	 * @return $this This QueryBuilder instance.
865
-	 */
866
-	public function where(...$predicates) {
867
-		if ($this->getQueryPart('where') !== null && $this->systemConfig->getValue('debug', false)) {
868
-			// Only logging a warning, not throwing for now.
869
-			$e = new QueryException('Using where() on non-empty WHERE part, please verify it is intentional to not call whereAnd() or whereOr() instead. Otherwise consider creating a new query builder object or call resetQueryPart(\'where\') first.');
870
-			$this->logger->warning($e->getMessage(), ['exception' => $e]);
871
-		}
872
-
873
-		call_user_func_array(
874
-			[$this->queryBuilder, 'where'],
875
-			$predicates
876
-		);
877
-
878
-		return $this;
879
-	}
880
-
881
-	/**
882
-	 * Adds one or more restrictions to the query results, forming a logical
883
-	 * conjunction with any previously specified restrictions.
884
-	 *
885
-	 * <code>
886
-	 *     $qb = $conn->getQueryBuilder()
887
-	 *         ->select('u')
888
-	 *         ->from('users', 'u')
889
-	 *         ->where('u.username LIKE ?')
890
-	 *         ->andWhere('u.is_active = 1');
891
-	 * </code>
892
-	 *
893
-	 * @param mixed ...$where The query restrictions.
894
-	 *
895
-	 * @return $this This QueryBuilder instance.
896
-	 *
897
-	 * @see where()
898
-	 */
899
-	public function andWhere(...$where) {
900
-		call_user_func_array(
901
-			[$this->queryBuilder, 'andWhere'],
902
-			$where
903
-		);
904
-
905
-		return $this;
906
-	}
907
-
908
-	/**
909
-	 * Adds one or more restrictions to the query results, forming a logical
910
-	 * disjunction with any previously specified restrictions.
911
-	 *
912
-	 * <code>
913
-	 *     $qb = $conn->getQueryBuilder()
914
-	 *         ->select('u.name')
915
-	 *         ->from('users', 'u')
916
-	 *         ->where('u.id = 1')
917
-	 *         ->orWhere('u.id = 2');
918
-	 * </code>
919
-	 *
920
-	 * @param mixed ...$where The WHERE statement.
921
-	 *
922
-	 * @return $this This QueryBuilder instance.
923
-	 *
924
-	 * @see where()
925
-	 */
926
-	public function orWhere(...$where) {
927
-		call_user_func_array(
928
-			[$this->queryBuilder, 'orWhere'],
929
-			$where
930
-		);
931
-
932
-		return $this;
933
-	}
934
-
935
-	/**
936
-	 * Specifies a grouping over the results of the query.
937
-	 * Replaces any previously specified groupings, if any.
938
-	 *
939
-	 * <code>
940
-	 *     $qb = $conn->getQueryBuilder()
941
-	 *         ->select('u.name')
942
-	 *         ->from('users', 'u')
943
-	 *         ->groupBy('u.id');
944
-	 * </code>
945
-	 *
946
-	 * @param mixed ...$groupBys The grouping expression.
947
-	 *
948
-	 * @return $this This QueryBuilder instance.
949
-	 */
950
-	public function groupBy(...$groupBys) {
951
-		if (count($groupBys) === 1 && is_array($groupBys[0])) {
952
-			$groupBys = $groupBys[0];
953
-		}
954
-
955
-		call_user_func_array(
956
-			[$this->queryBuilder, 'groupBy'],
957
-			$this->helper->quoteColumnNames($groupBys)
958
-		);
959
-
960
-		return $this;
961
-	}
962
-
963
-	/**
964
-	 * Adds a grouping expression to the query.
965
-	 *
966
-	 * <code>
967
-	 *     $qb = $conn->getQueryBuilder()
968
-	 *         ->select('u.name')
969
-	 *         ->from('users', 'u')
970
-	 *         ->groupBy('u.lastLogin');
971
-	 *         ->addGroupBy('u.createdAt')
972
-	 * </code>
973
-	 *
974
-	 * @param mixed ...$groupBy The grouping expression.
975
-	 *
976
-	 * @return $this This QueryBuilder instance.
977
-	 */
978
-	public function addGroupBy(...$groupBys) {
979
-		if (count($groupBys) === 1 && is_array($groupBys[0])) {
980
-			$$groupBys = $groupBys[0];
981
-		}
982
-
983
-		call_user_func_array(
984
-			[$this->queryBuilder, 'addGroupBy'],
985
-			$this->helper->quoteColumnNames($groupBys)
986
-		);
987
-
988
-		return $this;
989
-	}
990
-
991
-	/**
992
-	 * Sets a value for a column in an insert query.
993
-	 *
994
-	 * <code>
995
-	 *     $qb = $conn->getQueryBuilder()
996
-	 *         ->insert('users')
997
-	 *         ->values(
998
-	 *             array(
999
-	 *                 'name' => '?'
1000
-	 *             )
1001
-	 *         )
1002
-	 *         ->setValue('password', '?');
1003
-	 * </code>
1004
-	 *
1005
-	 * @param string $column The column into which the value should be inserted.
1006
-	 * @param IParameter|string $value The value that should be inserted into the column.
1007
-	 *
1008
-	 * @return $this This QueryBuilder instance.
1009
-	 */
1010
-	public function setValue($column, $value) {
1011
-		$this->queryBuilder->setValue(
1012
-			$this->helper->quoteColumnName($column),
1013
-			(string) $value
1014
-		);
1015
-
1016
-		return $this;
1017
-	}
1018
-
1019
-	/**
1020
-	 * Specifies values for an insert query indexed by column names.
1021
-	 * Replaces any previous values, if any.
1022
-	 *
1023
-	 * <code>
1024
-	 *     $qb = $conn->getQueryBuilder()
1025
-	 *         ->insert('users')
1026
-	 *         ->values(
1027
-	 *             array(
1028
-	 *                 'name' => '?',
1029
-	 *                 'password' => '?'
1030
-	 *             )
1031
-	 *         );
1032
-	 * </code>
1033
-	 *
1034
-	 * @param array $values The values to specify for the insert query indexed by column names.
1035
-	 *
1036
-	 * @return $this This QueryBuilder instance.
1037
-	 */
1038
-	public function values(array $values) {
1039
-		$quotedValues = [];
1040
-		foreach ($values as $key => $value) {
1041
-			$quotedValues[$this->helper->quoteColumnName($key)] = $value;
1042
-		}
1043
-
1044
-		$this->queryBuilder->values($quotedValues);
1045
-
1046
-		return $this;
1047
-	}
1048
-
1049
-	/**
1050
-	 * Specifies a restriction over the groups of the query.
1051
-	 * Replaces any previous having restrictions, if any.
1052
-	 *
1053
-	 * @param mixed ...$having The restriction over the groups.
1054
-	 *
1055
-	 * @return $this This QueryBuilder instance.
1056
-	 */
1057
-	public function having(...$having) {
1058
-		call_user_func_array(
1059
-			[$this->queryBuilder, 'having'],
1060
-			$having
1061
-		);
1062
-
1063
-		return $this;
1064
-	}
1065
-
1066
-	/**
1067
-	 * Adds a restriction over the groups of the query, forming a logical
1068
-	 * conjunction with any existing having restrictions.
1069
-	 *
1070
-	 * @param mixed ...$having The restriction to append.
1071
-	 *
1072
-	 * @return $this This QueryBuilder instance.
1073
-	 */
1074
-	public function andHaving(...$having) {
1075
-		call_user_func_array(
1076
-			[$this->queryBuilder, 'andHaving'],
1077
-			$having
1078
-		);
1079
-
1080
-		return $this;
1081
-	}
1082
-
1083
-	/**
1084
-	 * Adds a restriction over the groups of the query, forming a logical
1085
-	 * disjunction with any existing having restrictions.
1086
-	 *
1087
-	 * @param mixed ...$having The restriction to add.
1088
-	 *
1089
-	 * @return $this This QueryBuilder instance.
1090
-	 */
1091
-	public function orHaving(...$having) {
1092
-		call_user_func_array(
1093
-			[$this->queryBuilder, 'orHaving'],
1094
-			$having
1095
-		);
1096
-
1097
-		return $this;
1098
-	}
1099
-
1100
-	/**
1101
-	 * Specifies an ordering for the query results.
1102
-	 * Replaces any previously specified orderings, if any.
1103
-	 *
1104
-	 * @param string|IQueryFunction|ILiteral|IParameter $sort The ordering expression.
1105
-	 * @param string $order The ordering direction.
1106
-	 *
1107
-	 * @return $this This QueryBuilder instance.
1108
-	 */
1109
-	public function orderBy($sort, $order = null) {
1110
-		$this->queryBuilder->orderBy(
1111
-			$this->helper->quoteColumnName($sort),
1112
-			$order
1113
-		);
1114
-
1115
-		return $this;
1116
-	}
1117
-
1118
-	/**
1119
-	 * Adds an ordering to the query results.
1120
-	 *
1121
-	 * @param string|ILiteral|IParameter|IQueryFunction $sort The ordering expression.
1122
-	 * @param string $order The ordering direction.
1123
-	 *
1124
-	 * @return $this This QueryBuilder instance.
1125
-	 */
1126
-	public function addOrderBy($sort, $order = null) {
1127
-		$this->queryBuilder->addOrderBy(
1128
-			$this->helper->quoteColumnName($sort),
1129
-			$order
1130
-		);
1131
-
1132
-		return $this;
1133
-	}
1134
-
1135
-	/**
1136
-	 * Gets a query part by its name.
1137
-	 *
1138
-	 * @param string $queryPartName
1139
-	 *
1140
-	 * @return mixed
1141
-	 */
1142
-	public function getQueryPart($queryPartName) {
1143
-		return $this->queryBuilder->getQueryPart($queryPartName);
1144
-	}
1145
-
1146
-	/**
1147
-	 * Gets all query parts.
1148
-	 *
1149
-	 * @return array
1150
-	 */
1151
-	public function getQueryParts() {
1152
-		return $this->queryBuilder->getQueryParts();
1153
-	}
1154
-
1155
-	/**
1156
-	 * Resets SQL parts.
1157
-	 *
1158
-	 * @param array|null $queryPartNames
1159
-	 *
1160
-	 * @return $this This QueryBuilder instance.
1161
-	 */
1162
-	public function resetQueryParts($queryPartNames = null) {
1163
-		$this->queryBuilder->resetQueryParts($queryPartNames);
1164
-
1165
-		return $this;
1166
-	}
1167
-
1168
-	/**
1169
-	 * Resets a single SQL part.
1170
-	 *
1171
-	 * @param string $queryPartName
1172
-	 *
1173
-	 * @return $this This QueryBuilder instance.
1174
-	 */
1175
-	public function resetQueryPart($queryPartName) {
1176
-		$this->queryBuilder->resetQueryPart($queryPartName);
1177
-
1178
-		return $this;
1179
-	}
1180
-
1181
-	/**
1182
-	 * Creates a new named parameter and bind the value $value to it.
1183
-	 *
1184
-	 * This method provides a shortcut for PDOStatement::bindValue
1185
-	 * when using prepared statements.
1186
-	 *
1187
-	 * The parameter $value specifies the value that you want to bind. If
1188
-	 * $placeholder is not provided bindValue() will automatically create a
1189
-	 * placeholder for you. An automatic placeholder will be of the name
1190
-	 * ':dcValue1', ':dcValue2' etc.
1191
-	 *
1192
-	 * For more information see {@link https://www.php.net/pdostatement-bindparam}
1193
-	 *
1194
-	 * Example:
1195
-	 * <code>
1196
-	 * $value = 2;
1197
-	 * $q->eq( 'id', $q->bindValue( $value ) );
1198
-	 * $stmt = $q->executeQuery(); // executed with 'id = 2'
1199
-	 * </code>
1200
-	 *
1201
-	 * @license New BSD License
1202
-	 * @link http://www.zetacomponents.org
1203
-	 *
1204
-	 * @param mixed $value
1205
-	 * @param mixed $type
1206
-	 * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
1207
-	 *
1208
-	 * @return IParameter the placeholder name used.
1209
-	 */
1210
-	public function createNamedParameter($value, $type = IQueryBuilder::PARAM_STR, $placeHolder = null) {
1211
-		return new Parameter($this->queryBuilder->createNamedParameter($value, $type, $placeHolder));
1212
-	}
1213
-
1214
-	/**
1215
-	 * Creates a new positional parameter and bind the given value to it.
1216
-	 *
1217
-	 * Attention: If you are using positional parameters with the query builder you have
1218
-	 * to be very careful to bind all parameters in the order they appear in the SQL
1219
-	 * statement , otherwise they get bound in the wrong order which can lead to serious
1220
-	 * bugs in your code.
1221
-	 *
1222
-	 * Example:
1223
-	 * <code>
1224
-	 *  $qb = $conn->getQueryBuilder();
1225
-	 *  $qb->select('u.*')
1226
-	 *     ->from('users', 'u')
1227
-	 *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR))
1228
-	 *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR))
1229
-	 * </code>
1230
-	 *
1231
-	 * @param mixed $value
1232
-	 * @param integer $type
1233
-	 *
1234
-	 * @return IParameter
1235
-	 */
1236
-	public function createPositionalParameter($value, $type = IQueryBuilder::PARAM_STR) {
1237
-		return new Parameter($this->queryBuilder->createPositionalParameter($value, $type));
1238
-	}
1239
-
1240
-	/**
1241
-	 * Creates a new parameter
1242
-	 *
1243
-	 * Example:
1244
-	 * <code>
1245
-	 *  $qb = $conn->getQueryBuilder();
1246
-	 *  $qb->select('u.*')
1247
-	 *     ->from('users', 'u')
1248
-	 *     ->where('u.username = ' . $qb->createParameter('name'))
1249
-	 *     ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR))
1250
-	 * </code>
1251
-	 *
1252
-	 * @param string $name
1253
-	 *
1254
-	 * @return IParameter
1255
-	 */
1256
-	public function createParameter($name) {
1257
-		return new Parameter(':' . $name);
1258
-	}
1259
-
1260
-	/**
1261
-	 * Creates a new function
1262
-	 *
1263
-	 * Attention: Column names inside the call have to be quoted before hand
1264
-	 *
1265
-	 * Example:
1266
-	 * <code>
1267
-	 *  $qb = $conn->getQueryBuilder();
1268
-	 *  $qb->select($qb->createFunction('COUNT(*)'))
1269
-	 *     ->from('users', 'u')
1270
-	 *  echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u
1271
-	 * </code>
1272
-	 * <code>
1273
-	 *  $qb = $conn->getQueryBuilder();
1274
-	 *  $qb->select($qb->createFunction('COUNT(`column`)'))
1275
-	 *     ->from('users', 'u')
1276
-	 *  echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u
1277
-	 * </code>
1278
-	 *
1279
-	 * @param string $call
1280
-	 *
1281
-	 * @return IQueryFunction
1282
-	 */
1283
-	public function createFunction($call) {
1284
-		return new QueryFunction($call);
1285
-	}
1286
-
1287
-	/**
1288
-	 * Used to get the id of the last inserted element
1289
-	 * @return int
1290
-	 * @throws \BadMethodCallException When being called before an insert query has been run.
1291
-	 */
1292
-	public function getLastInsertId(): int {
1293
-		if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT && $this->lastInsertedTable) {
1294
-			// lastInsertId() needs the prefix but no quotes
1295
-			$table = $this->prefixTableName($this->lastInsertedTable);
1296
-			return $this->connection->lastInsertId($table);
1297
-		}
1298
-
1299
-		throw new \BadMethodCallException('Invalid call to getLastInsertId without using insert() before.');
1300
-	}
1301
-
1302
-	/**
1303
-	 * Returns the table name quoted and with database prefix as needed by the implementation
1304
-	 *
1305
-	 * @param string|IQueryFunction $table
1306
-	 * @return string
1307
-	 */
1308
-	public function getTableName($table) {
1309
-		if ($table instanceof IQueryFunction) {
1310
-			return (string) $table;
1311
-		}
1312
-
1313
-		$table = $this->prefixTableName($table);
1314
-		return $this->helper->quoteColumnName($table);
1315
-	}
1316
-
1317
-	/**
1318
-	 * Returns the table name with database prefix as needed by the implementation
1319
-	 *
1320
-	 * @param string $table
1321
-	 * @return string
1322
-	 */
1323
-	protected function prefixTableName($table) {
1324
-		if ($this->automaticTablePrefix === false || str_starts_with($table, '*PREFIX*')) {
1325
-			return $table;
1326
-		}
1327
-
1328
-		return '*PREFIX*' . $table;
1329
-	}
1330
-
1331
-	/**
1332
-	 * Returns the column name quoted and with table alias prefix as needed by the implementation
1333
-	 *
1334
-	 * @param string $column
1335
-	 * @param string $tableAlias
1336
-	 * @return string
1337
-	 */
1338
-	public function getColumnName($column, $tableAlias = '') {
1339
-		if ($tableAlias !== '') {
1340
-			$tableAlias .= '.';
1341
-		}
1342
-
1343
-		return $this->helper->quoteColumnName($tableAlias . $column);
1344
-	}
1345
-
1346
-	/**
1347
-	 * Returns the column name quoted and with table alias prefix as needed by the implementation
1348
-	 *
1349
-	 * @param string $alias
1350
-	 * @return string
1351
-	 */
1352
-	public function quoteAlias($alias) {
1353
-		if ($alias === '' || $alias === null) {
1354
-			return $alias;
1355
-		}
1356
-
1357
-		return $this->helper->quoteColumnName($alias);
1358
-	}
59
+    /** @var ConnectionAdapter */
60
+    private $connection;
61
+
62
+    /** @var SystemConfig */
63
+    private $systemConfig;
64
+
65
+    private LoggerInterface $logger;
66
+
67
+    /** @var \Doctrine\DBAL\Query\QueryBuilder */
68
+    private $queryBuilder;
69
+
70
+    /** @var QuoteHelper */
71
+    private $helper;
72
+
73
+    /** @var bool */
74
+    private $automaticTablePrefix = true;
75
+
76
+    /** @var string */
77
+    protected $lastInsertedTable;
78
+
79
+    /**
80
+     * Initializes a new QueryBuilder.
81
+     *
82
+     * @param ConnectionAdapter $connection
83
+     * @param SystemConfig $systemConfig
84
+     */
85
+    public function __construct(ConnectionAdapter $connection, SystemConfig $systemConfig, LoggerInterface $logger) {
86
+        $this->connection = $connection;
87
+        $this->systemConfig = $systemConfig;
88
+        $this->logger = $logger;
89
+        $this->queryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection->getInner());
90
+        $this->helper = new QuoteHelper();
91
+    }
92
+
93
+    /**
94
+     * Enable/disable automatic prefixing of table names with the oc_ prefix
95
+     *
96
+     * @param bool $enabled If set to true table names will be prefixed with the
97
+     * owncloud database prefix automatically.
98
+     * @since 8.2.0
99
+     */
100
+    public function automaticTablePrefix($enabled) {
101
+        $this->automaticTablePrefix = (bool) $enabled;
102
+    }
103
+
104
+    /**
105
+     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
106
+     * This producer method is intended for convenient inline usage. Example:
107
+     *
108
+     * <code>
109
+     *     $qb = $conn->getQueryBuilder()
110
+     *         ->select('u')
111
+     *         ->from('users', 'u')
112
+     *         ->where($qb->expr()->eq('u.id', 1));
113
+     * </code>
114
+     *
115
+     * For more complex expression construction, consider storing the expression
116
+     * builder object in a local variable.
117
+     *
118
+     * @return \OCP\DB\QueryBuilder\IExpressionBuilder
119
+     */
120
+    public function expr() {
121
+        if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
122
+            return new OCIExpressionBuilder($this->connection, $this);
123
+        }
124
+        if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
125
+            return new PgSqlExpressionBuilder($this->connection, $this);
126
+        }
127
+        if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
128
+            return new MySqlExpressionBuilder($this->connection, $this);
129
+        }
130
+        if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
131
+            return new SqliteExpressionBuilder($this->connection, $this);
132
+        }
133
+
134
+        return new ExpressionBuilder($this->connection, $this);
135
+    }
136
+
137
+    /**
138
+     * Gets an FunctionBuilder used for object-oriented construction of query functions.
139
+     * This producer method is intended for convenient inline usage. Example:
140
+     *
141
+     * <code>
142
+     *     $qb = $conn->getQueryBuilder()
143
+     *         ->select('u')
144
+     *         ->from('users', 'u')
145
+     *         ->where($qb->fun()->md5('u.id'));
146
+     * </code>
147
+     *
148
+     * For more complex function construction, consider storing the function
149
+     * builder object in a local variable.
150
+     *
151
+     * @return \OCP\DB\QueryBuilder\IFunctionBuilder
152
+     */
153
+    public function func() {
154
+        if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
155
+            return new OCIFunctionBuilder($this->connection, $this, $this->helper);
156
+        }
157
+        if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) {
158
+            return new SqliteFunctionBuilder($this->connection, $this, $this->helper);
159
+        }
160
+        if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
161
+            return new PgSqlFunctionBuilder($this->connection, $this, $this->helper);
162
+        }
163
+
164
+        return new FunctionBuilder($this->connection, $this, $this->helper);
165
+    }
166
+
167
+    /**
168
+     * Gets the type of the currently built query.
169
+     *
170
+     * @return integer
171
+     */
172
+    public function getType() {
173
+        return $this->queryBuilder->getType();
174
+    }
175
+
176
+    /**
177
+     * Gets the associated DBAL Connection for this query builder.
178
+     *
179
+     * @return \OCP\IDBConnection
180
+     */
181
+    public function getConnection() {
182
+        return $this->connection;
183
+    }
184
+
185
+    /**
186
+     * Gets the state of this query builder instance.
187
+     *
188
+     * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
189
+     */
190
+    public function getState() {
191
+        return $this->queryBuilder->getState();
192
+    }
193
+
194
+    /**
195
+     * Executes this query using the bound parameters and their types.
196
+     *
197
+     * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate}
198
+     * for insert, update and delete statements.
199
+     *
200
+     * @return IResult|int
201
+     */
202
+    public function execute() {
203
+        if ($this->systemConfig->getValue('log_query', false)) {
204
+            try {
205
+                $params = [];
206
+                foreach ($this->getParameters() as $placeholder => $value) {
207
+                    if ($value instanceof \DateTime) {
208
+                        $params[] = $placeholder . ' => DateTime:\'' . $value->format('c') . '\'';
209
+                    } elseif (is_array($value)) {
210
+                        $params[] = $placeholder . ' => (\'' . implode('\', \'', $value) . '\')';
211
+                    } else {
212
+                        $params[] = $placeholder . ' => \'' . $value . '\'';
213
+                    }
214
+                }
215
+                if (empty($params)) {
216
+                    $this->logger->debug('DB QueryBuilder: \'{query}\'', [
217
+                        'query' => $this->getSQL(),
218
+                        'app' => 'core',
219
+                    ]);
220
+                } else {
221
+                    $this->logger->debug('DB QueryBuilder: \'{query}\' with parameters: {params}', [
222
+                        'query' => $this->getSQL(),
223
+                        'params' => implode(', ', $params),
224
+                        'app' => 'core',
225
+                    ]);
226
+                }
227
+            } catch (\Error $e) {
228
+                // likely an error during conversion of $value to string
229
+                $this->logger->error('DB QueryBuilder: error trying to log SQL query', ['exception' => $e]);
230
+            }
231
+        }
232
+
233
+        if (!empty($this->getQueryPart('select'))) {
234
+            $select = $this->getQueryPart('select');
235
+            $hasSelectAll = array_filter($select, static function ($s) {
236
+                return $s === '*';
237
+            });
238
+            $hasSelectSpecific = array_filter($select, static function ($s) {
239
+                return $s !== '*';
240
+            });
241
+
242
+            if (empty($hasSelectAll) === empty($hasSelectSpecific)) {
243
+                $exception = new QueryException('Query is selecting * and specific values in the same query. This is not supported in Oracle.');
244
+                $this->logger->error($exception->getMessage(), [
245
+                    'query' => $this->getSQL(),
246
+                    'app' => 'core',
247
+                    'exception' => $exception,
248
+                ]);
249
+            }
250
+        }
251
+
252
+        $numberOfParameters = 0;
253
+        $hasTooLargeArrayParameter = false;
254
+        foreach ($this->getParameters() as $parameter) {
255
+            if (is_array($parameter)) {
256
+                $count = count($parameter);
257
+                $numberOfParameters += $count;
258
+                $hasTooLargeArrayParameter = $hasTooLargeArrayParameter || ($count > 1000);
259
+            }
260
+        }
261
+
262
+        if ($hasTooLargeArrayParameter) {
263
+            $exception = new QueryException('More than 1000 expressions in a list are not allowed on Oracle.');
264
+            $this->logger->error($exception->getMessage(), [
265
+                'query' => $this->getSQL(),
266
+                'app' => 'core',
267
+                'exception' => $exception,
268
+            ]);
269
+        }
270
+
271
+        if ($numberOfParameters > 65535) {
272
+            $exception = new QueryException('The number of parameters must not exceed 65535. Restriction by PostgreSQL.');
273
+            $this->logger->error($exception->getMessage(), [
274
+                'query' => $this->getSQL(),
275
+                'app' => 'core',
276
+                'exception' => $exception,
277
+            ]);
278
+        }
279
+
280
+        $result = $this->queryBuilder->execute();
281
+        if (is_int($result)) {
282
+            return $result;
283
+        }
284
+        return new ResultAdapter($result);
285
+    }
286
+
287
+    public function executeQuery(): IResult {
288
+        if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
289
+            throw new \RuntimeException('Invalid query type, expected SELECT query');
290
+        }
291
+
292
+        try {
293
+            $result = $this->execute();
294
+        } catch (\Doctrine\DBAL\Exception $e) {
295
+            throw \OC\DB\Exceptions\DbalException::wrap($e);
296
+        }
297
+
298
+        if ($result instanceof IResult) {
299
+            return $result;
300
+        }
301
+
302
+        throw new \RuntimeException('Invalid return type for query');
303
+    }
304
+
305
+    /**
306
+     * Monkey-patched compatibility layer for apps that were adapted for Nextcloud 22 before
307
+     * the first beta, where executeStatement was named executeUpdate.
308
+     *
309
+     * Static analysis should catch those misuses, but until then let's try to keep things
310
+     * running.
311
+     *
312
+     * @internal
313
+     * @deprecated
314
+     * @todo drop ASAP
315
+     */
316
+    public function executeUpdate(): int {
317
+        return $this->executeStatement();
318
+    }
319
+
320
+    public function executeStatement(): int {
321
+        if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
322
+            throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
323
+        }
324
+
325
+        try {
326
+            $result = $this->execute();
327
+        } catch (\Doctrine\DBAL\Exception $e) {
328
+            throw \OC\DB\Exceptions\DbalException::wrap($e);
329
+        }
330
+
331
+        if (!is_int($result)) {
332
+            throw new \RuntimeException('Invalid return type for statement');
333
+        }
334
+
335
+        return $result;
336
+    }
337
+
338
+
339
+    /**
340
+     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
341
+     *
342
+     * <code>
343
+     *     $qb = $conn->getQueryBuilder()
344
+     *         ->select('u')
345
+     *         ->from('User', 'u')
346
+     *     echo $qb->getSQL(); // SELECT u FROM User u
347
+     * </code>
348
+     *
349
+     * @return string The SQL query string.
350
+     */
351
+    public function getSQL() {
352
+        return $this->queryBuilder->getSQL();
353
+    }
354
+
355
+    /**
356
+     * Sets a query parameter for the query being constructed.
357
+     *
358
+     * <code>
359
+     *     $qb = $conn->getQueryBuilder()
360
+     *         ->select('u')
361
+     *         ->from('users', 'u')
362
+     *         ->where('u.id = :user_id')
363
+     *         ->setParameter(':user_id', 1);
364
+     * </code>
365
+     *
366
+     * @param string|integer $key The parameter position or name.
367
+     * @param mixed $value The parameter value.
368
+     * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants.
369
+     *
370
+     * @return $this This QueryBuilder instance.
371
+     */
372
+    public function setParameter($key, $value, $type = null) {
373
+        $this->queryBuilder->setParameter($key, $value, $type);
374
+
375
+        return $this;
376
+    }
377
+
378
+    /**
379
+     * Sets a collection of query parameters for the query being constructed.
380
+     *
381
+     * <code>
382
+     *     $qb = $conn->getQueryBuilder()
383
+     *         ->select('u')
384
+     *         ->from('users', 'u')
385
+     *         ->where('u.id = :user_id1 OR u.id = :user_id2')
386
+     *         ->setParameters(array(
387
+     *             ':user_id1' => 1,
388
+     *             ':user_id2' => 2
389
+     *         ));
390
+     * </code>
391
+     *
392
+     * @param array $params The query parameters to set.
393
+     * @param array $types The query parameters types to set.
394
+     *
395
+     * @return $this This QueryBuilder instance.
396
+     */
397
+    public function setParameters(array $params, array $types = []) {
398
+        $this->queryBuilder->setParameters($params, $types);
399
+
400
+        return $this;
401
+    }
402
+
403
+    /**
404
+     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
405
+     *
406
+     * @return array The currently defined query parameters indexed by parameter index or name.
407
+     */
408
+    public function getParameters() {
409
+        return $this->queryBuilder->getParameters();
410
+    }
411
+
412
+    /**
413
+     * Gets a (previously set) query parameter of the query being constructed.
414
+     *
415
+     * @param mixed $key The key (index or name) of the bound parameter.
416
+     *
417
+     * @return mixed The value of the bound parameter.
418
+     */
419
+    public function getParameter($key) {
420
+        return $this->queryBuilder->getParameter($key);
421
+    }
422
+
423
+    /**
424
+     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
425
+     *
426
+     * @return array The currently defined query parameter types indexed by parameter index or name.
427
+     */
428
+    public function getParameterTypes() {
429
+        return $this->queryBuilder->getParameterTypes();
430
+    }
431
+
432
+    /**
433
+     * Gets a (previously set) query parameter type of the query being constructed.
434
+     *
435
+     * @param mixed $key The key (index or name) of the bound parameter type.
436
+     *
437
+     * @return mixed The value of the bound parameter type.
438
+     */
439
+    public function getParameterType($key) {
440
+        return $this->queryBuilder->getParameterType($key);
441
+    }
442
+
443
+    /**
444
+     * Sets the position of the first result to retrieve (the "offset").
445
+     *
446
+     * @param int $firstResult The first result to return.
447
+     *
448
+     * @return $this This QueryBuilder instance.
449
+     */
450
+    public function setFirstResult($firstResult) {
451
+        $this->queryBuilder->setFirstResult((int) $firstResult);
452
+
453
+        return $this;
454
+    }
455
+
456
+    /**
457
+     * Gets the position of the first result the query object was set to retrieve (the "offset").
458
+     * Returns 0 if {@link setFirstResult} was not applied to this QueryBuilder.
459
+     *
460
+     * @return int The position of the first result.
461
+     */
462
+    public function getFirstResult() {
463
+        return $this->queryBuilder->getFirstResult();
464
+    }
465
+
466
+    /**
467
+     * Sets the maximum number of results to retrieve (the "limit").
468
+     *
469
+     * NOTE: Setting max results to "0" will cause mixed behaviour. While most
470
+     * of the databases will just return an empty result set, Oracle will return
471
+     * all entries.
472
+     *
473
+     * @param int|null $maxResults The maximum number of results to retrieve.
474
+     *
475
+     * @return $this This QueryBuilder instance.
476
+     */
477
+    public function setMaxResults($maxResults) {
478
+        if ($maxResults === null) {
479
+            $this->queryBuilder->setMaxResults($maxResults);
480
+        } else {
481
+            $this->queryBuilder->setMaxResults((int) $maxResults);
482
+        }
483
+
484
+        return $this;
485
+    }
486
+
487
+    /**
488
+     * Gets the maximum number of results the query object was set to retrieve (the "limit").
489
+     * Returns NULL if {@link setMaxResults} was not applied to this query builder.
490
+     *
491
+     * @return int|null The maximum number of results.
492
+     */
493
+    public function getMaxResults() {
494
+        return $this->queryBuilder->getMaxResults();
495
+    }
496
+
497
+    /**
498
+     * Specifies an item that is to be returned in the query result.
499
+     * Replaces any previously specified selections, if any.
500
+     *
501
+     * <code>
502
+     *     $qb = $conn->getQueryBuilder()
503
+     *         ->select('u.id', 'p.id')
504
+     *         ->from('users', 'u')
505
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
506
+     * </code>
507
+     *
508
+     * @param mixed ...$selects The selection expressions.
509
+     *
510
+     * '@return $this This QueryBuilder instance.
511
+     */
512
+    public function select(...$selects) {
513
+        if (count($selects) === 1 && is_array($selects[0])) {
514
+            $selects = $selects[0];
515
+        }
516
+
517
+        $this->queryBuilder->select(
518
+            $this->helper->quoteColumnNames($selects)
519
+        );
520
+
521
+        return $this;
522
+    }
523
+
524
+    /**
525
+     * Specifies an item that is to be returned with a different name in the query result.
526
+     *
527
+     * <code>
528
+     *     $qb = $conn->getQueryBuilder()
529
+     *         ->selectAlias('u.id', 'user_id')
530
+     *         ->from('users', 'u')
531
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id');
532
+     * </code>
533
+     *
534
+     * @param mixed $select The selection expressions.
535
+     * @param string $alias The column alias used in the constructed query.
536
+     *
537
+     * @return $this This QueryBuilder instance.
538
+     */
539
+    public function selectAlias($select, $alias) {
540
+        $this->queryBuilder->addSelect(
541
+            $this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias)
542
+        );
543
+
544
+        return $this;
545
+    }
546
+
547
+    /**
548
+     * Specifies an item that is to be returned uniquely in the query result.
549
+     *
550
+     * <code>
551
+     *     $qb = $conn->getQueryBuilder()
552
+     *         ->selectDistinct('type')
553
+     *         ->from('users');
554
+     * </code>
555
+     *
556
+     * @param mixed $select The selection expressions.
557
+     *
558
+     * @return $this This QueryBuilder instance.
559
+     */
560
+    public function selectDistinct($select) {
561
+        if (!is_array($select)) {
562
+            $select = [$select];
563
+        }
564
+
565
+        $quotedSelect = $this->helper->quoteColumnNames($select);
566
+
567
+        $this->queryBuilder->addSelect(
568
+            'DISTINCT ' . implode(', ', $quotedSelect)
569
+        );
570
+
571
+        return $this;
572
+    }
573
+
574
+    /**
575
+     * Adds an item that is to be returned in the query result.
576
+     *
577
+     * <code>
578
+     *     $qb = $conn->getQueryBuilder()
579
+     *         ->select('u.id')
580
+     *         ->addSelect('p.id')
581
+     *         ->from('users', 'u')
582
+     *         ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
583
+     * </code>
584
+     *
585
+     * @param mixed ...$selects The selection expression.
586
+     *
587
+     * @return $this This QueryBuilder instance.
588
+     */
589
+    public function addSelect(...$selects) {
590
+        if (count($selects) === 1 && is_array($selects[0])) {
591
+            $selects = $selects[0];
592
+        }
593
+
594
+        $this->queryBuilder->addSelect(
595
+            $this->helper->quoteColumnNames($selects)
596
+        );
597
+
598
+        return $this;
599
+    }
600
+
601
+    /**
602
+     * Turns the query being built into a bulk delete query that ranges over
603
+     * a certain table.
604
+     *
605
+     * <code>
606
+     *     $qb = $conn->getQueryBuilder()
607
+     *         ->delete('users', 'u')
608
+     *         ->where('u.id = :user_id');
609
+     *         ->setParameter(':user_id', 1);
610
+     * </code>
611
+     *
612
+     * @param string $delete The table whose rows are subject to the deletion.
613
+     * @param string $alias The table alias used in the constructed query.
614
+     *
615
+     * @return $this This QueryBuilder instance.
616
+     */
617
+    public function delete($delete = null, $alias = null) {
618
+        $this->queryBuilder->delete(
619
+            $this->getTableName($delete),
620
+            $alias
621
+        );
622
+
623
+        return $this;
624
+    }
625
+
626
+    /**
627
+     * Turns the query being built into a bulk update query that ranges over
628
+     * a certain table
629
+     *
630
+     * <code>
631
+     *     $qb = $conn->getQueryBuilder()
632
+     *         ->update('users', 'u')
633
+     *         ->set('u.password', md5('password'))
634
+     *         ->where('u.id = ?');
635
+     * </code>
636
+     *
637
+     * @param string $update The table whose rows are subject to the update.
638
+     * @param string $alias The table alias used in the constructed query.
639
+     *
640
+     * @return $this This QueryBuilder instance.
641
+     */
642
+    public function update($update = null, $alias = null) {
643
+        $this->queryBuilder->update(
644
+            $this->getTableName($update),
645
+            $alias
646
+        );
647
+
648
+        return $this;
649
+    }
650
+
651
+    /**
652
+     * Turns the query being built into an insert query that inserts into
653
+     * a certain table
654
+     *
655
+     * <code>
656
+     *     $qb = $conn->getQueryBuilder()
657
+     *         ->insert('users')
658
+     *         ->values(
659
+     *             array(
660
+     *                 'name' => '?',
661
+     *                 'password' => '?'
662
+     *             )
663
+     *         );
664
+     * </code>
665
+     *
666
+     * @param string $insert The table into which the rows should be inserted.
667
+     *
668
+     * @return $this This QueryBuilder instance.
669
+     */
670
+    public function insert($insert = null) {
671
+        $this->queryBuilder->insert(
672
+            $this->getTableName($insert)
673
+        );
674
+
675
+        $this->lastInsertedTable = $insert;
676
+
677
+        return $this;
678
+    }
679
+
680
+    /**
681
+     * Creates and adds a query root corresponding to the table identified by the
682
+     * given alias, forming a cartesian product with any existing query roots.
683
+     *
684
+     * <code>
685
+     *     $qb = $conn->getQueryBuilder()
686
+     *         ->select('u.id')
687
+     *         ->from('users', 'u')
688
+     * </code>
689
+     *
690
+     * @param string|IQueryFunction $from The table.
691
+     * @param string|null $alias The alias of the table.
692
+     *
693
+     * @return $this This QueryBuilder instance.
694
+     */
695
+    public function from($from, $alias = null) {
696
+        $this->queryBuilder->from(
697
+            $this->getTableName($from),
698
+            $this->quoteAlias($alias)
699
+        );
700
+
701
+        return $this;
702
+    }
703
+
704
+    /**
705
+     * Creates and adds a join to the query.
706
+     *
707
+     * <code>
708
+     *     $qb = $conn->getQueryBuilder()
709
+     *         ->select('u.name')
710
+     *         ->from('users', 'u')
711
+     *         ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1');
712
+     * </code>
713
+     *
714
+     * @param string $fromAlias The alias that points to a from clause.
715
+     * @param string $join The table name to join.
716
+     * @param string $alias The alias of the join table.
717
+     * @param string|ICompositeExpression|null $condition The condition for the join.
718
+     *
719
+     * @return $this This QueryBuilder instance.
720
+     */
721
+    public function join($fromAlias, $join, $alias, $condition = null) {
722
+        $this->queryBuilder->join(
723
+            $this->quoteAlias($fromAlias),
724
+            $this->getTableName($join),
725
+            $this->quoteAlias($alias),
726
+            $condition
727
+        );
728
+
729
+        return $this;
730
+    }
731
+
732
+    /**
733
+     * Creates and adds a join to the query.
734
+     *
735
+     * <code>
736
+     *     $qb = $conn->getQueryBuilder()
737
+     *         ->select('u.name')
738
+     *         ->from('users', 'u')
739
+     *         ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
740
+     * </code>
741
+     *
742
+     * @param string $fromAlias The alias that points to a from clause.
743
+     * @param string $join The table name to join.
744
+     * @param string $alias The alias of the join table.
745
+     * @param string|ICompositeExpression|null $condition The condition for the join.
746
+     *
747
+     * @return $this This QueryBuilder instance.
748
+     */
749
+    public function innerJoin($fromAlias, $join, $alias, $condition = null) {
750
+        $this->queryBuilder->innerJoin(
751
+            $this->quoteAlias($fromAlias),
752
+            $this->getTableName($join),
753
+            $this->quoteAlias($alias),
754
+            $condition
755
+        );
756
+
757
+        return $this;
758
+    }
759
+
760
+    /**
761
+     * Creates and adds a left join to the query.
762
+     *
763
+     * <code>
764
+     *     $qb = $conn->getQueryBuilder()
765
+     *         ->select('u.name')
766
+     *         ->from('users', 'u')
767
+     *         ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
768
+     * </code>
769
+     *
770
+     * @param string $fromAlias The alias that points to a from clause.
771
+     * @param string $join The table name to join.
772
+     * @param string $alias The alias of the join table.
773
+     * @param string|ICompositeExpression|null $condition The condition for the join.
774
+     *
775
+     * @return $this This QueryBuilder instance.
776
+     */
777
+    public function leftJoin($fromAlias, $join, $alias, $condition = null) {
778
+        $this->queryBuilder->leftJoin(
779
+            $this->quoteAlias($fromAlias),
780
+            $this->getTableName($join),
781
+            $this->quoteAlias($alias),
782
+            $condition
783
+        );
784
+
785
+        return $this;
786
+    }
787
+
788
+    /**
789
+     * Creates and adds a right join to the query.
790
+     *
791
+     * <code>
792
+     *     $qb = $conn->getQueryBuilder()
793
+     *         ->select('u.name')
794
+     *         ->from('users', 'u')
795
+     *         ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1');
796
+     * </code>
797
+     *
798
+     * @param string $fromAlias The alias that points to a from clause.
799
+     * @param string $join The table name to join.
800
+     * @param string $alias The alias of the join table.
801
+     * @param string|ICompositeExpression|null $condition The condition for the join.
802
+     *
803
+     * @return $this This QueryBuilder instance.
804
+     */
805
+    public function rightJoin($fromAlias, $join, $alias, $condition = null) {
806
+        $this->queryBuilder->rightJoin(
807
+            $this->quoteAlias($fromAlias),
808
+            $this->getTableName($join),
809
+            $this->quoteAlias($alias),
810
+            $condition
811
+        );
812
+
813
+        return $this;
814
+    }
815
+
816
+    /**
817
+     * Sets a new value for a column in a bulk update query.
818
+     *
819
+     * <code>
820
+     *     $qb = $conn->getQueryBuilder()
821
+     *         ->update('users', 'u')
822
+     *         ->set('u.password', md5('password'))
823
+     *         ->where('u.id = ?');
824
+     * </code>
825
+     *
826
+     * @param string $key The column to set.
827
+     * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc.
828
+     *
829
+     * @return $this This QueryBuilder instance.
830
+     */
831
+    public function set($key, $value) {
832
+        $this->queryBuilder->set(
833
+            $this->helper->quoteColumnName($key),
834
+            $this->helper->quoteColumnName($value)
835
+        );
836
+
837
+        return $this;
838
+    }
839
+
840
+    /**
841
+     * Specifies one or more restrictions to the query result.
842
+     * Replaces any previously specified restrictions, if any.
843
+     *
844
+     * <code>
845
+     *     $qb = $conn->getQueryBuilder()
846
+     *         ->select('u.name')
847
+     *         ->from('users', 'u')
848
+     *         ->where('u.id = ?');
849
+     *
850
+     *     // You can optionally programmatically build and/or expressions
851
+     *     $qb = $conn->getQueryBuilder();
852
+     *
853
+     *     $or = $qb->expr()->orx();
854
+     *     $or->add($qb->expr()->eq('u.id', 1));
855
+     *     $or->add($qb->expr()->eq('u.id', 2));
856
+     *
857
+     *     $qb->update('users', 'u')
858
+     *         ->set('u.password', md5('password'))
859
+     *         ->where($or);
860
+     * </code>
861
+     *
862
+     * @param mixed ...$predicates The restriction predicates.
863
+     *
864
+     * @return $this This QueryBuilder instance.
865
+     */
866
+    public function where(...$predicates) {
867
+        if ($this->getQueryPart('where') !== null && $this->systemConfig->getValue('debug', false)) {
868
+            // Only logging a warning, not throwing for now.
869
+            $e = new QueryException('Using where() on non-empty WHERE part, please verify it is intentional to not call whereAnd() or whereOr() instead. Otherwise consider creating a new query builder object or call resetQueryPart(\'where\') first.');
870
+            $this->logger->warning($e->getMessage(), ['exception' => $e]);
871
+        }
872
+
873
+        call_user_func_array(
874
+            [$this->queryBuilder, 'where'],
875
+            $predicates
876
+        );
877
+
878
+        return $this;
879
+    }
880
+
881
+    /**
882
+     * Adds one or more restrictions to the query results, forming a logical
883
+     * conjunction with any previously specified restrictions.
884
+     *
885
+     * <code>
886
+     *     $qb = $conn->getQueryBuilder()
887
+     *         ->select('u')
888
+     *         ->from('users', 'u')
889
+     *         ->where('u.username LIKE ?')
890
+     *         ->andWhere('u.is_active = 1');
891
+     * </code>
892
+     *
893
+     * @param mixed ...$where The query restrictions.
894
+     *
895
+     * @return $this This QueryBuilder instance.
896
+     *
897
+     * @see where()
898
+     */
899
+    public function andWhere(...$where) {
900
+        call_user_func_array(
901
+            [$this->queryBuilder, 'andWhere'],
902
+            $where
903
+        );
904
+
905
+        return $this;
906
+    }
907
+
908
+    /**
909
+     * Adds one or more restrictions to the query results, forming a logical
910
+     * disjunction with any previously specified restrictions.
911
+     *
912
+     * <code>
913
+     *     $qb = $conn->getQueryBuilder()
914
+     *         ->select('u.name')
915
+     *         ->from('users', 'u')
916
+     *         ->where('u.id = 1')
917
+     *         ->orWhere('u.id = 2');
918
+     * </code>
919
+     *
920
+     * @param mixed ...$where The WHERE statement.
921
+     *
922
+     * @return $this This QueryBuilder instance.
923
+     *
924
+     * @see where()
925
+     */
926
+    public function orWhere(...$where) {
927
+        call_user_func_array(
928
+            [$this->queryBuilder, 'orWhere'],
929
+            $where
930
+        );
931
+
932
+        return $this;
933
+    }
934
+
935
+    /**
936
+     * Specifies a grouping over the results of the query.
937
+     * Replaces any previously specified groupings, if any.
938
+     *
939
+     * <code>
940
+     *     $qb = $conn->getQueryBuilder()
941
+     *         ->select('u.name')
942
+     *         ->from('users', 'u')
943
+     *         ->groupBy('u.id');
944
+     * </code>
945
+     *
946
+     * @param mixed ...$groupBys The grouping expression.
947
+     *
948
+     * @return $this This QueryBuilder instance.
949
+     */
950
+    public function groupBy(...$groupBys) {
951
+        if (count($groupBys) === 1 && is_array($groupBys[0])) {
952
+            $groupBys = $groupBys[0];
953
+        }
954
+
955
+        call_user_func_array(
956
+            [$this->queryBuilder, 'groupBy'],
957
+            $this->helper->quoteColumnNames($groupBys)
958
+        );
959
+
960
+        return $this;
961
+    }
962
+
963
+    /**
964
+     * Adds a grouping expression to the query.
965
+     *
966
+     * <code>
967
+     *     $qb = $conn->getQueryBuilder()
968
+     *         ->select('u.name')
969
+     *         ->from('users', 'u')
970
+     *         ->groupBy('u.lastLogin');
971
+     *         ->addGroupBy('u.createdAt')
972
+     * </code>
973
+     *
974
+     * @param mixed ...$groupBy The grouping expression.
975
+     *
976
+     * @return $this This QueryBuilder instance.
977
+     */
978
+    public function addGroupBy(...$groupBys) {
979
+        if (count($groupBys) === 1 && is_array($groupBys[0])) {
980
+            $$groupBys = $groupBys[0];
981
+        }
982
+
983
+        call_user_func_array(
984
+            [$this->queryBuilder, 'addGroupBy'],
985
+            $this->helper->quoteColumnNames($groupBys)
986
+        );
987
+
988
+        return $this;
989
+    }
990
+
991
+    /**
992
+     * Sets a value for a column in an insert query.
993
+     *
994
+     * <code>
995
+     *     $qb = $conn->getQueryBuilder()
996
+     *         ->insert('users')
997
+     *         ->values(
998
+     *             array(
999
+     *                 'name' => '?'
1000
+     *             )
1001
+     *         )
1002
+     *         ->setValue('password', '?');
1003
+     * </code>
1004
+     *
1005
+     * @param string $column The column into which the value should be inserted.
1006
+     * @param IParameter|string $value The value that should be inserted into the column.
1007
+     *
1008
+     * @return $this This QueryBuilder instance.
1009
+     */
1010
+    public function setValue($column, $value) {
1011
+        $this->queryBuilder->setValue(
1012
+            $this->helper->quoteColumnName($column),
1013
+            (string) $value
1014
+        );
1015
+
1016
+        return $this;
1017
+    }
1018
+
1019
+    /**
1020
+     * Specifies values for an insert query indexed by column names.
1021
+     * Replaces any previous values, if any.
1022
+     *
1023
+     * <code>
1024
+     *     $qb = $conn->getQueryBuilder()
1025
+     *         ->insert('users')
1026
+     *         ->values(
1027
+     *             array(
1028
+     *                 'name' => '?',
1029
+     *                 'password' => '?'
1030
+     *             )
1031
+     *         );
1032
+     * </code>
1033
+     *
1034
+     * @param array $values The values to specify for the insert query indexed by column names.
1035
+     *
1036
+     * @return $this This QueryBuilder instance.
1037
+     */
1038
+    public function values(array $values) {
1039
+        $quotedValues = [];
1040
+        foreach ($values as $key => $value) {
1041
+            $quotedValues[$this->helper->quoteColumnName($key)] = $value;
1042
+        }
1043
+
1044
+        $this->queryBuilder->values($quotedValues);
1045
+
1046
+        return $this;
1047
+    }
1048
+
1049
+    /**
1050
+     * Specifies a restriction over the groups of the query.
1051
+     * Replaces any previous having restrictions, if any.
1052
+     *
1053
+     * @param mixed ...$having The restriction over the groups.
1054
+     *
1055
+     * @return $this This QueryBuilder instance.
1056
+     */
1057
+    public function having(...$having) {
1058
+        call_user_func_array(
1059
+            [$this->queryBuilder, 'having'],
1060
+            $having
1061
+        );
1062
+
1063
+        return $this;
1064
+    }
1065
+
1066
+    /**
1067
+     * Adds a restriction over the groups of the query, forming a logical
1068
+     * conjunction with any existing having restrictions.
1069
+     *
1070
+     * @param mixed ...$having The restriction to append.
1071
+     *
1072
+     * @return $this This QueryBuilder instance.
1073
+     */
1074
+    public function andHaving(...$having) {
1075
+        call_user_func_array(
1076
+            [$this->queryBuilder, 'andHaving'],
1077
+            $having
1078
+        );
1079
+
1080
+        return $this;
1081
+    }
1082
+
1083
+    /**
1084
+     * Adds a restriction over the groups of the query, forming a logical
1085
+     * disjunction with any existing having restrictions.
1086
+     *
1087
+     * @param mixed ...$having The restriction to add.
1088
+     *
1089
+     * @return $this This QueryBuilder instance.
1090
+     */
1091
+    public function orHaving(...$having) {
1092
+        call_user_func_array(
1093
+            [$this->queryBuilder, 'orHaving'],
1094
+            $having
1095
+        );
1096
+
1097
+        return $this;
1098
+    }
1099
+
1100
+    /**
1101
+     * Specifies an ordering for the query results.
1102
+     * Replaces any previously specified orderings, if any.
1103
+     *
1104
+     * @param string|IQueryFunction|ILiteral|IParameter $sort The ordering expression.
1105
+     * @param string $order The ordering direction.
1106
+     *
1107
+     * @return $this This QueryBuilder instance.
1108
+     */
1109
+    public function orderBy($sort, $order = null) {
1110
+        $this->queryBuilder->orderBy(
1111
+            $this->helper->quoteColumnName($sort),
1112
+            $order
1113
+        );
1114
+
1115
+        return $this;
1116
+    }
1117
+
1118
+    /**
1119
+     * Adds an ordering to the query results.
1120
+     *
1121
+     * @param string|ILiteral|IParameter|IQueryFunction $sort The ordering expression.
1122
+     * @param string $order The ordering direction.
1123
+     *
1124
+     * @return $this This QueryBuilder instance.
1125
+     */
1126
+    public function addOrderBy($sort, $order = null) {
1127
+        $this->queryBuilder->addOrderBy(
1128
+            $this->helper->quoteColumnName($sort),
1129
+            $order
1130
+        );
1131
+
1132
+        return $this;
1133
+    }
1134
+
1135
+    /**
1136
+     * Gets a query part by its name.
1137
+     *
1138
+     * @param string $queryPartName
1139
+     *
1140
+     * @return mixed
1141
+     */
1142
+    public function getQueryPart($queryPartName) {
1143
+        return $this->queryBuilder->getQueryPart($queryPartName);
1144
+    }
1145
+
1146
+    /**
1147
+     * Gets all query parts.
1148
+     *
1149
+     * @return array
1150
+     */
1151
+    public function getQueryParts() {
1152
+        return $this->queryBuilder->getQueryParts();
1153
+    }
1154
+
1155
+    /**
1156
+     * Resets SQL parts.
1157
+     *
1158
+     * @param array|null $queryPartNames
1159
+     *
1160
+     * @return $this This QueryBuilder instance.
1161
+     */
1162
+    public function resetQueryParts($queryPartNames = null) {
1163
+        $this->queryBuilder->resetQueryParts($queryPartNames);
1164
+
1165
+        return $this;
1166
+    }
1167
+
1168
+    /**
1169
+     * Resets a single SQL part.
1170
+     *
1171
+     * @param string $queryPartName
1172
+     *
1173
+     * @return $this This QueryBuilder instance.
1174
+     */
1175
+    public function resetQueryPart($queryPartName) {
1176
+        $this->queryBuilder->resetQueryPart($queryPartName);
1177
+
1178
+        return $this;
1179
+    }
1180
+
1181
+    /**
1182
+     * Creates a new named parameter and bind the value $value to it.
1183
+     *
1184
+     * This method provides a shortcut for PDOStatement::bindValue
1185
+     * when using prepared statements.
1186
+     *
1187
+     * The parameter $value specifies the value that you want to bind. If
1188
+     * $placeholder is not provided bindValue() will automatically create a
1189
+     * placeholder for you. An automatic placeholder will be of the name
1190
+     * ':dcValue1', ':dcValue2' etc.
1191
+     *
1192
+     * For more information see {@link https://www.php.net/pdostatement-bindparam}
1193
+     *
1194
+     * Example:
1195
+     * <code>
1196
+     * $value = 2;
1197
+     * $q->eq( 'id', $q->bindValue( $value ) );
1198
+     * $stmt = $q->executeQuery(); // executed with 'id = 2'
1199
+     * </code>
1200
+     *
1201
+     * @license New BSD License
1202
+     * @link http://www.zetacomponents.org
1203
+     *
1204
+     * @param mixed $value
1205
+     * @param mixed $type
1206
+     * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
1207
+     *
1208
+     * @return IParameter the placeholder name used.
1209
+     */
1210
+    public function createNamedParameter($value, $type = IQueryBuilder::PARAM_STR, $placeHolder = null) {
1211
+        return new Parameter($this->queryBuilder->createNamedParameter($value, $type, $placeHolder));
1212
+    }
1213
+
1214
+    /**
1215
+     * Creates a new positional parameter and bind the given value to it.
1216
+     *
1217
+     * Attention: If you are using positional parameters with the query builder you have
1218
+     * to be very careful to bind all parameters in the order they appear in the SQL
1219
+     * statement , otherwise they get bound in the wrong order which can lead to serious
1220
+     * bugs in your code.
1221
+     *
1222
+     * Example:
1223
+     * <code>
1224
+     *  $qb = $conn->getQueryBuilder();
1225
+     *  $qb->select('u.*')
1226
+     *     ->from('users', 'u')
1227
+     *     ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR))
1228
+     *     ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR))
1229
+     * </code>
1230
+     *
1231
+     * @param mixed $value
1232
+     * @param integer $type
1233
+     *
1234
+     * @return IParameter
1235
+     */
1236
+    public function createPositionalParameter($value, $type = IQueryBuilder::PARAM_STR) {
1237
+        return new Parameter($this->queryBuilder->createPositionalParameter($value, $type));
1238
+    }
1239
+
1240
+    /**
1241
+     * Creates a new parameter
1242
+     *
1243
+     * Example:
1244
+     * <code>
1245
+     *  $qb = $conn->getQueryBuilder();
1246
+     *  $qb->select('u.*')
1247
+     *     ->from('users', 'u')
1248
+     *     ->where('u.username = ' . $qb->createParameter('name'))
1249
+     *     ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR))
1250
+     * </code>
1251
+     *
1252
+     * @param string $name
1253
+     *
1254
+     * @return IParameter
1255
+     */
1256
+    public function createParameter($name) {
1257
+        return new Parameter(':' . $name);
1258
+    }
1259
+
1260
+    /**
1261
+     * Creates a new function
1262
+     *
1263
+     * Attention: Column names inside the call have to be quoted before hand
1264
+     *
1265
+     * Example:
1266
+     * <code>
1267
+     *  $qb = $conn->getQueryBuilder();
1268
+     *  $qb->select($qb->createFunction('COUNT(*)'))
1269
+     *     ->from('users', 'u')
1270
+     *  echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u
1271
+     * </code>
1272
+     * <code>
1273
+     *  $qb = $conn->getQueryBuilder();
1274
+     *  $qb->select($qb->createFunction('COUNT(`column`)'))
1275
+     *     ->from('users', 'u')
1276
+     *  echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u
1277
+     * </code>
1278
+     *
1279
+     * @param string $call
1280
+     *
1281
+     * @return IQueryFunction
1282
+     */
1283
+    public function createFunction($call) {
1284
+        return new QueryFunction($call);
1285
+    }
1286
+
1287
+    /**
1288
+     * Used to get the id of the last inserted element
1289
+     * @return int
1290
+     * @throws \BadMethodCallException When being called before an insert query has been run.
1291
+     */
1292
+    public function getLastInsertId(): int {
1293
+        if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT && $this->lastInsertedTable) {
1294
+            // lastInsertId() needs the prefix but no quotes
1295
+            $table = $this->prefixTableName($this->lastInsertedTable);
1296
+            return $this->connection->lastInsertId($table);
1297
+        }
1298
+
1299
+        throw new \BadMethodCallException('Invalid call to getLastInsertId without using insert() before.');
1300
+    }
1301
+
1302
+    /**
1303
+     * Returns the table name quoted and with database prefix as needed by the implementation
1304
+     *
1305
+     * @param string|IQueryFunction $table
1306
+     * @return string
1307
+     */
1308
+    public function getTableName($table) {
1309
+        if ($table instanceof IQueryFunction) {
1310
+            return (string) $table;
1311
+        }
1312
+
1313
+        $table = $this->prefixTableName($table);
1314
+        return $this->helper->quoteColumnName($table);
1315
+    }
1316
+
1317
+    /**
1318
+     * Returns the table name with database prefix as needed by the implementation
1319
+     *
1320
+     * @param string $table
1321
+     * @return string
1322
+     */
1323
+    protected function prefixTableName($table) {
1324
+        if ($this->automaticTablePrefix === false || str_starts_with($table, '*PREFIX*')) {
1325
+            return $table;
1326
+        }
1327
+
1328
+        return '*PREFIX*' . $table;
1329
+    }
1330
+
1331
+    /**
1332
+     * Returns the column name quoted and with table alias prefix as needed by the implementation
1333
+     *
1334
+     * @param string $column
1335
+     * @param string $tableAlias
1336
+     * @return string
1337
+     */
1338
+    public function getColumnName($column, $tableAlias = '') {
1339
+        if ($tableAlias !== '') {
1340
+            $tableAlias .= '.';
1341
+        }
1342
+
1343
+        return $this->helper->quoteColumnName($tableAlias . $column);
1344
+    }
1345
+
1346
+    /**
1347
+     * Returns the column name quoted and with table alias prefix as needed by the implementation
1348
+     *
1349
+     * @param string $alias
1350
+     * @return string
1351
+     */
1352
+    public function quoteAlias($alias) {
1353
+        if ($alias === '' || $alias === null) {
1354
+            return $alias;
1355
+        }
1356
+
1357
+        return $this->helper->quoteColumnName($alias);
1358
+    }
1359 1359
 }
Please login to merge, or discard this patch.
lib/private/DB/MySqlTools.php 1 patch
Indentation   +33 added lines, -33 removed lines patch added patch discarded remove patch
@@ -29,41 +29,41 @@
 block discarded – undo
29 29
  * Various MySQL specific helper functions.
30 30
  */
31 31
 class MySqlTools {
32
-	/**
33
-	 * @param IDBConnection $connection
34
-	 * @return bool
35
-	 */
36
-	public function supports4ByteCharset(IDBConnection $connection) {
37
-		$variables = ['innodb_file_per_table' => 'ON'];
38
-		if (!$this->isMariaDBWithLargePrefix($connection)) {
39
-			$variables['innodb_file_format'] = 'Barracuda';
40
-			$variables['innodb_large_prefix'] = 'ON';
41
-		}
32
+    /**
33
+     * @param IDBConnection $connection
34
+     * @return bool
35
+     */
36
+    public function supports4ByteCharset(IDBConnection $connection) {
37
+        $variables = ['innodb_file_per_table' => 'ON'];
38
+        if (!$this->isMariaDBWithLargePrefix($connection)) {
39
+            $variables['innodb_file_format'] = 'Barracuda';
40
+            $variables['innodb_large_prefix'] = 'ON';
41
+        }
42 42
 
43
-		foreach ($variables as $var => $val) {
44
-			$result = $connection->executeQuery("SHOW VARIABLES LIKE '$var'");
45
-			$row = $result->fetch();
46
-			$result->closeCursor();
47
-			if ($row === false) {
48
-				return false;
49
-			}
50
-			if (strcasecmp($row['Value'], $val) !== 0) {
51
-				return false;
52
-			}
53
-		}
54
-		return true;
55
-	}
43
+        foreach ($variables as $var => $val) {
44
+            $result = $connection->executeQuery("SHOW VARIABLES LIKE '$var'");
45
+            $row = $result->fetch();
46
+            $result->closeCursor();
47
+            if ($row === false) {
48
+                return false;
49
+            }
50
+            if (strcasecmp($row['Value'], $val) !== 0) {
51
+                return false;
52
+            }
53
+        }
54
+        return true;
55
+    }
56 56
 
57
-	protected function isMariaDBWithLargePrefix(IDBConnection $connection) {
58
-		$result = $connection->executeQuery('SELECT VERSION()');
59
-		$row = strtolower($result->fetchColumn());
60
-		$result->closeCursor();
57
+    protected function isMariaDBWithLargePrefix(IDBConnection $connection) {
58
+        $result = $connection->executeQuery('SELECT VERSION()');
59
+        $row = strtolower($result->fetchColumn());
60
+        $result->closeCursor();
61 61
 
62
-		if ($row === false) {
63
-			return false;
64
-		}
62
+        if ($row === false) {
63
+            return false;
64
+        }
65 65
 
66
-		return str_contains($row, 'maria') && version_compare($row, '10.3', '>=') ||
67
-			!str_contains($row, 'maria') && version_compare($row, '8.0', '>=');
68
-	}
66
+        return str_contains($row, 'maria') && version_compare($row, '10.3', '>=') ||
67
+            !str_contains($row, 'maria') && version_compare($row, '8.0', '>=');
68
+    }
69 69
 }
Please login to merge, or discard this patch.