Skip to content

Commit 175fb56

Browse files
committed
Allow DAV Object properties
The current implementation only saves them as string. It seems they can be more complex than that, and that objects were saved directly. You may find such objects saved in some production databases by executing: ```sql SELECT * from oc_properties where propertyvalue = 'Object'; ``` This commit adds a repair job to clean all of these "broken" properties values, adds a new database column to save the type of the property, and handles converting from and to correct values. Implementation is very similar to SabreDAV's own PDO backend: https://github.com/nextcloud/3rdparty/blob/4921806dfb1c5c309eac60195ed34e2749baf3c1/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php Signed-off-by: Thomas Citharel <[email protected]>
1 parent 33ffaad commit 175fb56

File tree

6 files changed

+194
-25
lines changed

6 files changed

+194
-25
lines changed

apps/dav/appinfo/info.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<name>WebDAV</name>
66
<summary>WebDAV endpoint</summary>
77
<description>WebDAV endpoint</description>
8-
<version>1.23.0</version>
8+
<version>1.24.0</version>
99
<licence>agpl</licence>
1010
<author>owncloud.org</author>
1111
<namespace>DAV</namespace>
@@ -39,6 +39,7 @@
3939
<step>OCA\DAV\Migration\RemoveOrphanEventsAndContacts</step>
4040
<step>OCA\DAV\Migration\RemoveClassifiedEventActivity</step>
4141
<step>OCA\DAV\Migration\RemoveDeletedUsersCalendarSubscriptions</step>
42+
<step>OCA\DAV\Migration\RemoveObjectProperties</step>
4243
</post-migration>
4344
<live-migration>
4445
<step>OCA\DAV\Migration\ChunkCleanup</step>

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@
252252
'OCA\\DAV\\Migration\\RegisterBuildReminderIndexBackgroundJob' => $baseDir . '/../lib/Migration/RegisterBuildReminderIndexBackgroundJob.php',
253253
'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => $baseDir . '/../lib/Migration/RemoveClassifiedEventActivity.php',
254254
'OCA\\DAV\\Migration\\RemoveDeletedUsersCalendarSubscriptions' => $baseDir . '/../lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php',
255+
'OCA\\DAV\\Migration\\RemoveObjectProperties' => $baseDir . '/../lib/Migration/RemoveObjectProperties.php',
255256
'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => $baseDir . '/../lib/Migration/RemoveOrphanEventsAndContacts.php',
256257
'OCA\\DAV\\Migration\\Version1004Date20170825134824' => $baseDir . '/../lib/Migration/Version1004Date20170825134824.php',
257258
'OCA\\DAV\\Migration\\Version1004Date20170919104507' => $baseDir . '/../lib/Migration/Version1004Date20170919104507.php',
@@ -273,6 +274,7 @@
273274
'OCA\\DAV\\Migration\\Version1016Date20201109085907' => $baseDir . '/../lib/Migration/Version1016Date20201109085907.php',
274275
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => $baseDir . '/../lib/Migration/Version1017Date20210216083742.php',
275276
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => $baseDir . '/../lib/Migration/Version1018Date20210312100735.php',
277+
'OCA\\DAV\\Migration\\Version1024Date20211221144219' => $baseDir . '/../lib/Migration/Version1024Date20211221144219.php',
276278
'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php',
277279
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
278280
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ class ComposerStaticInitDAV
267267
'OCA\\DAV\\Migration\\RegisterBuildReminderIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/Migration/RegisterBuildReminderIndexBackgroundJob.php',
268268
'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => __DIR__ . '/..' . '/../lib/Migration/RemoveClassifiedEventActivity.php',
269269
'OCA\\DAV\\Migration\\RemoveDeletedUsersCalendarSubscriptions' => __DIR__ . '/..' . '/../lib/Migration/RemoveDeletedUsersCalendarSubscriptions.php',
270+
'OCA\\DAV\\Migration\\RemoveObjectProperties' => __DIR__ . '/..' . '/../lib/Migration/RemoveObjectProperties.php',
270271
'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => __DIR__ . '/..' . '/../lib/Migration/RemoveOrphanEventsAndContacts.php',
271272
'OCA\\DAV\\Migration\\Version1004Date20170825134824' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170825134824.php',
272273
'OCA\\DAV\\Migration\\Version1004Date20170919104507' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170919104507.php',
@@ -288,6 +289,7 @@ class ComposerStaticInitDAV
288289
'OCA\\DAV\\Migration\\Version1016Date20201109085907' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date20201109085907.php',
289290
'OCA\\DAV\\Migration\\Version1017Date20210216083742' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20210216083742.php',
290291
'OCA\\DAV\\Migration\\Version1018Date20210312100735' => __DIR__ . '/..' . '/../lib/Migration/Version1018Date20210312100735.php',
292+
'OCA\\DAV\\Migration\\Version1024Date20211221144219' => __DIR__ . '/..' . '/../lib/Migration/Version1024Date20211221144219.php',
291293
'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php',
292294
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
293295
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',

