diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml
index 0ef960b017..d9418456d8 100644
--- a/apps/dav/appinfo/info.xml
+++ b/apps/dav/appinfo/info.xml
@@ -5,7 +5,7 @@
WebDAV
WebDAV endpoint
WebDAV endpoint
- 1.9.1
+ 1.9.2
agpl
owncloud.org
DAV
@@ -31,6 +31,7 @@
OCA\DAV\Migration\CalDAVRemoveEmptyValue
OCA\DAV\Migration\BuildCalendarSearchIndex
OCA\DAV\Migration\RefreshWebcalJobRegistrar
+ OCA\DAV\Migration\RemoveOrphanEventsAndContacts
OCA\DAV\Migration\RemoveClassifiedEventActivity
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index e9aaf35f64..d2bb79495f 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -158,6 +158,7 @@ return array(
'OCA\\DAV\\Migration\\FixBirthdayCalendarComponent' => $baseDir . '/../lib/Migration/FixBirthdayCalendarComponent.php',
'OCA\\DAV\\Migration\\RefreshWebcalJobRegistrar' => $baseDir . '/../lib/Migration/RefreshWebcalJobRegistrar.php',
'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => $baseDir . '/../lib/Migration/RemoveClassifiedEventActivity.php',
+ 'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => $baseDir . '/../lib/Migration/RemoveOrphanEventsAndContacts.php',
'OCA\\DAV\\Migration\\Version1004Date20170825134824' => $baseDir . '/../lib/Migration/Version1004Date20170825134824.php',
'OCA\\DAV\\Migration\\Version1004Date20170919104507' => $baseDir . '/../lib/Migration/Version1004Date20170919104507.php',
'OCA\\DAV\\Migration\\Version1004Date20170924124212' => $baseDir . '/../lib/Migration/Version1004Date20170924124212.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index 40fb07033f..48667db5e3 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -173,6 +173,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Migration\\FixBirthdayCalendarComponent' => __DIR__ . '/..' . '/../lib/Migration/FixBirthdayCalendarComponent.php',
'OCA\\DAV\\Migration\\RefreshWebcalJobRegistrar' => __DIR__ . '/..' . '/../lib/Migration/RefreshWebcalJobRegistrar.php',
'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => __DIR__ . '/..' . '/../lib/Migration/RemoveClassifiedEventActivity.php',
+ 'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => __DIR__ . '/..' . '/../lib/Migration/RemoveOrphanEventsAndContacts.php',
'OCA\\DAV\\Migration\\Version1004Date20170825134824' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170825134824.php',
'OCA\\DAV\\Migration\\Version1004Date20170919104507' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170919104507.php',
'OCA\\DAV\\Migration\\Version1004Date20170924124212' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170924124212.php',
diff --git a/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php b/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php
new file mode 100644
index 0000000000..1764358790
--- /dev/null
+++ b/apps/dav/lib/Migration/RemoveOrphanEventsAndContacts.php
@@ -0,0 +1,94 @@
+
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace OCA\DAV\Migration;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class RemoveOrphanEventsAndContacts implements IRepairStep {
+
+ /** @var IDBConnection */
+ private $connection;
+
+ public function __construct(IDBConnection $connection) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getName(): string {
+ return 'Clean up orphan event and contact data';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run(IOutput $output) {
+ $orphanItems = $this->removeOrphanChildren('calendarobjects', 'calendars', 'calendarid');
+ $output->info(sprintf('%d events without a calendar have been cleaned up', $orphanItems));
+ $orphanItems = $this->removeOrphanChildren('calendarobjects_props', 'calendarobjects', 'objectid');
+ $output->info(sprintf('%d properties without an events have been cleaned up', $orphanItems));
+ $orphanItems = $this->removeOrphanChildren('calendarchanges', 'calendars', 'calendarid');
+ $output->info(sprintf('%d changes without a calendar have been cleaned up', $orphanItems));
+
+ $orphanItems = $this->removeOrphanChildren('cards', 'addressbooks', 'addressbookid');
+ $output->info(sprintf('%d contacts without an addressbook have been cleaned up', $orphanItems));
+ $orphanItems = $this->removeOrphanChildren('cards_properties', 'cards', 'cardid');
+ $output->info(sprintf('%d properties without a contact have been cleaned up', $orphanItems));
+ $orphanItems = $this->removeOrphanChildren('addressbookchanges', 'addressbooks', 'addressbookid');
+ $output->info(sprintf('%d changes without an addressbook have been cleaned up', $orphanItems));
+ }
+
+ protected function removeOrphanChildren($childTable, $parentTable, $parentId): int {
+ $qb = $this->connection->getQueryBuilder();
+
+ $qb->select('c.id')
+ ->from($childTable, 'c')
+ ->leftJoin('c', $parentTable, 'p', $qb->expr()->eq('c.' . $parentId, 'p.id'))
+ ->where($qb->expr()->isNull('p.id'));
+ $result = $qb->execute();
+
+ $orphanItems = array();
+ while ($row = $result->fetch()) {
+ $orphanItems[] = (int) $row['id'];
+ }
+ $result->closeCursor();
+
+ if (!empty($orphanItems)) {
+ $qb->delete($childTable)
+ ->where($qb->expr()->in('id', $qb->createParameter('ids')));
+
+ $orphanItemsBatch = array_chunk($orphanItems, 200);
+ foreach ($orphanItemsBatch as $items) {
+ $qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY);
+ $qb->execute();
+ }
+ }
+
+ return count($orphanItems);
+ }
+}