Skip to content

Commit 4c5842e

Browse files
author
Carl Schwan
committed
refactor(CustomPropertiesBackend): Modernize class
- Use query builder - Add chunking - Add type hinting where we can - Use match expression Signed-off-by: Carl Schwan <carl.schwan@nextclound.com>
1 parent d18b10c commit 4c5842e

3 files changed

Lines changed: 66 additions & 87 deletions

File tree

apps/dav/lib/DAV/CustomPropertiesBackend.php

Lines changed: 63 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use OCP\DB\QueryBuilder\IQueryBuilder;
2323
use OCP\IDBConnection;
2424
use OCP\IUser;
25+
use Override;
2526
use Sabre\CalDAV\Schedule\Inbox;
2627
use Sabre\DAV\Exception as DavException;
2728
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
@@ -98,13 +99,15 @@ class CustomPropertiesBackend implements BackendInterface {
9899
/**
99100
* Map of custom XML elements to parse when trying to deserialize an instance of
100101
* \Sabre\DAV\Xml\Property\Complex to find a more specialized PROPERTY_TYPE_*
102+
* @var array<string, class-string>
101103
*/
102104
private const COMPLEX_XML_ELEMENT_MAP = [
103105
'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => Href::class,
104106
];
105107

106108
/**
107109
* Map of well-known property names to default values
110+
* @var array<string, string>
108111
*/
109112
private const PROPERTY_DEFAULT_VALUES = [
110113
'{http://owncloud.org/ns}calendar-enabled' => '1',
@@ -118,17 +121,15 @@ class CustomPropertiesBackend implements BackendInterface {
118121
private XmlService $xmlService;
119122

120123
/**
121-
* @param Tree $tree node tree
122-
* @param IDBConnection $connection database connection
123124
* @param IUser $user owner of the tree and properties
124125
*/
125126
public function __construct(
126-
private Server $server,
127-
private Tree $tree,
128-
private IDBConnection $connection,
129-
private IUser $user,
130-
private PropertyMapper $propertyMapper,
131-
private DefaultCalendarValidator $defaultCalendarValidator,
127+
private readonly Server $server,
128+
private readonly Tree $tree,
129+
private readonly IDBConnection $connection,
130+
private readonly IUser $user,
131+
private readonly PropertyMapper $propertyMapper,
132+
private readonly DefaultCalendarValidator $defaultCalendarValidator,
132133
) {
133134
$this->xmlService = new XmlService();
134135
$this->xmlService->elementMap = array_merge(
@@ -142,9 +143,9 @@ public function __construct(
142143
*
143144
* @param string $path
144145
* @param PropFind $propFind
145-
* @return void
146146
*/
147-
public function propFind($path, PropFind $propFind) {
147+
#[Override]
148+
public function propFind($path, PropFind $propFind): void {
148149
$requestedProps = $propFind->get404Properties();
149150

150151
$requestedProps = array_filter(
@@ -257,12 +258,10 @@ private function isPropertyAllowed(string $property): bool {
257258
* Updates properties for a path
258259
*
259260
* @param string $path
260-
* @param PropPatch $propPatch
261-
*
262-
* @return void
263261
*/
264-
public function propPatch($path, PropPatch $propPatch) {
265-
$propPatch->handleRemaining(function ($changedProps) use ($path) {
262+
#[Override]
263+
public function propPatch($path, PropPatch $propPatch): void {
264+
$propPatch->handleRemaining(function (array $changedProps) use ($path) {
266265
return $this->updateProperties($path, $changedProps);
267266
});
268267
}
@@ -272,13 +271,13 @@ public function propPatch($path, PropPatch $propPatch) {
272271
*
273272
* @param string $path path of node for which to delete properties
274273
*/
275-
public function delete($path) {
276-
$statement = $this->connection->prepare(
277-
'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'
278-
);
279-
$statement->execute([$this->user->getUID(), $this->formatPath($path)]);
280-
$statement->closeCursor();
281-
274+
#[Override]
275+
public function delete($path): void {
276+
$qb = $this->connection->getQueryBuilder();
277+
$qb->delete('properties')
278+
->where($qb->expr()->eq('userid', $qb->createNamedParameter($this->user->getUID())))
279+
->andWhere($qb->expr()->eq('propertypath', $qb->createNamedParameter($this->formatPath($path))));
280+
$qb->executeStatement();
282281
unset($this->userCache[$path]);
283282
}
284283

@@ -287,16 +286,15 @@ public function delete($path) {
287286
*
288287
* @param string $source
289288
* @param string $destination
290-
*
291-
* @return void
292289
*/
293-
public function move($source, $destination) {
294-
$statement = $this->connection->prepare(
295-
'UPDATE `*PREFIX*properties` SET `propertypath` = ?'
296-
. ' WHERE `userid` = ? AND `propertypath` = ?'
297-
);
298-
$statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]);
299-
$statement->closeCursor();
290+
#[Override]
291+
public function move($source, $destination): void {
292+
$qb = $this->connection->getQueryBuilder();
293+
$qb->update('properties')
294+
->set('propertypath', $qb->createNamedParameter($this->formatPath($destination)))
295+
->where($qb->expr()->eq('userid', $qb->createNamedParameter($this->user->getUID())))
296+
->andWhere($qb->expr()->eq('propertypath', $qb->createNamedParameter($this->formatPath($source))));
297+
$qb->executeStatement();
300298
}
301299

302300
/**
@@ -325,10 +323,10 @@ private function validateProperty(string $path, string $propName, mixed $propVal
325323
}
326324

327325
/**
328-
* @param string $path
329326
* @param string[] $requestedProperties
330327
*
331-
* @return array
328+
* @return array<string, mixed|Complex|Href|string>
329+
* @throws \OCP\DB\Exception
332330
*/
333331
private function getPublishedProperties(string $path, array $requestedProperties): array {
334332
$allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties);
@@ -356,7 +354,7 @@ private function getPublishedProperties(string $path, array $requestedProperties
356354
}
357355

358356
/**
359-
* prefetch all user properties in a directory
357+
* Prefetch all user properties in a directory
360358
*/
361359
private function cacheDirectory(string $path, Directory $node): void {
362360
$prefix = ltrim($path . '/', '/');
@@ -449,45 +447,44 @@ private function cacheCalendars(CalendarHome $node, array $requestedProperties):
449447
/**
450448
* Returns a list of properties for the given path and current user
451449
*
452-
* @param string $path
453450
* @param array $requestedProperties requested properties or empty array for "all"
454-
* @return array
451+
* @return array<string, mixed>
455452
* @note The properties list is a list of propertynames the client
456453
* requested, encoded as xmlnamespace#tagName, for example:
457454
* http://www.example.org/namespace#author If the array is empty, all
458455
* properties should be returned
459456
*/
460-
private function getUserProperties(string $path, array $requestedProperties) {
457+
private function getUserProperties(string $path, array $requestedProperties): array {
461458
if (isset($this->userCache[$path])) {
462459
return $this->userCache[$path];
463460
}
464461

465-
// TODO: chunking if more than 1000 properties
466-
$sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?';
462+
$props = [];
467463

468-
$whereValues = [$this->user->getUID(), $this->formatPath($path)];
469-
$whereTypes = [null, null];
464+
$qb = $this->connection->getQueryBuilder();
465+
$qb->select('*')
466+
->from('properties')
467+
->where($qb->expr()->eq('userid', $qb->createNamedParameter($this->user->getUID(), IQueryBuilder::PARAM_STR)))
468+
->andWhere($qb->expr()->eq('propertypath', $qb->createNamedParameter($this->formatPath($path), IQueryBuilder::PARAM_STR)));
470469

471470
if (!empty($requestedProperties)) {
472471
// request only a subset
473-
$sql .= ' AND `propertyname` in (?)';
474-
$whereValues[] = $requestedProperties;
475-
$whereTypes[] = IQueryBuilder::PARAM_STR_ARRAY;
476-
}
477-
478-
$result = $this->connection->executeQuery(
479-
$sql,
480-
$whereValues,
481-
$whereTypes
482-
);
483-
484-
$props = [];
485-
while ($row = $result->fetch()) {
486-
$props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
472+
$qb->andWhere($qb->expr()->in('propertyname', $qb->createParameter('requestedProperties')));
473+
$chunks = array_chunk($requestedProperties, 1000);
474+
foreach ($chunks as $chunk) {
475+
$qb->setParameter('requestedProperties', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
476+
$result = $qb->executeQuery();
477+
while ($row = $result->fetch()) {
478+
$props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
479+
}
480+
}
481+
} else {
482+
$result = $qb->executeQuery();
483+
while ($row = $result->fetch()) {
484+
$props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
485+
}
487486
}
488487

489-
$result->closeCursor();
490-
491488
$this->userCache[$path] = $props;
492489
return $props;
493490
}
@@ -501,6 +498,7 @@ private function isPropertyDefaultValue(string $name, mixed $value): bool {
501498
}
502499

503500
/**
501+
* @param array<string, string> $properties
504502
* @throws Exception
505503
*/
506504
private function updateProperties(string $path, array $properties): bool {
@@ -558,10 +556,7 @@ private function updateProperties(string $path, array $properties): bool {
558556
}
559557

560558
/**
561-
* long paths are hashed to ensure they fit in the database
562-
*
563-
* @param string $path
564-
* @return string
559+
* Long paths are hashed to ensure they fit in the database
565560
*/
566561
private function formatPath(string $path): string {
567562
if (strlen($path) > 250) {
@@ -616,20 +611,13 @@ private function encodeValueForDatabase(string $path, string $name, mixed $value
616611
/**
617612
* @return mixed|Complex|string
618613
*/
619-
private function decodeValueFromDatabase(string $value, int $valueType) {
620-
switch ($valueType) {
621-
case self::PROPERTY_TYPE_XML:
622-
return new Complex($value);
623-
case self::PROPERTY_TYPE_HREF:
624-
return new Href($value);
625-
case self::PROPERTY_TYPE_OBJECT:
626-
// some databases can not handel null characters, these are custom encoded during serialization
627-
// this custom encoding needs to be first reversed before unserializing
628-
return unserialize(str_replace('\x00', chr(0), $value));
629-
case self::PROPERTY_TYPE_STRING:
630-
default:
631-
return $value;
632-
}
614+
private function decodeValueFromDatabase(string $value, int $valueType): mixed {
615+
return match ($valueType) {
616+
self::PROPERTY_TYPE_XML => new Complex($value),
617+
self::PROPERTY_TYPE_HREF => new Href($value),
618+
self::PROPERTY_TYPE_OBJECT => unserialize(str_replace('\x00', chr(0), $value)),
619+
default => $value,
620+
};
633621
}
634622

635623
private function encodeDefaultCalendarUrl(Href $value): Href {

apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ protected function setUp(): void {
6767
protected function tearDown(): void {
6868
$query = $this->dbConnection->getQueryBuilder();
6969
$query->delete('properties');
70-
$query->execute();
70+
$query->executeStatement();
7171

7272
parent::tearDown();
7373
}
@@ -102,7 +102,7 @@ protected function insertProp(string $user, string $path, string $name, mixed $v
102102
'propertyvalue' => $query->createNamedParameter($value),
103103
'valuetype' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT)
104104
]);
105-
$query->execute();
105+
$query->executeStatement();
106106
}
107107

108108
protected function getProps(string $user, string $path): array {
@@ -112,7 +112,7 @@ protected function getProps(string $user, string $path): array {
112112
->where($query->expr()->eq('userid', $query->createNamedParameter($user)))
113113
->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path))));
114114

115-
$result = $query->execute();
115+
$result = $query->executeQuery();
116116
$data = [];
117117
while ($row = $result->fetch()) {
118118
$value = $row['propertyvalue'];

build/psalm-baseline.xml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -791,15 +791,6 @@
791791
<code><![CDATA[$vEvent->DTSTAMP]]></code>
792792
</UndefinedPropertyAssignment>
793793
</file>
794-
<file src="apps/dav/lib/DAV/CustomPropertiesBackend.php">
795-
<DeprecatedMethod>
796-
<code><![CDATA[closeCursor]]></code>
797-
<code><![CDATA[closeCursor]]></code>
798-
</DeprecatedMethod>
799-
<InvalidArgument>
800-
<code><![CDATA[$whereValues]]></code>
801-
</InvalidArgument>
802-
</file>
803794
<file src="apps/dav/lib/DAV/GroupPrincipalBackend.php">
804795
<InvalidNullableReturnType>
805796
<code><![CDATA[array]]></code>

0 commit comments

Comments
 (0)