apps/dav/lib/DAV/CustomPropertiesBackend.php

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,29 @@
3131
use Sabre\DAV\PropFind;
3232
use Sabre\DAV\PropPatch;
3333
use Sabre\DAV\Tree;
34+
use Sabre\DAV\Xml\Property\Complex;
3435
use function array_intersect;
3536

3637
class CustomPropertiesBackend implements BackendInterface {
3738

3839
/** @var string */
3940
private const TABLE_NAME = 'properties';
4041

42+
/**
43+
* Value is stored as string.
44+
*/
45+
public const PROPERTY_TYPE_STRING = 1;
46+
47+
/**
48+
* Value is stored as XML fragment.
49+
*/
50+
public const PROPERTY_TYPE_XML = 2;
51+
52+
/**
53+
* Value is stored as a property object.
54+
*/
55+
public const PROPERTY_TYPE_OBJECT = 3;
56+
4157
/**
4258
* Ignored properties
4359
*
@@ -239,7 +255,7 @@ private function getPublishedProperties(string $path, array $requestedProperties
239255
$result = $qb->executeQuery();
240256
$props = [];
241257
while ($row = $result->fetch()) {
242-
$props[$row['propertyname']] = $row['propertyvalue'];
258+
$props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
243259
}
244260
$result->closeCursor();
245261
return $props;
@@ -282,7 +298,7 @@ private function getUserProperties(string $path, array $requestedProperties) {
282298

283299
$props = [];
284300
while ($row = $result->fetch()) {
285-
$props[$row['propertyname']] = $row['propertyvalue'];
301+
$props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
286302
}
287303

288304
$result->closeCursor();
@@ -304,9 +320,9 @@ private function updateProperties(string $path, array $properties) {
304320
' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
305321

306322
$insertStatement = 'INSERT INTO `*PREFIX*properties`' .
307-
' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)';
323+
' (`userid`,`propertypath`,`propertyname`,`propertyvalue`, `valuetype`) VALUES(?,?,?,?,?)';
308324

309-
$updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' .
325+
$updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?, `valuetype` = ?' .
310326
' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?';
311327

312328
// TODO: use "insert or update" strategy ?
@@ -325,29 +341,27 @@ private function updateProperties(string $path, array $properties) {
325341
);
326342
}
327343
} else {
328-
if ($propertyValue instanceOf \Sabre\DAV\Xml\Property\Complex) {
329-
$propertyValue = $propertyValue->getXml();
330-
} elseif (!is_string($propertyValue)) {
331-
$propertyValue = (string)$propertyValue;
332-
}
344+
[$value, $valueType] = $this->encodeValueForDatabase($propertyValue);
333345
if (!array_key_exists($propertyName, $existing)) {
334346
$this->connection->executeUpdate($insertStatement,
335-
[
336-
$this->user->getUID(),
337-
$this->formatPath($path),
338-
$propertyName,
339-
$propertyValue,
340-
]
341-
);
347+
[
348+
$this->user->getUID(),
349+
$this->formatPath($path),
350+
$propertyName,
351+
$value,
352+
$valueType
353+
]
354+
);
342355
} else {
343356
$this->connection->executeUpdate($updateStatement,
344-
[
345-
$propertyValue,
346-
$this->user->getUID(),
347-
$this->formatPath($path),
348-
$propertyName,
349-
]
350-
);
357+
[
358+
$value,
359+
$valueType,
360+
$this->user->getUID(),
361+
$this->formatPath($path),
362+
$propertyName,
363+
]
364+
);
351365
}
352366
}
353367
}
@@ -367,8 +381,41 @@ private function updateProperties(string $path, array $properties) {
367381
private function formatPath(string $path): string {
368382
if (strlen($path) > 250) {
369383
return sha1($path);
384+
}
385+
386+
return $path;
387+
}
388+
389+
/**
390+
* @param mixed $value
391+
* @return array
392+
*/
393+
private function encodeValueForDatabase($value): array {
394+
if (is_scalar($value)) {
395+
$valueType = self::PROPERTY_TYPE_STRING;
396+
} elseif ($value instanceof Complex) {
397+
$valueType = self::PROPERTY_TYPE_XML;
398+
$value = $value->getXml();
370399
} else {
371-
return $path;
400+
$valueType = self::PROPERTY_TYPE_OBJECT;
401+
$value = serialize($value);
402+
}
403+
return [$value, $valueType];
404+
}
405+
406+
/**
407+
* @return mixed|Complex|string
408+
*/
409+
private function decodeValueFromDatabase(string $value, int $valueType) {
410+
switch ($valueType) {
411+
case self::PROPERTY_TYPE_XML:
412+
return new Complex($value);
413+
case self::PROPERTY_TYPE_OBJECT:
414+
return unserialize($value);
415+
case null:
416+
case self::PROPERTY_TYPE_STRING:
417+
default:
418+
return $value;
372419
}
373420
}
374421
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) 2021, Thomas Citharel <[email protected]>.
4+
*
5+
* @author Thomas Citharel <[email protected]>
6+
*
7+
* @license AGPL-3.0
8+
*
9+
* This code is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License, version 3,
11+
* as published by the Free Software Foundation.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License, version 3,
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>
20+
*
21+
*/
22+
namespace OCA\DAV\Migration;
23+
24+
use OCP\DB\QueryBuilder\IQueryBuilder;
25+
use OCP\IDBConnection;
26+
use OCP\Migration\IOutput;
27+
use OCP\Migration\IRepairStep;
28+
29+
class RemoveObjectProperties implements IRepairStep {
30+
private const RESOURCE_TYPE_PROPERTY = '{DAV:}resourcetype';
31+
private const ME_CARD_PROPERTY = '{http://calendarserver.org/ns/}me-card';
32+
private const CALENDAR_TRANSP_PROPERTY = '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp';
33+
34+
/** @var IDBConnection */
35+
private $connection;
36+
37+
/**
38+
* RemoveObjectProperties constructor.
39+
*
40+
* @param IDBConnection $connection
41+
*/
42+
public function __construct(IDBConnection $connection) {
43+
$this->connection = $connection;
44+
}
45+
46+
/**
47+
* @inheritdoc
48+
*/
49+
public function getName() {
50+
return 'Remove invalid object properties';
51+
}
52+
53+
/**
54+
* @inheritdoc
55+
*/
56+
public function run(IOutput $output) {
57+
$query = $this->connection->getQueryBuilder();
58+
$updated = $query->delete('properties')
59+
->where($query->expr()->in('propertyname', $query->createNamedParameter([self::RESOURCE_TYPE_PROPERTY, self::ME_CARD_PROPERTY, self::CALENDAR_TRANSP_PROPERTY], IQueryBuilder::PARAM_STR_ARRAY)))
60+
->andWhere($query->expr()->eq('propertyvalue', $query->createNamedParameter('Object')))
61+
->executeStatement();
62+
63+
$output->info("$updated invalid object properties removed.");
64+
}
65+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\DAV\Migration;
6+
7+
use Closure;
8+
use Doctrine\DBAL\Schema\SchemaException;
9+
use OCP\DB\ISchemaWrapper;
10+
use OCP\DB\Types;
11+
use OCP\Migration\IOutput;
12+
use OCP\Migration\SimpleMigrationStep;
13+
14+
/**
15+
* Auto-generated migration step: Please modify to your needs!
16+
*/
17+
class Version1024Date20211221144219 extends SimpleMigrationStep {
18+
19+
/**
20+
* @param IOutput $output
21+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
22+
* @param array $options
23+
*/
24+
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
25+
}
26+
27+
/**
28+
* @param IOutput $output
29+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
30+
* @param array $options
31+
* @return null|ISchemaWrapper
32+
* @throws SchemaException
33+
*/
34+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
35+
/** @var ISchemaWrapper $schema */
36+
$schema = $schemaClosure();
37+
$propertiesTable = $schema->getTable('properties');
38+
$propertiesTable->addColumn('valuetype', Types::SMALLINT, [
39+
'notnull' => false,
40+
'default' => 1 // CustomPropertiesBackend::PROPERTY_TYPE_STRING
41+
]);
42+
return $schema;
43+
}
44+
45+
/**
46+
* @param IOutput $output
47+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
48+
* @param array $options
49+
*/
50+
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
51+
}
52+
}

0 commit comments

Comments
 (0)