Skip to content

Commit d0ec837

Browse files
Reference resources by id (#39)
* fixed undefined array key bug * reference resources by id instead of uri
1 parent 480318c commit d0ec837

16 files changed

+182
-132
lines changed

appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The current WebDAV-Push draft is provided by [@bitfireAT](https://github.com/bit
2020
2121
This Nextcloud extension has been developed by [@verdigado](https://github.com/verdigado).
2222
]]></description>
23-
<version>0.0.2</version>
23+
<version>0.0.3</version>
2424
<licence>agpl</licence>
2525
<author mail="[email protected]" homepage="https://github.com/bitfireAT/webdav-push">bitfire web engineering</author>
2626
<author mail="[email protected]">Jonathan Treffler</author>

lib/Dav/ServiceDetectionPlugin.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@
3333
use OCP\IUser;
3434
use OCP\IUserSession;
3535

36-
use OCP\AppFramework\Db\DoesNotExistException;
37-
use OCA\DAV\CalDAV\Calendar;
38-
3936
use Sabre\DAV\INode;
4037
use Sabre\DAV\PropFind;
4138
use Sabre\DAV\Server;
@@ -58,7 +55,11 @@ public function propFind(PropFind $propFind, INode $node) {
5855
return;
5956
}
6057

61-
if (!($node instanceof Calendar)) {
58+
if(($node instanceof \OCA\DAV\CalDAV\Calendar)) {
59+
$resourceType = "calendar";
60+
} else if($node instanceof \OCA\DAV\CardDAV\AddressBook) {
61+
$resourceType = "addressbook";
62+
} else {
6263
return;
6364
}
6465

@@ -96,7 +97,7 @@ function () use ($node) {
9697

9798
// return "test-return-push";
9899
//},
99-
$node->getName()
100+
$resourceType . "-" . $node->getResourceId(),
100101
);
101102
}
102103
}

lib/Dav/SubscriptionManagementPlugin.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,14 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
176176

177177
$node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
178178

179+
if(($node instanceof \OCA\DAV\CalDAV\Calendar)) {
180+
$resourceType = "calendar";
181+
} else if($node instanceof \OCA\DAV\CardDAV\AddressBook) {
182+
$resourceType = "addressbook";
183+
} else {
184+
return;
185+
}
186+
179187
$requestBody = $request->getBodyAsString();
180188

181189
// If this request handler could not deal with this POST request, it
@@ -225,13 +233,13 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
225233
} else {
226234
$user = $this->userSession->getUser();
227235

228-
$existingSubscriptionId = $transport->getSubscriptionIdFromOptions($user->getUID(), $node->getName(), $subscriptionOptions);
236+
$existingSubscriptionId = $transport->getSubscriptionIdFromOptions($user->getUID(), $resourceType, $node->getResourceId(), $subscriptionOptions);
229237

230238
if(!is_int($existingSubscriptionId)) {
231239
try {
232240
// create new subscription entry in db and register with transport. If the transport register fails roll back db transaction
233-
$this->atomic(function () use ($user, $node, $subscriptionType, $subscriptionExpires, $subscriptionOptions, &$subscription, $transport, &$errors, &$responseStatus, &$responseContent, &$unsubscribeLink) {
234-
$subscription = $this->subscriptionService->create($user->getUID(), $node->getName(), $subscriptionType, $subscriptionExpires);
241+
$this->atomic(function () use ($user, $resourceType, $node, $subscriptionType, $subscriptionExpires, $subscriptionOptions, &$subscription, $transport, &$errors, &$responseStatus, &$responseContent, &$unsubscribeLink) {
242+
$subscription = $this->subscriptionService->create($user->getUID(), $resourceType, $node->getResourceId(), $subscriptionType, $subscriptionExpires);
235243

236244
[
237245
'success' => $registerSuccess,
@@ -260,8 +268,8 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
260268
// implicitly checks if subscription found by transport is really owned by correct user
261269
$subscription = $this->subscriptionService->find($user->getUID(), $existingSubscriptionId);
262270

263-
// check if subscription found by transport is really for correct collection
264-
if($subscription->getCollectionName() !== $node->getName()) {
271+
// check if subscription found by transport is really for correct calendar/addressbook
272+
if($subscription->getResourceType() !== $resourceType || $subscription->getResourceId() !== $node->getResourceId()) {
265273
$errors[] = "subscription update error";
266274
} else {
267275
[

lib/Db/Subscription.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010

1111
class Subscription extends Entity implements JsonSerializable, TableSerializable {
1212
protected $userId;
13-
protected $collectionName;
13+
protected $resourceType;
14+
protected $resourceId;
1415
protected $transport;
1516
protected $creationTimestamp;
1617
protected $expirationTimestamp;
1718
protected $failCounter;
1819

1920
public function __construct() {
21+
$this->addType('resourceId','integer');
2022
$this->addType('creationTimestamp','integer');
2123
$this->addType('expirationTimestamp','integer');
2224
$this->addType('failCounter','integer');
@@ -26,7 +28,8 @@ public function jsonSerialize(): array {
2628
return [
2729
'id' => $this->id,
2830
'userId' => $this->userId,
29-
'collectionName' => $this->collectionName,
31+
'resourceType' => $this->resourceType,
32+
'resourceId' => $this->resourceId,
3033
'transport' => $this->transport,
3134
'creationTimestamp' => $this->creationTimestamp,
3235
'expirationTimestamp' => $this->expirationTimestamp,
@@ -37,8 +40,9 @@ public function jsonSerialize(): array {
3740
public function tableSerialize(?array $params = null): array {
3841
return [
3942
'Id' => $this->id,
40-
'User Id' => $this->userId,
41-
'DAV Collection Name' => $this->collectionName,
43+
'User ID' => $this->userId,
44+
'Resource Type' => $this->resourceType,
45+
'Resource ID' => $this->resourceId,
4246
'Transport' => $this->transport,
4347
'Creation Timestamp' => $this->creationTimestamp,
4448
'Expiration Timestamp' => $this->expirationTimestamp,

lib/Db/SubscriptionMapper.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,18 @@ public function find(string $userId, int $id): Subscription {
3434
}
3535

3636
/**
37-
* @param string $collectionName
37+
* @param string resourceType
38+
* @param int resourceId
3839
* @return Subscription[]
3940
*/
40-
public function findAll(string $collectionName): array {
41+
public function findAll(string $resourceType, int $resourceId): array {
4142
/* @var $qb IQueryBuilder */
4243
$qb = $this->db->getQueryBuilder();
4344

4445
$qb->select('*')
4546
->from(self::TABLENAME)
46-
->where($qb->expr()->eq('collection_name', $qb->createNamedParameter($collectionName)));
47+
->where($qb->expr()->eq('resource_type', $qb->createNamedParameter($resourceType)))
48+
->andWhere($qb->expr()->eq('resource_id', $qb->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT)));
4749

4850
return $this->findEntities($qb);
4951
}
@@ -76,6 +78,8 @@ public function cleanupAll(int $maxFails = 3): int {
7678
$qb->expr()->gt('fail_counter', $qb->createNamedParameter($maxFails, IQueryBuilder::PARAM_INT)),
7779
$qb->expr()->lt('expiration_timestamp', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT)),
7880
));
81+
82+
// TODO: call transport deleteSubscription
7983

8084
return $qb->executeStatement();
8185
}

lib/Db/WebPushSubscriptionMapper.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ public function findBySubscriptionId(int $subscriptionId): WebPushSubscription {
3232
return $this->findEntity($qb);
3333
}
3434

35-
/**
35+
/**
3636
* @param string $pushResource
3737
* @return Entity|WebPushSubscription
3838
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
3939
* @throws DoesNotExistException
4040
*/
41-
public function findByPushResource(string $userId, string $collectionName, string $pushResource): WebPushSubscription {
41+
public function findByPushResource(string $userId, string $resourceType, int $resourceId, string $pushResource): WebPushSubscription {
4242
/* @var $qb IQueryBuilder */
4343
$qb = $this->db->getQueryBuilder();
4444
$qb->select('webpush.*')
@@ -48,7 +48,8 @@ public function findByPushResource(string $userId, string $collectionName, strin
4848

4949
$qb->where($qb->expr()->eq('webpush.push_resource', $qb->createNamedParameter($pushResource)))
5050
->andWhere($qb->expr()->eq('subscription.user_id', $qb->createNamedParameter($userId)))
51-
->andWhere($qb->expr()->eq('subscription.collection_name', $qb->createNamedParameter($collectionName)));
51+
->andWhere($qb->expr()->eq('subscription.resource_type', $qb->createNamedParameter($resourceType)))
52+
->andWhere($qb->expr()->eq('subscription.resource_id', $qb->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT)));
5253

5354
return $this->findEntity($qb);
5455
}

lib/Listener/CalendarListener.php

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,44 +47,45 @@
4747

4848
class CalendarListener implements IEventListener {
4949

50-
public function __construct(
50+
public function __construct(
5151
private LoggerInterface $logger,
5252
private SubscriptionService $subscriptionService,
5353
private TransportManager $transportManager,
5454
private ErrorHandlingHelper $errorHandlingHelper,
5555
private $userId,
5656
) {}
5757

58-
public function handle(Event $event): void {
59-
if (($event instanceOf CalendarObjectCreatedEvent) || ($event instanceOf CalendarObjectDeletedEvent) ||
60-
($event instanceOf CalendarObjectUpdatedEvent) || ($event instanceOf CalendarObjectMovedToTrashEvent) ||
58+
public function handle(Event $event): void {
59+
if (($event instanceOf CalendarObjectCreatedEvent) || ($event instanceOf CalendarObjectDeletedEvent) ||
60+
($event instanceOf CalendarObjectUpdatedEvent) || ($event instanceOf CalendarObjectMovedToTrashEvent) ||
6161
($event instanceOf CalendarObjectRestoredEvent)) {
62-
$this->notifyAllForCalendar($event->getCalendarData());
63-
}
62+
$this->notifyAllSubscriptionsToResource("calendar", $event->getCalendarData()['id'], $event->getCalendarData()['{http://sabredav.org/ns}sync-token']);
63+
}
6464

6565
if($event instanceOf CalendarObjectMovedEvent) {
66-
$this->notifyAllForCalendar($event->getSourceCalendarData());
67-
$this->notifyAllForCalendar($event->getTargetCalendarData());
66+
$this->notifyAllSubscriptionsToResource("calendar", $event->getSourceCalendarData()['id'], $event->getSourceCalendarData()['{http://sabredav.org/ns}sync-token']);
67+
$this->notifyAllSubscriptionsToResource("calendar", $event->getTargetCalendarData()['id'], $event->getTargetCalendarData()['{http://sabredav.org/ns}sync-token']);
6868
}
69-
}
70-
private function notifyAllForCalendar(array $calendarData): void {
71-
$collectionName = $calendarData['uri'];
72-
$syncToken = $calendarData['{http://sabredav.org/ns}sync-token'];
73-
74-
$subscriptions = $this->subscriptionService->findAll($collectionName);
69+
}
7570

76-
$this->errorHandlingHelper->convertErrorsToExceptions(function () use ($subscriptions, $collectionName, $syncToken) {
71+
private function notifyAllSubscriptionsToResource(string $resourceType, int $resourceId, string $syncToken): void {
72+
$subscriptions = $this->subscriptionService->findAll($resourceType, $resourceId);
73+
74+
$this->errorHandlingHelper->convertErrorsToExceptions(function () use ($subscriptions, $resourceType, $resourceId, $syncToken) {
7775
foreach($subscriptions as $subscription) {
76+
// TODO: The subscription was able to be registered and has not expired yet, that means the user had access to the resource as of recently,
77+
// but we need to check if that is actually still the case here
78+
7879
$transport = $this->transportManager->getTransport($subscription->getTransport());
7980

8081
try {
81-
$transport->notify($subscription->getId(), $subscription->getUserId(), $collectionName, $syncToken);
82+
$transport->notify($subscription->getId(), $subscription->getUserId(), $resourceType, $resourceId, $syncToken);
8283
$this->subscriptionService->update($subscription->getUserId(), $subscription->getId(), failCounter: 0);
8384
} catch (\Throwable $e) {
8485
$this->logger->error("transport " . $subscription->getTransport() . " failed to deliver notification to subscription " . $subscription->getId() . ". error message: " . $e->getMessage());
8586
$this->subscriptionService->update($subscription->getUserId(), $subscription->getId(), failCounter: $subscription->getFailCounter() + 1);
8687
}
8788
}
88-
});
89+
});
8990
}
9091
}

lib/Migration/Version001Date20240515221000.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,15 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
5656
'notnull' => true,
5757
'length' => 200,
5858
]);
59-
$table->addColumn('collection_name', Types::STRING, [
59+
// Either calendar or addressbook
60+
$table->addColumn('resource_type', 'string', [
6061
'notnull' => true,
61-
'length' => 100,
62+
'length' => 255,
63+
]);
64+
$table->addColumn('resource_id', Types::BIGINT, [
65+
'notnull' => true,
66+
'length' => 11,
67+
'unsigned' => true,
6268
]);
6369
$table->addColumn('transport', Types::STRING, [
6470
'notnull' => true,

lib/Migration/Version002Date20250119180000.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
4545
/** @var ISchemaWrapper $schema */
4646
$schema = $schemaClosure();
4747

48-
$table = $schema->getTable(self::SUBSCRIPTIONS_TABLE);
48+
$table = $schema->getTable(self::SUBSCRIPTIONS_TABLE);
4949

50-
$table->addColumn('fail_counter', Types::INTEGER, [
51-
'notnull' => true,
52-
'default' => 0,
53-
]);
50+
$table->addColumn('fail_counter', Types::INTEGER, [
51+
'notnull' => true,
52+
'default' => 0,
53+
]);
5454

5555
return $schema;
5656
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\DavPush\Migration;
6+
7+
/**
8+
* @copyright 2025 Jonathan Treffler <[email protected]>
9+
*
10+
* @author Jonathan Treffler <[email protected]>
11+
*
12+
* @license GNU AGPL version 3 or any later version
13+
*
14+
* This program is free software: you can redistribute it and/or modify
15+
* it under the terms of the GNU Affero General Public License as
16+
* published by the Free Software Foundation, either version 3 of the
17+
* License, or (at your option) any later version.
18+
*
19+
* This program is distributed in the hope that it will be useful,
20+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
* GNU Affero General Public License for more details.
23+
*
24+
* You should have received a copy of the GNU Affero General Public License
25+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
26+
*
27+
*/
28+
29+
use Closure;
30+
use OCP\DB\Types;
31+
use OCP\DB\ISchemaWrapper;
32+
use OCP\IDBConnection;
33+
use OCP\Migration\SimpleMigrationStep;
34+
use OCP\Migration\IOutput;
35+
36+
use Psr\Log\LoggerInterface;
37+
38+
class Version003Date20250220223000 extends SimpleMigrationStep {
39+
public const SUBSCRIPTIONS_TABLE = "dav_push_subscriptions";
40+
41+
public function __construct(
42+
private IDBConnection $connection,
43+
private LoggerInterface $logger,
44+
) {}
45+
46+
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
47+
/** @var ISchemaWrapper $schema */
48+
$schema = $schemaClosure();
49+
$table = $schema->getTable(self::SUBSCRIPTIONS_TABLE);
50+
51+
// this migration was made specifically to allow a migration path from alpha version 0.0.2 to 0.0.3 (even though our alpha releases generally do not guarantee a migration path)
52+
// not needed for instances that started on a later version and therefore never had the collection_name column
53+
if($table->hasColumn("collection_name")) {
54+
$this->logger->debug("dav_push was initially installed on this instance before alpha version 0.0.3, all subscriptions will be cleared to provide a migration path to more modern versions");
55+
// clear all subscriptions created with the old way of referencing calendars
56+
$this->connection
57+
->getQueryBuilder()
58+
->delete(self::SUBSCRIPTIONS_TABLE)
59+
->executeStatement();
60+
} else {
61+
$this->logger->debug("dav_push was initially installed on this instance after alpha version 0.0.3, migration Version003Date20250220223000 is not needed and wil NOOP");
62+
}
63+
}
64+
65+
/**
66+
* @param IOutput $output
67+
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
68+
* @param array $options
69+
* @return null|ISchemaWrapper
70+
*/
71+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
72+
/** @var ISchemaWrapper $schema */
73+
$schema = $schemaClosure();
74+
$table = $schema->getTable(self::SUBSCRIPTIONS_TABLE);
75+
76+
if($table->hasColumn("collection_name")) {
77+
// Either calendar or addressbook
78+
$table->addColumn('resource_type', 'string', [
79+
'notnull' => true,
80+
'length' => 255,
81+
]);
82+
$table->addColumn('resource_id', Types::BIGINT, [
83+
'notnull' => true,
84+
'length' => 11,
85+
'unsigned' => true,
86+
]);
87+
$table->dropColumn('collection_name');
88+
}
89+
90+
return $schema;
91+
}
92+
}

0 commit comments

Comments
 (0)